Compare commits
174 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c51612982 | ||
|
|
a472adeb74 | ||
|
|
2e61839387 | ||
|
|
51805ef657 | ||
|
|
5856e41a62 | ||
|
|
ffbe6b7d76 | ||
|
|
ed6a966534 | ||
|
|
97fc2a2a3a | ||
|
|
3e1be7a33a | ||
|
|
4a4dd7f987 | ||
|
|
005c628352 | ||
|
|
1bd316de80 | ||
|
|
49b44d8238 | ||
|
|
8bc1981891 | ||
|
|
db6dc6431b | ||
|
|
f214673c3c | ||
|
|
ffe00ee398 | ||
|
|
6cade5bd7f | ||
|
|
a531b5917e | ||
|
|
ca561daedf | ||
|
|
f4cb577cb4 | ||
|
|
91be57cbce | ||
|
|
78596545b0 | ||
|
|
9f6cc33858 | ||
|
|
d82de45b7e | ||
|
|
b7bbf58624 | ||
|
|
945d941c7b | ||
|
|
2605bf966f | ||
|
|
83b72a1ede | ||
|
|
6190ca4307 | ||
|
|
c2fcf7fc4a | ||
|
|
37cb4339e2 | ||
|
|
46f229e308 | ||
|
|
7f8f38f666 | ||
|
|
fb0769a327 | ||
|
|
b84cc3d49e | ||
|
|
0cba506bc4 | ||
|
|
5cdfbba55c | ||
|
|
6145231936 | ||
|
|
513b07ddcf | ||
|
|
150971fa92 | ||
|
|
ac85d88c9c | ||
|
|
1c79d6fb5d | ||
|
|
d41321254a | ||
|
|
515b4656e6 | ||
|
|
34c96ff925 | ||
|
|
b8a04cc4ef | ||
|
|
ddc3f6e9c6 | ||
|
|
3699e61c20 | ||
|
|
2820218609 | ||
|
|
eb3e739f7b | ||
|
|
87f6e83988 | ||
|
|
5728efd32b | ||
|
|
bac367b005 | ||
|
|
0d89131f66 | ||
|
|
8380be4be3 | ||
|
|
d0f51363bf | ||
|
|
15160d3b61 | ||
|
|
c5efb77bad | ||
|
|
b877751b2d | ||
|
|
40db482fd8 | ||
|
|
98554e919c | ||
|
|
017bf1e905 | ||
|
|
6498fc3a9e | ||
|
|
8ba71c94f4 | ||
|
|
f2f3eb76e1 | ||
|
|
5fe945fca8 | ||
|
|
8ef0851a49 | ||
|
|
e45956db6c | ||
|
|
7cc9f7e2e0 | ||
|
|
df3903532d | ||
|
|
46456c9a2a | ||
|
|
e98f2fc013 | ||
|
|
7df7a505ee | ||
|
|
d40bdcc6ac | ||
|
|
1cce56b024 | ||
|
|
2126f9afc1 | ||
|
|
41420eedcf | ||
|
|
1b6ab19b6d | ||
|
|
fbe5c18762 | ||
|
|
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 |
21
.gitignore
vendored
@@ -1,18 +1,9 @@
|
||||
lib-cov
|
||||
*.seed
|
||||
*.log
|
||||
*.csv
|
||||
*.dat
|
||||
*.out
|
||||
*.pid
|
||||
*.gz
|
||||
|
||||
pids
|
||||
logs
|
||||
results
|
||||
build
|
||||
node_modules
|
||||
tests/output.jpg
|
||||
tests/output.png
|
||||
coverage
|
||||
test/bench/node_modules
|
||||
test/fixtures/output.*
|
||||
test/leak/libvips.supp
|
||||
|
||||
npm-debug.log
|
||||
# Mac OS X
|
||||
.DS_Store
|
||||
|
||||
3
.jshintignore
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
test/bench/node_modules
|
||||
coverage
|
||||
8
.jshintrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"strict": true,
|
||||
"node": true,
|
||||
"globals": {
|
||||
"describe": true,
|
||||
"it": true
|
||||
}
|
||||
}
|
||||
8
.npmignore
Normal file
@@ -0,0 +1,8 @@
|
||||
build
|
||||
node_modules
|
||||
coverage
|
||||
.jshintignore
|
||||
.jshintrc
|
||||
.gitignore
|
||||
test
|
||||
.travis.yml
|
||||
8
.travis.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "0.10"
|
||||
- "0.11"
|
||||
before_install:
|
||||
- curl -s https://raw.githubusercontent.com/lovell/sharp/master/preinstall.sh | sudo bash -
|
||||
after_success:
|
||||
- cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
|
||||
605
README.md
@@ -1,58 +1,89 @@
|
||||
# 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)
|
||||
* [Thanks](https://github.com/lovell/sharp#thanks)
|
||||
* [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.
|
||||
|
||||
The performance of JPEG resizing is typically 15x-25x faster than ImageMagick and GraphicsMagick, based mainly on the number of CPU cores available.
|
||||
Memory usage is kept to a minimum, no child processes are spawned, everything remains non-blocking thanks to _libuv_ and Promises/A+ are supported.
|
||||
|
||||
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.
|
||||
This module supports reading and writing JPEG, PNG and WebP images to and from Streams, Buffer objects and the filesystem. It also supports reading images of many other types from the filesystem via libmagick++ or libgraphicsmagick++ if present.
|
||||
|
||||
## Prerequisites
|
||||
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/).
|
||||
|
||||
* Node.js v0.8+
|
||||
* [libvips](https://github.com/jcupitt/libvips) v7.38+
|
||||
Anyone who has used the Node.js bindings for [GraphicsMagick](https://github.com/aheckmann/gm) will find the API similarly fluent.
|
||||
|
||||
For the sharpest results, please compile libvips from source.
|
||||
This module is powered by the blazingly fast [libvips](https://github.com/jcupitt/libvips) image processing library, originally created in 1989 at Birkbeck College and currently maintained by [John Cupitt](https://github.com/jcupitt).
|
||||
|
||||
## 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)
|
||||
To install the latest version of libvips on the following Operating Systems:
|
||||
|
||||
Scale and crop to `width` x `height` calling `callback` when complete.
|
||||
* Mac OS
|
||||
* Homebrew
|
||||
* MacPorts
|
||||
* Debian Linux
|
||||
* Debian 7, 8
|
||||
* Ubuntu 12.04, 14.04, 14.10
|
||||
* Mint 13, 17
|
||||
* Red Hat Linux
|
||||
* RHEL/Centos/Scientific 6, 7
|
||||
* Fedora 21, 22
|
||||
|
||||
`input` can either be a filename String or a Buffer. When using a filename libvips will `mmap` the file for improved performance.
|
||||
run the following as a user with `sudo` access:
|
||||
|
||||
`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`.
|
||||
curl -s https://raw.githubusercontent.com/lovell/sharp/master/preinstall.sh | sudo bash -
|
||||
|
||||
`width` is the Number of pixels wide the resultant image should be.
|
||||
or run the following as `root`:
|
||||
|
||||
`height` is the Number of pixels high the resultant image should be.
|
||||
curl -s https://raw.githubusercontent.com/lovell/sharp/master/preinstall.sh | bash -
|
||||
|
||||
`options` is optional, and can contain one or more of:
|
||||
The [preinstall.sh](https://github.com/lovell/sharp/blob/master/preinstall.sh) script requires `curl` and `pkg-config`.
|
||||
|
||||
* `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.
|
||||
### Mac OS tips
|
||||
|
||||
`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.
|
||||
Manual install via homebrew:
|
||||
|
||||
### Examples
|
||||
brew install homebrew/science/vips --with-webp --with-graphicsmagick
|
||||
|
||||
A missing or incorrectly configured _Xcode Command Line Tools_ installation [can lead](https://github.com/lovell/sharp/issues/80) to a `library not found for -ljpeg` error. If so, please try:
|
||||
|
||||
xcode-select --install
|
||||
|
||||
The _gettext_ dependency of _libvips_ [can lead](https://github.com/lovell/sharp/issues/9) to a `library not found for -lintl` error. If so, please try:
|
||||
|
||||
brew link gettext --force
|
||||
|
||||
### Install libvips on Heroku
|
||||
|
||||
[Alessandro Tagliapietra](https://github.com/alex88) maintains an [Heroku buildpack for libvips](https://github.com/alex88/heroku-buildpack-vips) and its dependencies.
|
||||
|
||||
### Using with gulp.js
|
||||
|
||||
[Mohammad Prabowo](https://github.com/rizalp) maintains a [gulp.js plugin](https://github.com/rizalp/gulp-sharp).
|
||||
|
||||
## 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 +93,492 @@ 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) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
// buffer contains progressive JPEG image data
|
||||
var transformer = sharp().resize(300, 200).crop(sharp.gravity.north);
|
||||
readableStream.pipe(transformer).pipe(writableStream);
|
||||
// Read image data from readableStream, resize and write image data to writableStream
|
||||
```
|
||||
|
||||
```javascript
|
||||
var image = sharp(inputJpg);
|
||||
image.metadata(function(err, metadata) {
|
||||
image.resize(metadata.width / 2).webp().toBuffer(function(err, outputBuffer, info) {
|
||||
// outputBuffer contains a WebP image half the width and height of the original JPEG
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
```javascript
|
||||
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)
|
||||
});
|
||||
var pipeline = sharp()
|
||||
.rotate()
|
||||
.resize(null, 200)
|
||||
.progressive()
|
||||
.toBuffer(function(err, outputBuffer, info) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
// outputBuffer contains 200px high progressive JPEG image data,
|
||||
// auto-rotated using EXIF Orientation tag
|
||||
// info.width and info.height contain the dimensions of the resized image
|
||||
});
|
||||
readableStream.pipe(pipeline);
|
||||
```
|
||||
|
||||
```javascript
|
||||
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('input.png')
|
||||
.rotate(180)
|
||||
.resize(300)
|
||||
.flatten()
|
||||
.background('#ff6600')
|
||||
.sharpen()
|
||||
.withMetadata()
|
||||
.quality(90)
|
||||
.webp()
|
||||
.toBuffer()
|
||||
.then(function(outputBuffer) {
|
||||
// outputBuffer contains upside down, 300px wide, alpha channel flattened
|
||||
// onto orange background, sharpened, with metadata, 90% quality WebP image
|
||||
// data
|
||||
});
|
||||
```
|
||||
|
||||
```javascript
|
||||
sharp.resize("input.jpg", sharp.buffer.jpeg, 200, 300, {canvas: sharp.canvas.embedBlack}, function(err, buffer) {
|
||||
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
|
||||
});
|
||||
http.createServer(function(request, response) {
|
||||
response.writeHead(200, {'Content-Type': 'image/webp'});
|
||||
sharp('input.jpg').rotate().resize(200).webp().pipe(response);
|
||||
}).listen(8000);
|
||||
// Create HTTP server that always returns auto-rotated 'input.jpg',
|
||||
// resized to 200 pixels wide, in WebP format
|
||||
```
|
||||
|
||||
```javascript
|
||||
sharp(input)
|
||||
.extract(top, left, width, height)
|
||||
.toFile(output);
|
||||
// Extract a region of the input image, saving in the same format.
|
||||
```
|
||||
|
||||
```javascript
|
||||
sharp(input)
|
||||
.extract(topOffsetPre, leftOffsetPre, widthPre, heightPre)
|
||||
.resize(width, height)
|
||||
.extract(topOffsetPost, leftOffsetPost, widthPost, heightPost)
|
||||
.toFile(output);
|
||||
// Extract a region, resize, then extract from the resized image
|
||||
```
|
||||
|
||||
```javascript
|
||||
sharp(inputBuffer)
|
||||
.resize(200, 300)
|
||||
.interpolateWith(sharp.interpolator.nohalo)
|
||||
.background('white')
|
||||
.embed()
|
||||
.toFile('output.tiff')
|
||||
.then(function() {
|
||||
// output.tiff is a 200 pixels wide and 300 pixels high image
|
||||
// containing a bicubic scaled version, embedded on a white canvas,
|
||||
// of the image data in inputBuffer
|
||||
});
|
||||
```
|
||||
|
||||
```javascript
|
||||
sharp('input.gif')
|
||||
.resize(200, 300)
|
||||
.background({r: 0, g: 0, b: 0, a: 0})
|
||||
.embed()
|
||||
.webp()
|
||||
.toBuffer(function(err, outputBuffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
// outputBuffer contains WebP image data of a 200 pixels wide and 300 pixels high
|
||||
// containing a scaled version, embedded on a transparent canvas, of input.gif
|
||||
});
|
||||
```
|
||||
|
||||
```javascript
|
||||
sharp(inputBuffer)
|
||||
.resize(200, 200)
|
||||
.max()
|
||||
.jpeg()
|
||||
.toBuffer().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
|
||||
|
||||
### Input methods
|
||||
|
||||
#### sharp([input])
|
||||
|
||||
Constructor to which further methods are chained. `input`, if present, can be one of:
|
||||
|
||||
* Buffer containing JPEG, PNG or WebP image data, or
|
||||
* String containing the filename of an image, with most major formats supported.
|
||||
|
||||
The object returned implements the [stream.Duplex](http://nodejs.org/api/stream.html#stream_class_stream_duplex) class.
|
||||
|
||||
JPEG, PNG or WebP format image data can be streamed into the object when `input` is not provided.
|
||||
|
||||
JPEG, PNG or WebP format image data can be streamed out from this object.
|
||||
|
||||
#### metadata([callback])
|
||||
|
||||
Fast access to image metadata without decoding any compressed image data.
|
||||
|
||||
`callback`, if present, gets the arguments `(err, metadata)` where `metadata` has the attributes:
|
||||
|
||||
* `format`: Name of decoder to be used to decompress image data e.g. `jpeg`, `png`, `webp` (for file-based input additionally `tiff` and `magick`)
|
||||
* `width`: Number of pixels wide
|
||||
* `height`: Number of pixels high
|
||||
* `space`: Name of colour space interpretation e.g. `srgb`, `rgb`, `scrgb`, `cmyk`, `lab`, `xyz`, `b-w` [...](https://github.com/jcupitt/libvips/blob/master/libvips/iofuncs/enumtypes.c#L502)
|
||||
* `channels`: Number of bands e.g. `3` for sRGB, `4` for CMYK
|
||||
* `hasAlpha`: Boolean indicating the presence of an alpha transparency channel
|
||||
* `orientation`: Number value of the EXIF Orientation header, if present
|
||||
|
||||
A Promises/A+ promise is returned when `callback` is not provided.
|
||||
|
||||
#### sequentialRead()
|
||||
|
||||
An advanced setting that switches the libvips access method to `VIPS_ACCESS_SEQUENTIAL`. This will reduce memory usage and can improve performance on some systems.
|
||||
|
||||
### Image transformation options
|
||||
|
||||
#### resize(width, [height])
|
||||
|
||||
Scale output to `width` x `height`. By default, the resized image is cropped to the exact size specified.
|
||||
|
||||
`width` is the Number of pixels wide the resultant image should be. Use `null` or `undefined` to auto-scale the width to match the height.
|
||||
|
||||
`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.
|
||||
|
||||
#### extract(top, left, width, height)
|
||||
|
||||
Extract a region of the image. Can be used with or without a `resize` operation.
|
||||
|
||||
`top` and `left` are the offset, in pixels, from the top-left corner.
|
||||
|
||||
`width` and `height` are the dimensions of the extracted image.
|
||||
|
||||
Use `extract` before `resize` for pre-resize extraction. Use `extract` after `resize` for post-resize extraction. Use `extract` before and after for both.
|
||||
|
||||
#### crop([gravity])
|
||||
|
||||
Crop the resized image to the exact size specified, the default behaviour.
|
||||
|
||||
`gravity`, if present, is an attribute of the `sharp.gravity` Object e.g. `sharp.gravity.north`.
|
||||
|
||||
Possible values are `north`, `east`, `south`, `west`, `center` and `centre`. The default gravity is `center`/`centre`.
|
||||
|
||||
#### max()
|
||||
|
||||
Preserving aspect ratio, resize the image to the maximum `width` or `height` specified.
|
||||
|
||||
Both `width` and `height` must be provided via `resize` otherwise the behaviour will default to `crop`.
|
||||
|
||||
#### background(rgba)
|
||||
|
||||
Set the background for the `embed` and `flatten` operations.
|
||||
|
||||
`rgba` is parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha.
|
||||
|
||||
The alpha value is a float between `0` (transparent) and `1` (opaque).
|
||||
|
||||
The default background is `{r: 0, g: 0, b: 0, a: 1}`, black without transparency.
|
||||
|
||||
#### embed()
|
||||
|
||||
Preserving aspect ratio, resize the image to the maximum `width` or `height` specified then embed on a background of the exact `width` and `height` specified.
|
||||
|
||||
If the background contains an alpha value then WebP and PNG format output images will contain an alpha channel, even when the input image does not.
|
||||
|
||||
#### flatten()
|
||||
|
||||
Merge alpha transparency channel, if any, with `background`.
|
||||
|
||||
#### rotate([angle])
|
||||
|
||||
Rotate the output image by either an explicit angle or auto-orient based on the EXIF `Orientation` tag.
|
||||
|
||||
`angle`, if present, is a Number with a value of `0`, `90`, `180` or `270`.
|
||||
|
||||
Use this method without `angle` to determine the angle from EXIF data. Mirroring is supported and may infer the use of a `flip` operation.
|
||||
|
||||
#### flip()
|
||||
|
||||
Flip the image about the vertical Y axis. This always occurs after rotation, if any.
|
||||
|
||||
#### flop()
|
||||
|
||||
Flop the image about the horizontal X axis. This always occurs after rotation, if any.
|
||||
|
||||
#### withoutEnlargement()
|
||||
|
||||
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 output image. This typically reduces performance by 10%.
|
||||
|
||||
#### interpolateWith(interpolator)
|
||||
|
||||
Use the given interpolator for image resizing, where `interpolator` is an attribute of the `sharp.interpolator` Object e.g. `sharp.interpolator.bicubic`.
|
||||
|
||||
Possible interpolators, in order of performance, are:
|
||||
|
||||
* `nearest`: Use [nearest neighbour interpolation](http://en.wikipedia.org/wiki/Nearest-neighbor_interpolation), suitable for image enlargement only.
|
||||
* `bilinear`: Use [bilinear interpolation](http://en.wikipedia.org/wiki/Bilinear_interpolation), the default and fastest image reduction interpolation.
|
||||
* `bicubic`: Use [bicubic interpolation](http://en.wikipedia.org/wiki/Bicubic_interpolation), which typically reduces performance by 5%.
|
||||
* `vertexSplitQuadraticBasisSpline`: Use [VSQBS interpolation](https://github.com/jcupitt/libvips/blob/master/libvips/resample/vsqbs.cpp#L48), which prevents "staircasing" and typically reduces performance by 5%.
|
||||
* `locallyBoundedBicubic`: Use [LBB interpolation](https://github.com/jcupitt/libvips/blob/master/libvips/resample/lbb.cpp#L100), which prevents some "[acutance](http://en.wikipedia.org/wiki/Acutance)" and typically reduces performance by a factor of 2.
|
||||
* `nohalo`: Use [Nohalo interpolation](http://eprints.soton.ac.uk/268086/), which prevents acutance and typically reduces performance by a factor of 3.
|
||||
|
||||
#### gamma([gamma])
|
||||
|
||||
Apply a gamma correction by reducing the encoding (darken) pre-resize at a factor of `1/gamma` then increasing the encoding (brighten) post-resize at a factor of `gamma`.
|
||||
|
||||
`gamma`, if present, is a Number betweem 1 and 3. The default value is `2.2`, a suitable approximation for sRGB images.
|
||||
|
||||
This can improve the perceived brightness of a resized image in non-linear colour spaces.
|
||||
|
||||
JPEG input images will not take advantage of the shrink-on-load performance optimisation when applying a gamma correction.
|
||||
|
||||
#### grayscale() / greyscale()
|
||||
|
||||
Convert to 8-bit greyscale; 256 shades of grey.
|
||||
|
||||
This is a linear operation. If the input image is in a non-linear colour space such as sRGB, use `gamma()` with `greyscale()` for the best results.
|
||||
|
||||
The output image will still be web-friendly sRGB and contain three (identical) channels.
|
||||
|
||||
### Output options
|
||||
|
||||
#### jpeg()
|
||||
|
||||
Use JPEG format for the output image.
|
||||
|
||||
#### png()
|
||||
|
||||
Use PNG format for the output image.
|
||||
|
||||
#### webp()
|
||||
|
||||
Use WebP format for the output image.
|
||||
|
||||
#### 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.
|
||||
|
||||
#### 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.
|
||||
|
||||
#### withMetadata()
|
||||
|
||||
Include all metadata (ICC, EXIF, XMP) from the input image in the output image. The default behaviour is to strip all metadata.
|
||||
|
||||
#### 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 0 and 9.
|
||||
|
||||
### Output methods
|
||||
|
||||
#### toFile(filename, [callback])
|
||||
|
||||
`filename` is a String containing the filename to write the image data to. The format is inferred from the extension, with JPEG, PNG, WebP and TIFF supported.
|
||||
|
||||
`callback`, if present, is called with two arguments `(err, info)` where:
|
||||
|
||||
* `err` contains an error message, if any.
|
||||
* `info` contains the output image `format`, `width` and `height`.
|
||||
|
||||
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 by default. JPEG, PNG and WebP are supported.
|
||||
|
||||
`callback`, if present, gets three arguments `(err, buffer, info)` where:
|
||||
|
||||
* `err` is an error message, if any.
|
||||
* `buffer` is the output image data.
|
||||
* `info` contains the output image `format`, `width` and `height`.
|
||||
|
||||
A Promises/A+ promise is returned when `callback` is not provided.
|
||||
|
||||
### Utility methods
|
||||
|
||||
#### sharp.cache([memory], [items])
|
||||
|
||||
If `memory` or `items` are provided, set the limits of _libvips'_ operation cache.
|
||||
|
||||
* `memory` is the maximum memory in MB to use for this cache, with a default value of 100
|
||||
* `items` is the maximum number of operations to cache, with a default value of 500
|
||||
|
||||
This method always returns cache statistics, useful for determining how much working memory is required for a particular task.
|
||||
|
||||
```javascript
|
||||
var stats = sharp.cache(); // { current: 75, high: 99, memory: 100, items: 500 }
|
||||
sharp.cache(200); // { current: 75, high: 99, memory: 200, items: 500 }
|
||||
sharp.cache(50, 200); // { current: 49, high: 99, memory: 50, items: 200}
|
||||
```
|
||||
|
||||
#### sharp.concurrency([threads])
|
||||
|
||||
`threads`, if provided, is the Number of threads _libvips'_ should create for image processing. The default value is the number of CPU cores. A value of `0` will reset to this default.
|
||||
|
||||
This method always returns the current concurrency.
|
||||
|
||||
```javascript
|
||||
var threads = sharp.concurrency(); // 4
|
||||
sharp.concurrency(2); // 2
|
||||
sharp.concurrency(0); // 4
|
||||
```
|
||||
|
||||
#### sharp.counters()
|
||||
|
||||
Provides access to internal task counters.
|
||||
|
||||
* `queue` is the number of tasks this module has queued waiting for _libuv_ to provide a worker thread from its pool.
|
||||
* `process` is the number of resize tasks currently being processed.
|
||||
|
||||
```javascript
|
||||
var counters = sharp.counters(); // { queue: 2, process: 4 }
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
npm test
|
||||
### Functional tests
|
||||
|
||||
#### Coverage
|
||||
|
||||
[](https://coveralls.io/r/lovell/sharp?branch=master)
|
||||
|
||||
#### Ubuntu 12.04
|
||||
|
||||
[](https://travis-ci.org/lovell/sharp)
|
||||
|
||||
#### Centos 6.5
|
||||
|
||||
[](https://snap-ci.com/lovell/sharp/branch/master)
|
||||
|
||||
#### It worked on my machine
|
||||
|
||||
```
|
||||
npm test
|
||||
```
|
||||
|
||||
### Memory leak tests
|
||||
|
||||
```
|
||||
cd sharp/test/leak
|
||||
./leak.sh
|
||||
```
|
||||
|
||||
Requires _valgrind_:
|
||||
|
||||
```
|
||||
brew install valgrind
|
||||
```
|
||||
|
||||
```
|
||||
sudo apt-get install -qq valgrind
|
||||
```
|
||||
|
||||
### Benchmark tests
|
||||
|
||||
```
|
||||
cd sharp/test/bench
|
||||
npm install
|
||||
npm test
|
||||
```
|
||||
|
||||
Requires both _ImageMagick_ and _GraphicsMagick_:
|
||||
|
||||
```
|
||||
brew install imagemagick
|
||||
brew install graphicsmagick
|
||||
```
|
||||
|
||||
```
|
||||
sudo apt-get install -qq imagemagick graphicsmagick libmagick++-dev
|
||||
```
|
||||
|
||||
```
|
||||
sudo yum install ImageMagick
|
||||
sudo yum install -y http://download.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm
|
||||
sudo yum install -y --enablerepo=epel GraphicsMagick
|
||||
```
|
||||
|
||||
## 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
|
||||
* AWS EC2 [c3.xlarge](http://aws.amazon.com/ec2/instance-types/#Compute_Optimized)
|
||||
* Ubuntu 14.04
|
||||
* libvips 7.40.8
|
||||
* liborc 0.4.22
|
||||
|
||||
`-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) v1.2.2 - Supports Buffers only and blocks main V8 thread whilst processing.
|
||||
* [imagemagick](https://github.com/yourdeveloper/node-imagemagick) v0.1.3 - Supports filesystem only and "has been unmaintained for a long time".
|
||||
* [gm](https://github.com/aheckmann/gm) v1.16.0 - Fully featured wrapper around GraphicsMagick.
|
||||
* sharp v0.6.2 - 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 | 1.58 | 1 |
|
||||
| imagemagick | file | file | 6.23 | 3.9 |
|
||||
| gm | buffer | file | 5.32 | 3.4 |
|
||||
| gm | buffer | buffer | 5.32 | 3.4 |
|
||||
| gm | file | file | 5.36 | 3.4 |
|
||||
| gm | file | buffer | 5.36 | 3.4 |
|
||||
| sharp | buffer | file | 22.05 | 14.0 |
|
||||
| sharp | buffer | buffer | 22.14 | 14.0 |
|
||||
| sharp | file | file | 21.79 | 13.8 |
|
||||
| sharp | file | buffer | 21.90 | 13.9 |
|
||||
| sharp | stream | stream | 20.87 | 13.2 |
|
||||
| sharp +promise | file | buffer | 21.89 | 13.9 |
|
||||
| sharp +sharpen | file | buffer | 19.69 | 12.5 |
|
||||
| sharp +progressive | file | buffer | 16.93 | 10.7 |
|
||||
| sharp +sequentialRead | file | buffer | 21.60 | 13.7 |
|
||||
|
||||
### PNG
|
||||
You can expect greater performance with caching enabled (default) and using 8+ core machines.
|
||||
|
||||
* 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)
|
||||
## Thanks
|
||||
|
||||
* 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)
|
||||
This module would never have been possible without the help and code contributions of the following people:
|
||||
|
||||
* [John Cupitt](https://github.com/jcupitt)
|
||||
* [Pierre Inglebert](https://github.com/pierreinglebert)
|
||||
* [Jonathan Ong](https://github.com/jonathanong)
|
||||
* [Chanon Sajjamanochai](https://github.com/chanon)
|
||||
* [Juliano Julio](https://github.com/julianojulio)
|
||||
* [Daniel Gasienica](https://github.com/gasi)
|
||||
* [Julian Walker](https://github.com/julianwa)
|
||||
* [Amit Pitaru](https://github.com/apitaru)
|
||||
* [Brandon Aaron](https://github.com/brandonaaron)
|
||||
* [Andreas Lind](https://github.com/papandreou)
|
||||
|
||||
Thank you!
|
||||
|
||||
## Licence
|
||||
|
||||
Copyright 2013, 2014 Lovell Fuller
|
||||
Copyright 2013, 2014 Lovell Fuller and contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
39
binding.gyp
@@ -1,19 +1,38 @@
|
||||
{
|
||||
'targets': [{
|
||||
'target_name': 'sharp',
|
||||
'sources': ['src/sharp.cc'],
|
||||
'sources': [
|
||||
'src/common.cc',
|
||||
'src/utilities.cc',
|
||||
'src/metadata.cc',
|
||||
'src/resize.cc',
|
||||
'src/sharp.cc'
|
||||
],
|
||||
'variables': {
|
||||
'PKG_CONFIG_PATH': '<!(which brew >/dev/null 2>&1 && eval $(brew --env) && echo $PKG_CONFIG_LIBDIR || true):$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig:/usr/lib/pkgconfig'
|
||||
},
|
||||
'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/local/include/glib-2.0',
|
||||
'/usr/local/lib/glib-2.0/include',
|
||||
'/usr/include/glib-2.0',
|
||||
'/usr/lib/glib-2.0/include',
|
||||
'/usr/lib/x86_64-linux-gnu/glib-2.0/include'
|
||||
'<!(PKG_CONFIG_PATH="<(PKG_CONFIG_PATH)" pkg-config --cflags vips glib-2.0)',
|
||||
'<!(node -e "require(\'nan\')")'
|
||||
],
|
||||
'cflags': ['-fexceptions', '-pedantic', '-Wall', '-O3'],
|
||||
'cflags_cc': ['-fexceptions', '-pedantic', '-Wall', '-O3']
|
||||
'cflags_cc': [
|
||||
'-std=c++0x',
|
||||
'-fexceptions',
|
||||
'-Wall',
|
||||
'-O3'
|
||||
],
|
||||
'xcode_settings': {
|
||||
'OTHER_CPLUSPLUSFLAGS': [
|
||||
'-std=c++11',
|
||||
'-stdlib=libc++',
|
||||
'-fexceptions',
|
||||
'-Wall',
|
||||
'-O3'
|
||||
],
|
||||
'MACOSX_DEPLOYMENT_TARGET': '10.7'
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
569
index.js
@@ -1,64 +1,505 @@
|
||||
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);
|
||||
};
|
||||
'use strict';
|
||||
|
||||
var util = require('util');
|
||||
var stream = require('stream');
|
||||
|
||||
var color = require('color');
|
||||
var BluebirdPromise = require('bluebird');
|
||||
|
||||
var sharp = require('./build/Release/sharp');
|
||||
|
||||
var Sharp = function(input) {
|
||||
if (!(this instanceof Sharp)) {
|
||||
return new Sharp(input);
|
||||
}
|
||||
stream.Duplex.call(this);
|
||||
this.options = {
|
||||
// input options
|
||||
streamIn: false,
|
||||
sequentialRead: false,
|
||||
// resize options
|
||||
topOffsetPre: -1,
|
||||
leftOffsetPre: -1,
|
||||
widthPre: -1,
|
||||
heightPre: -1,
|
||||
topOffsetPost: -1,
|
||||
leftOffsetPost: -1,
|
||||
widthPost: -1,
|
||||
heightPost: -1,
|
||||
width: -1,
|
||||
height: -1,
|
||||
canvas: 'c',
|
||||
gravity: 0,
|
||||
angle: 0,
|
||||
flip: false,
|
||||
flop: false,
|
||||
withoutEnlargement: false,
|
||||
interpolator: 'bilinear',
|
||||
// operations
|
||||
background: [0, 0, 0, 255],
|
||||
flatten: false,
|
||||
sharpen: false,
|
||||
gamma: 0,
|
||||
greyscale: false,
|
||||
// output options
|
||||
output: '__input',
|
||||
progressive: false,
|
||||
quality: 80,
|
||||
compressionLevel: 6,
|
||||
streamOut: false,
|
||||
withMetadata: false
|
||||
};
|
||||
if (typeof input === 'string') {
|
||||
// input=file
|
||||
this.options.fileIn = input;
|
||||
} else if (typeof input === 'object' && input instanceof Buffer) {
|
||||
// input=buffer
|
||||
if (
|
||||
(input.length > 1) &&
|
||||
(input[0] === 0xff && input[1] === 0xd8) || // JPEG
|
||||
(input[0] === 0x89 && input[1] === 0x50) || // PNG
|
||||
(input[0] === 0x52 && input[1] === 0x49) // WebP
|
||||
) {
|
||||
this.options.bufferIn = input;
|
||||
} else {
|
||||
throw new Error('Buffer contains an unsupported image format. JPEG, PNG and WebP are currently supported.');
|
||||
}
|
||||
} else {
|
||||
// input=stream
|
||||
this.options.streamIn = true;
|
||||
}
|
||||
return this;
|
||||
};
|
||||
module.exports = Sharp;
|
||||
util.inherits(Sharp, stream.Duplex);
|
||||
|
||||
/*
|
||||
Handle incoming chunk on Writable Stream
|
||||
*/
|
||||
Sharp.prototype._write = function(chunk, encoding, callback) {
|
||||
/*jslint unused: false */
|
||||
if (this.options.streamIn) {
|
||||
if (typeof chunk === 'object' && chunk instanceof Buffer) {
|
||||
if (typeof this.options.bufferIn === 'undefined') {
|
||||
// Create new Buffer
|
||||
this.options.bufferIn = new Buffer(chunk.length);
|
||||
chunk.copy(this.options.bufferIn);
|
||||
} else {
|
||||
// Append to existing Buffer
|
||||
this.options.bufferIn = Buffer.concat(
|
||||
[this.options.bufferIn, chunk],
|
||||
this.options.bufferIn.length + chunk.length
|
||||
);
|
||||
}
|
||||
callback();
|
||||
} else {
|
||||
callback(new Error('Non-Buffer data on Writable Stream'));
|
||||
}
|
||||
} else {
|
||||
callback(new Error('Unexpected data on Writable Stream'));
|
||||
}
|
||||
};
|
||||
|
||||
// Crop this part of the resized image (Center/Centre, North, East, South, West)
|
||||
module.exports.gravity = {'center': 0, 'centre': 0, 'north': 1, 'east': 2, 'south': 3, 'west': 4};
|
||||
|
||||
Sharp.prototype.crop = function(gravity) {
|
||||
this.options.canvas = 'c';
|
||||
if (typeof gravity === 'number' && !Number.isNaN(gravity) && gravity >= 0 && gravity <= 4) {
|
||||
this.options.gravity = gravity;
|
||||
} else {
|
||||
throw new Error('Unsupported crop gravity ' + gravity);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
Sharp.prototype.extract = function(topOffset, leftOffset, width, height) {
|
||||
/*jslint unused: false */
|
||||
var suffix = this.options.width === -1 && this.options.height === -1 ? 'Pre' : 'Post';
|
||||
var values = arguments;
|
||||
['topOffset', 'leftOffset', 'width', 'height'].forEach(function(name, index) {
|
||||
this.options[name + suffix] = values[index];
|
||||
}.bind(this));
|
||||
return this;
|
||||
};
|
||||
|
||||
/*
|
||||
Deprecated embed* methods, to be removed in v0.8.0
|
||||
*/
|
||||
Sharp.prototype.embedWhite = util.deprecate(function() {
|
||||
return this.background('white').embed();
|
||||
}, "embedWhite() is deprecated, use background('white').embed() instead");
|
||||
Sharp.prototype.embedBlack = util.deprecate(function() {
|
||||
return this.background('black').embed();
|
||||
}, "embedBlack() is deprecated, use background('black').embed() instead");
|
||||
|
||||
/*
|
||||
Set the background colour for embed and flatten operations.
|
||||
Delegates to the 'Color' module, which can throw an Error
|
||||
but is liberal in what it accepts, clamping values to sensible min/max.
|
||||
*/
|
||||
Sharp.prototype.background = function(rgba) {
|
||||
var colour = color(rgba);
|
||||
this.options.background = colour.rgbArray();
|
||||
this.options.background.push(colour.alpha() * 255);
|
||||
return this;
|
||||
};
|
||||
|
||||
Sharp.prototype.embed = function() {
|
||||
this.options.canvas = 'e';
|
||||
return this;
|
||||
};
|
||||
|
||||
Sharp.prototype.max = function() {
|
||||
this.options.canvas = 'm';
|
||||
return this;
|
||||
};
|
||||
|
||||
Sharp.prototype.flatten = function(flatten) {
|
||||
this.options.flatten = (typeof flatten === 'boolean') ? flatten : true;
|
||||
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;
|
||||
};
|
||||
|
||||
/*
|
||||
Flip the image vertically, about the Y axis
|
||||
*/
|
||||
Sharp.prototype.flip = function(flip) {
|
||||
this.options.flip = (typeof flip === 'boolean') ? flip : true;
|
||||
return this;
|
||||
};
|
||||
|
||||
/*
|
||||
Flop the image horizontally, about the X axis
|
||||
*/
|
||||
Sharp.prototype.flop = function(flop) {
|
||||
this.options.flop = (typeof flop === 'boolean') ? flop : true;
|
||||
return this;
|
||||
};
|
||||
|
||||
/*
|
||||
Do not enlarge the output if the input width *or* height are already less than the required dimensions
|
||||
This is equivalent to GraphicsMagick's ">" geometry option:
|
||||
"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;
|
||||
};
|
||||
|
||||
/*
|
||||
Set the interpolator to use for the affine transformation
|
||||
*/
|
||||
module.exports.interpolator = {
|
||||
nearest: 'nearest',
|
||||
bilinear: 'bilinear',
|
||||
bicubic: 'bicubic',
|
||||
nohalo: 'nohalo',
|
||||
locallyBoundedBicubic: 'lbb',
|
||||
vertexSplitQuadraticBasisSpline: 'vsqbs'
|
||||
};
|
||||
Sharp.prototype.interpolateWith = function(interpolator) {
|
||||
this.options.interpolator = interpolator;
|
||||
return this;
|
||||
};
|
||||
|
||||
/*
|
||||
Darken image pre-resize (1/gamma) and brighten post-resize (gamma).
|
||||
Improves brightness of resized image in non-linear colour spaces.
|
||||
*/
|
||||
Sharp.prototype.gamma = function(gamma) {
|
||||
if (typeof gamma === 'undefined') {
|
||||
// Default gamma correction of 2.2 (sRGB)
|
||||
this.options.gamma = 2.2;
|
||||
} else if (!Number.isNaN(gamma) && gamma >= 1 && gamma <= 3) {
|
||||
this.options.gamma = gamma;
|
||||
} else {
|
||||
throw new Error('Invalid gamma correction (1.0 to 3.0) ' + gamma);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
/*
|
||||
Convert to greyscale
|
||||
*/
|
||||
Sharp.prototype.greyscale = function(greyscale) {
|
||||
this.options.greyscale = (typeof greyscale === 'boolean') ? greyscale : true;
|
||||
return this;
|
||||
};
|
||||
Sharp.prototype.grayscale = Sharp.prototype.greyscale;
|
||||
|
||||
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 >= 0 && compressionLevel <= 9) {
|
||||
this.options.compressionLevel = compressionLevel;
|
||||
} else {
|
||||
throw new Error('Invalid compressionLevel (0 to 9) ' + compressionLevel);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
Sharp.prototype.withMetadata = function(withMetadata) {
|
||||
this.options.withMetadata = (typeof withMetadata === 'boolean') ? withMetadata : true;
|
||||
return this;
|
||||
};
|
||||
|
||||
Sharp.prototype.resize = function(width, height) {
|
||||
if (!width) {
|
||||
this.options.width = -1;
|
||||
} else {
|
||||
if (typeof width === 'number' && !Number.isNaN(width)) {
|
||||
this.options.width = width;
|
||||
} else {
|
||||
throw new Error('Invalid width ' + width);
|
||||
}
|
||||
}
|
||||
if (!height) {
|
||||
this.options.height = -1;
|
||||
} else {
|
||||
if (typeof height === 'number' && !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 BluebirdPromise.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 BluebirdPromise.reject(errOutputIsInput);
|
||||
}
|
||||
} else {
|
||||
this.options.output = output;
|
||||
return this._sharp(callback);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
Sharp.prototype.toBuffer = function(callback) {
|
||||
return this._sharp(callback);
|
||||
};
|
||||
|
||||
Sharp.prototype.jpeg = function() {
|
||||
this.options.output = '__jpeg';
|
||||
return this;
|
||||
};
|
||||
|
||||
Sharp.prototype.png = function() {
|
||||
this.options.output = '__png';
|
||||
return this;
|
||||
};
|
||||
|
||||
Sharp.prototype.webp = function() {
|
||||
this.options.output = '__webp';
|
||||
return this;
|
||||
};
|
||||
|
||||
/*
|
||||
Used by a Writable Stream to notify that it is ready for data
|
||||
*/
|
||||
Sharp.prototype._read = function() {
|
||||
if (!this.options.streamOut) {
|
||||
this.options.streamOut = true;
|
||||
this._sharp();
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
Invoke the C++ image processing pipeline
|
||||
Supports callback, stream and promise variants
|
||||
*/
|
||||
Sharp.prototype._sharp = function(callback) {
|
||||
var that = this;
|
||||
if (typeof callback === 'function') {
|
||||
// output=file/buffer
|
||||
if (this.options.streamIn) {
|
||||
// output=file/buffer, input=stream
|
||||
this.on('finish', function() {
|
||||
sharp.resize(that.options, callback);
|
||||
});
|
||||
} else {
|
||||
// output=file/buffer, input=file/buffer
|
||||
sharp.resize(this.options, callback);
|
||||
}
|
||||
return this;
|
||||
} else if (this.options.streamOut) {
|
||||
// output=stream
|
||||
if (this.options.streamIn) {
|
||||
// output=stream, input=stream
|
||||
this.on('finish', function() {
|
||||
sharp.resize(that.options, function(err, data) {
|
||||
if (err) {
|
||||
that.emit('error', new Error(err));
|
||||
} else {
|
||||
that.push(data);
|
||||
}
|
||||
that.push(null);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// output=stream, input=file/buffer
|
||||
sharp.resize(this.options, function(err, data) {
|
||||
if (err) {
|
||||
that.emit('error', new Error(err));
|
||||
} else {
|
||||
that.push(data);
|
||||
}
|
||||
that.push(null);
|
||||
});
|
||||
}
|
||||
return this;
|
||||
} else {
|
||||
// output=promise
|
||||
if (this.options.streamIn) {
|
||||
// output=promise, input=stream
|
||||
return new BluebirdPromise(function(resolve, reject) {
|
||||
that.on('finish', function() {
|
||||
sharp.resize(that.options, function(err, data) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// output=promise, input=file/buffer
|
||||
return new BluebirdPromise(function(resolve, reject) {
|
||||
sharp.resize(that.options, function(err, data) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
Reads the image header and returns metadata
|
||||
Supports callback, stream and promise variants
|
||||
*/
|
||||
Sharp.prototype.metadata = function(callback) {
|
||||
var that = this;
|
||||
if (typeof callback === 'function') {
|
||||
if (this.options.streamIn) {
|
||||
this.on('finish', function() {
|
||||
sharp.metadata(that.options, callback);
|
||||
});
|
||||
} else {
|
||||
sharp.metadata(this.options, callback);
|
||||
}
|
||||
return this;
|
||||
} else {
|
||||
if (this.options.streamIn) {
|
||||
return new BluebirdPromise(function(resolve, reject) {
|
||||
that.on('finish', function() {
|
||||
sharp.metadata(that.options, function(err, data) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return new BluebirdPromise(function(resolve, reject) {
|
||||
sharp.metadata(that.options, function(err, data) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
Get and set cache memory and item limits
|
||||
*/
|
||||
module.exports.cache = function(memory, items) {
|
||||
if (typeof memory !== 'number' || Number.isNaN(memory)) {
|
||||
memory = null;
|
||||
}
|
||||
if (typeof items !== 'number' || Number.isNaN(items)) {
|
||||
items = null;
|
||||
}
|
||||
return sharp.cache(memory, items);
|
||||
};
|
||||
|
||||
/*
|
||||
Get and set size of thread pool
|
||||
*/
|
||||
module.exports.concurrency = function(concurrency) {
|
||||
if (typeof concurrency !== 'number' || Number.isNaN(concurrency)) {
|
||||
concurrency = null;
|
||||
}
|
||||
return sharp.concurrency(concurrency);
|
||||
};
|
||||
|
||||
/*
|
||||
Get internal counters
|
||||
*/
|
||||
module.exports.counters = function() {
|
||||
return sharp.counters();
|
||||
};
|
||||
|
||||
45
package.json
@@ -1,10 +1,21 @@
|
||||
{
|
||||
"name": "sharp",
|
||||
"version": "0.1.5",
|
||||
"author": "Lovell Fuller",
|
||||
"description": "High performance module to resize JPEG and PNG images using the libvips image processing library",
|
||||
"version": "0.7.1",
|
||||
"author": "Lovell Fuller <npm@lovell.info>",
|
||||
"contributors": [
|
||||
"Pierre Inglebert <pierre.inglebert@gmail.com>",
|
||||
"Jonathan Ong <jonathanrichardong@gmail.com>",
|
||||
"Chanon Sajjamanochai <chanon.s@gmail.com>",
|
||||
"Juliano Julio <julianojulio@gmail.com>",
|
||||
"Daniel Gasienica <daniel@gasienica.ch>",
|
||||
"Julian Walker <julian@fiftythree.com>",
|
||||
"Amit Pitaru <pitaru.amit@gmail.com>",
|
||||
"Brandon Aaron <hello.brandon@aaron.sh>",
|
||||
"Andreas Lind <andreas@one.com>"
|
||||
],
|
||||
"description": "High performance Node.js module to resize JPEG, PNG and WebP images using the libvips library",
|
||||
"scripts": {
|
||||
"test": "node tests/perf.js"
|
||||
"test": "node ./node_modules/istanbul/lib/cli.js cover ./node_modules/mocha/bin/_mocha -- --slow=5000 --timeout=10000 ./test/unit/*.js"
|
||||
},
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
@@ -14,24 +25,34 @@
|
||||
"keywords": [
|
||||
"jpeg",
|
||||
"png",
|
||||
"webp",
|
||||
"tiff",
|
||||
"gif",
|
||||
"resize",
|
||||
"thumbnail",
|
||||
"sharpen",
|
||||
"crop",
|
||||
"extract",
|
||||
"embed",
|
||||
"libvips",
|
||||
"vips",
|
||||
"fast",
|
||||
"buffer"
|
||||
"buffer",
|
||||
"stream"
|
||||
],
|
||||
"dependencies": {
|
||||
"bluebird": "^2.3.9",
|
||||
"color": "^0.7.1",
|
||||
"nan": "^1.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"imagemagick": "*",
|
||||
"gm": "*",
|
||||
"epeg": "*",
|
||||
"async": "*",
|
||||
"benchmark": "*"
|
||||
"mocha": "^2.0.1",
|
||||
"mocha-jshint": "^0.0.9",
|
||||
"istanbul": "^0.3.2",
|
||||
"coveralls": "^2.11.2"
|
||||
},
|
||||
"license": "Apache 2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
"node": ">=0.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
145
preinstall.sh
Executable file
@@ -0,0 +1,145 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Ensures libvips is installed and attempts to install it if not
|
||||
# Currently supports:
|
||||
# * Mac OS
|
||||
# * Debian Linux
|
||||
# * Debian 7, 8
|
||||
# * Ubuntu 12.04, 14.04, 14.10
|
||||
# * Mint 13, 17
|
||||
# * Red Hat Linux
|
||||
# * RHEL/Centos/Scientific 6, 7
|
||||
# * Fedora 21, 22
|
||||
|
||||
vips_version_minimum=7.38.5
|
||||
vips_version_latest_major=7.40
|
||||
vips_version_latest_minor=11
|
||||
|
||||
install_libvips_from_source() {
|
||||
echo "Compiling libvips $vips_version_latest_major.$vips_version_latest_minor from source"
|
||||
curl -O http://www.vips.ecs.soton.ac.uk/supported/$vips_version_latest_major/vips-$vips_version_latest_major.$vips_version_latest_minor.tar.gz
|
||||
tar zvxf vips-$vips_version_latest_major.$vips_version_latest_minor.tar.gz
|
||||
cd vips-$vips_version_latest_major.$vips_version_latest_minor
|
||||
./configure --enable-debug=no --enable-docs=no --enable-cxx=yes --without-python --without-orc --without-fftw $1
|
||||
make
|
||||
make install
|
||||
cd ..
|
||||
rm -rf vips-$vips_version_latest_major.$vips_version_latest_minor
|
||||
rm vips-$vips_version_latest_major.$vips_version_latest_minor.tar.gz
|
||||
ldconfig
|
||||
echo "Installed libvips $vips_version_latest_major.$vips_version_latest_minor"
|
||||
}
|
||||
|
||||
sorry() {
|
||||
echo "Sorry, I don't yet know how to install libvips on $1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Is libvips already installed, and is it at least the minimum required version?
|
||||
|
||||
if ! type pkg-config >/dev/null; then
|
||||
sorry "a system without pkg-config"
|
||||
fi
|
||||
|
||||
pkg_config_path_homebrew=`which brew >/dev/null 2>&1 && eval $(brew --env) && echo $PKG_CONFIG_LIBDIR || true`
|
||||
pkg_config_path="$pkg_config_path_homebrew:$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig:/usr/lib/pkgconfig"
|
||||
|
||||
PKG_CONFIG_PATH=$pkg_config_path pkg-config --exists vips
|
||||
if [ $? -eq 0 ]; then
|
||||
vips_version_found=$(PKG_CONFIG_PATH=$pkg_config_path pkg-config --modversion vips)
|
||||
pkg-config --atleast-version=$vips_version_minimum vips
|
||||
if [ $? -eq 0 ]; then
|
||||
# Found suitable version of libvips
|
||||
echo "Found libvips $vips_version_found"
|
||||
exit 0
|
||||
fi
|
||||
echo "Found libvips $vips_version_found but require $vips_version_minimum"
|
||||
else
|
||||
echo "Could not find libvips using a PKG_CONFIG_PATH of '$pkg_config_path'"
|
||||
fi
|
||||
|
||||
# Verify root/sudo access
|
||||
if [ "$(id -u)" -ne "0" ]; then
|
||||
echo "Sorry, I need root/sudo access to continue"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# OS-specific installations of libvips follows
|
||||
|
||||
case $(uname -s) in
|
||||
*[Dd]arwin*)
|
||||
# Mac OS
|
||||
echo "Detected Mac OS"
|
||||
if type "brew" > /dev/null; then
|
||||
echo "Installing libvips via homebrew"
|
||||
brew install homebrew/science/vips --with-webp --with-graphicsmagick
|
||||
elif type "port" > /dev/null; then
|
||||
echo "Installing libvips via MacPorts"
|
||||
port install vips
|
||||
else
|
||||
sorry "Mac OS without homebrew or MacPorts"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
if [ -f /etc/debian_version ]; then
|
||||
# Debian Linux
|
||||
DISTRO=$(lsb_release -c -s)
|
||||
echo "Detected Debian Linux '$DISTRO'"
|
||||
case "$DISTRO" in
|
||||
jessie|trusty|utopic|qiana)
|
||||
# Debian 8, Ubuntu 14, Mint 17
|
||||
echo "Installing libvips via apt-get"
|
||||
apt-get install -y libvips-dev
|
||||
;;
|
||||
precise|wheezy|maya)
|
||||
# Debian 7, Ubuntu 12.04, Mint 13
|
||||
echo "Installing libvips dependencies via apt-get"
|
||||
add-apt-repository -y ppa:lyrasis/precise-backports
|
||||
apt-get update
|
||||
apt-get install -y automake build-essential gobject-introspection gtk-doc-tools libglib2.0-dev libjpeg-turbo8-dev libpng12-dev libwebp-dev libtiff4-dev libexif-dev libxml2-dev swig libmagickwand-dev curl
|
||||
install_libvips_from_source
|
||||
;;
|
||||
*)
|
||||
# Unsupported Debian-based OS
|
||||
sorry "Debian-based $DISTRO"
|
||||
;;
|
||||
esac
|
||||
elif [ -f /etc/redhat-release ]; then
|
||||
# Red Hat Linux
|
||||
RELEASE=$(cat /etc/redhat-release)
|
||||
echo "Detected Red Hat Linux '$RELEASE'"
|
||||
case $RELEASE in
|
||||
"Red Hat Enterprise Linux release 7."*|"CentOS Linux release 7."*|"Scientific Linux release 7."*)
|
||||
# RHEL/CentOS 7
|
||||
echo "Installing libvips dependencies via yum"
|
||||
yum groupinstall -y "Development Tools"
|
||||
yum install -y gtk-doc libxml2-devel libjpeg-turbo-devel libpng-devel libtiff-devel libexif-devel ImageMagick-devel gobject-introspection-devel libwebp-devel curl
|
||||
install_libvips_from_source "--prefix=/usr"
|
||||
;;
|
||||
"Red Hat Enterprise Linux release 6."*|"CentOS release 6."*|"Scientific Linux release 6."*)
|
||||
# RHEL/CentOS 6
|
||||
echo "Installing libvips dependencies via yum"
|
||||
yum groupinstall -y "Development Tools"
|
||||
yum install -y gtk-doc libxml2-devel libjpeg-turbo-devel libpng-devel libtiff-devel libexif-devel ImageMagick-devel curl
|
||||
yum install -y http://li.nux.ro/download/nux/dextop/el6/x86_64/nux-dextop-release-0-2.el6.nux.noarch.rpm
|
||||
yum install -y --enablerepo=nux-dextop gobject-introspection-devel
|
||||
yum install -y http://rpms.famillecollet.com/enterprise/remi-release-6.rpm
|
||||
yum install -y --enablerepo=remi libwebp-devel
|
||||
install_libvips_from_source "--prefix=/usr"
|
||||
;;
|
||||
"Fedora release 21 "*|"Fedora release 22 "*)
|
||||
# Fedora 21, 22
|
||||
echo "Installing libvips via yum"
|
||||
yum install vips-devel
|
||||
;;
|
||||
*)
|
||||
# Unsupported RHEL-based OS
|
||||
sorry "$RELEASE"
|
||||
;;
|
||||
esac
|
||||
else
|
||||
# Unsupported OS
|
||||
sorry "$(uname -a)"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
99
src/common.cc
Executable file
@@ -0,0 +1,99 @@
|
||||
#include <string>
|
||||
#include <string.h>
|
||||
#include <vips/vips.h>
|
||||
|
||||
#include "common.h"
|
||||
|
||||
// How many tasks are in the queue?
|
||||
volatile int counter_queue = 0;
|
||||
|
||||
// How many tasks are being processed?
|
||||
volatile int counter_process = 0;
|
||||
|
||||
// Filename extension checkers
|
||||
static bool ends_with(std::string const &str, std::string const &end) {
|
||||
return str.length() >= end.length() && 0 == str.compare(str.length() - end.length(), end.length(), end);
|
||||
}
|
||||
bool is_jpeg(std::string const &str) {
|
||||
return ends_with(str, ".jpg") || ends_with(str, ".jpeg") || ends_with(str, ".JPG") || ends_with(str, ".JPEG");
|
||||
}
|
||||
bool is_png(std::string const &str) {
|
||||
return ends_with(str, ".png") || ends_with(str, ".PNG");
|
||||
}
|
||||
bool is_webp(std::string const &str) {
|
||||
return ends_with(str, ".webp") || ends_with(str, ".WEBP");
|
||||
}
|
||||
bool is_tiff(std::string const &str) {
|
||||
return ends_with(str, ".tif") || ends_with(str, ".tiff") || ends_with(str, ".TIF") || ends_with(str, ".TIFF");
|
||||
}
|
||||
|
||||
unsigned char const MARKER_JPEG[] = {0xff, 0xd8};
|
||||
unsigned char const MARKER_PNG[] = {0x89, 0x50};
|
||||
unsigned char const MARKER_WEBP[] = {0x52, 0x49};
|
||||
|
||||
/*
|
||||
Initialise a VipsImage from a buffer. Supports JPEG, PNG and WebP.
|
||||
Returns the ImageType detected, if any.
|
||||
*/
|
||||
ImageType
|
||||
sharp_init_image_from_buffer(VipsImage **image, void *buffer, size_t const length, VipsAccess const access) {
|
||||
ImageType imageType = UNKNOWN;
|
||||
if (memcmp(MARKER_JPEG, buffer, 2) == 0) {
|
||||
if (!vips_jpegload_buffer(buffer, length, image, "access", access, NULL)) {
|
||||
imageType = JPEG;
|
||||
}
|
||||
} else if(memcmp(MARKER_PNG, buffer, 2) == 0) {
|
||||
if (!vips_pngload_buffer(buffer, length, image, "access", access, NULL)) {
|
||||
imageType = PNG;
|
||||
}
|
||||
} else if(memcmp(MARKER_WEBP, buffer, 2) == 0) {
|
||||
if (!vips_webpload_buffer(buffer, length, image, "access", access, NULL)) {
|
||||
imageType = WEBP;
|
||||
}
|
||||
}
|
||||
return imageType;
|
||||
}
|
||||
|
||||
/*
|
||||
Initialise a VipsImage from a file.
|
||||
Returns the ImageType detected, if any.
|
||||
*/
|
||||
ImageType
|
||||
sharp_init_image_from_file(VipsImage **image, char const *file, VipsAccess const access) {
|
||||
ImageType imageType = UNKNOWN;
|
||||
if (vips_foreign_is_a("jpegload", file)) {
|
||||
if (!vips_jpegload(file, image, "access", access, NULL)) {
|
||||
imageType = JPEG;
|
||||
}
|
||||
} else if (vips_foreign_is_a("pngload", file)) {
|
||||
if (!vips_pngload(file, image, "access", access, NULL)) {
|
||||
imageType = PNG;
|
||||
}
|
||||
} else if (vips_foreign_is_a("webpload", file)) {
|
||||
if (!vips_webpload(file, image, "access", access, NULL)) {
|
||||
imageType = WEBP;
|
||||
}
|
||||
} else if (vips_foreign_is_a("tiffload", file)) {
|
||||
if (!vips_tiffload(file, image, "access", access, NULL)) {
|
||||
imageType = TIFF;
|
||||
}
|
||||
} else if(vips_foreign_is_a("magickload", file)) {
|
||||
if (!vips_magickload(file, image, "access", access, NULL)) {
|
||||
imageType = MAGICK;
|
||||
}
|
||||
}
|
||||
return imageType;
|
||||
}
|
||||
|
||||
/*
|
||||
Does this image have an alpha channel?
|
||||
Uses colour space interpretation with number of channels to guess this.
|
||||
*/
|
||||
bool
|
||||
sharp_image_has_alpha(VipsImage *image) {
|
||||
return (
|
||||
(image->Bands == 2 && image->Type == VIPS_INTERPRETATION_B_W) ||
|
||||
(image->Bands == 4 && image->Type != VIPS_INTERPRETATION_CMYK) ||
|
||||
(image->Bands == 5 && image->Type == VIPS_INTERPRETATION_CMYK)
|
||||
);
|
||||
}
|
||||
46
src/common.h
Executable file
@@ -0,0 +1,46 @@
|
||||
#ifndef SHARP_COMMON_H
|
||||
#define SHARP_COMMON_H
|
||||
|
||||
typedef enum {
|
||||
UNKNOWN,
|
||||
JPEG,
|
||||
PNG,
|
||||
WEBP,
|
||||
TIFF,
|
||||
MAGICK
|
||||
} ImageType;
|
||||
|
||||
// Filename extension checkers
|
||||
bool is_jpeg(std::string const &str);
|
||||
bool is_png(std::string const &str);
|
||||
bool is_webp(std::string const &str);
|
||||
bool is_tiff(std::string const &str);
|
||||
|
||||
// How many tasks are in the queue?
|
||||
extern volatile int counter_queue;
|
||||
|
||||
// How many tasks are being processed?
|
||||
extern volatile int counter_process;
|
||||
|
||||
/*
|
||||
Initialise a VipsImage from a buffer. Supports JPEG, PNG and WebP.
|
||||
Returns the ImageType detected, if any.
|
||||
*/
|
||||
ImageType
|
||||
sharp_init_image_from_buffer(VipsImage **image, void *buffer, size_t const length, VipsAccess const access);
|
||||
|
||||
/*
|
||||
Initialise a VipsImage from a file.
|
||||
Returns the ImageType detected, if any.
|
||||
*/
|
||||
ImageType
|
||||
sharp_init_image_from_file(VipsImage **image, char const *file, VipsAccess const access);
|
||||
|
||||
/*
|
||||
Does this image have an alpha channel?
|
||||
Uses colour space interpretation with number of channels to guess this.
|
||||
*/
|
||||
bool
|
||||
sharp_image_has_alpha(VipsImage *image);
|
||||
|
||||
#endif
|
||||
144
src/metadata.cc
Executable file
@@ -0,0 +1,144 @@
|
||||
#include <node.h>
|
||||
#include <vips/vips.h>
|
||||
|
||||
#include "nan.h"
|
||||
|
||||
#include "common.h"
|
||||
#include "metadata.h"
|
||||
|
||||
using namespace v8;
|
||||
|
||||
struct MetadataBaton {
|
||||
// Input
|
||||
std::string fileIn;
|
||||
void* bufferIn;
|
||||
size_t bufferInLength;
|
||||
// Output
|
||||
std::string format;
|
||||
int width;
|
||||
int height;
|
||||
std::string space;
|
||||
int channels;
|
||||
bool hasAlpha;
|
||||
int orientation;
|
||||
std::string err;
|
||||
|
||||
MetadataBaton():
|
||||
bufferInLength(0),
|
||||
orientation(0) {}
|
||||
};
|
||||
|
||||
class MetadataWorker : public NanAsyncWorker {
|
||||
|
||||
public:
|
||||
MetadataWorker(NanCallback *callback, MetadataBaton *baton) : NanAsyncWorker(callback), baton(baton) {}
|
||||
~MetadataWorker() {}
|
||||
|
||||
void Execute() {
|
||||
// Decrement queued task counter
|
||||
g_atomic_int_dec_and_test(&counter_queue);
|
||||
|
||||
ImageType imageType = UNKNOWN;
|
||||
VipsImage *image;
|
||||
if (baton->bufferInLength > 1) {
|
||||
// From buffer
|
||||
imageType = sharp_init_image_from_buffer(&image, baton->bufferIn, baton->bufferInLength, VIPS_ACCESS_RANDOM);
|
||||
if (imageType == UNKNOWN) {
|
||||
(baton->err).append("Input buffer contains unsupported image format");
|
||||
}
|
||||
} else {
|
||||
// From file
|
||||
imageType = sharp_init_image_from_file(&image, baton->fileIn.c_str(), VIPS_ACCESS_RANDOM);
|
||||
if (imageType == UNKNOWN) {
|
||||
(baton->err).append("File is of an unsupported image format");
|
||||
}
|
||||
}
|
||||
if (imageType != UNKNOWN) {
|
||||
// Image type
|
||||
switch (imageType) {
|
||||
case JPEG: baton->format = "jpeg"; break;
|
||||
case PNG: baton->format = "png"; break;
|
||||
case WEBP: baton->format = "webp"; break;
|
||||
case TIFF: baton->format = "tiff"; break;
|
||||
case MAGICK: baton->format = "magick"; break;
|
||||
case UNKNOWN: default: baton->format = "";
|
||||
}
|
||||
// VipsImage attributes
|
||||
baton->width = image->Xsize;
|
||||
baton->height = image->Ysize;
|
||||
baton->space = vips_enum_nick(VIPS_TYPE_INTERPRETATION, image->Type);
|
||||
baton->channels = image->Bands;
|
||||
baton->hasAlpha = sharp_image_has_alpha(image);
|
||||
// EXIF Orientation
|
||||
const char *exif;
|
||||
if (!vips_image_get_string(image, "exif-ifd0-Orientation", &exif)) {
|
||||
baton->orientation = atoi(&exif[0]);
|
||||
}
|
||||
}
|
||||
// Clean up
|
||||
if (imageType != UNKNOWN) {
|
||||
g_object_unref(image);
|
||||
}
|
||||
vips_error_clear();
|
||||
vips_thread_shutdown();
|
||||
}
|
||||
|
||||
void HandleOKCallback () {
|
||||
NanScope();
|
||||
|
||||
Handle<Value> argv[2] = { NanNull(), NanNull() };
|
||||
if (!baton->err.empty()) {
|
||||
// Error
|
||||
argv[0] = Exception::Error(NanNew<String>(baton->err.data(), baton->err.size()));
|
||||
} else {
|
||||
// Metadata Object
|
||||
Local<Object> info = NanNew<Object>();
|
||||
info->Set(NanNew<String>("format"), NanNew<String>(baton->format));
|
||||
info->Set(NanNew<String>("width"), NanNew<Number>(baton->width));
|
||||
info->Set(NanNew<String>("height"), NanNew<Number>(baton->height));
|
||||
info->Set(NanNew<String>("space"), NanNew<String>(baton->space));
|
||||
info->Set(NanNew<String>("channels"), NanNew<Number>(baton->channels));
|
||||
info->Set(NanNew<String>("hasAlpha"), NanNew<Boolean>(baton->hasAlpha));
|
||||
if (baton->orientation > 0) {
|
||||
info->Set(NanNew<String>("orientation"), NanNew<Number>(baton->orientation));
|
||||
}
|
||||
argv[1] = info;
|
||||
}
|
||||
delete baton;
|
||||
|
||||
// Return to JavaScript
|
||||
callback->Call(2, argv);
|
||||
}
|
||||
|
||||
private:
|
||||
MetadataBaton* baton;
|
||||
};
|
||||
|
||||
/*
|
||||
metadata(options, callback)
|
||||
*/
|
||||
NAN_METHOD(metadata) {
|
||||
NanScope();
|
||||
|
||||
// V8 objects are converted to non-V8 types held in the baton struct
|
||||
MetadataBaton *baton = new MetadataBaton;
|
||||
Local<Object> options = args[0]->ToObject();
|
||||
|
||||
// Input filename
|
||||
baton->fileIn = *String::Utf8Value(options->Get(NanNew<String>("fileIn"))->ToString());
|
||||
// Input Buffer object
|
||||
if (options->Get(NanNew<String>("bufferIn"))->IsObject()) {
|
||||
Local<Object> buffer = options->Get(NanNew<String>("bufferIn"))->ToObject();
|
||||
baton->bufferInLength = node::Buffer::Length(buffer);
|
||||
baton->bufferIn = node::Buffer::Data(buffer);
|
||||
}
|
||||
|
||||
// Join queue for worker thread
|
||||
NanCallback *callback = new NanCallback(args[1].As<v8::Function>());
|
||||
NanAsyncQueueWorker(new MetadataWorker(callback, baton));
|
||||
|
||||
// Increment queued task counter
|
||||
g_atomic_int_inc(&counter_queue);
|
||||
|
||||
NanReturnUndefined();
|
||||
}
|
||||
8
src/metadata.h
Executable file
@@ -0,0 +1,8 @@
|
||||
#ifndef SHARP_METADATA_H
|
||||
#define SHARP_METADATA_H
|
||||
|
||||
#include "nan.h"
|
||||
|
||||
NAN_METHOD(metadata);
|
||||
|
||||
#endif
|
||||
790
src/resize.cc
Executable file
@@ -0,0 +1,790 @@
|
||||
#include <tuple>
|
||||
#include <math.h>
|
||||
#include <node.h>
|
||||
#include <node_buffer.h>
|
||||
#include <vips/vips.h>
|
||||
|
||||
#include "nan.h"
|
||||
|
||||
#include "common.h"
|
||||
#include "resize.h"
|
||||
|
||||
using namespace v8;
|
||||
|
||||
typedef enum {
|
||||
CROP,
|
||||
MAX,
|
||||
EMBED
|
||||
} Canvas;
|
||||
|
||||
typedef enum {
|
||||
ANGLE_0,
|
||||
ANGLE_90,
|
||||
ANGLE_180,
|
||||
ANGLE_270,
|
||||
ANGLE_LAST
|
||||
} Angle;
|
||||
|
||||
struct ResizeBaton {
|
||||
std::string fileIn;
|
||||
void* bufferIn;
|
||||
size_t bufferInLength;
|
||||
std::string output;
|
||||
std::string outputFormat;
|
||||
void* bufferOut;
|
||||
size_t bufferOutLength;
|
||||
int topOffsetPre;
|
||||
int leftOffsetPre;
|
||||
int widthPre;
|
||||
int heightPre;
|
||||
int topOffsetPost;
|
||||
int leftOffsetPost;
|
||||
int widthPost;
|
||||
int heightPost;
|
||||
int width;
|
||||
int height;
|
||||
Canvas canvas;
|
||||
int gravity;
|
||||
std::string interpolator;
|
||||
double background[4];
|
||||
bool flatten;
|
||||
bool sharpen;
|
||||
double gamma;
|
||||
bool greyscale;
|
||||
int angle;
|
||||
bool flip;
|
||||
bool flop;
|
||||
bool progressive;
|
||||
bool withoutEnlargement;
|
||||
VipsAccess accessMethod;
|
||||
int quality;
|
||||
int compressionLevel;
|
||||
std::string err;
|
||||
bool withMetadata;
|
||||
|
||||
ResizeBaton():
|
||||
bufferInLength(0),
|
||||
outputFormat(""),
|
||||
bufferOutLength(0),
|
||||
topOffsetPre(-1),
|
||||
topOffsetPost(-1),
|
||||
canvas(CROP),
|
||||
gravity(0),
|
||||
background{0.0, 0.0, 0.0, 255.0},
|
||||
flatten(false),
|
||||
sharpen(false),
|
||||
gamma(0.0),
|
||||
greyscale(false),
|
||||
flip(false),
|
||||
flop(false),
|
||||
progressive(false),
|
||||
withoutEnlargement(false),
|
||||
withMetadata(false) {}
|
||||
};
|
||||
|
||||
class ResizeWorker : public NanAsyncWorker {
|
||||
|
||||
public:
|
||||
ResizeWorker(NanCallback *callback, ResizeBaton *baton) : NanAsyncWorker(callback), baton(baton) {}
|
||||
~ResizeWorker() {}
|
||||
|
||||
/*
|
||||
libuv worker
|
||||
*/
|
||||
void Execute() {
|
||||
// Decrement queued task counter
|
||||
g_atomic_int_dec_and_test(&counter_queue);
|
||||
// Increment processing task counter
|
||||
g_atomic_int_inc(&counter_process);
|
||||
|
||||
// Hang image references from this hook object
|
||||
VipsObject *hook = reinterpret_cast<VipsObject*>(vips_image_new());
|
||||
|
||||
// Input
|
||||
ImageType inputImageType = UNKNOWN;
|
||||
VipsImage *image = vips_image_new();
|
||||
vips_object_local(hook, image);
|
||||
|
||||
if (baton->bufferInLength > 1) {
|
||||
// From buffer
|
||||
inputImageType = sharp_init_image_from_buffer(&image, baton->bufferIn, baton->bufferInLength, baton->accessMethod);
|
||||
if (inputImageType == UNKNOWN) {
|
||||
(baton->err).append("Input buffer contains unsupported image format");
|
||||
}
|
||||
} else {
|
||||
// From file
|
||||
inputImageType = sharp_init_image_from_file(&image, baton->fileIn.c_str(), baton->accessMethod);
|
||||
if (inputImageType == UNKNOWN) {
|
||||
(baton->err).append("File is of an unsupported image format");
|
||||
}
|
||||
}
|
||||
if (inputImageType == UNKNOWN) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
|
||||
// Pre extraction
|
||||
if (baton->topOffsetPre != -1) {
|
||||
VipsImage *extractedPre = vips_image_new();
|
||||
vips_object_local(hook, extractedPre);
|
||||
if (vips_extract_area(image, &extractedPre, baton->leftOffsetPre, baton->topOffsetPre, baton->widthPre, baton->heightPre, NULL)) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
g_object_unref(image);
|
||||
image = extractedPre;
|
||||
}
|
||||
|
||||
// Get input image width and height
|
||||
int inputWidth = image->Xsize;
|
||||
int inputHeight = image->Ysize;
|
||||
|
||||
// Calculate angle of rotation, to be carried out later
|
||||
Angle rotation;
|
||||
bool flip;
|
||||
std::tie(rotation, flip) = CalculateRotationAndFlip(baton->angle, image);
|
||||
if (rotation == ANGLE_90 || rotation == ANGLE_270) {
|
||||
// Swap input output width and height when rotating by 90 or 270 degrees
|
||||
int swap = inputWidth;
|
||||
inputWidth = inputHeight;
|
||||
inputHeight = swap;
|
||||
}
|
||||
if (flip && !baton->flip) {
|
||||
// Add flip operation due to EXIF mirroring
|
||||
baton->flip = TRUE;
|
||||
}
|
||||
|
||||
// Scaling calculations
|
||||
double factor;
|
||||
if (baton->width > 0 && baton->height > 0) {
|
||||
// Fixed width and height
|
||||
double xfactor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
|
||||
double yfactor = static_cast<double>(inputHeight) / static_cast<double>(baton->height);
|
||||
factor = (baton->canvas == CROP) ? std::min(xfactor, yfactor) : std::max(xfactor, yfactor);
|
||||
// if max is set, we need to compute the real size of the thumb image
|
||||
if (baton->canvas == MAX) {
|
||||
if (xfactor > yfactor) {
|
||||
baton->height = round(static_cast<double>(inputHeight) / xfactor);
|
||||
} else {
|
||||
baton->width = round(static_cast<double>(inputWidth) / yfactor);
|
||||
}
|
||||
}
|
||||
} else if (baton->width > 0) {
|
||||
// Fixed width, auto height
|
||||
factor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
|
||||
baton->height = floor(static_cast<double>(inputHeight) / factor);
|
||||
} else if (baton->height > 0) {
|
||||
// Fixed height, auto width
|
||||
factor = static_cast<double>(inputHeight) / static_cast<double>(baton->height);
|
||||
baton->width = floor(static_cast<double>(inputWidth) / factor);
|
||||
} else {
|
||||
// Identity transform
|
||||
factor = 1;
|
||||
baton->width = inputWidth;
|
||||
baton->height = inputHeight;
|
||||
}
|
||||
int shrink = floor(factor);
|
||||
if (shrink < 1) {
|
||||
shrink = 1;
|
||||
}
|
||||
double residual = static_cast<double>(shrink) / factor;
|
||||
|
||||
// Do not enlarge the output if the input width *or* height are already less than the required dimensions
|
||||
if (baton->withoutEnlargement) {
|
||||
if (inputWidth < baton->width || inputHeight < baton->height) {
|
||||
factor = 1;
|
||||
shrink = 1;
|
||||
residual = 0;
|
||||
baton->width = inputWidth;
|
||||
baton->height = inputHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to use libjpeg shrink-on-load, but not when applying gamma correction or pre-resize extract
|
||||
int shrink_on_load = 1;
|
||||
if (inputImageType == JPEG && baton->gamma == 0 && baton->topOffsetPre == -1) {
|
||||
if (shrink >= 8) {
|
||||
factor = factor / 8;
|
||||
shrink_on_load = 8;
|
||||
} else if (shrink >= 4) {
|
||||
factor = factor / 4;
|
||||
shrink_on_load = 4;
|
||||
} else if (shrink >= 2) {
|
||||
factor = factor / 2;
|
||||
shrink_on_load = 2;
|
||||
}
|
||||
}
|
||||
if (shrink_on_load > 1) {
|
||||
// Recalculate integral shrink and double residual
|
||||
factor = std::max(factor, 1.0);
|
||||
shrink = floor(factor);
|
||||
residual = static_cast<double>(shrink) / factor;
|
||||
// Reload input using shrink-on-load
|
||||
g_object_unref(image);
|
||||
if (baton->bufferInLength > 1) {
|
||||
if (vips_jpegload_buffer(baton->bufferIn, baton->bufferInLength, &image, "shrink", shrink_on_load, NULL)) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
} else {
|
||||
if (vips_jpegload((baton->fileIn).c_str(), &image, "shrink", shrink_on_load, NULL)) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle colour profile, if any, for non sRGB images
|
||||
if (image->Type != VIPS_INTERPRETATION_sRGB && vips_image_get_typeof(image, VIPS_META_ICC_NAME)) {
|
||||
// Import embedded profile
|
||||
VipsImage *profile = vips_image_new();
|
||||
vips_object_local(hook, profile);
|
||||
if (vips_icc_import(image, &profile, NULL, "embedded", TRUE, "pcs", VIPS_PCS_XYZ, NULL)) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
g_object_unref(image);
|
||||
image = profile;
|
||||
// Convert to sRGB colour space
|
||||
VipsImage *colourspaced = vips_image_new();
|
||||
vips_object_local(hook, colourspaced);
|
||||
if (vips_colourspace(image, &colourspaced, VIPS_INTERPRETATION_sRGB, NULL)) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
g_object_unref(image);
|
||||
image = colourspaced;
|
||||
}
|
||||
|
||||
// Flatten image to remove alpha channel
|
||||
if (baton->flatten && sharp_image_has_alpha(image)) {
|
||||
// Background colour
|
||||
VipsArrayDouble *background = vips_array_double_newv(
|
||||
3, // Ignore alpha channel as we're about to remove it
|
||||
baton->background[0],
|
||||
baton->background[1],
|
||||
baton->background[2]
|
||||
);
|
||||
VipsImage *flattened = vips_image_new();
|
||||
vips_object_local(hook, flattened);
|
||||
if (vips_flatten(image, &flattened, "background", background, NULL)) {
|
||||
vips_area_unref(reinterpret_cast<VipsArea*>(background));
|
||||
return Error(baton, hook);
|
||||
};
|
||||
vips_area_unref(reinterpret_cast<VipsArea*>(background));
|
||||
g_object_unref(image);
|
||||
image = flattened;
|
||||
}
|
||||
|
||||
// Gamma encoding (darken)
|
||||
if (baton->gamma >= 1 && baton->gamma <= 3) {
|
||||
VipsImage *gammaEncoded = vips_image_new();
|
||||
vips_object_local(hook, gammaEncoded);
|
||||
if (vips_gamma(image, &gammaEncoded, "exponent", 1.0 / baton->gamma, NULL)) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
g_object_unref(image);
|
||||
image = gammaEncoded;
|
||||
}
|
||||
|
||||
// Convert to greyscale (linear, therefore after gamma encoding, if any)
|
||||
if (baton->greyscale) {
|
||||
VipsImage *greyscale = vips_image_new();
|
||||
vips_object_local(hook, greyscale);
|
||||
if (vips_colourspace(image, &greyscale, VIPS_INTERPRETATION_B_W, NULL)) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
g_object_unref(image);
|
||||
image = greyscale;
|
||||
}
|
||||
|
||||
if (shrink > 1) {
|
||||
VipsImage *shrunk = vips_image_new();
|
||||
vips_object_local(hook, shrunk);
|
||||
// Use vips_shrink with the integral reduction
|
||||
if (vips_shrink(image, &shrunk, shrink, shrink, NULL)) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
g_object_unref(image);
|
||||
image = shrunk;
|
||||
// Recalculate residual float based on dimensions of required vs shrunk images
|
||||
double shrunkWidth = shrunk->Xsize;
|
||||
double shrunkHeight = shrunk->Ysize;
|
||||
if (rotation == ANGLE_90 || rotation == ANGLE_270) {
|
||||
// Swap input output width and height when rotating by 90 or 270 degrees
|
||||
int swap = shrunkWidth;
|
||||
shrunkWidth = shrunkHeight;
|
||||
shrunkHeight = swap;
|
||||
}
|
||||
double residualx = static_cast<double>(baton->width) / static_cast<double>(shrunkWidth);
|
||||
double residualy = static_cast<double>(baton->height) / static_cast<double>(shrunkHeight);
|
||||
if (baton->canvas == EMBED) {
|
||||
residual = std::min(residualx, residualy);
|
||||
} else {
|
||||
residual = std::max(residualx, residualy);
|
||||
}
|
||||
}
|
||||
|
||||
// Use vips_affine with the remaining float part
|
||||
if (residual != 0) {
|
||||
VipsImage *affined = vips_image_new();
|
||||
vips_object_local(hook, affined);
|
||||
// Create interpolator - "bilinear" (default), "bicubic" or "nohalo"
|
||||
VipsInterpolate *interpolator = vips_interpolate_new(baton->interpolator.c_str());
|
||||
// Perform affine transformation
|
||||
if (vips_affine(image, &affined, residual, 0, 0, residual, "interpolate", interpolator, NULL)) {
|
||||
g_object_unref(interpolator);
|
||||
return Error(baton, hook);
|
||||
}
|
||||
g_object_unref(interpolator);
|
||||
g_object_unref(image);
|
||||
image = affined;
|
||||
}
|
||||
|
||||
// Rotate
|
||||
if (rotation != ANGLE_0) {
|
||||
VipsImage *rotated = vips_image_new();
|
||||
vips_object_local(hook, rotated);
|
||||
if (vips_rot(image, &rotated, static_cast<VipsAngle>(rotation), NULL)) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
g_object_unref(image);
|
||||
image = rotated;
|
||||
}
|
||||
|
||||
// Flip (mirror about Y axis)
|
||||
if (baton->flip) {
|
||||
VipsImage *flipped = vips_image_new();
|
||||
vips_object_local(hook, flipped);
|
||||
if (vips_flip(image, &flipped, VIPS_DIRECTION_VERTICAL, NULL)) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
g_object_unref(image);
|
||||
image = flipped;
|
||||
}
|
||||
|
||||
// Flop (mirror about X axis)
|
||||
if (baton->flop) {
|
||||
VipsImage *flopped = vips_image_new();
|
||||
vips_object_local(hook, flopped);
|
||||
if (vips_flip(image, &flopped, VIPS_DIRECTION_HORIZONTAL, NULL)) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
g_object_unref(image);
|
||||
image = flopped;
|
||||
}
|
||||
|
||||
// Crop/embed
|
||||
if (image->Xsize != baton->width || image->Ysize != baton->height) {
|
||||
if (baton->canvas == EMBED) {
|
||||
// Match background colour space, namely sRGB
|
||||
if (image->Type != VIPS_INTERPRETATION_sRGB) {
|
||||
// Convert to sRGB colour space
|
||||
VipsImage *colourspaced = vips_image_new();
|
||||
vips_object_local(hook, colourspaced);
|
||||
if (vips_colourspace(image, &colourspaced, VIPS_INTERPRETATION_sRGB, NULL)) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
g_object_unref(image);
|
||||
image = colourspaced;
|
||||
}
|
||||
// Add non-transparent alpha channel, if required
|
||||
if (baton->background[3] < 255.0 && !sharp_image_has_alpha(image)) {
|
||||
// Create single-channel transparency
|
||||
VipsImage *black = vips_image_new();
|
||||
vips_object_local(hook, black);
|
||||
if (vips_black(&black, image->Xsize, image->Ysize, "bands", 1, NULL)) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
// Invert to become non-transparent
|
||||
VipsImage *alpha = vips_image_new();
|
||||
vips_object_local(hook, alpha);
|
||||
if (vips_invert(black, &alpha, NULL)) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
g_object_unref(black);
|
||||
// Append alpha channel to existing image
|
||||
VipsImage *joined = vips_image_new();
|
||||
vips_object_local(hook, joined);
|
||||
if (vips_bandjoin2(image, alpha, &joined, NULL)) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
g_object_unref(alpha);
|
||||
g_object_unref(image);
|
||||
image = joined;
|
||||
}
|
||||
// Create background
|
||||
VipsArrayDouble *background;
|
||||
if (baton->background[3] < 255.0) {
|
||||
background = vips_array_double_newv(
|
||||
4, baton->background[0], baton->background[1], baton->background[2], baton->background[3]
|
||||
);
|
||||
} else {
|
||||
background = vips_array_double_newv(
|
||||
3, baton->background[0], baton->background[1], baton->background[2]
|
||||
);
|
||||
}
|
||||
// Embed
|
||||
int left = (baton->width - image->Xsize) / 2;
|
||||
int top = (baton->height - image->Ysize) / 2;
|
||||
VipsImage *embedded = vips_image_new();
|
||||
vips_object_local(hook, embedded);
|
||||
if (vips_embed(image, &embedded, left, top, baton->width, baton->height,
|
||||
"extend", VIPS_EXTEND_BACKGROUND, "background", background, NULL
|
||||
)) {
|
||||
vips_area_unref(reinterpret_cast<VipsArea*>(background));
|
||||
return Error(baton, hook);
|
||||
}
|
||||
vips_area_unref(reinterpret_cast<VipsArea*>(background));
|
||||
g_object_unref(image);
|
||||
image = embedded;
|
||||
} else {
|
||||
// Crop/max
|
||||
int left;
|
||||
int top;
|
||||
std::tie(left, top) = CalculateCrop(image->Xsize, image->Ysize, baton->width, baton->height, baton->gravity);
|
||||
int width = std::min(image->Xsize, baton->width);
|
||||
int height = std::min(image->Ysize, baton->height);
|
||||
VipsImage *extracted = vips_image_new();
|
||||
vips_object_local(hook, extracted);
|
||||
if (vips_extract_area(image, &extracted, left, top, width, height, NULL)) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
g_object_unref(image);
|
||||
image = extracted;
|
||||
}
|
||||
}
|
||||
|
||||
// Post extraction
|
||||
if (baton->topOffsetPost != -1) {
|
||||
VipsImage *extractedPost = vips_image_new();
|
||||
vips_object_local(hook, extractedPost);
|
||||
if (vips_extract_area(image, &extractedPost, baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost, NULL)) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
g_object_unref(image);
|
||||
image = extractedPost;
|
||||
}
|
||||
|
||||
// Mild sharpen
|
||||
if (baton->sharpen) {
|
||||
VipsImage *sharpened = vips_image_new();
|
||||
vips_object_local(hook, sharpened);
|
||||
VipsImage *sharpen = vips_image_new_matrixv(3, 3,
|
||||
-1.0, -1.0, -1.0,
|
||||
-1.0, 32.0, -1.0,
|
||||
-1.0, -1.0, -1.0);
|
||||
vips_image_set_double(sharpen, "scale", 24);
|
||||
vips_object_local(hook, sharpen);
|
||||
if (vips_conv(image, &sharpened, sharpen, NULL)) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
g_object_unref(image);
|
||||
image = sharpened;
|
||||
}
|
||||
|
||||
// Gamma decoding (brighten)
|
||||
if (baton->gamma >= 1 && baton->gamma <= 3) {
|
||||
VipsImage *gammaDecoded = vips_image_new();
|
||||
vips_object_local(hook, gammaDecoded);
|
||||
if (vips_gamma(image, &gammaDecoded, "exponent", baton->gamma, NULL)) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
g_object_unref(image);
|
||||
image = gammaDecoded;
|
||||
}
|
||||
|
||||
// Convert to sRGB colour space, if not already
|
||||
if (image->Type != VIPS_INTERPRETATION_sRGB) {
|
||||
VipsImage *colourspaced = vips_image_new();
|
||||
vips_object_local(hook, colourspaced);
|
||||
if (vips_colourspace(image, &colourspaced, VIPS_INTERPRETATION_sRGB, NULL)) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
g_object_unref(image);
|
||||
image = colourspaced;
|
||||
}
|
||||
|
||||
// Generate image tile cache when interlace output is required
|
||||
if (baton->progressive) {
|
||||
VipsImage *cached = vips_image_new();
|
||||
vips_object_local(hook, cached);
|
||||
if (vips_tilecache(image, &cached, "threaded", TRUE, "persistent", TRUE, "max_tiles", -1, NULL)) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
g_object_unref(image);
|
||||
image = cached;
|
||||
}
|
||||
|
||||
// Output
|
||||
if (baton->output == "__jpeg" || (baton->output == "__input" && inputImageType == JPEG)) {
|
||||
// Write JPEG to buffer
|
||||
if (vips_jpegsave_buffer(image, &baton->bufferOut, &baton->bufferOutLength, "strip", !baton->withMetadata,
|
||||
"Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progressive, NULL)) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
baton->outputFormat = "jpeg";
|
||||
} else if (baton->output == "__png" || (baton->output == "__input" && inputImageType == PNG)) {
|
||||
// Write PNG to buffer
|
||||
if (vips_pngsave_buffer(image, &baton->bufferOut, &baton->bufferOutLength, "strip", !baton->withMetadata,
|
||||
"compression", baton->compressionLevel, "interlace", baton->progressive, NULL)) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
baton->outputFormat = "png";
|
||||
} else if (baton->output == "__webp" || (baton->output == "__input" && inputImageType == WEBP)) {
|
||||
// Write WEBP to buffer
|
||||
if (vips_webpsave_buffer(image, &baton->bufferOut, &baton->bufferOutLength, "strip", !baton->withMetadata,
|
||||
"Q", baton->quality, NULL)) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
baton->outputFormat = "webp";
|
||||
} else {
|
||||
bool output_jpeg = is_jpeg(baton->output);
|
||||
bool output_png = is_png(baton->output);
|
||||
bool output_webp = is_webp(baton->output);
|
||||
bool output_tiff = is_tiff(baton->output);
|
||||
bool match_input = !(output_jpeg || output_png || output_webp || output_tiff);
|
||||
if (output_jpeg || (match_input && inputImageType == JPEG)) {
|
||||
// Write JPEG to file
|
||||
if (vips_jpegsave(image, baton->output.c_str(), "strip", !baton->withMetadata,
|
||||
"Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progressive, NULL)) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
baton->outputFormat = "jpeg";
|
||||
} else if (output_png || (match_input && inputImageType == PNG)) {
|
||||
// Write PNG to file
|
||||
if (vips_pngsave(image, baton->output.c_str(), "strip", !baton->withMetadata,
|
||||
"compression", baton->compressionLevel, "interlace", baton->progressive, NULL)) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
baton->outputFormat = "png";
|
||||
} else if (output_webp || (match_input && inputImageType == WEBP)) {
|
||||
// Write WEBP to file
|
||||
if (vips_webpsave(image, baton->output.c_str(), "strip", !baton->withMetadata,
|
||||
"Q", baton->quality, NULL)) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
baton->outputFormat = "webp";
|
||||
} else if (output_tiff || (match_input && inputImageType == TIFF)) {
|
||||
// Write TIFF to file
|
||||
if (vips_tiffsave(image, baton->output.c_str(), "strip", !baton->withMetadata,
|
||||
"compression", VIPS_FOREIGN_TIFF_COMPRESSION_JPEG, "Q", baton->quality, NULL)) {
|
||||
return Error(baton, hook);
|
||||
}
|
||||
baton->outputFormat = "tiff";
|
||||
} else {
|
||||
(baton->err).append("Unsupported output " + baton->output);
|
||||
g_object_unref(image);
|
||||
return Error(baton, hook);
|
||||
}
|
||||
}
|
||||
// Clean up any dangling image references
|
||||
g_object_unref(image);
|
||||
g_object_unref(hook);
|
||||
// Clean up libvips' per-request data and threads
|
||||
vips_error_clear();
|
||||
vips_thread_shutdown();
|
||||
}
|
||||
|
||||
void HandleOKCallback () {
|
||||
NanScope();
|
||||
|
||||
Handle<Value> argv[3] = { NanNull(), NanNull(), NanNull() };
|
||||
if (!baton->err.empty()) {
|
||||
// Error
|
||||
argv[0] = Exception::Error(NanNew<String>(baton->err.data(), baton->err.size()));
|
||||
} else {
|
||||
int width = baton->width;
|
||||
int height = baton->height;
|
||||
if (baton->topOffsetPre != -1 && (baton->width == -1 || baton->height == -1)) {
|
||||
width = baton->widthPre;
|
||||
height = baton->heightPre;
|
||||
}
|
||||
if (baton->topOffsetPost != -1) {
|
||||
width = baton->widthPost;
|
||||
height = baton->heightPost;
|
||||
}
|
||||
// Info Object
|
||||
Local<Object> info = NanNew<Object>();
|
||||
info->Set(NanNew<String>("format"), NanNew<String>(baton->outputFormat));
|
||||
info->Set(NanNew<String>("width"), NanNew<Number>(width));
|
||||
info->Set(NanNew<String>("height"), NanNew<Number>(height));
|
||||
|
||||
if (baton->bufferOutLength > 0) {
|
||||
// Buffer
|
||||
argv[1] = NanNewBufferHandle(static_cast<char*>(baton->bufferOut), baton->bufferOutLength);
|
||||
g_free(baton->bufferOut);
|
||||
argv[2] = info;
|
||||
} else {
|
||||
// File
|
||||
argv[1] = info;
|
||||
}
|
||||
}
|
||||
delete baton;
|
||||
|
||||
// Decrement processing task counter
|
||||
g_atomic_int_dec_and_test(&counter_process);
|
||||
|
||||
// Return to JavaScript
|
||||
callback->Call(3, argv);
|
||||
}
|
||||
|
||||
private:
|
||||
ResizeBaton* baton;
|
||||
|
||||
/*
|
||||
Calculate the angle of rotation and need-to-flip for the output image.
|
||||
In order of priority:
|
||||
1. Use explicitly requested angle (supports 90, 180, 270)
|
||||
2. Use input image EXIF Orientation header - supports mirroring
|
||||
3. Otherwise default to zero, i.e. no rotation
|
||||
*/
|
||||
std::tuple<Angle, bool>
|
||||
CalculateRotationAndFlip(int const angle, VipsImage const *input) {
|
||||
Angle rotate = ANGLE_0;
|
||||
bool flip = FALSE;
|
||||
if (angle == -1) {
|
||||
const char *exif;
|
||||
if (!vips_image_get_string(input, "exif-ifd0-Orientation", &exif)) {
|
||||
if (exif[0] == 0x36) { // "6"
|
||||
rotate = ANGLE_90;
|
||||
} else if (exif[0] == 0x33) { // "3"
|
||||
rotate = ANGLE_180;
|
||||
} else if (exif[0] == 0x38) { // "8"
|
||||
rotate = ANGLE_270;
|
||||
} else if (exif[0] == 0x32) { // "2" (flip 1)
|
||||
flip = TRUE;
|
||||
} else if (exif[0] == 0x37) { // "7" (flip 6)
|
||||
rotate = ANGLE_90;
|
||||
flip = TRUE;
|
||||
} else if (exif[0] == 0x34) { // "4" (flip 3)
|
||||
rotate = ANGLE_180;
|
||||
flip = TRUE;
|
||||
} else if (exif[0] == 0x35) { // "5" (flip 8)
|
||||
rotate = ANGLE_270;
|
||||
flip = TRUE;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (angle == 90) {
|
||||
rotate = ANGLE_90;
|
||||
} else if (angle == 180) {
|
||||
rotate = ANGLE_180;
|
||||
} else if (angle == 270) {
|
||||
rotate = ANGLE_270;
|
||||
}
|
||||
}
|
||||
return std::make_tuple(rotate, flip);
|
||||
}
|
||||
|
||||
/*
|
||||
Calculate the (left, top) coordinates of the output image
|
||||
within the input image, applying the given gravity.
|
||||
*/
|
||||
std::tuple<int, int>
|
||||
CalculateCrop(int const inWidth, int const inHeight, int const outWidth, int const outHeight, int const gravity) {
|
||||
int left = 0;
|
||||
int top = 0;
|
||||
switch (gravity) {
|
||||
case 1: // North
|
||||
left = (inWidth - outWidth + 1) / 2;
|
||||
break;
|
||||
case 2: // East
|
||||
left = inWidth - outWidth;
|
||||
top = (inHeight - outHeight + 1) / 2;
|
||||
break;
|
||||
case 3: // South
|
||||
left = (inWidth - outWidth + 1) / 2;
|
||||
top = inHeight - outHeight;
|
||||
break;
|
||||
case 4: // West
|
||||
top = (inHeight - outHeight + 1) / 2;
|
||||
break;
|
||||
default: // Centre
|
||||
left = (inWidth - outWidth + 1) / 2;
|
||||
top = (inHeight - outHeight + 1) / 2;
|
||||
}
|
||||
return std::make_tuple(left, top);
|
||||
}
|
||||
|
||||
/*
|
||||
Copy then clear the error message.
|
||||
Unref all transitional images on the hook.
|
||||
Clear all thread-local data.
|
||||
*/
|
||||
void Error(ResizeBaton *baton, VipsObject *hook) {
|
||||
(baton->err).append(vips_error_buffer());
|
||||
vips_error_clear();
|
||||
g_object_unref(hook);
|
||||
vips_thread_shutdown();
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
resize(options, output, callback)
|
||||
*/
|
||||
NAN_METHOD(resize) {
|
||||
NanScope();
|
||||
|
||||
// V8 objects are converted to non-V8 types held in the baton struct
|
||||
ResizeBaton *baton = new ResizeBaton;
|
||||
Local<Object> options = args[0]->ToObject();
|
||||
|
||||
// Input filename
|
||||
baton->fileIn = *String::Utf8Value(options->Get(NanNew<String>("fileIn"))->ToString());
|
||||
baton->accessMethod = options->Get(NanNew<String>("sequentialRead"))->BooleanValue() ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM;
|
||||
// Input Buffer object
|
||||
if (options->Get(NanNew<String>("bufferIn"))->IsObject()) {
|
||||
Local<Object> buffer = options->Get(NanNew<String>("bufferIn"))->ToObject();
|
||||
baton->bufferInLength = node::Buffer::Length(buffer);
|
||||
baton->bufferIn = node::Buffer::Data(buffer);
|
||||
}
|
||||
// Extract image options
|
||||
baton->topOffsetPre = options->Get(NanNew<String>("topOffsetPre"))->Int32Value();
|
||||
baton->leftOffsetPre = options->Get(NanNew<String>("leftOffsetPre"))->Int32Value();
|
||||
baton->widthPre = options->Get(NanNew<String>("widthPre"))->Int32Value();
|
||||
baton->heightPre = options->Get(NanNew<String>("heightPre"))->Int32Value();
|
||||
baton->topOffsetPost = options->Get(NanNew<String>("topOffsetPost"))->Int32Value();
|
||||
baton->leftOffsetPost = options->Get(NanNew<String>("leftOffsetPost"))->Int32Value();
|
||||
baton->widthPost = options->Get(NanNew<String>("widthPost"))->Int32Value();
|
||||
baton->heightPost = options->Get(NanNew<String>("heightPost"))->Int32Value();
|
||||
// Output image dimensions
|
||||
baton->width = options->Get(NanNew<String>("width"))->Int32Value();
|
||||
baton->height = options->Get(NanNew<String>("height"))->Int32Value();
|
||||
// Canvas option
|
||||
Local<String> canvas = options->Get(NanNew<String>("canvas"))->ToString();
|
||||
if (canvas->Equals(NanNew<String>("c"))) {
|
||||
baton->canvas = CROP;
|
||||
} else if (canvas->Equals(NanNew<String>("m"))) {
|
||||
baton->canvas = MAX;
|
||||
} else if (canvas->Equals(NanNew<String>("e"))) {
|
||||
baton->canvas = EMBED;
|
||||
}
|
||||
// Background colour
|
||||
Local<Array> background = Local<Array>::Cast(options->Get(NanNew<String>("background")));
|
||||
for (int i = 0; i < 4; i++) {
|
||||
baton->background[i] = background->Get(i)->NumberValue();
|
||||
}
|
||||
// Resize options
|
||||
baton->withoutEnlargement = options->Get(NanNew<String>("withoutEnlargement"))->BooleanValue();
|
||||
baton->gravity = options->Get(NanNew<String>("gravity"))->Int32Value();
|
||||
baton->interpolator = *String::Utf8Value(options->Get(NanNew<String>("interpolator"))->ToString());
|
||||
// Operators
|
||||
baton->flatten = options->Get(NanNew<String>("flatten"))->BooleanValue();
|
||||
baton->sharpen = options->Get(NanNew<String>("sharpen"))->BooleanValue();
|
||||
baton->gamma = options->Get(NanNew<String>("gamma"))->NumberValue();
|
||||
baton->greyscale = options->Get(NanNew<String>("greyscale"))->BooleanValue();
|
||||
baton->angle = options->Get(NanNew<String>("angle"))->Int32Value();
|
||||
baton->flip = options->Get(NanNew<String>("flip"))->BooleanValue();
|
||||
baton->flop = options->Get(NanNew<String>("flop"))->BooleanValue();
|
||||
// Output options
|
||||
baton->progressive = options->Get(NanNew<String>("progressive"))->BooleanValue();
|
||||
baton->quality = options->Get(NanNew<String>("quality"))->Int32Value();
|
||||
baton->compressionLevel = options->Get(NanNew<String>("compressionLevel"))->Int32Value();
|
||||
baton->withMetadata = options->Get(NanNew<String>("withMetadata"))->BooleanValue();
|
||||
// Output filename or __format for Buffer
|
||||
baton->output = *String::Utf8Value(options->Get(NanNew<String>("output"))->ToString());
|
||||
|
||||
// Join queue for worker thread
|
||||
NanCallback *callback = new NanCallback(args[1].As<v8::Function>());
|
||||
NanAsyncQueueWorker(new ResizeWorker(callback, baton));
|
||||
|
||||
// Increment queued task counter
|
||||
g_atomic_int_inc(&counter_queue);
|
||||
|
||||
NanReturnUndefined();
|
||||
}
|
||||
8
src/resize.h
Executable file
@@ -0,0 +1,8 @@
|
||||
#ifndef SHARP_RESIZE_H
|
||||
#define SHARP_RESIZE_H
|
||||
|
||||
#include "nan.h"
|
||||
|
||||
NAN_METHOD(resize);
|
||||
|
||||
#endif
|
||||
312
src/sharp.cc
@@ -1,302 +1,38 @@
|
||||
#include <node.h>
|
||||
#include <node_buffer.h>
|
||||
#include <math.h>
|
||||
#include <string>
|
||||
#include <string.h>
|
||||
#include <vips/vips.h>
|
||||
|
||||
#include "nan.h"
|
||||
|
||||
#include "common.h"
|
||||
#include "metadata.h"
|
||||
#include "resize.h"
|
||||
#include "utilities.h"
|
||||
|
||||
using namespace v8;
|
||||
using namespace node;
|
||||
|
||||
struct resize_baton {
|
||||
std::string file_in;
|
||||
void* buffer_in;
|
||||
size_t buffer_in_len;
|
||||
std::string file_out;
|
||||
void* buffer_out;
|
||||
size_t buffer_out_len;
|
||||
int width;
|
||||
int height;
|
||||
bool crop;
|
||||
VipsExtend extend;
|
||||
bool sharpen;
|
||||
bool progessive;
|
||||
VipsAccess access_method;
|
||||
std::string err;
|
||||
Persistent<Function> callback;
|
||||
|
||||
resize_baton(): buffer_in_len(0), buffer_out_len(0) {}
|
||||
};
|
||||
|
||||
typedef enum {
|
||||
JPEG,
|
||||
PNG
|
||||
} ImageType;
|
||||
|
||||
unsigned char MARKER_JPEG[] = {0xff, 0xd8};
|
||||
unsigned char MARKER_PNG[] = {0x89, 0x50};
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
bool is_png(std::string const &str) {
|
||||
return ends_with(str, ".png");
|
||||
}
|
||||
|
||||
void resize_error(resize_baton *baton, VipsImage *unref) {
|
||||
(baton->err).append(vips_error_buffer());
|
||||
vips_error_clear();
|
||||
g_object_unref(unref);
|
||||
vips_thread_shutdown();
|
||||
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);
|
||||
}
|
||||
} 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 = 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 = 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(shrunk_on_load, &shrunk, shrink, shrink, NULL)) {
|
||||
return resize_error(baton, shrunk_on_load);
|
||||
}
|
||||
} else {
|
||||
vips_copy(shrunk_on_load, &shrunk, NULL);
|
||||
}
|
||||
g_object_unref(shrunk_on_load);
|
||||
|
||||
// Use vips_affine with the remaining float part using bilinear interpolation
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
vips_copy(shrunk, &affined, NULL);
|
||||
}
|
||||
g_object_unref(shrunk);
|
||||
|
||||
VipsImage *canvased = vips_image_new();
|
||||
if (affined->Xsize != baton->width || affined->Ysize != baton->height) {
|
||||
if (baton->crop) {
|
||||
// Crop
|
||||
int width = std::min(affined->Xsize, baton->width);
|
||||
int height = std::min(affined->Ysize, baton->height);
|
||||
int left = (affined->Xsize - width + 1) / 2;
|
||||
int top = (affined->Ysize - height + 1) / 2;
|
||||
if (vips_extract_area(affined, &canvased, left, top, width, height, NULL)) {
|
||||
return resize_error(baton, affined);
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
vips_copy(affined, &canvased, NULL);
|
||||
}
|
||||
g_object_unref(affined);
|
||||
|
||||
// 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);
|
||||
}
|
||||
} else {
|
||||
vips_copy(canvased, &sharpened, NULL);
|
||||
}
|
||||
g_object_unref(canvased);
|
||||
|
||||
if (baton->file_out == "__jpeg") {
|
||||
// Write JPEG to buffer
|
||||
if (vips_jpegsave_buffer(sharpened, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", 80, "optimize_coding", TRUE, "interlace", baton->progessive, NULL)) {
|
||||
return resize_error(baton, sharpened);
|
||||
}
|
||||
} else if (baton->file_out == "__png") {
|
||||
// Write PNG to buffer
|
||||
if (vips_pngsave_buffer(sharpened, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "compression", 6, "interlace", baton->progessive, NULL)) {
|
||||
return resize_error(baton, sharpened);
|
||||
}
|
||||
} else if (is_jpeg(baton->file_out)) {
|
||||
// Write JPEG to file
|
||||
if (vips_jpegsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "Q", 80, "optimize_coding", TRUE, "interlace", baton->progessive, NULL)) {
|
||||
return resize_error(baton, sharpened);
|
||||
}
|
||||
} else if (is_png(baton->file_out)) {
|
||||
// Write PNG to file
|
||||
if (vips_pngsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "compression", 6, "interlace", baton->progessive, NULL)) {
|
||||
return resize_error(baton, sharpened);
|
||||
}
|
||||
} else {
|
||||
(baton->err).append("Unsupported output " + baton->file_out);
|
||||
}
|
||||
g_object_unref(sharpened);
|
||||
vips_thread_shutdown();
|
||||
}
|
||||
|
||||
void resize_async_after(uv_work_t *work, int status) {
|
||||
HandleScope scope;
|
||||
|
||||
resize_baton *baton = static_cast<resize_baton*>(work->data);
|
||||
|
||||
// Free temporary copy of input buffer
|
||||
if (baton->buffer_in_len > 0) {
|
||||
g_free(baton->buffer_in);
|
||||
}
|
||||
|
||||
Handle<Value> argv[2] = { Null(), Null() };
|
||||
if (!baton->err.empty()) {
|
||||
// Error
|
||||
argv[0] = scope.Close(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] = scope.Close(bufferConstructor->NewInstance(3, constructorArgs));
|
||||
g_free(baton->buffer_out);
|
||||
}
|
||||
|
||||
baton->callback->Call(Context::GetCurrent()->Global(), 2, argv);
|
||||
baton->callback.Dispose();
|
||||
delete baton;
|
||||
delete work;
|
||||
}
|
||||
|
||||
Handle<Value> resize(const Arguments& args) {
|
||||
HandleScope scope;
|
||||
|
||||
resize_baton *baton = new resize_baton;
|
||||
baton->file_in = *String::Utf8Value(args[0]->ToString());
|
||||
if (args[1]->IsObject()) {
|
||||
Local<Object> buffer = args[1]->ToObject();
|
||||
// Take temporary copy of input buffer
|
||||
if (Buffer::Length(buffer) > 0) {
|
||||
baton->buffer_in_len = Buffer::Length(buffer);
|
||||
baton->buffer_in = g_malloc(baton->buffer_in_len);
|
||||
memcpy(baton->buffer_in, Buffer::Data(buffer), baton->buffer_in_len);
|
||||
}
|
||||
}
|
||||
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]));
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
static void at_exit(void* arg) {
|
||||
HandleScope scope;
|
||||
NanScope();
|
||||
vips_shutdown();
|
||||
}
|
||||
|
||||
extern "C" void init(Handle<Object> target) {
|
||||
HandleScope scope;
|
||||
vips_init("");
|
||||
AtExit(at_exit);
|
||||
NanScope();
|
||||
vips_init("sharp");
|
||||
node::AtExit(at_exit);
|
||||
|
||||
// Set libvips operation cache limits
|
||||
vips_cache_set_max_mem(100 * 1048576); // 100 MB
|
||||
vips_cache_set_max(500); // 500 operations
|
||||
|
||||
// Notify the V8 garbage collector of max cache size
|
||||
NanAdjustExternalMemory(vips_cache_get_max_mem());
|
||||
|
||||
// Methods available to JavaScript
|
||||
NODE_SET_METHOD(target, "metadata", metadata);
|
||||
NODE_SET_METHOD(target, "resize", resize);
|
||||
NODE_SET_METHOD(target, "cache", cache);
|
||||
NODE_SET_METHOD(target, "concurrency", concurrency);
|
||||
NODE_SET_METHOD(target, "counters", counters);
|
||||
}
|
||||
|
||||
NODE_MODULE(sharp, init)
|
||||
|
||||
64
src/utilities.cc
Executable file
@@ -0,0 +1,64 @@
|
||||
#include <node.h>
|
||||
#include <vips/vips.h>
|
||||
|
||||
#include "nan.h"
|
||||
|
||||
#include "common.h"
|
||||
#include "utilities.h"
|
||||
|
||||
using namespace v8;
|
||||
|
||||
/*
|
||||
Get and set cache memory and item limits
|
||||
*/
|
||||
NAN_METHOD(cache) {
|
||||
NanScope();
|
||||
|
||||
// Set cache memory limit
|
||||
if (args[0]->IsInt32()) {
|
||||
int newMax = args[0]->Int32Value() * 1048576;
|
||||
int oldMax = vips_cache_get_max_mem();
|
||||
vips_cache_set_max_mem(newMax);
|
||||
|
||||
// Notify the V8 garbage collector of delta in max cache size
|
||||
NanAdjustExternalMemory(newMax - oldMax);
|
||||
}
|
||||
|
||||
// Set cache items limit
|
||||
if (args[1]->IsInt32()) {
|
||||
vips_cache_set_max(args[1]->Int32Value());
|
||||
}
|
||||
|
||||
// Get cache statistics
|
||||
Local<Object> cache = NanNew<Object>();
|
||||
cache->Set(NanNew<String>("current"), NanNew<Number>(vips_tracked_get_mem() / 1048576));
|
||||
cache->Set(NanNew<String>("high"), NanNew<Number>(vips_tracked_get_mem_highwater() / 1048576));
|
||||
cache->Set(NanNew<String>("memory"), NanNew<Number>(vips_cache_get_max_mem() / 1048576));
|
||||
cache->Set(NanNew<String>("items"), NanNew<Number>(vips_cache_get_max()));
|
||||
NanReturnValue(cache);
|
||||
}
|
||||
|
||||
/*
|
||||
Get and set size of thread pool
|
||||
*/
|
||||
NAN_METHOD(concurrency) {
|
||||
NanScope();
|
||||
|
||||
// Set concurrency
|
||||
if (args[0]->IsInt32()) {
|
||||
vips_concurrency_set(args[0]->Int32Value());
|
||||
}
|
||||
// Get concurrency
|
||||
NanReturnValue(NanNew<Number>(vips_concurrency_get()));
|
||||
}
|
||||
|
||||
/*
|
||||
Get internal counters (queued tasks, processing tasks)
|
||||
*/
|
||||
NAN_METHOD(counters) {
|
||||
NanScope();
|
||||
Local<Object> counters = NanNew<Object>();
|
||||
counters->Set(NanNew<String>("queue"), NanNew<Number>(counter_queue));
|
||||
counters->Set(NanNew<String>("process"), NanNew<Number>(counter_process));
|
||||
NanReturnValue(counters);
|
||||
}
|
||||
10
src/utilities.h
Executable file
@@ -0,0 +1,10 @@
|
||||
#ifndef SHARP_UTILITIES_H
|
||||
#define SHARP_UTILITIES_H
|
||||
|
||||
#include "nan.h"
|
||||
|
||||
NAN_METHOD(cache);
|
||||
NAN_METHOD(concurrency);
|
||||
NAN_METHOD(counters);
|
||||
|
||||
#endif
|
||||
21
test/bench/package.json
Executable file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "sharp-benchmark",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"author": "Lovell Fuller <npm@lovell.info>",
|
||||
"description": "Benchmark and performance tests for sharp",
|
||||
"scripts": {
|
||||
"test": "node perf && node random && node parallel"
|
||||
},
|
||||
"devDependencies": {
|
||||
"imagemagick": "^0.1.3",
|
||||
"imagemagick-native": "^1.4.0",
|
||||
"gm": "^1.16.0",
|
||||
"async": "^0.9.0",
|
||||
"benchmark": "^1.0.0"
|
||||
},
|
||||
"license": "Apache 2.0",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
}
|
||||
41
test/bench/parallel.js
Executable file
@@ -0,0 +1,41 @@
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert');
|
||||
var async = require('async');
|
||||
|
||||
var sharp = require('../../index');
|
||||
var fixtures = require('../fixtures');
|
||||
|
||||
var width = 720;
|
||||
var height = 480;
|
||||
|
||||
sharp.concurrency(1);
|
||||
|
||||
var timer = setInterval(function() {
|
||||
console.dir(sharp.counters());
|
||||
}, 100);
|
||||
|
||||
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) {
|
||||
/*jslint unused: false */
|
||||
sharp(fixtures.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() {
|
||||
clearInterval(timer);
|
||||
console.dir(sharp.counters());
|
||||
});
|
||||
493
test/bench/perf.js
Executable file
@@ -0,0 +1,493 @@
|
||||
'use strict';
|
||||
|
||||
var fs = require('fs');
|
||||
|
||||
var async = require('async');
|
||||
var assert = require('assert');
|
||||
var Benchmark = require('benchmark');
|
||||
|
||||
var imagemagick = require('imagemagick');
|
||||
var imagemagickNative = require('imagemagick-native');
|
||||
var gm = require('gm');
|
||||
var sharp = require('../../index');
|
||||
|
||||
var fixtures = require('../fixtures');
|
||||
|
||||
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(fixtures.inputJpg);
|
||||
(new Benchmark.Suite('jpeg')).add('imagemagick-file-file', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
imagemagick.resize({
|
||||
srcPath: fixtures.inputJpg,
|
||||
dstPath: fixtures.outputJpg,
|
||||
quality: 0.8,
|
||||
width: width,
|
||||
height: height
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).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(fixtures.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) {
|
||||
gm(fixtures.inputJpg).resize(width, height).quality(80).write(fixtures.outputJpg, function (err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add('gm-file-buffer', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
gm(fixtures.inputJpg).resize(width, height).quality(80).toBuffer(function (err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add('sharp-buffer-file', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputJpgBuffer).resize(width, height).toFile(fixtures.outputJpg, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add('sharp-buffer-buffer', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputJpgBuffer).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(fixtures.inputJpg).resize(width, height).toFile(fixtures.outputJpg, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add('sharp-stream-stream', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
var readable = fs.createReadStream(fixtures.inputJpg);
|
||||
var writable = fs.createWriteStream(fixtures.outputJpg);
|
||||
writable.on('finish', function() {
|
||||
deferred.resolve();
|
||||
});
|
||||
var pipeline = sharp().resize(width, height);
|
||||
readable.pipe(pipeline).pipe(writable);
|
||||
}
|
||||
}).add('sharp-file-buffer', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(fixtures.inputJpg).resize(width, height).toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add('sharp-promise', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputJpgBuffer).resize(width, height).toBuffer().then(function(buffer) {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
});
|
||||
}
|
||||
}).add('sharp-sharpen', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputJpgBuffer).resize(width, height).sharpen().toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add('sharp-nearest-neighbour', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputJpgBuffer).resize(width, height).interpolateWith(sharp.interpolator.nearest).toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add('sharp-bicubic', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputJpgBuffer).resize(width, height).interpolateWith(sharp.interpolator.bicubic).toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add('sharp-nohalo', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputJpgBuffer).resize(width, height).interpolateWith(sharp.interpolator.nohalo).toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add('sharp-locallyBoundedBicubic', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputJpgBuffer).resize(width, height).interpolateWith(sharp.interpolator.locallyBoundedBicubic).toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add('sharp-vertexSplitQuadraticBasisSpline', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputJpgBuffer).resize(width, height).interpolateWith(sharp.interpolator.vertexSplitQuadraticBasisSpline).toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add('sharp-gamma', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputJpgBuffer).resize(width, height).gamma().toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add('sharp-greyscale', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputJpgBuffer).resize(width, height).greyscale().toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add('sharp-greyscale-gamma', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputJpgBuffer).resize(width, height).gamma().greyscale().toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add('sharp-progressive', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputJpgBuffer).resize(width, height).progressive().toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add('sharp-rotate', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputJpgBuffer).rotate(90).resize(width, height).toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add('sharp-sequentialRead', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputJpgBuffer).resize(width, height).sequentialRead().toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).on('cycle', function(event) {
|
||||
console.log('jpeg ' + String(event.target));
|
||||
}).on('complete', function() {
|
||||
callback(null, this.filter('fastest').pluck('name'));
|
||||
}).run();
|
||||
},
|
||||
png: function(callback) {
|
||||
var inputPngBuffer = fs.readFileSync(fixtures.inputPng);
|
||||
(new Benchmark.Suite('png')).add('imagemagick-file-file', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
imagemagick.resize({
|
||||
srcPath: fixtures.inputPng,
|
||||
dstPath: fixtures.outputPng,
|
||||
width: width,
|
||||
height: height
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).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) {
|
||||
gm(fixtures.inputPng).resize(width, height).write(fixtures.outputPng, function (err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add('gm-file-buffer', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
gm(fixtures.inputPng).resize(width, height).quality(80).toBuffer(function (err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add('sharp-buffer-file', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputPngBuffer).resize(width, height).toFile(fixtures.outputPng, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add('sharp-buffer-buffer', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputPngBuffer).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(fixtures.inputPng).resize(width, height).toFile(fixtures.outputPng, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add('sharp-file-buffer', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(fixtures.inputPng).resize(width, height).toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add('sharp-progressive', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputPngBuffer).resize(width, height).progressive().toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).on('cycle', function(event) {
|
||||
console.log(' png ' + String(event.target));
|
||||
}).on('complete', function() {
|
||||
callback(null, this.filter('fastest').pluck('name'));
|
||||
}).run();
|
||||
},
|
||||
webp: function(callback) {
|
||||
var inputWebPBuffer = fs.readFileSync(fixtures.inputWebP);
|
||||
(new Benchmark.Suite('webp')).add('sharp-buffer-file', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputWebPBuffer).resize(width, height).toFile(fixtures.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(fixtures.inputWebP).resize(width, height).toFile(fixtures.outputWebP, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add('sharp-file-buffer', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(fixtures.inputWebp).resize(width, height).toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).on('cycle', function(event) {
|
||||
console.log('webp ' + String(event.target));
|
||||
}).on('complete', function() {
|
||||
callback(null, this.filter('fastest').pluck('name'));
|
||||
}).run();
|
||||
}
|
||||
}, function(err, results) {
|
||||
assert(!err, err);
|
||||
Object.keys(results).forEach(function(format) {
|
||||
if (results[format].toString().substr(0, 5) !== 'sharp') {
|
||||
console.log('sharp was slower than ' + results[format] + ' for ' + format);
|
||||
}
|
||||
});
|
||||
console.dir(sharp.cache());
|
||||
});
|
||||
65
test/bench/random.js
Executable file
@@ -0,0 +1,65 @@
|
||||
'use strict';
|
||||
|
||||
var imagemagick = require('imagemagick');
|
||||
var gm = require('gm');
|
||||
var assert = require('assert');
|
||||
var Benchmark = require('benchmark');
|
||||
|
||||
var sharp = require('../../index');
|
||||
var fixtures = require('../fixtures');
|
||||
|
||||
var min = 320;
|
||||
var max = 960;
|
||||
|
||||
var randomDimension = function() {
|
||||
return Math.random() * (max - min) + min;
|
||||
};
|
||||
|
||||
new Benchmark.Suite('random').add('imagemagick', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
imagemagick.resize({
|
||||
srcPath: fixtures.inputJpg,
|
||||
dstPath: fixtures.outputJpg,
|
||||
quality: 0.8,
|
||||
width: randomDimension(),
|
||||
height: randomDimension()
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add('gm', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
gm(fixtures.inputJpg).resize(randomDimension(), randomDimension()).quality(80).toBuffer(function (err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add('sharp', {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(fixtures.inputJpg).resize(randomDimension(), randomDimension()).toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).on('cycle', function(event) {
|
||||
console.log(String(event.target));
|
||||
}).on('complete', function() {
|
||||
var winner = this.filter('fastest').pluck('name');
|
||||
assert.strictEqual('sharp', String(winner), 'sharp was slower than ' + winner);
|
||||
console.dir(sharp.cache());
|
||||
}).run();
|
||||
|
Before Width: | Height: | Size: 813 KiB After Width: | Height: | Size: 813 KiB |
BIN
test/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
test/fixtures/Channel_digital_image_CMYK_color.jpg
vendored
Normal file
|
After Width: | Height: | Size: 714 KiB |
BIN
test/fixtures/Crash_test.gif
vendored
Normal file
|
After Width: | Height: | Size: 278 KiB |
BIN
test/fixtures/G31D.TIF
vendored
Normal file
BIN
test/fixtures/Landscape_5.jpg
vendored
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
test/fixtures/Landscape_8.jpg
vendored
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
test/fixtures/blackbug.png
vendored
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
test/fixtures/gamma_dalai_lama_gray.jpg
vendored
Normal file
|
After Width: | Height: | Size: 83 KiB |
31
test/fixtures/index.js
vendored
Executable file
@@ -0,0 +1,31 @@
|
||||
'use strict';
|
||||
|
||||
var path = require('path');
|
||||
|
||||
var getPath = function(filename) {
|
||||
return path.join(__dirname, filename);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
||||
inputJpg: getPath('2569067123_aca715a2ee_o.jpg'), // http://www.flickr.com/photos/grizdave/2569067123/
|
||||
inputJpgWithExif: getPath('Landscape_8.jpg'), // https://github.com/recurser/exif-orientation-examples/blob/master/Landscape_8.jpg
|
||||
inputJpgWithExifMirroring: getPath('Landscape_5.jpg'), // https://github.com/recurser/exif-orientation-examples/blob/master/Landscape_5.jpg
|
||||
inputJpgWithGammaHoliness: getPath('gamma_dalai_lama_gray.jpg'), // http://www.4p8.com/eric.brasseur/gamma.html
|
||||
inputJpgWithCmykProfile: getPath('Channel_digital_image_CMYK_color.jpg'), // http://en.wikipedia.org/wiki/File:Channel_digital_image_CMYK_color.jpg
|
||||
|
||||
inputPng: getPath('50020484-00001.png'), // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png
|
||||
inputPngWithTransparency: getPath('blackbug.png'), // public domain
|
||||
|
||||
inputWebP: getPath('4.webp'), // http://www.gstatic.com/webp/gallery/4.webp
|
||||
inputTiff: getPath('G31D.TIF'), // http://www.fileformat.info/format/tiff/sample/e6c9a6e5253348f4aef6d17b534360ab/index.htm
|
||||
inputGif: getPath('Crash_test.gif'), // http://upload.wikimedia.org/wikipedia/commons/e/e3/Crash_test.gif
|
||||
|
||||
outputJpg: getPath('output.jpg'),
|
||||
outputPng: getPath('output.png'),
|
||||
outputWebP: getPath('output.webp'),
|
||||
outputZoinks: getPath('output.zoinks'), // an 'unknown' file extension
|
||||
|
||||
path: getPath // allows tests to write files to fixtures directory (for testing with human eyes)
|
||||
|
||||
};
|
||||
8
test/leak/leak.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
if ! type valgrind >/dev/null; then
|
||||
echo "Please install valgrind before running memory leak tests"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
curl -O https://raw.githubusercontent.com/jcupitt/libvips/master/libvips.supp test/leak/libvips.supp
|
||||
cd ../../
|
||||
G_SLICE=always-malloc G_DEBUG=gc-friendly valgrind --suppressions=test/leak/libvips.supp --suppressions=test/leak/sharp.supp --leak-check=full --show-leak-kinds=definite,indirect,possible --num-callers=20 npm test
|
||||
111
test/leak/sharp.supp
Executable file
@@ -0,0 +1,111 @@
|
||||
# libjpeg warnings
|
||||
{
|
||||
cond_jpeg_read_scanlines
|
||||
Memcheck:Cond
|
||||
...
|
||||
fun:jpeg_read_scanlines
|
||||
}
|
||||
{
|
||||
value_jpeg_read_scanlines
|
||||
Memcheck:Value8
|
||||
...
|
||||
fun:jpeg_read_scanlines
|
||||
}
|
||||
{
|
||||
cond_jpeg_write_scanlines
|
||||
Memcheck:Cond
|
||||
...
|
||||
fun:jpeg_write_scanlines
|
||||
}
|
||||
{
|
||||
cond_jpeg_finish_compress
|
||||
Memcheck:Cond
|
||||
...
|
||||
fun:jpeg_finish_compress
|
||||
}
|
||||
{
|
||||
value_jpeg_finish_compress
|
||||
Memcheck:Value8
|
||||
...
|
||||
fun:jpeg_finish_compress
|
||||
}
|
||||
|
||||
# libvips interpolator warnings
|
||||
{
|
||||
cond_libvips_interpolate_lbb
|
||||
Memcheck:Cond
|
||||
...
|
||||
fun:_ZL32vips_interpolate_lbb_interpolateP16_VipsInterpolatePvP11_VipsRegiondd
|
||||
fun:vips_affine_gen
|
||||
}
|
||||
|
||||
# libuv warnings
|
||||
{
|
||||
free_libuv
|
||||
Memcheck:Free
|
||||
...
|
||||
fun:uv__work_done
|
||||
}
|
||||
|
||||
# nodejs warnings
|
||||
{
|
||||
param_nodejs_write_buffer
|
||||
Memcheck:Param
|
||||
write(buf)
|
||||
...
|
||||
obj:/usr/bin/nodejs
|
||||
}
|
||||
{
|
||||
leak_nodejs_ImmutableAsciiSource_CreateFromLiteral
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: definite
|
||||
...
|
||||
fun:_ZN4node20ImmutableAsciiSource17CreateFromLiteralEPKcm
|
||||
}
|
||||
{
|
||||
leak_nodejs_Buffer_New
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: definite
|
||||
...
|
||||
fun:_ZN4node6Buffer3NewERKN2v89ArgumentsE
|
||||
}
|
||||
{
|
||||
leak_nodejs_Buffer_Replace
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: indirect,possible
|
||||
...
|
||||
fun:_ZN4node6Buffer7ReplaceEPcmPFvS1_PvES2_
|
||||
}
|
||||
{
|
||||
leak_nodejs_SignalWrap_New
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: possible
|
||||
...
|
||||
fun:_ZN4node10SignalWrap3NewERKN2v89ArgumentsE
|
||||
}
|
||||
{
|
||||
leak_nodejs_TTYWrap_New
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: possible
|
||||
...
|
||||
fun:_ZN4node7TTYWrap3NewERKN2v89ArgumentsE
|
||||
}
|
||||
{
|
||||
leak_nodejs_ares_init_options
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: reachable
|
||||
fun:malloc
|
||||
fun:strdup
|
||||
...
|
||||
fun:ares_init_options
|
||||
}
|
||||
|
||||
# vips__init warnings
|
||||
{
|
||||
leak_libvips_init
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: reachable
|
||||
fun:malloc
|
||||
...
|
||||
fun:vips__init
|
||||
}
|
||||
62
test/unit/alpha.js
Executable file
@@ -0,0 +1,62 @@
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert');
|
||||
|
||||
var sharp = require('../../index');
|
||||
var fixtures = require('../fixtures');
|
||||
|
||||
describe('Alpha transparency', function() {
|
||||
|
||||
it('Flatten to black', function(done) {
|
||||
sharp(fixtures.inputPngWithTransparency)
|
||||
.flatten()
|
||||
.resize(400, 300)
|
||||
.toFile(fixtures.path('output.flatten-black.jpg'), done);
|
||||
});
|
||||
|
||||
it('Flatten to RGB orange', function(done) {
|
||||
sharp(fixtures.inputPngWithTransparency)
|
||||
.flatten()
|
||||
.background({r: 255, g: 102, b: 0})
|
||||
.resize(400, 300)
|
||||
.toFile(fixtures.path('output.flatten-rgb-orange.jpg'), done);
|
||||
});
|
||||
|
||||
it('Flatten to CSS/hex orange', function(done) {
|
||||
sharp(fixtures.inputPngWithTransparency)
|
||||
.flatten()
|
||||
.background('#ff6600')
|
||||
.resize(400, 300)
|
||||
.toFile(fixtures.path('output.flatten-hex-orange.jpg'), done);
|
||||
});
|
||||
|
||||
it('Do not flatten', function(done) {
|
||||
sharp(fixtures.inputPngWithTransparency)
|
||||
.flatten(false)
|
||||
.toBuffer(function(err, data) {
|
||||
if (err) throw err;
|
||||
sharp(data).metadata(function(err, metadata) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('png', metadata.format);
|
||||
assert.strictEqual(4, metadata.channels);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Ignored for JPEG', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.background('#ff0000')
|
||||
.flatten()
|
||||
.toBuffer(function(err, data) {
|
||||
if (err) throw err;
|
||||
sharp(data).metadata(function(err, metadata) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('jpeg', metadata.format);
|
||||
assert.strictEqual(3, metadata.channels);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
69
test/unit/colourspace.js
Executable file
@@ -0,0 +1,69 @@
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert');
|
||||
|
||||
var sharp = require('../../index');
|
||||
var fixtures = require('../fixtures');
|
||||
|
||||
describe('Colour space conversion', function() {
|
||||
|
||||
it('To greyscale', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(320, 240)
|
||||
.greyscale()
|
||||
.toFile(fixtures.path('output.greyscale-gamma-0.0.jpg'), done);
|
||||
});
|
||||
|
||||
it('To greyscale with gamma correction', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(320, 240)
|
||||
.gamma()
|
||||
.greyscale()
|
||||
.toFile(fixtures.path('output.greyscale-gamma-2.2.jpg'), done);
|
||||
});
|
||||
|
||||
it('Not to greyscale', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(320, 240)
|
||||
.greyscale(false)
|
||||
.toFile(fixtures.path('output.greyscale-not.jpg'), done);
|
||||
});
|
||||
|
||||
it('From 1-bit TIFF to sRGB WebP [slow]', function(done) {
|
||||
sharp(fixtures.inputTiff)
|
||||
.webp()
|
||||
.toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('webp', info.format);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('From CMYK to sRGB', function(done) {
|
||||
sharp(fixtures.inputJpgWithCmykProfile)
|
||||
.resize(320)
|
||||
.toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('From CMYK to sRGB with white background, not yellow', function(done) {
|
||||
sharp(fixtures.inputJpgWithCmykProfile)
|
||||
.resize(320, 240)
|
||||
.background('white')
|
||||
.embed()
|
||||
.toFile(fixtures.path('output.cmyk2srgb.jpg'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(240, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
93
test/unit/crop.js
Executable file
@@ -0,0 +1,93 @@
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert');
|
||||
|
||||
var sharp = require('../../index');
|
||||
var fixtures = require('../fixtures');
|
||||
|
||||
describe('Crop gravities', function() {
|
||||
|
||||
it('North', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(320, 80)
|
||||
.crop(sharp.gravity.north)
|
||||
.toFile(fixtures.path('output.gravity-north.jpg'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(80, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('East', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(80, 320)
|
||||
.crop(sharp.gravity.east)
|
||||
.toFile(fixtures.path('output.gravity-east.jpg'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(80, info.width);
|
||||
assert.strictEqual(320, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('South', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(320, 80)
|
||||
.crop(sharp.gravity.south)
|
||||
.toFile(fixtures.path('output.gravity-south.jpg'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(80, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('West', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(80, 320)
|
||||
.crop(sharp.gravity.west)
|
||||
.toFile(fixtures.path('output.gravity-west.jpg'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(80, info.width);
|
||||
assert.strictEqual(320, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Center', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(320, 80)
|
||||
.crop(sharp.gravity.center)
|
||||
.toFile(fixtures.path('output.gravity-center.jpg'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(80, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Centre', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(80, 320)
|
||||
.crop(sharp.gravity.centre)
|
||||
.toFile(fixtures.path('output.gravity-centre.jpg'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(80, info.width);
|
||||
assert.strictEqual(320, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Invalid', function(done) {
|
||||
var isValid = true;
|
||||
try {
|
||||
sharp(fixtures.inputJpg).crop(5);
|
||||
} catch (err) {
|
||||
isValid = false;
|
||||
}
|
||||
assert.strictEqual(false, isValid);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
49
test/unit/embed.js
Executable file
@@ -0,0 +1,49 @@
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert');
|
||||
|
||||
var sharp = require('../../index');
|
||||
var fixtures = require('../fixtures');
|
||||
|
||||
describe('Embed', function() {
|
||||
|
||||
it('JPEG within PNG, no alpha channel', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.embed()
|
||||
.resize(320, 240)
|
||||
.png()
|
||||
.toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('png', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(240, info.height);
|
||||
sharp(data).metadata(function(err, metadata) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(3, metadata.channels);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('JPEG within WebP, to include alpha channel', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(320, 240)
|
||||
.background({r: 0, g: 0, b: 0, a: 0})
|
||||
.embed()
|
||||
.webp()
|
||||
.toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('webp', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(240, info.height);
|
||||
sharp(data).metadata(function(err, metadata) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(4, metadata.channels);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
93
test/unit/extract.js
Executable file
@@ -0,0 +1,93 @@
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert');
|
||||
|
||||
var sharp = require('../../index');
|
||||
var fixtures = require('../fixtures');
|
||||
|
||||
describe('Partial image extraction', function() {
|
||||
|
||||
it('JPEG', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.extract(2, 2, 20, 20)
|
||||
.toFile(fixtures.path('output.extract.jpg'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(20, info.width);
|
||||
assert.strictEqual(20, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('PNG', function(done) {
|
||||
sharp(fixtures.inputPng)
|
||||
.extract(300, 200, 400, 200)
|
||||
.toFile(fixtures.path('output.extract.png'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(400, info.width);
|
||||
assert.strictEqual(200, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('WebP', function(done) {
|
||||
sharp(fixtures.inputWebP)
|
||||
.extract(50, 100, 125, 200)
|
||||
.toFile(fixtures.path('output.extract.webp'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(125, info.width);
|
||||
assert.strictEqual(200, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('TIFF', function(done) {
|
||||
sharp(fixtures.inputTiff)
|
||||
.extract(63, 34, 341, 529)
|
||||
.toFile(fixtures.path('output.extract.tiff'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(341, info.width);
|
||||
assert.strictEqual(529, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Before resize', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.extract(10, 10, 10, 500, 500)
|
||||
.resize(100, 100)
|
||||
.toFile(fixtures.path('output.extract.resize.jpg'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(100, info.width);
|
||||
assert.strictEqual(100, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('After resize and crop', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(500, 500)
|
||||
.crop(sharp.gravity.north)
|
||||
.extract(10, 10, 100, 100)
|
||||
.toFile(fixtures.path('output.resize.crop.extract.jpg'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(100, info.width);
|
||||
assert.strictEqual(100, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Before and after resize and crop', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.extract(0, 0, 700, 700)
|
||||
.resize(500, 500)
|
||||
.crop(sharp.gravity.north)
|
||||
.extract(10, 10, 100, 100)
|
||||
.toFile(fixtures.path('output.extract.resize.crop.extract.jpg'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(100, info.width);
|
||||
assert.strictEqual(100, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
41
test/unit/gamma.js
Executable file
@@ -0,0 +1,41 @@
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert');
|
||||
|
||||
var sharp = require('../../index');
|
||||
var fixtures = require('../fixtures');
|
||||
|
||||
describe('Gamma correction', function() {
|
||||
|
||||
it('value of 0.0 (disabled)', function(done) {
|
||||
sharp(fixtures.inputJpgWithGammaHoliness)
|
||||
.resize(129, 111)
|
||||
.toFile(fixtures.path('output.gamma-0.0.jpg'), done);
|
||||
});
|
||||
|
||||
it('value of 2.2 (default)', function(done) {
|
||||
sharp(fixtures.inputJpgWithGammaHoliness)
|
||||
.resize(129, 111)
|
||||
.gamma()
|
||||
.toFile(fixtures.path('output.gamma-2.2.jpg'), done);
|
||||
});
|
||||
|
||||
it('value of 3.0', function(done) {
|
||||
sharp(fixtures.inputJpgWithGammaHoliness)
|
||||
.resize(129, 111)
|
||||
.gamma(3)
|
||||
.toFile(fixtures.path('output.gamma-3.0.jpg'), done);
|
||||
});
|
||||
|
||||
it('invalid value', function(done) {
|
||||
var isValid = true;
|
||||
try {
|
||||
sharp(fixtures.inputJpgWithGammaHoliness).gamma(4);
|
||||
} catch (err) {
|
||||
isValid = false;
|
||||
}
|
||||
assert.strictEqual(false, isValid);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
94
test/unit/interpolation.js
Executable file
@@ -0,0 +1,94 @@
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert');
|
||||
|
||||
var sharp = require('../../index');
|
||||
var fixtures = require('../fixtures');
|
||||
|
||||
describe('Interpolation', function() {
|
||||
|
||||
it('nearest neighbour', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(320, 240)
|
||||
.interpolateWith(sharp.interpolator.nearest)
|
||||
.toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(240, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('bilinear', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(320, 240)
|
||||
.interpolateWith(sharp.interpolator.bilinear)
|
||||
.toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(240, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('bicubic', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(320, 240)
|
||||
.interpolateWith(sharp.interpolator.bicubic)
|
||||
.toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(240, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('nohalo', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(320, 240)
|
||||
.interpolateWith(sharp.interpolator.nohalo)
|
||||
.toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(240, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('locally bounded bicubic (LBB)', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(320, 240)
|
||||
.interpolateWith(sharp.interpolator.locallyBoundedBicubic)
|
||||
.toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(240, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('vertex split quadratic basis spline (VSQBS)', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(320, 240)
|
||||
.interpolateWith(sharp.interpolator.vertexSplitQuadraticBasisSpline)
|
||||
.toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(240, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
370
test/unit/io.js
Executable file
@@ -0,0 +1,370 @@
|
||||
'use strict';
|
||||
|
||||
var fs = require('fs');
|
||||
var assert = require('assert');
|
||||
|
||||
var sharp = require('../../index');
|
||||
var fixtures = require('../fixtures');
|
||||
|
||||
describe('Input/output', function() {
|
||||
|
||||
it('Read from File and write to Stream', function(done) {
|
||||
var writable = fs.createWriteStream(fixtures.outputJpg);
|
||||
writable.on('finish', function() {
|
||||
sharp(fixtures.outputJpg).toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(240, info.height);
|
||||
fs.unlinkSync(fixtures.outputJpg);
|
||||
done();
|
||||
});
|
||||
});
|
||||
sharp(fixtures.inputJpg).resize(320, 240).pipe(writable);
|
||||
});
|
||||
|
||||
it('Read from Buffer and write to Stream', function(done) {
|
||||
var inputJpgBuffer = fs.readFileSync(fixtures.inputJpg);
|
||||
var writable = fs.createWriteStream(fixtures.outputJpg);
|
||||
writable.on('finish', function() {
|
||||
sharp(fixtures.outputJpg).toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(240, info.height);
|
||||
fs.unlinkSync(fixtures.outputJpg);
|
||||
done();
|
||||
});
|
||||
});
|
||||
sharp(inputJpgBuffer).resize(320, 240).pipe(writable);
|
||||
});
|
||||
|
||||
it('Read from Stream and write to File', function(done) {
|
||||
var readable = fs.createReadStream(fixtures.inputJpg);
|
||||
var pipeline = sharp().resize(320, 240).toFile(fixtures.outputJpg, function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(240, info.height);
|
||||
fs.unlinkSync(fixtures.outputJpg);
|
||||
done();
|
||||
});
|
||||
readable.pipe(pipeline);
|
||||
});
|
||||
|
||||
it('Read from Stream and write to Buffer', function(done) {
|
||||
var readable = fs.createReadStream(fixtures.inputJpg);
|
||||
var pipeline = sharp().resize(320, 240).toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(240, info.height);
|
||||
done();
|
||||
});
|
||||
readable.pipe(pipeline);
|
||||
});
|
||||
|
||||
it('Read from Stream and write to Buffer via Promise', function(done) {
|
||||
var readable = fs.createReadStream(fixtures.inputJpg);
|
||||
var pipeline = sharp().resize(1, 1);
|
||||
pipeline.toBuffer().then(function(data) {
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
done();
|
||||
}).catch(function(err) {
|
||||
throw err;
|
||||
});
|
||||
readable.pipe(pipeline);
|
||||
});
|
||||
|
||||
it('Read from Stream and write to Stream', function(done) {
|
||||
var readable = fs.createReadStream(fixtures.inputJpg);
|
||||
var writable = fs.createWriteStream(fixtures.outputJpg);
|
||||
writable.on('finish', function() {
|
||||
sharp(fixtures.outputJpg).toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(240, info.height);
|
||||
fs.unlinkSync(fixtures.outputJpg);
|
||||
done();
|
||||
});
|
||||
});
|
||||
var pipeline = sharp().resize(320, 240);
|
||||
readable.pipe(pipeline).pipe(writable);
|
||||
});
|
||||
|
||||
it('Handle Stream to Stream error ', function(done) {
|
||||
var pipeline = sharp().resize(320, 240);
|
||||
var anErrorWasEmitted = false;
|
||||
pipeline.on('error', function(err) {
|
||||
anErrorWasEmitted = !!err;
|
||||
}).on('end', function() {
|
||||
assert(anErrorWasEmitted);
|
||||
fs.unlinkSync(fixtures.outputJpg);
|
||||
done();
|
||||
});
|
||||
var readableButNotAnImage = fs.createReadStream(__filename);
|
||||
var writable = fs.createWriteStream(fixtures.outputJpg);
|
||||
readableButNotAnImage.pipe(pipeline).pipe(writable);
|
||||
});
|
||||
|
||||
it('Handle File to Stream error', function(done) {
|
||||
var readableButNotAnImage = sharp(__filename).resize(320, 240);
|
||||
var anErrorWasEmitted = false;
|
||||
readableButNotAnImage.on('error', function(err) {
|
||||
anErrorWasEmitted = !!err;
|
||||
}).on('end', function() {
|
||||
assert(anErrorWasEmitted);
|
||||
fs.unlinkSync(fixtures.outputJpg);
|
||||
done();
|
||||
});
|
||||
var writable = fs.createWriteStream(fixtures.outputJpg);
|
||||
readableButNotAnImage.pipe(writable);
|
||||
});
|
||||
|
||||
it('Sequential read', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.sequentialRead()
|
||||
.resize(320, 240)
|
||||
.toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(240, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Not sequential read', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.sequentialRead(false)
|
||||
.resize(320, 240)
|
||||
.toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(240, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Fail when output File is input File', function(done) {
|
||||
sharp(fixtures.inputJpg).toFile(fixtures.inputJpg, function(err) {
|
||||
assert(!!err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Fail when output File is input File via Promise', function(done) {
|
||||
sharp(fixtures.inputJpg).toFile(fixtures.inputJpg).then(function(data) {
|
||||
assert(false);
|
||||
done();
|
||||
}).catch(function(err) {
|
||||
assert(!!err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Fail when output File is empty', function(done) {
|
||||
sharp(fixtures.inputJpg).toFile('', function(err) {
|
||||
assert(!!err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Fail when output File is empty via Promise', function(done) {
|
||||
sharp(fixtures.inputJpg).toFile('').then(function(data) {
|
||||
assert(false);
|
||||
done();
|
||||
}).catch(function(err) {
|
||||
assert(!!err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Fail when input is empty Buffer', function(done) {
|
||||
var failed = true;
|
||||
try {
|
||||
sharp(new Buffer(0));
|
||||
failed = false;
|
||||
} catch (err) {
|
||||
assert(err instanceof Error);
|
||||
}
|
||||
assert(failed);
|
||||
done();
|
||||
});
|
||||
|
||||
it('Fail when input is invalid Buffer', function(done) {
|
||||
var failed = true;
|
||||
try {
|
||||
sharp(new Buffer([0x1, 0x2, 0x3, 0x4]));
|
||||
failed = false;
|
||||
} catch (err) {
|
||||
assert(err instanceof Error);
|
||||
}
|
||||
assert(failed);
|
||||
done();
|
||||
});
|
||||
|
||||
it('Promises/A+', function(done) {
|
||||
sharp(fixtures.inputJpg).resize(320, 240).toBuffer().then(function(data) {
|
||||
sharp(data).toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(240, info.height);
|
||||
done();
|
||||
});
|
||||
}).catch(function(err) {
|
||||
throw err;
|
||||
});
|
||||
});
|
||||
|
||||
it('JPEG quality', function(done) {
|
||||
sharp(fixtures.inputJpg).resize(320, 240).quality(70).toBuffer(function(err, buffer70) {
|
||||
if (err) throw err;
|
||||
sharp(fixtures.inputJpg).resize(320, 240).toBuffer(function(err, buffer80) {
|
||||
if (err) throw err;
|
||||
sharp(fixtures.inputJpg).resize(320, 240).quality(90).toBuffer(function(err, buffer90) {
|
||||
if (err) throw err;
|
||||
assert(buffer70.length < buffer80.length);
|
||||
assert(buffer80.length < buffer90.length);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Invalid quality', function(done) {
|
||||
var isValid = true;
|
||||
try {
|
||||
sharp(fixtures.inputJpg).quality(-1);
|
||||
} catch (err) {
|
||||
isValid = false;
|
||||
}
|
||||
assert.strictEqual(false, isValid);
|
||||
done();
|
||||
});
|
||||
|
||||
it('Progressive image', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(320, 240)
|
||||
.png()
|
||||
.progressive(false)
|
||||
.toBuffer(function(err, nonProgressive, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, nonProgressive.length > 0);
|
||||
assert.strictEqual('png', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(240, info.height);
|
||||
sharp(nonProgressive)
|
||||
.progressive()
|
||||
.toBuffer(function(err, progressive, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, progressive.length > 0);
|
||||
assert.strictEqual(true, progressive.length > nonProgressive.length);
|
||||
assert.strictEqual('png', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(240, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Output filename without extension uses input format', function() {
|
||||
|
||||
it('JPEG', function(done) {
|
||||
sharp(fixtures.inputJpg).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(80, info.height);
|
||||
fs.unlinkSync(fixtures.outputZoinks);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('PNG', function(done) {
|
||||
sharp(fixtures.inputPng).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('png', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(80, info.height);
|
||||
fs.unlinkSync(fixtures.outputZoinks);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Transparent PNG', function(done) {
|
||||
sharp(fixtures.inputPngWithTransparency).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('png', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(80, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('WebP', function(done) {
|
||||
sharp(fixtures.inputWebP).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('webp', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(80, info.height);
|
||||
fs.unlinkSync(fixtures.outputZoinks);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('TIFF', function(done) {
|
||||
sharp(fixtures.inputTiff).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('tiff', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(80, info.height);
|
||||
fs.unlinkSync(fixtures.outputZoinks);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Fail with GIF', function(done) {
|
||||
sharp(fixtures.inputGif).resize(320, 80).toFile(fixtures.outputZoinks, function(err) {
|
||||
assert(!!err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('PNG compression level', function() {
|
||||
|
||||
it('valid', function(done) {
|
||||
var isValid = false;
|
||||
try {
|
||||
sharp().compressionLevel(0);
|
||||
isValid = true;
|
||||
} catch (e) {}
|
||||
assert(isValid);
|
||||
done();
|
||||
});
|
||||
|
||||
it('invalid', function(done) {
|
||||
var isValid = false;
|
||||
try {
|
||||
sharp().compressionLevel(-1);
|
||||
isValid = true;
|
||||
} catch (e) {}
|
||||
assert(!isValid);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
1
test/unit/jshint.js
Executable file
@@ -0,0 +1 @@
|
||||
require('mocha-jshint')();
|
||||
188
test/unit/metadata.js
Executable file
@@ -0,0 +1,188 @@
|
||||
'use strict';
|
||||
|
||||
var fs = require('fs');
|
||||
var assert = require('assert');
|
||||
|
||||
var sharp = require('../../index');
|
||||
var fixtures = require('../fixtures');
|
||||
|
||||
describe('Image metadata', function() {
|
||||
|
||||
it('JPEG', function(done) {
|
||||
sharp(fixtures.inputJpg).metadata(function(err, metadata) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('jpeg', metadata.format);
|
||||
assert.strictEqual(2725, metadata.width);
|
||||
assert.strictEqual(2225, metadata.height);
|
||||
assert.strictEqual('srgb', metadata.space);
|
||||
assert.strictEqual(3, metadata.channels);
|
||||
assert.strictEqual('undefined', typeof metadata.orientation);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('JPEG with EXIF', function(done) {
|
||||
sharp(fixtures.inputJpgWithExif).metadata(function(err, metadata) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('jpeg', metadata.format);
|
||||
assert.strictEqual(450, metadata.width);
|
||||
assert.strictEqual(600, metadata.height);
|
||||
assert.strictEqual('srgb', metadata.space);
|
||||
assert.strictEqual(3, metadata.channels);
|
||||
assert.strictEqual(false, metadata.hasAlpha);
|
||||
assert.strictEqual(8, metadata.orientation);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('TIFF', function(done) {
|
||||
sharp(fixtures.inputTiff).metadata(function(err, metadata) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('tiff', metadata.format);
|
||||
assert.strictEqual(2464, metadata.width);
|
||||
assert.strictEqual(3248, metadata.height);
|
||||
assert.strictEqual('b-w', metadata.space);
|
||||
assert.strictEqual(1, metadata.channels);
|
||||
assert.strictEqual(false, metadata.hasAlpha);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('PNG', function(done) {
|
||||
sharp(fixtures.inputPng).metadata(function(err, metadata) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('png', metadata.format);
|
||||
assert.strictEqual(2809, metadata.width);
|
||||
assert.strictEqual(2074, metadata.height);
|
||||
assert.strictEqual('b-w', metadata.space);
|
||||
assert.strictEqual(1, metadata.channels);
|
||||
assert.strictEqual(false, metadata.hasAlpha);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Transparent PNG', function(done) {
|
||||
sharp(fixtures.inputPngWithTransparency).metadata(function(err, metadata) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('png', metadata.format);
|
||||
assert.strictEqual(2048, metadata.width);
|
||||
assert.strictEqual(1536, metadata.height);
|
||||
assert.strictEqual('srgb', metadata.space);
|
||||
assert.strictEqual(4, metadata.channels);
|
||||
assert.strictEqual(true, metadata.hasAlpha);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('WebP', function(done) {
|
||||
sharp(fixtures.inputWebP).metadata(function(err, metadata) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('webp', metadata.format);
|
||||
assert.strictEqual(1024, metadata.width);
|
||||
assert.strictEqual(772, metadata.height);
|
||||
assert.strictEqual('srgb', metadata.space);
|
||||
assert.strictEqual(3, metadata.channels);
|
||||
assert.strictEqual(false, metadata.hasAlpha);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('GIF via libmagick', function(done) {
|
||||
sharp(fixtures.inputGif).metadata(function(err, metadata) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('magick', metadata.format);
|
||||
assert.strictEqual(800, metadata.width);
|
||||
assert.strictEqual(533, metadata.height);
|
||||
assert.strictEqual(3, metadata.channels);
|
||||
assert.strictEqual(false, metadata.hasAlpha);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('File in, Promise out', function(done) {
|
||||
sharp(fixtures.inputJpg).metadata().then(function(metadata) {
|
||||
assert.strictEqual('jpeg', metadata.format);
|
||||
assert.strictEqual(2725, metadata.width);
|
||||
assert.strictEqual(2225, metadata.height);
|
||||
assert.strictEqual('srgb', metadata.space);
|
||||
assert.strictEqual(3, metadata.channels);
|
||||
assert.strictEqual(false, metadata.hasAlpha);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Stream in, Promise out', function(done) {
|
||||
var readable = fs.createReadStream(fixtures.inputJpg);
|
||||
var pipeline = sharp();
|
||||
pipeline.metadata().then(function(metadata) {
|
||||
assert.strictEqual('jpeg', metadata.format);
|
||||
assert.strictEqual(2725, metadata.width);
|
||||
assert.strictEqual(2225, metadata.height);
|
||||
assert.strictEqual('srgb', metadata.space);
|
||||
assert.strictEqual(3, metadata.channels);
|
||||
assert.strictEqual(false, metadata.hasAlpha);
|
||||
done();
|
||||
}).catch(function(err) {
|
||||
throw err;
|
||||
});
|
||||
readable.pipe(pipeline);
|
||||
});
|
||||
|
||||
it('Stream', function(done) {
|
||||
var readable = fs.createReadStream(fixtures.inputJpg);
|
||||
var pipeline = sharp().metadata(function(err, metadata) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('jpeg', metadata.format);
|
||||
assert.strictEqual(2725, metadata.width);
|
||||
assert.strictEqual(2225, metadata.height);
|
||||
assert.strictEqual('srgb', metadata.space);
|
||||
assert.strictEqual(3, metadata.channels);
|
||||
assert.strictEqual(false, metadata.hasAlpha);
|
||||
done();
|
||||
});
|
||||
readable.pipe(pipeline);
|
||||
});
|
||||
|
||||
it('Resize to half width using metadata', function(done) {
|
||||
var image = sharp(fixtures.inputJpg);
|
||||
image.metadata(function(err, metadata) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('jpeg', metadata.format);
|
||||
assert.strictEqual(2725, metadata.width);
|
||||
assert.strictEqual(2225, metadata.height);
|
||||
assert.strictEqual('srgb', metadata.space);
|
||||
assert.strictEqual(3, metadata.channels);
|
||||
assert.strictEqual(false, metadata.hasAlpha);
|
||||
image.resize(metadata.width / 2).toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual(1362, info.width);
|
||||
assert.strictEqual(1112, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Keep EXIF metadata after a resize', function(done) {
|
||||
sharp(fixtures.inputJpgWithExif).resize(320, 240).withMetadata().toBuffer(function(err, buffer) {
|
||||
if (err) throw err;
|
||||
sharp(buffer).metadata(function(err, metadata) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(8, metadata.orientation);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Remove EXIF metadata after a resize', function(done) {
|
||||
sharp(fixtures.inputJpgWithExif).resize(320, 240).withMetadata(false).toBuffer(function(err, buffer) {
|
||||
if (err) throw err;
|
||||
sharp(buffer).metadata(function(err, metadata) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('undefined', typeof metadata.orientation);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
184
test/unit/resize.js
Executable file
@@ -0,0 +1,184 @@
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert');
|
||||
|
||||
var sharp = require('../../index');
|
||||
var fixtures = require('../fixtures');
|
||||
|
||||
describe('Resize dimensions', function() {
|
||||
|
||||
it('Exact crop', function(done) {
|
||||
sharp(fixtures.inputJpg).resize(320, 240).toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(240, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Fixed width', function(done) {
|
||||
sharp(fixtures.inputJpg).resize(320).toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(261, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Fixed height', function(done) {
|
||||
sharp(fixtures.inputJpg).resize(null, 320).toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(391, info.width);
|
||||
assert.strictEqual(320, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Identity transform', function(done) {
|
||||
sharp(fixtures.inputJpg).toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(2725, info.width);
|
||||
assert.strictEqual(2225, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Upscale', function(done) {
|
||||
sharp(fixtures.inputJpg).resize(3000).toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(3000, info.width);
|
||||
assert.strictEqual(2449, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Invalid width', function(done) {
|
||||
var isValid = true;
|
||||
try {
|
||||
sharp(fixtures.inputJpg).resize('spoons', 240);
|
||||
} catch (err) {
|
||||
isValid = false;
|
||||
}
|
||||
assert.strictEqual(false, isValid);
|
||||
done();
|
||||
});
|
||||
|
||||
it('Invalid height', function(done) {
|
||||
var isValid = true;
|
||||
try {
|
||||
sharp(fixtures.inputJpg).resize(320, 'spoons');
|
||||
} catch (err) {
|
||||
isValid = false;
|
||||
}
|
||||
assert.strictEqual(false, isValid);
|
||||
done();
|
||||
});
|
||||
|
||||
it('TIFF embed known to cause rounding errors', function(done) {
|
||||
sharp(fixtures.inputTiff).resize(240, 320).embed().jpeg().toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(240, info.width);
|
||||
assert.strictEqual(320, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('TIFF known to cause rounding errors', function(done) {
|
||||
sharp(fixtures.inputTiff).resize(240, 320).jpeg().toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(240, info.width);
|
||||
assert.strictEqual(320, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Max width or height considering ratio (landscape)', function(done) {
|
||||
sharp(fixtures.inputJpg).resize(320, 320).max().toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(261, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Max width or height considering ratio (portrait)', function(done) {
|
||||
sharp(fixtures.inputTiff).resize(320, 320).max().jpeg().toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(243, info.width);
|
||||
assert.strictEqual(320, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Provide only one dimension with max, should default to crop', function(done) {
|
||||
sharp(fixtures.inputJpg).resize(320).max().toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(261, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Do not enlarge when input width is already less than output width', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(2800)
|
||||
.withoutEnlargement()
|
||||
.toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(2725, info.width);
|
||||
assert.strictEqual(2225, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Do not enlarge when input height is already less than output height', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(null, 2300)
|
||||
.withoutEnlargement()
|
||||
.toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(2725, info.width);
|
||||
assert.strictEqual(2225, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Do enlarge when input width is less than output width', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(2800)
|
||||
.withoutEnlargement(false)
|
||||
.toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(2800, info.width);
|
||||
assert.strictEqual(2286, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
134
test/unit/rotate.js
Executable file
@@ -0,0 +1,134 @@
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert');
|
||||
|
||||
var sharp = require('../../index');
|
||||
var fixtures = require('../fixtures');
|
||||
|
||||
describe('Rotation', function() {
|
||||
|
||||
it('Rotate by 90 degrees, respecting output input size', function(done) {
|
||||
sharp(fixtures.inputJpg).rotate(90).resize(320, 240).toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(240, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Input image has Orientation EXIF tag but do not rotate output', function(done) {
|
||||
sharp(fixtures.inputJpgWithExif)
|
||||
.resize(320)
|
||||
.toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(426, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Input image has Orientation EXIF tag value of 8 (270 degrees), auto-rotate', function(done) {
|
||||
sharp(fixtures.inputJpgWithExif)
|
||||
.rotate()
|
||||
.resize(320)
|
||||
.toFile(fixtures.path('output.exif.8.jpg'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(240, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Input image has Orientation EXIF tag value of 5 (270 degrees + flip), auto-rotate', function(done) {
|
||||
sharp(fixtures.inputJpgWithExifMirroring)
|
||||
.rotate()
|
||||
.resize(320)
|
||||
.toFile(fixtures.path('output.exif.5.jpg'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(240, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Attempt to auto-rotate using image that has no EXIF', function(done) {
|
||||
sharp(fixtures.inputJpg).rotate().resize(320).toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(261, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Rotate to an invalid angle, should fail', function(done) {
|
||||
var fail = false;
|
||||
try {
|
||||
sharp(fixtures.inputJpg).rotate(1);
|
||||
fail = true;
|
||||
} catch (e) {}
|
||||
assert(!fail);
|
||||
done();
|
||||
});
|
||||
|
||||
it('Flip - vertical', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(320)
|
||||
.flip()
|
||||
.toFile(fixtures.path('output.flip.jpg'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(261, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Flop - horizontal', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(320)
|
||||
.flop()
|
||||
.toFile(fixtures.path('output.flop.jpg'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(261, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Flip and flop', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(320)
|
||||
.flop()
|
||||
.toFile(fixtures.path('output.flip.flop.jpg'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(261, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Neither flip nor flop', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(320)
|
||||
.flip(false)
|
||||
.flop(false)
|
||||
.toFile(fixtures.path('output.flip.flop.nope.jpg'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(261, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
34
test/unit/sharpen.js
Executable file
@@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert');
|
||||
|
||||
var sharp = require('../../index');
|
||||
var fixtures = require('../fixtures');
|
||||
|
||||
describe('Sharpen', function() {
|
||||
|
||||
it('sharpen image is larger than non-sharpen', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(320, 240)
|
||||
.sharpen(false)
|
||||
.toBuffer(function(err, notSharpened, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, notSharpened.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(240, info.height);
|
||||
sharp(notSharpened)
|
||||
.sharpen()
|
||||
.toBuffer(function(err, sharpened, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, sharpened.length > 0);
|
||||
assert.strictEqual(true, sharpened.length > notSharpened.length);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(240, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
53
test/unit/util.js
Executable file
@@ -0,0 +1,53 @@
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert');
|
||||
var sharp = require('../../index');
|
||||
|
||||
var defaultConcurrency = sharp.concurrency();
|
||||
|
||||
describe('Utilities', function() {
|
||||
|
||||
describe('Cache', function() {
|
||||
it('Can be disabled', function() {
|
||||
var cache = sharp.cache(0, 0);
|
||||
assert.strictEqual(0, cache.memory);
|
||||
assert.strictEqual(0, cache.items);
|
||||
});
|
||||
it('Can be set to a maximum of 50MB and 500 items', function() {
|
||||
var cache = sharp.cache(50, 500);
|
||||
assert.strictEqual(50, cache.memory);
|
||||
assert.strictEqual(500, cache.items);
|
||||
});
|
||||
it('Ignores invalid values', function() {
|
||||
sharp.cache(50, 500);
|
||||
var cache = sharp.cache('spoons');
|
||||
assert.strictEqual(50, cache.memory);
|
||||
assert.strictEqual(500, cache.items);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Concurrency', function() {
|
||||
it('Can be set to use 16 threads', function() {
|
||||
sharp.concurrency(16);
|
||||
assert.strictEqual(16, sharp.concurrency());
|
||||
});
|
||||
it('Can be reset to default', function() {
|
||||
sharp.concurrency(0);
|
||||
assert.strictEqual(defaultConcurrency, sharp.concurrency());
|
||||
});
|
||||
it('Ignores invalid values', function() {
|
||||
sharp.concurrency(0);
|
||||
sharp.concurrency('spoons');
|
||||
assert.strictEqual(defaultConcurrency, sharp.concurrency());
|
||||
});
|
||||
});
|
||||
|
||||
describe('Counters', function() {
|
||||
it('Have zero value at rest', function() {
|
||||
var counters = sharp.counters();
|
||||
assert.strictEqual(0, counters.queue);
|
||||
assert.strictEqual(0, counters.process);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
var sharp = require("../index");
|
||||
var fs = require("fs");
|
||||
var assert = require("assert");
|
||||
var async = require("async");
|
||||
|
||||
var inputJpg = __dirname + "/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.resize(inputJpg, sharp.buffer.jpeg, width, height, 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() {});
|
||||
299
tests/perf.js
@@ -1,299 +0,0 @@
|
||||
var sharp = require("../index");
|
||||
var fs = require("fs");
|
||||
var imagemagick = require("imagemagick");
|
||||
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 inputPng = __dirname + "/50020484-00001.png"; // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png
|
||||
var outputPng = __dirname + "/output.png";
|
||||
|
||||
var width = 720;
|
||||
var height = 480;
|
||||
|
||||
async.series({
|
||||
jpeg: function(callback) {
|
||||
var inputJpgBuffer = fs.readFileSync(inputJpg);
|
||||
(new Benchmark.Suite("jpeg")).add("imagemagick", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
imagemagick.resize({
|
||||
srcPath: inputJpg,
|
||||
dstPath: outputJpg,
|
||||
quality: 0.8,
|
||||
width: width,
|
||||
height: height
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("gm-file-file", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
gm(inputJpg).resize(width, height).quality(80).write(outputJpg, function (err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("gm-file-buffer", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
gm(inputJpg).resize(width, height).quality(80).toBuffer(function (err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).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) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("sharp-buffer-buffer", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp.resize(inputJpgBuffer, sharp.buffer.jpeg, width, height, function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("sharp-file-file", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp.resize(inputJpg, outputJpg, width, height, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("sharp-file-buffer", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp.resize(inputJpg, sharp.buffer.jpeg, width, height, 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.resize(inputJpg, sharp.buffer.jpeg, width, height, {sharpen: true}, function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("sharp-file-buffer-progressive", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp.resize(inputJpg, sharp.buffer.jpeg, 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(inputJpg, sharp.buffer.jpeg, width, height, {sequentialRead: true}, function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).on("cycle", function(event) {
|
||||
console.log("jpeg " + String(event.target));
|
||||
}).on("complete", function() {
|
||||
callback(null, this.filter("fastest").pluck("name"));
|
||||
}).run();
|
||||
},
|
||||
png: function(callback) {
|
||||
var inputPngBuffer = fs.readFileSync(inputPng);
|
||||
(new Benchmark.Suite("png")).add("imagemagick", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
imagemagick.resize({
|
||||
srcPath: inputPng,
|
||||
dstPath: outputPng,
|
||||
width: width,
|
||||
height: height
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("gm-file-file", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
gm(inputPng).resize(width, height).write(outputPng, function (err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("gm-file-buffer", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
gm(inputPng).resize(width, height).quality(80).toBuffer(function (err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("sharp-buffer-file", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp.resize(inputPngBuffer, outputPng, width, height, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("sharp-buffer-buffer", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp.resize(inputPngBuffer, sharp.buffer.png, width, height, function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("sharp-file-file", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp.resize(inputPng, outputPng, width, height, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("sharp-file-buffer", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp.resize(inputPng, sharp.buffer.png, width, height, 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.resize(inputPng, sharp.buffer.png, width, height, {sharpen: true}, function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("sharp-file-buffer-progressive", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp.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) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).on("cycle", function(event) {
|
||||
console.log(" png " + 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);
|
||||
});
|
||||
});
|
||||