Compare commits

..

73 Commits

Author SHA1 Message Date
Lovell Fuller
3ca2f009f4 Remove confusing CSS equivs introduced in 77bbbb9 2015-02-27 15:04:11 +00:00
Lovell Fuller
a900c28f7c Version bumps 2015-02-27 14:46:03 +00:00
Lovell Fuller
77bbbb9715 Improve min/max docs, thanks @LinusU
Add requirement for C++11 compiler

Init scaling factor to silence compiler warning
2015-02-27 13:49:16 +00:00
Lovell Fuller
88753a6333 Merge pull request #175 from LinusU/feature-min
feature: min
2015-02-27 13:25:13 +00:00
Linus Unnebäck
bcd82f4893 feature: min 2015-02-27 13:50:52 +01:00
Lovell Fuller
749dc61f85 JSON and the agro-noughts 2015-02-26 19:49:54 +00:00
Lovell Fuller
c7ccf6801d Expose runtime format availability
Aids addition of new format/method combos

Dogfood this in the test code
2015-02-26 19:41:33 +00:00
Lovell Fuller
1565522ecc Add libgsf dependency ahead of Deep Zoom support 2015-02-26 19:36:42 +00:00
Lovell Fuller
a44df2f533 Merge pull request #173 from LinusU/patch-1
Install libgsf-1-dev on Debian 8
2015-02-26 14:00:51 +00:00
Linus Unnebäck
317510746f Install libgsf-1-dev on Debian 8
This was needed on my docker container running Debian Jessie. This is the error without it:

```text
Package libgsf-1 was not found in the pkg-config search path.
Perhaps you should add the directory containing `libgsf-1.pc'
to the PKG_CONFIG_PATH environment variable
Package 'libgsf-1', required by 'vips', not found
gyp: Call to 'PKG_CONFIG_PATH=":$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig:/usr/lib/pkgconfig" pkg-config --libs vips' returned exit status 1. while trying to load binding.gyp
```
2015-02-26 13:44:00 +01:00
Lovell Fuller
ef54e327b7 Document GIF input via Buffer and Stream
Ensure @mcuelenaere is credited
2015-02-16 13:51:47 +00:00
Lovell Fuller
d8d0158774 Version bump of libvips to 7.42.3
Rename version vars to major, minor and patch
2015-02-16 13:49:22 +00:00
Lovell Fuller
4d75f27a25 Merge branch 'mcuelenaere-imagemagick-buffer' 2015-02-16 13:27:51 +00:00
Lovell Fuller
f89e9d726d Add support for libvips v8.0.0 2015-02-16 13:27:22 +00:00
Lovell Fuller
5194b37460 Merge branch 'imagemagick-buffer' of https://github.com/mcuelenaere/sharp into mcuelenaere-imagemagick-buffer 2015-02-16 12:02:27 +00:00
Lovell Fuller
55ea432711 Add assumeyes flag for Fedora #167 2015-02-16 11:18:42 +00:00
Maurus Cuelenaere
ab7408c96f Add support for loading images through ImageMagick as a buffer 2015-02-16 10:12:59 +01:00
Lovell Fuller
1f7e80e581 Add chroma subsampling options for JPEG output 2015-02-13 09:41:42 +00:00
Lovell Fuller
0e91ca90d6 Remove lingering NanAdjustExternalMemory
Should have been removed in fe34548b
2015-02-12 12:15:56 +00:00
Lovell Fuller
8f41fed9c2 Add toFormat convenience method #137 2015-02-12 11:37:56 +00:00
Lovell Fuller
96dd40cee1 Add Node.js 0.12 stable to CI build
Replaces 0.11 unstable
2015-02-09 17:09:37 +00:00
Lovell Fuller
62767d072b Version bumps 2015-02-09 09:52:27 +00:00
Lovell Fuller
33880ce19e Merge pull request #161 from ide/nan
Change nan dependency back to ^1.6.2
2015-02-06 21:32:27 +00:00
James Ide
988176846d Change nan dependency back to ^1.6.2
The issue with nan on io.js was fixed in https://github.com/rvagg/nan/pull/273. `npm install` on io.js 1.1.0 works now.
2015-02-06 12:30:15 -08:00
Lovell Fuller
657d436a0f Merge pull request #160 from jo/patch-1
Adjust comment in interpolation example
2015-02-06 14:28:59 +00:00
Johannes Jörg Schmidt
e5549e3063 Adjust comment in interpolation example 2015-02-06 15:17:10 +01:00
Lovell Fuller
0b2fb967b8 Add iojs to CI test matrix
Specific version of nan required
2015-02-06 09:36:45 +00:00
Lovell Fuller
f57478c1aa Update bench to latest imagemagick-native
Use 'Triangle' filter as bilinear equiv.
2015-02-02 16:16:45 +00:00
Lovell Fuller
e5a5e2ca7e Tighten 'extract' parameter validation #158 2015-01-29 22:46:04 +00:00
Lovell Fuller
797d503a99 Merge pull request #156 from jonathanong/patch-1
⬇️ nan@^1.5.1
2015-01-28 22:04:52 +00:00
Jonathan Ong
512a281986 ⬇️ nan@^1.5.1
1.6 breaks iojs. this lowers the minimum nan version so that developers can still do `nan@~1.5.1`.
2015-01-27 15:11:40 -08:00
Lovell Fuller
37c5ca7166 Skip SVG test when format is unavailable
It's possible to compile *magick without SVG support
2015-01-25 11:34:16 +00:00
Lovell Fuller
cda700ef73 Update libmagick references to the C library 2015-01-23 11:00:39 +00:00
Lovell Fuller
d32901da8d Dependency version bumps 2015-01-23 10:50:50 +00:00
Lovell Fuller
83ebe12061 Remove atexit handler as libvips defines this
New grunt-sharp build tool

Version bump for latest libvips
2015-01-22 14:17:20 +00:00
Lovell Fuller
fe34548bad Remove optional AdjustAmountOfExternalAllocatedMemory #151
Isolate not available when deleting the buffer
2015-01-21 20:13:43 +00:00
Lovell Fuller
855945bef2 Delete input buffer on postclose #151
Notify V8 GC of memory (de)allocation
2015-01-21 10:34:03 +00:00
Lovell Fuller
8421e3aa5f Add limitInputPixels option to reject input #115 2015-01-20 14:18:05 +00:00
Lovell Fuller
c93f79daa7 Guard against InitImage failure #150
Protects against truncated image headers
2015-01-20 10:38:44 +00:00
Lovell Fuller
35c53f78c8 Ensure bench/random test uses int dimensions 2015-01-16 22:31:46 +00:00
Lovell Fuller
c158d51f8b Explicit cast to uint32 required for nan 1.5.x
See rvagg/nan#229
2015-01-16 22:31:46 +00:00
Lovell Fuller
8e9a8dfede Remove cpplint namespace-related warnings 2015-01-16 22:31:46 +00:00
Lovell Fuller
67dc694cfb Link to new contributor guide
Remove now-duplicate content
2015-01-16 22:31:46 +00:00
Lovell Fuller
74704a132c Add a contribution guide to help those who help 2015-01-16 22:31:46 +00:00
Lovell Fuller
b86674f91f Add cpplint to test suite 2015-01-16 22:31:46 +00:00
Lovell Fuller
5dab3c8482 Allow rotate before pre-resize extraction #145 2015-01-16 22:30:57 +00:00
Lovell Fuller
a190ae6b08 Add raw, uncompressed image data output #136 2015-01-16 22:28:24 +00:00
Lovell Fuller
464fb1726d Keep output dimensions within WebP 14-bit range 2015-01-16 22:28:24 +00:00
Lovell Fuller
065ce6454b Explicit cast of size_t to 32 bit integer
Ensures compilation with nan 1.5.0+
2015-01-15 15:16:01 +00:00
Lovell Fuller
850c2ecdd6 Version bumps 2014-12-15 14:02:30 +00:00
Lovell Fuller
926c5603aa Improve documentation on concurrency/parallelism 2014-12-15 14:00:23 +00:00
Lovell Fuller
d3225fa193 Add 'size' attribute to callback's info Object #138 2014-12-15 13:54:19 +00:00
Lovell Fuller
f026a835fd Move unref of input Buffer to C++ #138 2014-12-14 10:31:25 +00:00
Lovell Fuller
47241db789 Let V8 garbage collect the Buffer earlier #138 2014-12-13 08:48:24 +00:00
Lovell Fuller
34a9970bd9 Remove useless re-definition of image #139 2014-12-12 22:04:55 +00:00
Lovell Fuller
57203f841a Copy input Buffer to avoid V8 heap compaction #138 2014-12-12 22:02:42 +00:00
Lovell Fuller
bd20bd1881 Version bumps 2014-12-11 13:32:52 +00:00
Lovell Fuller
60f1fda7ee Change interpretation to sRGB before transformation #133 2014-12-11 13:32:36 +00:00
Lovell Fuller
ea1013f6ec Add support for latest Amazon Linux 2014-12-08 10:52:59 +00:00
Lovell Fuller
247b607afd Add SVG and PSD fixtures and tests 2014-12-05 21:35:18 +00:00
Lovell Fuller
a56102a209 Ensure ICC transform of withMetadata output #133 2014-12-04 11:28:09 +00:00
Lovell Fuller
940b6f505f Add test for Promise rejection path 2014-12-04 10:48:45 +00:00
Lovell Fuller
e1b5574c4a Handle broken, embedded ICC profile #131 2014-12-03 10:23:35 +00:00
Lovell Fuller
f4cc6a2db4 Correct location of Dockerfile 2014-11-26 10:50:47 +00:00
Lovell Fuller
0acf865654 Faster ICC profile transform via lcms #125 2014-11-25 22:52:24 +00:00
Lovell Fuller
8460e50ee0 Remove spurious keywords 2014-11-25 19:16:01 +00:00
Lovell Fuller
f57a0e3b00 Ensure embedded profile, if any, is always used
Perform sRGB conversion at end of pipe only

withMetadata exports profile, should not convert

Convert one fixture to sRGB to help test

Discovered while investigating #125
2014-11-25 18:54:49 +00:00
Lovell Fuller
02b6016390 Add link to Dockerfile for libvips
Thanks @marcbachmann
2014-11-25 10:33:43 +00:00
Lovell Fuller
4e01d63195 Add hasProfile attribute to metadata response
At the very least will be useful investigating #125
2014-11-24 17:24:29 +00:00
Lovell Fuller
94b47508c0 imagemagick-native now supports async and filter 2014-11-24 15:13:47 +00:00
Lovell Fuller
328cda82c5 Updates for 7.42 stable release of libvips 2014-11-24 12:19:44 +00:00
Lovell Fuller
118b17aa2f Apply less blur before affine reduction #121 2014-11-24 11:52:48 +00:00
Lovell Fuller
b7c7fc22f3 Ensure correct Gaussian blur before affine #121
Use double sigma instead of int radius for blur
2014-11-20 13:59:39 +00:00
32 changed files with 1503 additions and 357 deletions

View File

@@ -1,6 +1,8 @@
{ {
"strict": true, "strict": true,
"node": true, "node": true,
"maxparams": 4,
"maxcomplexity": 13,
"globals": { "globals": {
"describe": true, "describe": true,
"it": true "it": true

View File

@@ -1,7 +1,8 @@
language: node_js language: node_js
node_js: node_js:
- "0.10" - "0.10"
- "0.11" - "0.12"
- iojs
before_install: before_install:
- curl -s https://raw.githubusercontent.com/lovell/sharp/master/preinstall.sh | sudo bash - - curl -s https://raw.githubusercontent.com/lovell/sharp/master/preinstall.sh | sudo bash -
after_success: after_success:

84
CONTRIBUTING.md Executable file
View File

@@ -0,0 +1,84 @@
# Contributing to sharp
Hello, thank you for your interest in helping!
## Submit a new bug report
Please create a [new issue](https://github.com/lovell/sharp/issues/new) containing the steps to reproduce the problem.
New bugs are assigned a `triage` label whilst under investigation.
If you're having problems with `npm install sharp`, please include the output of the following commands, perhaps as a [gist](https://gist.github.com/):
```sh
vips -v
pkg-config --print-provides vips
npm install --verbose sharp
```
## Submit a new feature request
If a [similar request](https://github.com/lovell/sharp/labels/enhancement) exists, it's probably fastest to add a comment to it about your requirement.
Implementation is usually straightforward if _libvips_ [already supports](http://www.vips.ecs.soton.ac.uk/supported/current/doc/html/libvips/ch03.html) the feature you need.
## Submit a Pull Request to fix a bug
Thank you! To prevent the problem occurring again, please add unit tests that would have failed.
Please select the `master` branch as the destination for your Pull Request so your fix can be included in the next minor release.
Please squash your changes into a single commit using a command like `git rebase -i upstream/master`.
## Submit a Pull Request with a new feature
Please add JavaScript [unit tests](https://github.com/lovell/sharp/tree/master/test/unit) to cover your new feature. A test coverage report for the JavaScript code is generated in the `coverage/lcov-report` directory.
You deserve to add your details to the [list of contributors](https://github.com/lovell/sharp/blob/master/package.json#L5).
Any change that modifies the existing public API should be added to the relevant work-in-progress branch for inclusion in the next major release.
| Release | WIP branch |
| ------: | :--------- |
| v0.9.0 | intake |
| v0.10.0 | judgement |
| v0.11.0 | knife |
Please squash your changes into a single commit using a command like `git rebase -i upstream/<wip-branch>`.
### Add a new public method
The API tries to be as fluent as possible. Image processing concepts follow the naming conventions from _libvips_ and, to a lesser extent, _ImageMagick_.
Most methods have optional parameters and assume sensible defaults. Methods with mandatory parameters often have names like `doSomethingWith(X)`.
Please ensure backwards compatibility where possible. Methods to modify previously default behaviour often have names like `withoutOptionY()` or `withExtraZ()`.
Feel free to create a [new issue](https://github.com/lovell/sharp/issues/new) to gather feedback on a potential API change.
### Remove an existing public method
A method to be removed should be deprecated in the next major version then removed in the following major version.
By way of example, the [bilinearInterpolation method](https://github.com/lovell/sharp/blob/v0.6.0/index.js#L155) present in v0.5.0 was deprecated in v0.6.0 and removed in v0.7.0.
## Run the tests
### Functional tests and static code analysis
```sh
npm test
```
### Memory leak tests
Requires _valgrind_.
```sh
cd sharp/test/leak
./leak.sh
```
## Finally
Please feel free to ask any questions via a [new issue](https://github.com/lovell/sharp/issues/new) or contact me by [e-mail](https://github.com/lovell/sharp/blob/master/package.json#L4).

188
README.md
View File

@@ -3,6 +3,7 @@
* [Installation](https://github.com/lovell/sharp#installation) * [Installation](https://github.com/lovell/sharp#installation)
* [Usage examples](https://github.com/lovell/sharp#usage-examples) * [Usage examples](https://github.com/lovell/sharp#usage-examples)
* [API](https://github.com/lovell/sharp#api) * [API](https://github.com/lovell/sharp#api)
* [Contributing](https://github.com/lovell/sharp#contributing)
* [Testing](https://github.com/lovell/sharp#testing) * [Testing](https://github.com/lovell/sharp#testing)
* [Performance](https://github.com/lovell/sharp#performance) * [Performance](https://github.com/lovell/sharp#performance)
* [Thanks](https://github.com/lovell/sharp#thanks) * [Thanks](https://github.com/lovell/sharp#thanks)
@@ -10,13 +11,15 @@
The typical use case for this high speed Node.js module is to convert large images of many formats to smaller, web-friendly JPEG, PNG and WebP images of varying dimensions. The typical use case for this high speed Node.js module is to convert large images of many formats to smaller, web-friendly JPEG, PNG and WebP images of varying dimensions.
The performance of JPEG resizing is typically 8x faster than ImageMagick and GraphicsMagick, based mainly on the number of CPU cores available. 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.
Colour spaces, embedded ICC profiles and alpha transparency channels are all handled correctly.
Memory usage is kept to a minimum, no child processes are spawned, everything remains non-blocking thanks to _libuv_ and Promises/A+ are supported. Only small regions of uncompressed image data are held in memory and processed at a time, taking full advantage of multiple CPU cores and L1/L2/L3 cache. Resizing an image is typically 4x faster than using the quickest ImageMagick and GraphicsMagick settings.
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. Huffman tables are optimised when generating JPEG output images without having to use separate command line tools like [jpegoptim](https://github.com/tjko/jpegoptim) and [jpegtran](http://jpegclub.org/jpegtran/). PNG filtering can be disabled, which for diagrams and line art often produces the same result as [pngcrush](http://pmt.sourceforge.net/pngcrush/).
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/). Everything remains non-blocking thanks to _libuv_, no child processes are spawned and Promises/A+ are supported.
Anyone who has used the Node.js bindings for [GraphicsMagick](https://github.com/aheckmann/gm) will find the API similarly fluent. Anyone who has used the Node.js bindings for [GraphicsMagick](https://github.com/aheckmann/gm) will find the API similarly fluent.
@@ -28,8 +31,9 @@ This module is powered by the blazingly fast [libvips](https://github.com/jcupit
### Prerequisites ### Prerequisites
* Node.js v0.10+ * Node.js v0.10+ or io.js
* [libvips](https://github.com/jcupitt/libvips) v7.40.0+ (7.42.0+ recommended) * [libvips](https://github.com/jcupitt/libvips) v7.40.0+ (7.42.0+ recommended)
* C++11 compatible compiler such as gcc 4.6+ or clang 3.0+
To install the most suitable version of libvips on the following Operating Systems: To install the most suitable version of libvips on the following Operating Systems:
@@ -43,6 +47,7 @@ To install the most suitable version of libvips on the following Operating Syste
* Red Hat Linux * Red Hat Linux
* RHEL/Centos/Scientific 6, 7 * RHEL/Centos/Scientific 6, 7
* Fedora 21, 22 * Fedora 21, 22
* Amazon Linux 2014.09
run the following as a user with `sudo` access: run the following as a user with `sudo` access:
@@ -68,13 +73,21 @@ The _gettext_ dependency of _libvips_ [can lead](https://github.com/lovell/sharp
brew link gettext --force brew link gettext --force
### Install libvips on Heroku ### Heroku
[Alessandro Tagliapietra](https://github.com/alex88) maintains an [Heroku buildpack for libvips](https://github.com/alex88/heroku-buildpack-vips) and its dependencies. [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 ### Docker
[Eugeny Vlasenko](https://github.com/mahnunchik) maintains [gulp-responsive](https://www.npmjs.org/package/gulp-responsive) and [Mohammad Prabowo](https://github.com/rizalp) maintains [gulp-sharp](https://www.npmjs.org/package/gulp-sharp). [Marc Bachmann](https://github.com/marcbachmann) maintains a [Dockerfile for libvips](https://github.com/marcbachmann/dockerfile-libvips).
docker pull marcbachmann/libvips
### Build tools
* [gulp-responsive](https://www.npmjs.com/package/gulp-responsive)
* [gulp-sharp](https://www.npmjs.com/package/gulp-sharp)
* [grunt-sharp](https://www.npmjs.com/package/grunt-sharp)
## Usage examples ## Usage examples
@@ -101,7 +114,7 @@ readableStream.pipe(transformer).pipe(writableStream);
```javascript ```javascript
var image = sharp(inputJpg); var image = sharp(inputJpg);
image.metadata(function(err, metadata) { image.metadata(function(err, metadata) {
image.resize(metadata.width / 2).webp().toBuffer(function(err, outputBuffer, info) { image.resize(Math.floor(metadata.width / 2)).webp().toBuffer(function(err, outputBuffer, info) {
// outputBuffer contains a WebP image half the width and height of the original JPEG // outputBuffer contains a WebP image half the width and height of the original JPEG
}); });
}); });
@@ -175,7 +188,7 @@ sharp(inputBuffer)
.toFile('output.tiff') .toFile('output.tiff')
.then(function() { .then(function() {
// output.tiff is a 200 pixels wide and 300 pixels high image // output.tiff is a 200 pixels wide and 300 pixels high image
// containing a bicubic scaled version, embedded on a white canvas, // containing a nohalo scaled version, embedded on a white canvas,
// of the image data in inputBuffer // of the image data in inputBuffer
}); });
``` ```
@@ -185,7 +198,7 @@ sharp('input.gif')
.resize(200, 300) .resize(200, 300)
.background({r: 0, g: 0, b: 0, a: 0}) .background({r: 0, g: 0, b: 0, a: 0})
.embed() .embed()
.webp() .toFormat(sharp.format.webp)
.toBuffer(function(err, outputBuffer) { .toBuffer(function(err, outputBuffer) {
if (err) { if (err) {
throw err; throw err;
@@ -199,30 +212,66 @@ sharp('input.gif')
sharp(inputBuffer) sharp(inputBuffer)
.resize(200, 200) .resize(200, 200)
.max() .max()
.jpeg() .toFormat('jpeg')
.toBuffer().then(function(outputBuffer) { .toBuffer().then(function(outputBuffer) {
// outputBuffer contains JPEG image data no wider than 200 pixels and no higher // outputBuffer contains JPEG image data no wider than 200 pixels and no higher
// than 200 pixels regardless of the inputBuffer image dimensions // than 200 pixels regardless of the inputBuffer image dimensions
}); });
``` ```
```javascript
// Runtime discovery of available formats
console.dir(sharp.format);
```
## API ## API
### Attributes
#### format
An Object containing nested boolean values
representing the available input and output formats/methods,
for example:
```javascript
{ jpeg: { id: 'jpeg',
input: { file: true, buffer: true, stream: true },
output: { file: true, buffer: true, stream: true } },
png: { id: 'png',
input: { file: true, buffer: true, stream: true },
output: { file: true, buffer: true, stream: true } },
webp: { id: 'webp',
input: { file: true, buffer: true, stream: true },
output: { file: true, buffer: true, stream: true } },
tiff: { id: 'tiff',
input: { file: true, buffer: true, stream: true },
output: { file: true, buffer: false, stream: false } },
magick: { id: 'magick',
input: { file: true, buffer: true, stream: true },
output: { file: false, buffer: false, stream: false } },
raw: { id: 'raw',
input: { file: false, buffer: false, stream: false },
output: { file: false, buffer: true, stream: true } } }
```
### Input methods ### Input methods
#### sharp([input]) #### sharp([input])
Constructor to which further methods are chained. `input`, if present, can be one of: Constructor to which further methods are chained. `input`, if present, can be one of:
* Buffer containing JPEG, PNG, WebP or TIFF image data, or * Buffer containing JPEG, PNG, WebP, GIF* or TIFF image data, or
* String containing the filename of an image, with most major formats supported. * String containing the filename of an image, with most major formats supported.
The object returned implements the [stream.Duplex](http://nodejs.org/api/stream.html#stream_class_stream_duplex) class. The object returned implements the [stream.Duplex](http://nodejs.org/api/stream.html#stream_class_stream_duplex) class.
JPEG, PNG, WebP or TIFF format image data can be streamed into the object when `input` is not provided. JPEG, PNG, WebP, GIF* or TIFF 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. JPEG, PNG or WebP format image data can be streamed out from this object.
\* libvips 8.0.0+ is required for Buffer/Stream input of GIF and other `magick` formats.
#### metadata([callback]) #### metadata([callback])
Fast access to image metadata without decoding any compressed image data. Fast access to image metadata without decoding any compressed image data.
@@ -234,6 +283,7 @@ Fast access to image metadata without decoding any compressed image data.
* `height`: Number of pixels high * `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#L522) * `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#L522)
* `channels`: Number of bands e.g. `3` for sRGB, `4` for CMYK * `channels`: Number of bands e.g. `3` for sRGB, `4` for CMYK
* `hasProfile`: Boolean indicating the presence of an embedded ICC profile
* `hasAlpha`: Boolean indicating the presence of an alpha transparency channel * `hasAlpha`: Boolean indicating the presence of an alpha transparency channel
* `orientation`: Number value of the EXIF Orientation header, if present * `orientation`: Number value of the EXIF Orientation header, if present
@@ -243,15 +293,21 @@ A Promises/A+ promise is returned when `callback` is not provided.
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. 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.
#### limitInputPixels(pixels)
Do not process input images where the number of pixels (width * height) exceeds this limit.
`pixels` is the integral Number of pixels, with a value between 1 and the default 268402689 (0x3FFF * 0x3FFF).
### Image transformation options ### Image transformation options
#### resize(width, [height]) #### resize(width, [height])
Scale output to `width` x `height`. By default, the resized image is cropped to the exact size specified. Scale output to `width` x `height`. By default, the resized image is cropped to the exact size specified.
`width` is the Number of pixels wide the resultant image should be. Use `null` or `undefined` to auto-scale the width to match the height. `width` is the integral Number of pixels wide the resultant image should be, between 1 and 16383 (0x3FFF). Use `null` or `undefined` to auto-scale the width to match the height.
`height` is the Number of pixels high the resultant image should be. Use `null` or `undefined` to auto-scale the height to match the width. `height` is the integral Number of pixels high the resultant image should be, between 1 and 16383. Use `null` or `undefined` to auto-scale the height to match the width.
#### extract(top, left, width, height) #### extract(top, left, width, height)
@@ -273,7 +329,19 @@ Possible values are `north`, `east`, `south`, `west`, `center` and `centre`. The
#### max() #### max()
Preserving aspect ratio, resize the image to the maximum `width` or `height` specified. Preserving aspect ratio,
resize the image to be as large as possible
while ensuring its dimensions are less than or equal to
the `width` and `height` specified.
Both `width` and `height` must be provided via `resize` otherwise the behaviour will default to `crop`.
#### min()
Preserving aspect ratio,
resize the image to be as small as possible
while ensuring its dimensions are greater than or equal to
the `width` and `height` specified.
Both `width` and `height` must be provided via `resize` otherwise the behaviour will default to `crop`. Both `width` and `height` must be provided via `resize` otherwise the behaviour will default to `crop`.
@@ -305,6 +373,8 @@ Rotate the output image by either an explicit angle or auto-orient based on the
Use this method without `angle` to determine the angle from EXIF data. Mirroring is supported and may infer the use of a `flip` operation. Use this method without `angle` to determine the angle from EXIF data. Mirroring is supported and may infer the use of a `flip` operation.
Method order is important when both rotating and extracting regions, for example `rotate(x).extract(y)` will produce a different result to `extract(y).rotate(x)`.
#### flip() #### flip()
Flip the image about the vertical Y axis. This always occurs after rotation, if any. Flip the image about the vertical Y axis. This always occurs after rotation, if any.
@@ -319,13 +389,13 @@ Do not enlarge the output image if the input image width *or* height are already
This is equivalent to GraphicsMagick's `>` geometry option: "change the dimensions of the image only if its width or height exceeds the geometry specification". This is equivalent to GraphicsMagick's `>` geometry option: "change the dimensions of the image only if its width or height exceeds the geometry specification".
#### blur([radius]) #### blur([sigma])
When used without parameters, performs a fast, mild blur of the output image. This typically reduces performance by 10%. When used without parameters, performs a fast, mild blur of the output image. This typically reduces performance by 10%.
When a `radius` is provided, performs a slower, more accurate Gaussian blur. This typically reduces performance by 30%. When a `sigma` is provided, performs a slower, more accurate Gaussian blur. This typically reduces performance by 25%.
* `radius`, if present, is an integral Number representing the approximate blur mask radius in pixels. * `sigma`, if present, is a Number between 0.3 and 1000 representing the approximate blur radius in pixels.
#### sharpen([radius], [flat], [jagged]) #### sharpen([radius], [flat], [jagged])
@@ -382,6 +452,25 @@ Use PNG format for the output image.
Use WebP format for the output image. Use WebP format for the output image.
#### raw()
_Requires libvips 7.42.0+_
Provide raw, uncompressed uint8 (unsigned char) image data for Buffer and Stream based output.
The number of channels depends on the input image and selected options.
* 1 channel for images converted to `greyscale()`, with each byte representing one pixel.
* 3 channels for colour images without alpha transparency, with bytes ordered \[red, green, blue, red, green, blue, etc.\]).
* 4 channels for colour images with alpha transparency, with bytes ordered \[red, green, blue, alpha, red, green, blue, alpha, etc.\].
#### toFormat(format)
Convenience method for the above output format methods, where `format` is either:
* an attribute of the `sharp.format` Object e.g. `sharp.format.jpeg`, or
* a String containing `jpeg`, `png`, `webp` or `raw`.
#### quality(quality) #### quality(quality)
The output quality to use for lossy JPEG, WebP and TIFF output formats. The default quality is `80`. The output quality to use for lossy JPEG, WebP and TIFF output formats. The default quality is `80`.
@@ -394,7 +483,18 @@ Use progressive (interlace) scan for JPEG and PNG output. This typically reduces
#### withMetadata() #### withMetadata()
Include all metadata (ICC, EXIF, XMP) from the input image in the output image. The default behaviour is to strip all metadata. Include all metadata (EXIF, XMP, IPTC) from the input image in the output image. This will also convert to and add the latest web-friendly v2 sRGB ICC profile.
The default behaviour is to strip all metadata and convert to the device-independent sRGB colour space.
#### withoutChromaSubsampling()
Disable the use of [chroma subsampling](http://en.wikipedia.org/wiki/Chroma_subsampling) with JPEG output (4:4:4).
This can improve colour representation at higher quality settings (90+),
but usually increases output file size and typically reduces performance by 25%.
The default behaviour is to use chroma subsampling (4:2:0).
#### compressionLevel(compressionLevel) #### compressionLevel(compressionLevel)
@@ -404,9 +504,9 @@ An advanced setting for the _zlib_ compression level of the lossless PNG output
#### withoutAdaptiveFiltering() #### withoutAdaptiveFiltering()
_Requires libvips 7.41.0+_ _Requires libvips 7.42.0+_
An advanced and experimental PNG output setting to disable adaptive row filtering. An advanced setting to disable adaptive row filtering for the lossless PNG output format.
### Output methods ### Output methods
@@ -417,7 +517,7 @@ An advanced and experimental PNG output setting to disable adaptive row filterin
`callback`, if present, is called with two arguments `(err, info)` where: `callback`, if present, is called with two arguments `(err, info)` where:
* `err` contains an error message, if any. * `err` contains an error message, if any.
* `info` contains the output image `format`, `width` and `height`. * `info` contains the output image `format`, `size` (bytes), `width` and `height`.
A Promises/A+ promise is returned when `callback` is not provided. A Promises/A+ promise is returned when `callback` is not provided.
@@ -429,7 +529,7 @@ Write image data to a Buffer, the format of which will match the input image by
* `err` is an error message, if any. * `err` is an error message, if any.
* `buffer` is the output image data. * `buffer` is the output image data.
* `info` contains the output image `format`, `width` and `height`. * `info` contains the output image `format`, `size` (bytes), `width` and `height`.
A Promises/A+ promise is returned when `callback` is not provided. A Promises/A+ promise is returned when `callback` is not provided.
@@ -452,7 +552,7 @@ sharp.cache(50, 200); // { current: 49, high: 99, memory: 50, items: 200}
#### sharp.concurrency([threads]) #### 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. `threads`, if provided, is the Number of threads _libvips'_ should create for processing each image. 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. This method always returns the current concurrency.
@@ -462,6 +562,8 @@ sharp.concurrency(2); // 2
sharp.concurrency(0); // 4 sharp.concurrency(0); // 4
``` ```
The maximum number of images that can be processed in parallel is limited by libuv's `UV_THREADPOOL_SIZE` environment variable.
#### sharp.counters() #### sharp.counters()
Provides access to internal task counters. Provides access to internal task counters.
@@ -473,6 +575,10 @@ Provides access to internal task counters.
var counters = sharp.counters(); // { queue: 2, process: 4 } var counters = sharp.counters(); // { queue: 2, process: 4 }
``` ```
## Contributing
A [guide for contributors](https://github.com/lovell/sharp/blob/master/CONTRIBUTING.md) covers reporting bugs, requesting features and submitting code changes.
## Testing ## Testing
### Functional tests ### Functional tests
@@ -489,29 +595,6 @@ var counters = sharp.counters(); // { queue: 2, process: 4 }
[![Centos 6.5 Build Status](https://snap-ci.com/lovell/sharp/branch/master/build_image)](https://snap-ci.com/lovell/sharp/branch/master) [![Centos 6.5 Build Status](https://snap-ci.com/lovell/sharp/branch/master/build_image)](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 ### Benchmark tests
``` ```
@@ -528,7 +611,7 @@ brew install graphicsmagick
``` ```
``` ```
sudo apt-get install -qq imagemagick graphicsmagick libmagick++-dev sudo apt-get install -qq imagemagick graphicsmagick libmagickcore-dev
``` ```
``` ```
@@ -548,7 +631,7 @@ sudo yum install -y --enablerepo=epel GraphicsMagick
### The contenders ### The contenders
* [imagemagick-native](https://github.com/mash/node-imagemagick-native) v1.2.2 - Supports Buffers only and blocks main V8 thread whilst processing. * [imagemagick-native](https://github.com/mash/node-imagemagick-native) v1.2.2 - Supports Buffers only
* [imagemagick](https://github.com/yourdeveloper/node-imagemagick) v0.1.3 - Supports filesystem only and "has been unmaintained for a long time". * [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. * [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. * sharp v0.6.2 - Caching within libvips disabled to ensure a fair comparison.
@@ -593,12 +676,13 @@ This module would never have been possible without the help and code contributio
* [Amit Pitaru](https://github.com/apitaru) * [Amit Pitaru](https://github.com/apitaru)
* [Brandon Aaron](https://github.com/brandonaaron) * [Brandon Aaron](https://github.com/brandonaaron)
* [Andreas Lind](https://github.com/papandreou) * [Andreas Lind](https://github.com/papandreou)
* [Maurus Cuelenaere](https://github.com/mcuelenaere)
Thank you! Thank you!
## Licence ## Licence
Copyright 2013, 2014 Lovell Fuller and contributors. Copyright 2013, 2014, 2015 Lovell Fuller and contributors.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

Binary file not shown.

157
index.js
View File

@@ -11,6 +11,12 @@ var BluebirdPromise = require('bluebird');
var sharp = require('./build/Release/sharp'); var sharp = require('./build/Release/sharp');
var libvipsVersion = sharp.libvipsVersion(); var libvipsVersion = sharp.libvipsVersion();
var maximum = {
width: 0x3FFF,
height: 0x3FFF,
pixels: Math.pow(0x3FFF, 2)
};
var Sharp = function(input) { var Sharp = function(input) {
if (!(this instanceof Sharp)) { if (!(this instanceof Sharp)) {
return new Sharp(input); return new Sharp(input);
@@ -18,10 +24,12 @@ var Sharp = function(input) {
stream.Duplex.call(this); stream.Duplex.call(this);
this.options = { this.options = {
// input options // input options
bufferIn: null,
streamIn: false, streamIn: false,
sequentialRead: false, sequentialRead: false,
// ICC profile to use when input CMYK image has no embedded profile limitInputPixels: maximum.pixels,
iccProfileCmyk: path.join(__dirname, 'icc', 'USWebCoatedSWOP.icc'), // ICC profiles
iccProfilePath: path.join(__dirname, 'icc') + path.sep,
// resize options // resize options
topOffsetPre: -1, topOffsetPre: -1,
leftOffsetPre: -1, leftOffsetPre: -1,
@@ -33,9 +41,10 @@ var Sharp = function(input) {
heightPost: -1, heightPost: -1,
width: -1, width: -1,
height: -1, height: -1,
canvas: 'c', canvas: 'crop',
gravity: 0, gravity: 0,
angle: 0, angle: 0,
rotateBeforePreExtract: false,
flip: false, flip: false,
flop: false, flop: false,
withoutEnlargement: false, withoutEnlargement: false,
@@ -43,7 +52,7 @@ var Sharp = function(input) {
// operations // operations
background: [0, 0, 0, 255], background: [0, 0, 0, 255],
flatten: false, flatten: false,
blurRadius: 0, blurSigma: 0,
sharpenRadius: 0, sharpenRadius: 0,
sharpenFlat: 1, sharpenFlat: 1,
sharpenJagged: 2, sharpenJagged: 2,
@@ -55,6 +64,7 @@ var Sharp = function(input) {
quality: 80, quality: 80,
compressionLevel: 6, compressionLevel: 6,
withoutAdaptiveFiltering: false, withoutAdaptiveFiltering: false,
withoutChromaSubsampling: false,
streamOut: false, streamOut: false,
withMetadata: false withMetadata: false
}; };
@@ -73,11 +83,13 @@ var Sharp = function(input) {
(input[0] === 0x52 && input[1] === 0x49) || (input[0] === 0x52 && input[1] === 0x49) ||
// TIFF // TIFF
(input[0] === 0x4D && input[1] === 0x4D && input[2] === 0x00 && (input[3] === 0x2A || input[3] === 0x2B)) || (input[0] === 0x4D && input[1] === 0x4D && input[2] === 0x00 && (input[3] === 0x2A || input[3] === 0x2B)) ||
(input[0] === 0x49 && input[1] === 0x49 && (input[2] === 0x2A || input[2] === 0x2B) && input[3] === 0x00) (input[0] === 0x49 && input[1] === 0x49 && (input[2] === 0x2A || input[2] === 0x2B) && input[3] === 0x00) ||
// GIF
(input[0] === 0x47 && input[1] === 0x49 && input[2] === 0x46 && input[3] === 0x38 && (input[4] === 0x37 || input[4] === 0x39) && input[5] === 0x61)
) { ) {
this.options.bufferIn = input; this.options.bufferIn = input;
} else { } else {
throw new Error('Buffer contains an unsupported image format. JPEG, PNG, WebP and TIFF are currently supported.'); throw new Error('Buffer contains an unsupported image format. JPEG, PNG, WebP, TIFF and GIF are currently supported.');
} }
} else { } else {
// input=stream // input=stream
@@ -88,6 +100,11 @@ var Sharp = function(input) {
module.exports = Sharp; module.exports = Sharp;
util.inherits(Sharp, stream.Duplex); util.inherits(Sharp, stream.Duplex);
/*
Supported image formats
*/
module.exports.format = sharp.format();
/* /*
Handle incoming chunk on Writable Stream Handle incoming chunk on Writable Stream
*/ */
@@ -95,16 +112,16 @@ Sharp.prototype._write = function(chunk, encoding, callback) {
/*jslint unused: false */ /*jslint unused: false */
if (this.options.streamIn) { if (this.options.streamIn) {
if (typeof chunk === 'object' && chunk instanceof Buffer) { if (typeof chunk === 'object' && chunk instanceof Buffer) {
if (typeof this.options.bufferIn === 'undefined') { if (this.options.bufferIn instanceof Buffer) {
// Create new Buffer
this.options.bufferIn = new Buffer(chunk.length);
chunk.copy(this.options.bufferIn);
} else {
// Append to existing Buffer // Append to existing Buffer
this.options.bufferIn = Buffer.concat( this.options.bufferIn = Buffer.concat(
[this.options.bufferIn, chunk], [this.options.bufferIn, chunk],
this.options.bufferIn.length + chunk.length this.options.bufferIn.length + chunk.length
); );
} else {
// Create new Buffer
this.options.bufferIn = new Buffer(chunk.length);
chunk.copy(this.options.bufferIn);
} }
callback(); callback();
} else { } else {
@@ -119,7 +136,7 @@ Sharp.prototype._write = function(chunk, encoding, callback) {
module.exports.gravity = {'center': 0, 'centre': 0, 'north': 1, 'east': 2, 'south': 3, 'west': 4}; module.exports.gravity = {'center': 0, 'centre': 0, 'north': 1, 'east': 2, 'south': 3, 'west': 4};
Sharp.prototype.crop = function(gravity) { Sharp.prototype.crop = function(gravity) {
this.options.canvas = 'c'; this.options.canvas = 'crop';
if (typeof gravity === 'number' && !Number.isNaN(gravity) && gravity >= 0 && gravity <= 4) { if (typeof gravity === 'number' && !Number.isNaN(gravity) && gravity >= 0 && gravity <= 4) {
this.options.gravity = gravity; this.options.gravity = gravity;
} else { } else {
@@ -133,8 +150,16 @@ Sharp.prototype.extract = function(topOffset, leftOffset, width, height) {
var suffix = this.options.width === -1 && this.options.height === -1 ? 'Pre' : 'Post'; var suffix = this.options.width === -1 && this.options.height === -1 ? 'Pre' : 'Post';
var values = arguments; var values = arguments;
['topOffset', 'leftOffset', 'width', 'height'].forEach(function(name, index) { ['topOffset', 'leftOffset', 'width', 'height'].forEach(function(name, index) {
this.options[name + suffix] = values[index]; if (typeof values[index] === 'number' && !Number.isNaN(values[index]) && (values[index] % 1 === 0) && values[index] >= 0) {
this.options[name + suffix] = values[index];
} else {
throw new Error('Non-integer value for ' + name + ' of ' + values[index]);
}
}.bind(this)); }.bind(this));
// Ensure existing rotation occurs before pre-resize extraction
if (suffix === 'Pre' && this.options.angle !== 0) {
this.options.rotateBeforePreExtract = true;
}
return this; return this;
}; };
@@ -151,12 +176,17 @@ Sharp.prototype.background = function(rgba) {
}; };
Sharp.prototype.embed = function() { Sharp.prototype.embed = function() {
this.options.canvas = 'e'; this.options.canvas = 'embed';
return this; return this;
}; };
Sharp.prototype.max = function() { Sharp.prototype.max = function() {
this.options.canvas = 'm'; this.options.canvas = 'max';
return this;
};
Sharp.prototype.min = function() {
this.options.canvas = 'min';
return this; return this;
}; };
@@ -208,21 +238,21 @@ Sharp.prototype.withoutEnlargement = function(withoutEnlargement) {
/* /*
Blur the output image. Blur the output image.
Call without a radius to use a fast, mild blur. Call without a sigma to use a fast, mild blur.
Call with a radius to use a slower, more accurate Gaussian blur. Call with a sigma to use a slower, more accurate Gaussian blur.
*/ */
Sharp.prototype.blur = function(radius) { Sharp.prototype.blur = function(sigma) {
if (typeof radius === 'undefined') { if (typeof sigma === 'undefined') {
// No arguments: default to mild blur // No arguments: default to mild blur
this.options.blurRadius = -1; this.options.blurSigma = -1;
} else if (typeof radius === 'boolean') { } else if (typeof sigma === 'boolean') {
// Boolean argument: apply mild blur? // Boolean argument: apply mild blur?
this.options.blurRadius = radius ? -1 : 0; this.options.blurSigma = sigma ? -1 : 0;
} else if (typeof radius === 'number' && !Number.isNaN(radius) && (radius % 1 === 0) && radius >= 1) { } else if (typeof sigma === 'number' && !Number.isNaN(sigma) && sigma >= 0.3 && sigma <= 1000) {
// Numeric argument: specific radius // Numeric argument: specific sigma
this.options.blurRadius = radius; this.options.blurSigma = sigma;
} else { } else {
throw new Error('Invalid blur radius ' + radius + ' (expected integer >= 1)'); throw new Error('Invalid blur sigma (0.3 to 1000.0) ' + sigma);
} }
return this; return this;
}; };
@@ -340,10 +370,10 @@ Sharp.prototype.compressionLevel = function(compressionLevel) {
}; };
/* /*
Disable the use of adaptive row filtering for PNG output - requires libvips 7.41.0+ Disable the use of adaptive row filtering for PNG output - requires libvips 7.42.0+
*/ */
Sharp.prototype.withoutAdaptiveFiltering = function(withoutAdaptiveFiltering) { Sharp.prototype.withoutAdaptiveFiltering = function(withoutAdaptiveFiltering) {
if (semver.gte(libvipsVersion, '7.41.0')) { if (semver.gte(libvipsVersion, '7.42.0')) {
this.options.withoutAdaptiveFiltering = (typeof withoutAdaptiveFiltering === 'boolean') ? withoutAdaptiveFiltering : true; this.options.withoutAdaptiveFiltering = (typeof withoutAdaptiveFiltering === 'boolean') ? withoutAdaptiveFiltering : true;
} else { } else {
console.error('withoutAdaptiveFiltering requires libvips 7.41.0+'); console.error('withoutAdaptiveFiltering requires libvips 7.41.0+');
@@ -351,6 +381,14 @@ Sharp.prototype.withoutAdaptiveFiltering = function(withoutAdaptiveFiltering) {
return this; return this;
}; };
/*
Disable the use of chroma subsampling for JPEG output
*/
Sharp.prototype.withoutChromaSubsampling = function(withoutChromaSubsampling) {
this.options.withoutChromaSubsampling = (typeof withoutChromaSubsampling === 'boolean') ? withoutChromaSubsampling : true;
return this;
};
Sharp.prototype.withMetadata = function(withMetadata) { Sharp.prototype.withMetadata = function(withMetadata) {
this.options.withMetadata = (typeof withMetadata === 'boolean') ? withMetadata : true; this.options.withMetadata = (typeof withMetadata === 'boolean') ? withMetadata : true;
return this; return this;
@@ -360,24 +398,37 @@ Sharp.prototype.resize = function(width, height) {
if (!width) { if (!width) {
this.options.width = -1; this.options.width = -1;
} else { } else {
if (typeof width === 'number' && !Number.isNaN(width)) { if (typeof width === 'number' && !Number.isNaN(width) && width % 1 === 0 && width > 0 && width <= maximum.width) {
this.options.width = width; this.options.width = width;
} else { } else {
throw new Error('Invalid width ' + width); throw new Error('Invalid width (1 to ' + maximum.width + ') ' + width);
} }
} }
if (!height) { if (!height) {
this.options.height = -1; this.options.height = -1;
} else { } else {
if (typeof height === 'number' && !Number.isNaN(height)) { if (typeof height === 'number' && !Number.isNaN(height) && height % 1 === 0 && height > 0 && height <= maximum.height) {
this.options.height = height; this.options.height = height;
} else { } else {
throw new Error('Invalid height ' + height); throw new Error('Invalid height (1 to ' + maximum.height + ') ' + height);
} }
} }
return this; return this;
}; };
/*
Limit the total number of pixels for input images
Assumes the image dimensions contained in the file header can be trusted
*/
Sharp.prototype.limitInputPixels = function(limit) {
if (typeof limit === 'number' && !Number.isNaN(limit) && limit % 1 === 0 && limit > 0 && limit <= maximum.pixels) {
this.options.limitInputPixels = limit;
} else {
throw new Error('Invalid pixel limit (1 to ' + maximum.pixels + ') ' + limit);
}
return this;
};
/* /*
Write output image data to a file Write output image data to a file
*/ */
@@ -405,25 +456,67 @@ Sharp.prototype.toFile = function(output, callback) {
return this; return this;
}; };
/*
Write output to a Buffer
*/
Sharp.prototype.toBuffer = function(callback) { Sharp.prototype.toBuffer = function(callback) {
return this._sharp(callback); return this._sharp(callback);
}; };
/*
Force JPEG output
*/
Sharp.prototype.jpeg = function() { Sharp.prototype.jpeg = function() {
this.options.output = '__jpeg'; this.options.output = '__jpeg';
return this; return this;
}; };
/*
Force PNG output
*/
Sharp.prototype.png = function() { Sharp.prototype.png = function() {
this.options.output = '__png'; this.options.output = '__png';
return this; return this;
}; };
/*
Force WebP output
*/
Sharp.prototype.webp = function() { Sharp.prototype.webp = function() {
this.options.output = '__webp'; this.options.output = '__webp';
return this; return this;
}; };
/*
Force raw, uint8 output
*/
Sharp.prototype.raw = function() {
var supportsRawOutput = module.exports.format.raw.output;
if (supportsRawOutput.file || supportsRawOutput.buffer || supportsRawOutput.stream) {
this.options.output = '__raw';
} else {
console.error('Raw output requires libvips 7.42.0+');
}
return this;
};
/*
Force output to a given format
@param format is either the id as a String or an Object with an 'id' attribute
*/
Sharp.prototype.toFormat = function(format) {
var id = format;
if (typeof format === 'object') {
id = format.id;
}
if (typeof id === 'string' && typeof module.exports.format[id] === 'object' && typeof this[id] === 'function') {
this[id]();
} else {
throw new Error('Unsupported format ' + format);
}
return this;
};
/* /*
Used by a Writable Stream to notify that it is ready for data Used by a Writable Stream to notify that it is ready for data
*/ */

30
package.json Executable file → Normal file
View File

@@ -1,6 +1,6 @@
{ {
"name": "sharp", "name": "sharp",
"version": "0.8.0", "version": "0.9.3",
"author": "Lovell Fuller <npm@lovell.info>", "author": "Lovell Fuller <npm@lovell.info>",
"contributors": [ "contributors": [
"Pierre Inglebert <pierre.inglebert@gmail.com>", "Pierre Inglebert <pierre.inglebert@gmail.com>",
@@ -11,11 +11,13 @@
"Julian Walker <julian@fiftythree.com>", "Julian Walker <julian@fiftythree.com>",
"Amit Pitaru <pitaru.amit@gmail.com>", "Amit Pitaru <pitaru.amit@gmail.com>",
"Brandon Aaron <hello.brandon@aaron.sh>", "Brandon Aaron <hello.brandon@aaron.sh>",
"Andreas Lind <andreas@one.com>" "Andreas Lind <andreas@one.com>",
"Maurus Cuelenaere <mcuelenaere@gmail.com>",
"Linus Unnebäck <linus@folkdatorn.se>"
], ],
"description": "High performance Node.js module to resize JPEG, PNG, WebP and TIFF images using the libvips library", "description": "High performance Node.js module to resize JPEG, PNG, WebP and TIFF images using the libvips library",
"scripts": { "scripts": {
"test": "node ./node_modules/istanbul/lib/cli.js cover ./node_modules/mocha/bin/_mocha -- --slow=5000 --timeout=10000 ./test/unit/*.js" "test": "VIPS_WARNING=0 node ./node_modules/istanbul/lib/cli.js cover ./node_modules/mocha/bin/_mocha -- --slow=5000 --timeout=10000 ./test/unit/*.js"
}, },
"main": "index.js", "main": "index.js",
"repository": { "repository": {
@@ -27,28 +29,24 @@
"png", "png",
"webp", "webp",
"tiff", "tiff",
"gif",
"resize", "resize",
"thumbnail", "thumbnail",
"sharpen",
"crop", "crop",
"extract",
"embed",
"libvips", "libvips",
"vips", "vips"
"stream"
], ],
"dependencies": { "dependencies": {
"bluebird": "^2.3.11", "bluebird": "^2.9.12",
"color": "^0.7.1", "color": "^0.7.3",
"nan": "^1.4.1", "nan": "^1.6.2",
"semver": "^4.1.0" "semver": "^4.3.1"
}, },
"devDependencies": { "devDependencies": {
"mocha": "^2.0.1", "mocha": "^2.1.0",
"mocha-jshint": "^0.0.9", "mocha-jshint": "^0.0.9",
"istanbul": "^0.3.2", "istanbul": "^0.3.6",
"coveralls": "^2.11.2" "coveralls": "^2.11.2",
"node-cpplint": "^0.4.0"
}, },
"license": "Apache 2.0", "license": "Apache 2.0",
"engines": { "engines": {

View File

@@ -10,24 +10,25 @@
# * Red Hat Linux # * Red Hat Linux
# * RHEL/Centos/Scientific 6, 7 # * RHEL/Centos/Scientific 6, 7
# * Fedora 21, 22 # * Fedora 21, 22
# * Amazon Linux 2014.09
vips_version_minimum=7.40.0 vips_version_minimum=7.40.0
vips_version_latest_major=7.40 vips_version_latest_major_minor=7.42
vips_version_latest_minor=11 vips_version_latest_patch=3
install_libvips_from_source() { install_libvips_from_source() {
echo "Compiling libvips $vips_version_latest_major.$vips_version_latest_minor from source" echo "Compiling libvips $vips_version_latest_major_minor.$vips_version_latest_patch 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 curl -O http://www.vips.ecs.soton.ac.uk/supported/$vips_version_latest_major_minor/vips-$vips_version_latest_major_minor.$vips_version_latest_patch.tar.gz
tar zvxf vips-$vips_version_latest_major.$vips_version_latest_minor.tar.gz tar zvxf vips-$vips_version_latest_major_minor.$vips_version_latest_patch.tar.gz
cd vips-$vips_version_latest_major.$vips_version_latest_minor cd vips-$vips_version_latest_major_minor.$vips_version_latest_patch
./configure --enable-debug=no --enable-docs=no --enable-cxx=yes --without-python --without-orc --without-fftw $1 ./configure --enable-debug=no --enable-docs=no --enable-cxx=yes --without-python --without-orc --without-fftw $1
make make
make install make install
cd .. cd ..
rm -rf vips-$vips_version_latest_major.$vips_version_latest_minor rm -rf vips-$vips_version_latest_major_minor.$vips_version_latest_patch
rm vips-$vips_version_latest_major.$vips_version_latest_minor.tar.gz rm vips-$vips_version_latest_major_minor.$vips_version_latest_patch.tar.gz
ldconfig ldconfig
echo "Installed libvips $vips_version_latest_major.$vips_version_latest_minor" echo "Installed libvips $vips_version_latest_major_minor.$vips_version_latest_patch"
} }
sorry() { sorry() {
@@ -89,12 +90,12 @@ case $(uname -s) in
jessie|vivid) jessie|vivid)
# Debian 8, Ubuntu 15 # Debian 8, Ubuntu 15
echo "Installing libvips via apt-get" echo "Installing libvips via apt-get"
apt-get install -y libvips-dev apt-get install -y libvips-dev libgsf-1-dev
;; ;;
trusty|utopic|qiana|rebecca) trusty|utopic|qiana|rebecca)
# Ubuntu 14, Mint 17 # Ubuntu 14, Mint 17
echo "Installing libvips dependencies via apt-get" echo "Installing libvips dependencies via apt-get"
apt-get install -y automake build-essential gobject-introspection gtk-doc-tools libglib2.0-dev libjpeg-turbo8-dev libpng12-dev libwebp-dev libtiff5-dev libexif-dev libxml2-dev swig libmagickwand-dev curl apt-get install -y automake build-essential gobject-introspection gtk-doc-tools libglib2.0-dev libjpeg-turbo8-dev libpng12-dev libwebp-dev libtiff5-dev libexif-dev libgsf-1-dev liblcms2-dev libxml2-dev swig libmagickcore-dev curl
install_libvips_from_source install_libvips_from_source
;; ;;
precise|wheezy|maya) precise|wheezy|maya)
@@ -102,7 +103,7 @@ case $(uname -s) in
echo "Installing libvips dependencies via apt-get" echo "Installing libvips dependencies via apt-get"
add-apt-repository -y ppa:lyrasis/precise-backports add-apt-repository -y ppa:lyrasis/precise-backports
apt-get update 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 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 libgsf-1-dev liblcms2-dev libxml2-dev swig libmagickcore-dev curl
install_libvips_from_source install_libvips_from_source
;; ;;
*) *)
@@ -119,14 +120,14 @@ case $(uname -s) in
# RHEL/CentOS 7 # RHEL/CentOS 7
echo "Installing libvips dependencies via yum" echo "Installing libvips dependencies via yum"
yum groupinstall -y "Development Tools" 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 yum install -y gtk-doc libxml2-devel libjpeg-turbo-devel libpng-devel libtiff-devel libexif-devel libgsf-devel lcms-devel ImageMagick-devel gobject-introspection-devel libwebp-devel curl
install_libvips_from_source "--prefix=/usr" install_libvips_from_source "--prefix=/usr"
;; ;;
"Red Hat Enterprise Linux release 6."*|"CentOS release 6."*|"Scientific Linux release 6."*) "Red Hat Enterprise Linux release 6."*|"CentOS release 6."*|"Scientific Linux release 6."*)
# RHEL/CentOS 6 # RHEL/CentOS 6
echo "Installing libvips dependencies via yum" echo "Installing libvips dependencies via yum"
yum groupinstall -y "Development Tools" 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 gtk-doc libxml2-devel libjpeg-turbo-devel libpng-devel libtiff-devel libexif-devel libgsf-devel lcms-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 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 --enablerepo=nux-dextop gobject-introspection-devel
yum install -y http://rpms.famillecollet.com/enterprise/remi-release-6.rpm yum install -y http://rpms.famillecollet.com/enterprise/remi-release-6.rpm
@@ -136,13 +137,26 @@ case $(uname -s) in
"Fedora release 21 "*|"Fedora release 22 "*) "Fedora release 21 "*|"Fedora release 22 "*)
# Fedora 21, 22 # Fedora 21, 22
echo "Installing libvips via yum" echo "Installing libvips via yum"
yum install vips-devel yum install -y vips-devel
;; ;;
*) *)
# Unsupported RHEL-based OS # Unsupported RHEL-based OS
sorry "$RELEASE" sorry "$RELEASE"
;; ;;
esac esac
elif [ -f /etc/system-release ]; then
# Probably Amazon Linux
RELEASE=$(cat /etc/system-release)
case $RELEASE in
"Amazon Linux AMI release 2014.09")
# Amazon Linux
echo "Detected '$RELEASE'"
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 libgsf-devel lcms-devel ImageMagick-devel gobject-introspection-devel libwebp-devel curl
install_libvips_from_source "--prefix=/usr"
;;
esac
else else
# Unsupported OS # Unsupported OS
sorry "$(uname -a)" sorry "$(uname -a)"

View File

@@ -43,6 +43,15 @@ namespace sharp {
); );
} }
static bool buffer_is_gif(char *buffer, size_t len) {
return (
len >= 6 && (
(buffer[0] == 'G' && buffer[1] == 'I' && buffer[2] == 'F' &&
buffer[3] == '8' && (buffer[4] == '7' || buffer[4] == '9') && buffer[5] == 'a')
)
);
}
/* /*
Determine image format of a buffer. Determine image format of a buffer.
*/ */
@@ -57,6 +66,8 @@ namespace sharp {
imageType = ImageType::WEBP; imageType = ImageType::WEBP;
} else if (buffer_is_tiff(static_cast<char*>(buffer), length)) { } else if (buffer_is_tiff(static_cast<char*>(buffer), length)) {
imageType = ImageType::TIFF; imageType = ImageType::TIFF;
} else if (buffer_is_gif(static_cast<char*>(buffer), length)) {
imageType = ImageType::MAGICK;
} }
} }
return imageType; return imageType;
@@ -75,6 +86,10 @@ namespace sharp {
vips_webpload_buffer(buffer, length, &image, "access", access, NULL); vips_webpload_buffer(buffer, length, &image, "access", access, NULL);
} else if (imageType == ImageType::TIFF) { } else if (imageType == ImageType::TIFF) {
vips_tiffload_buffer(buffer, length, &image, "access", access, NULL); vips_tiffload_buffer(buffer, length, &image, "access", access, NULL);
#if (VIPS_MAJOR_VERSION >= 8)
} else if (imageType == ImageType::MAGICK) {
vips_magickload_buffer(buffer, length, &image, "access", access, NULL);
#endif
} }
return image; return image;
} }
@@ -117,6 +132,13 @@ namespace sharp {
return image; return image;
} }
/*
Does this image have an embedded profile?
*/
bool HasProfile(VipsImage *image) {
return (vips_image_get_typeof(image, VIPS_META_ICC_NAME) > 0) ? TRUE : FALSE;
}
/* /*
Does this image have an alpha channel? Does this image have an alpha channel?
Uses colour space interpretation with number of channels to guess this. Uses colour space interpretation with number of channels to guess this.
@@ -155,4 +177,4 @@ namespace sharp {
return window_size; return window_size;
} }
} // namespace } // namespace sharp

View File

@@ -1,5 +1,5 @@
#ifndef SHARP_COMMON_H #ifndef SRC_COMMON_H_
#define SHARP_COMMON_H #define SRC_COMMON_H_
namespace sharp { namespace sharp {
@@ -44,6 +44,11 @@ namespace sharp {
*/ */
VipsImage* InitImage(ImageType imageType, char const *file, VipsAccess const access); VipsImage* InitImage(ImageType imageType, char const *file, VipsAccess const access);
/*
Does this image have an embedded profile?
*/
bool HasProfile(VipsImage *image);
/* /*
Does this image have an alpha channel? Does this image have an alpha channel?
Uses colour space interpretation with number of channels to guess this. Uses colour space interpretation with number of channels to guess this.
@@ -61,6 +66,6 @@ namespace sharp {
*/ */
int InterpolatorWindowSize(char const *name); int InterpolatorWindowSize(char const *name);
} // namespace } // namespace sharp
#endif #endif // SRC_COMMON_H_

View File

@@ -6,8 +6,23 @@
#include "common.h" #include "common.h"
#include "metadata.h" #include "metadata.h"
using namespace v8; using v8::Handle;
using namespace sharp; using v8::Local;
using v8::Value;
using v8::Object;
using v8::Number;
using v8::String;
using v8::Boolean;
using v8::Function;
using v8::Exception;
using sharp::ImageType;
using sharp::DetermineImageType;
using sharp::InitImage;
using sharp::HasProfile;
using sharp::HasAlpha;
using sharp::ExifOrientation;
using sharp::counterQueue;
struct MetadataBaton { struct MetadataBaton {
// Input // Input
@@ -20,6 +35,7 @@ struct MetadataBaton {
int height; int height;
std::string space; std::string space;
int channels; int channels;
bool hasProfile;
bool hasAlpha; bool hasAlpha;
int orientation; int orientation;
std::string err; std::string err;
@@ -46,6 +62,10 @@ class MetadataWorker : public NanAsyncWorker {
imageType = DetermineImageType(baton->bufferIn, baton->bufferInLength); imageType = DetermineImageType(baton->bufferIn, baton->bufferInLength);
if (imageType != ImageType::UNKNOWN) { if (imageType != ImageType::UNKNOWN) {
image = InitImage(imageType, baton->bufferIn, baton->bufferInLength, VIPS_ACCESS_RANDOM); image = InitImage(imageType, baton->bufferIn, baton->bufferInLength, VIPS_ACCESS_RANDOM);
if (image == NULL) {
(baton->err).append("Input buffer has corrupt header");
imageType = ImageType::UNKNOWN;
}
} else { } else {
(baton->err).append("Input buffer contains unsupported image format"); (baton->err).append("Input buffer contains unsupported image format");
} }
@@ -54,8 +74,12 @@ class MetadataWorker : public NanAsyncWorker {
imageType = DetermineImageType(baton->fileIn.c_str()); imageType = DetermineImageType(baton->fileIn.c_str());
if (imageType != ImageType::UNKNOWN) { if (imageType != ImageType::UNKNOWN) {
image = InitImage(imageType, baton->fileIn.c_str(), VIPS_ACCESS_RANDOM); image = InitImage(imageType, baton->fileIn.c_str(), VIPS_ACCESS_RANDOM);
if (image == NULL) {
(baton->err).append("Input file has corrupt header");
imageType = ImageType::UNKNOWN;
}
} else { } else {
(baton->err).append("File is of an unsupported image format"); (baton->err).append("Input file is of an unsupported image format");
} }
} }
if (image != NULL && imageType != ImageType::UNKNOWN) { if (image != NULL && imageType != ImageType::UNKNOWN) {
@@ -73,6 +97,7 @@ class MetadataWorker : public NanAsyncWorker {
baton->height = image->Ysize; baton->height = image->Ysize;
baton->space = vips_enum_nick(VIPS_TYPE_INTERPRETATION, image->Type); baton->space = vips_enum_nick(VIPS_TYPE_INTERPRETATION, image->Type);
baton->channels = image->Bands; baton->channels = image->Bands;
baton->hasProfile = HasProfile(image);
// Derived attributes // Derived attributes
baton->hasAlpha = HasAlpha(image); baton->hasAlpha = HasAlpha(image);
baton->orientation = ExifOrientation(image); baton->orientation = ExifOrientation(image);
@@ -99,6 +124,7 @@ class MetadataWorker : public NanAsyncWorker {
info->Set(NanNew<String>("height"), NanNew<Number>(baton->height)); info->Set(NanNew<String>("height"), NanNew<Number>(baton->height));
info->Set(NanNew<String>("space"), NanNew<String>(baton->space)); info->Set(NanNew<String>("space"), NanNew<String>(baton->space));
info->Set(NanNew<String>("channels"), NanNew<Number>(baton->channels)); info->Set(NanNew<String>("channels"), NanNew<Number>(baton->channels));
info->Set(NanNew<String>("hasProfile"), NanNew<Boolean>(baton->hasProfile));
info->Set(NanNew<String>("hasAlpha"), NanNew<Boolean>(baton->hasAlpha)); info->Set(NanNew<String>("hasAlpha"), NanNew<Boolean>(baton->hasAlpha));
if (baton->orientation > 0) { if (baton->orientation > 0) {
info->Set(NanNew<String>("orientation"), NanNew<Number>(baton->orientation)); info->Set(NanNew<String>("orientation"), NanNew<Number>(baton->orientation));

View File

@@ -1,8 +1,8 @@
#ifndef SHARP_METADATA_H #ifndef SRC_METADATA_H_
#define SHARP_METADATA_H #define SRC_METADATA_H_
#include "nan.h" #include "nan.h"
NAN_METHOD(metadata); NAN_METHOD(metadata);
#endif #endif // SRC_METADATA_H_

View File

@@ -10,31 +10,55 @@
#include "common.h" #include "common.h"
#include "resize.h" #include "resize.h"
using namespace v8; using v8::Handle;
using namespace sharp; using v8::Local;
using v8::Value;
using v8::Object;
using v8::Integer;
using v8::Uint32;
using v8::String;
using v8::Array;
using v8::Function;
using v8::Exception;
using sharp::ImageType;
using sharp::DetermineImageType;
using sharp::InitImage;
using sharp::InterpolatorWindowSize;
using sharp::HasProfile;
using sharp::HasAlpha;
using sharp::ExifOrientation;
using sharp::IsJpeg;
using sharp::IsPng;
using sharp::IsWebp;
using sharp::IsTiff;
using sharp::counterProcess;
using sharp::counterQueue;
enum class Canvas { enum class Canvas {
CROP, CROP,
EMBED,
MAX, MAX,
EMBED MIN
}; };
enum class Angle { enum class Angle {
D0, D0,
D90, D90,
D180, D180,
D270, D270,
DLAST DLAST
}; };
struct ResizeBaton { struct ResizeBaton {
std::string fileIn; std::string fileIn;
void* bufferIn; char *bufferIn;
size_t bufferInLength; size_t bufferInLength;
std::string iccProfileCmyk; std::string iccProfilePath;
int limitInputPixels;
std::string output; std::string output;
std::string outputFormat; std::string outputFormat;
void* bufferOut; void *bufferOut;
size_t bufferOutLength; size_t bufferOutLength;
int topOffsetPre; int topOffsetPre;
int leftOffsetPre; int leftOffsetPre;
@@ -51,13 +75,14 @@ struct ResizeBaton {
std::string interpolator; std::string interpolator;
double background[4]; double background[4];
bool flatten; bool flatten;
int blurRadius; double blurSigma;
int sharpenRadius; int sharpenRadius;
double sharpenFlat; double sharpenFlat;
double sharpenJagged; double sharpenJagged;
double gamma; double gamma;
bool greyscale; bool greyscale;
int angle; int angle;
bool rotateBeforePreExtract;
bool flip; bool flip;
bool flop; bool flop;
bool progressive; bool progressive;
@@ -66,11 +91,13 @@ struct ResizeBaton {
int quality; int quality;
int compressionLevel; int compressionLevel;
bool withoutAdaptiveFiltering; bool withoutAdaptiveFiltering;
bool withoutChromaSubsampling;
std::string err; std::string err;
bool withMetadata; bool withMetadata;
ResizeBaton(): ResizeBaton():
bufferInLength(0), bufferInLength(0),
limitInputPixels(0),
outputFormat(""), outputFormat(""),
bufferOutLength(0), bufferOutLength(0),
topOffsetPre(-1), topOffsetPre(-1),
@@ -78,7 +105,7 @@ struct ResizeBaton {
canvas(Canvas::CROP), canvas(Canvas::CROP),
gravity(0), gravity(0),
flatten(false), flatten(false),
blurRadius(0), blurSigma(0.0),
sharpenRadius(0), sharpenRadius(0),
sharpenFlat(1.0), sharpenFlat(1.0),
sharpenJagged(2.0), sharpenJagged(2.0),
@@ -92,6 +119,7 @@ struct ResizeBaton {
quality(80), quality(80),
compressionLevel(6), compressionLevel(6),
withoutAdaptiveFiltering(false), withoutAdaptiveFiltering(false),
withoutChromaSubsampling(false),
withMetadata(false) { withMetadata(false) {
background[0] = 0.0; background[0] = 0.0;
background[1] = 0.0; background[1] = 0.0;
@@ -100,6 +128,16 @@ struct ResizeBaton {
} }
}; };
/*
Delete input char[] buffer and notify V8 of memory deallocation
Used as the callback function for the "postclose" signal
*/
static void DeleteBuffer(VipsObject *object, char *buffer) {
if (buffer != NULL) {
delete[] buffer;
}
}
class ResizeWorker : public NanAsyncWorker { class ResizeWorker : public NanAsyncWorker {
public: public:
@@ -110,39 +148,82 @@ class ResizeWorker : public NanAsyncWorker {
libuv worker libuv worker
*/ */
void Execute() { void Execute() {
// Decrement queued task counter // Decrement queued task counter
g_atomic_int_dec_and_test(&counterQueue); g_atomic_int_dec_and_test(&counterQueue);
// Increment processing task counter // Increment processing task counter
g_atomic_int_inc(&counterProcess); g_atomic_int_inc(&counterProcess);
// Latest v2 sRGB ICC profile
std::string srgbProfile = baton->iccProfilePath + "sRGB_IEC61966-2-1_black_scaled.icc";
// Hang image references from this hook object // Hang image references from this hook object
VipsObject *hook = reinterpret_cast<VipsObject*>(vips_image_new()); VipsObject *hook = reinterpret_cast<VipsObject*>(vips_image_new());
// Input // Input
ImageType inputImageType = ImageType::UNKNOWN; ImageType inputImageType = ImageType::UNKNOWN;
VipsImage *image; VipsImage *image = NULL;
if (baton->bufferInLength > 1) { if (baton->bufferInLength > 1) {
// From buffer // From buffer
inputImageType = DetermineImageType(baton->bufferIn, baton->bufferInLength); inputImageType = DetermineImageType(baton->bufferIn, baton->bufferInLength);
if (inputImageType != ImageType::UNKNOWN) { if (inputImageType != ImageType::UNKNOWN) {
image = InitImage(inputImageType, baton->bufferIn, baton->bufferInLength, baton->accessMethod); image = InitImage(inputImageType, baton->bufferIn, baton->bufferInLength, baton->accessMethod);
if (image != NULL) {
// Listen for "postclose" signal to delete input buffer
g_signal_connect(image, "postclose", G_CALLBACK(DeleteBuffer), baton->bufferIn);
} else {
// Could not read header data
(baton->err).append("Input buffer has corrupt header");
inputImageType = ImageType::UNKNOWN;
DeleteBuffer(NULL, baton->bufferIn);
}
} else { } else {
(baton->err).append("Input buffer contains unsupported image format"); (baton->err).append("Input buffer contains unsupported image format");
DeleteBuffer(NULL, baton->bufferIn);
} }
} else { } else {
// From file // From file
inputImageType = DetermineImageType(baton->fileIn.c_str()); inputImageType = DetermineImageType(baton->fileIn.c_str());
if (inputImageType != ImageType::UNKNOWN) { if (inputImageType != ImageType::UNKNOWN) {
image = InitImage(inputImageType, baton->fileIn.c_str(), baton->accessMethod); image = InitImage(inputImageType, baton->fileIn.c_str(), baton->accessMethod);
if (image == NULL) {
(baton->err).append("Input file has corrupt header");
inputImageType = ImageType::UNKNOWN;
}
} else { } else {
(baton->err).append("File is of an unsupported image format"); (baton->err).append("Input file is of an unsupported image format");
} }
} }
if (inputImageType == ImageType::UNKNOWN) { if (image == NULL || inputImageType == ImageType::UNKNOWN) {
return Error(baton, hook); return Error(baton, hook);
} }
vips_object_local(hook, image); vips_object_local(hook, image);
// Limit input images to a given number of pixels, where pixels = width * height
if (image->Xsize * image->Ysize > baton->limitInputPixels) {
(baton->err).append("Input image exceeds pixel limit");
return Error(baton, hook);
}
// Calculate angle of rotation
Angle rotation;
bool flip;
std::tie(rotation, flip) = CalculateRotationAndFlip(baton->angle, image);
if (flip && !baton->flip) {
// Add flip operation due to EXIF mirroring
baton->flip = TRUE;
}
// Rotate pre-extract
if (baton->rotateBeforePreExtract && rotation != Angle::D0) {
VipsImage *rotated;
if (vips_rot(image, &rotated, static_cast<VipsAngle>(rotation), NULL)) {
return Error(baton, hook);
}
vips_object_local(hook, rotated);
image = rotated;
}
// Pre extraction // Pre extraction
if (baton->topOffsetPre != -1) { if (baton->topOffsetPre != -1) {
VipsImage *extractedPre; VipsImage *extractedPre;
@@ -153,42 +234,48 @@ class ResizeWorker : public NanAsyncWorker {
image = extractedPre; image = extractedPre;
} }
// Get input image width and height // Get pre-resize image width and height
int inputWidth = image->Xsize; int inputWidth = image->Xsize;
int inputHeight = image->Ysize; 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::D90 || rotation == Angle::D270) { if (rotation == Angle::D90 || rotation == Angle::D270) {
// Swap input output width and height when rotating by 90 or 270 degrees // Swap input output width and height when rotating by 90 or 270 degrees
int swap = inputWidth; int swap = inputWidth;
inputWidth = inputHeight; inputWidth = inputHeight;
inputHeight = swap; inputHeight = swap;
} }
if (flip && !baton->flip) {
// Add flip operation due to EXIF mirroring
baton->flip = TRUE;
}
// Get window size of interpolator, used for determining shrink vs affine // Get window size of interpolator, used for determining shrink vs affine
int interpolatorWindowSize = InterpolatorWindowSize(baton->interpolator.c_str()); int interpolatorWindowSize = InterpolatorWindowSize(baton->interpolator.c_str());
// Scaling calculations // Scaling calculations
double factor; double factor = 1.0;
if (baton->width > 0 && baton->height > 0) { if (baton->width > 0 && baton->height > 0) {
// Fixed width and height // Fixed width and height
double xfactor = static_cast<double>(inputWidth) / static_cast<double>(baton->width); double xfactor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
double yfactor = static_cast<double>(inputHeight) / static_cast<double>(baton->height); double yfactor = static_cast<double>(inputHeight) / static_cast<double>(baton->height);
factor = (baton->canvas == Canvas::CROP) ? std::min(xfactor, yfactor) : std::max(xfactor, yfactor); switch (baton->canvas) {
// if max is set, we need to compute the real size of the thumb image case Canvas::CROP:
if (baton->canvas == Canvas::MAX) { factor = std::min(xfactor, yfactor);
if (xfactor > yfactor) { break;
baton->height = round(static_cast<double>(inputHeight) / xfactor); case Canvas::EMBED:
} else { factor = std::max(xfactor, yfactor);
baton->width = round(static_cast<double>(inputWidth) / yfactor); break;
} case Canvas::MAX:
factor = std::max(xfactor, yfactor);
if (xfactor > yfactor) {
baton->height = round(static_cast<double>(inputHeight) / xfactor);
} else {
baton->width = round(static_cast<double>(inputWidth) / yfactor);
}
break;
case Canvas::MIN:
factor = std::min(xfactor, yfactor);
if (xfactor < yfactor) {
baton->height = round(static_cast<double>(inputHeight) / xfactor);
} else {
baton->width = round(static_cast<double>(inputWidth) / yfactor);
}
break;
} }
} else if (baton->width > 0) { } else if (baton->width > 0) {
// Fixed width, auto height // Fixed width, auto height
@@ -200,7 +287,6 @@ class ResizeWorker : public NanAsyncWorker {
baton->width = floor(static_cast<double>(inputWidth) / factor); baton->width = floor(static_cast<double>(inputWidth) / factor);
} else { } else {
// Identity transform // Identity transform
factor = 1;
baton->width = inputWidth; baton->width = inputWidth;
baton->height = inputHeight; baton->height = inputHeight;
} }
@@ -269,33 +355,24 @@ class ResizeWorker : public NanAsyncWorker {
image = shrunkOnLoad; image = shrunkOnLoad;
} }
// Handle colour profile, if any, for non sRGB images // Ensure we're using a device-independent colour space
if (image->Type != VIPS_INTERPRETATION_sRGB) { if (HasProfile(image)) {
// Get the input colour profile // Convert to sRGB using embedded profile
if (vips_image_get_typeof(image, VIPS_META_ICC_NAME)) { VipsImage *transformed;
VipsImage *profile; if (!vips_icc_transform(image, &transformed, srgbProfile.c_str(), "embedded", TRUE, NULL)) {
// Use embedded profile // Embedded profile can fail, so only update references on success
if (vips_icc_import(image, &profile, "pcs", VIPS_PCS_XYZ, "embedded", TRUE, NULL)) { vips_object_local(hook, transformed);
return Error(baton, hook); image = transformed;
}
vips_object_local(hook, profile);
image = profile;
} else if (image->Type == VIPS_INTERPRETATION_CMYK) {
VipsImage *profile;
// CMYK with no embedded profile
if (vips_icc_import(image, &profile, "pcs", VIPS_PCS_XYZ, "input_profile", (baton->iccProfileCmyk).c_str(), NULL)) {
return Error(baton, hook);
}
vips_object_local(hook, profile);
image = profile;
} }
// Attempt to convert to sRGB colour space } else if (image->Type == VIPS_INTERPRETATION_CMYK) {
VipsImage *colourspaced; // Convert to sRGB using default "USWebCoatedSWOP" CMYK profile
if (vips_colourspace(image, &colourspaced, VIPS_INTERPRETATION_sRGB, NULL)) { std::string cmykProfile = baton->iccProfilePath + "USWebCoatedSWOP.icc";
VipsImage *transformed;
if (vips_icc_transform(image, &transformed, srgbProfile.c_str(), "input_profile", cmykProfile.c_str(), NULL)) {
return Error(baton, hook); return Error(baton, hook);
} }
vips_object_local(hook, colourspaced); vips_object_local(hook, transformed);
image = colourspaced; image = transformed;
} }
// Flatten image to remove alpha channel // Flatten image to remove alpha channel
@@ -311,7 +388,7 @@ class ResizeWorker : public NanAsyncWorker {
if (vips_flatten(image, &flattened, "background", background, NULL)) { if (vips_flatten(image, &flattened, "background", background, NULL)) {
vips_area_unref(reinterpret_cast<VipsArea*>(background)); vips_area_unref(reinterpret_cast<VipsArea*>(background));
return Error(baton, hook); return Error(baton, hook);
}; }
vips_area_unref(reinterpret_cast<VipsArea*>(background)); vips_area_unref(reinterpret_cast<VipsArea*>(background));
vips_object_local(hook, flattened); vips_object_local(hook, flattened);
image = flattened; image = flattened;
@@ -364,22 +441,33 @@ class ResizeWorker : public NanAsyncWorker {
} }
// Use vips_affine with the remaining float part // Use vips_affine with the remaining float part
if (residual != 0) { if (residual != 0.0) {
// Apply variable blur radius of floor(residual) before large affine reductions // Apply Gaussian blur before large affine reductions
if (residual >= 1) { if (residual < 1.0) {
VipsImage *blurred; // Calculate standard deviation
if (vips_gaussblur(image, &blurred, floor(residual), NULL)) { double sigma = ((1.0 / residual) - 0.4) / 3.0;
return Error(baton, hook); if (sigma >= 0.3) {
// Create Gaussian function for standard deviation
VipsImage *gaussian;
if (vips_gaussmat(&gaussian, sigma, 0.2, "separable", TRUE, "integer", TRUE, NULL)) {
return Error(baton, hook);
}
vips_object_local(hook, gaussian);
// Apply Gaussian function
VipsImage *blurred;
if (vips_convsep(image, &blurred, gaussian, "precision", VIPS_PRECISION_INTEGER, NULL)) {
return Error(baton, hook);
}
vips_object_local(hook, blurred);
image = blurred;
} }
vips_object_local(hook, blurred);
image = blurred;
} }
// Create interpolator - "bilinear" (default), "bicubic" or "nohalo" // Create interpolator - "bilinear" (default), "bicubic" or "nohalo"
VipsInterpolate *interpolator = vips_interpolate_new(baton->interpolator.c_str()); VipsInterpolate *interpolator = vips_interpolate_new(baton->interpolator.c_str());
vips_object_local(hook, interpolator); vips_object_local(hook, interpolator);
// Perform affine transformation // Perform affine transformation
VipsImage *affined; VipsImage *affined;
if (vips_affine(image, &affined, residual, 0, 0, residual, "interpolate", interpolator, NULL)) { if (vips_affine(image, &affined, residual, 0.0, 0.0, residual, "interpolate", interpolator, NULL)) {
return Error(baton, hook); return Error(baton, hook);
} }
vips_object_local(hook, affined); vips_object_local(hook, affined);
@@ -387,7 +475,7 @@ class ResizeWorker : public NanAsyncWorker {
} }
// Rotate // Rotate
if (rotation != Angle::D0) { if (!baton->rotateBeforePreExtract && rotation != Angle::D0) {
VipsImage *rotated; VipsImage *rotated;
if (vips_rot(image, &rotated, static_cast<VipsAngle>(rotation), NULL)) { if (vips_rot(image, &rotated, static_cast<VipsAngle>(rotation), NULL)) {
return Error(baton, hook); return Error(baton, hook);
@@ -476,7 +564,7 @@ class ResizeWorker : public NanAsyncWorker {
vips_object_local(hook, embedded); vips_object_local(hook, embedded);
image = embedded; image = embedded;
} else { } else {
// Crop/max // Crop/max/min
int left; int left;
int top; int top;
std::tie(left, top) = CalculateCrop(image->Xsize, image->Ysize, baton->width, baton->height, baton->gravity); std::tie(left, top) = CalculateCrop(image->Xsize, image->Ysize, baton->width, baton->height, baton->gravity);
@@ -494,7 +582,9 @@ class ResizeWorker : public NanAsyncWorker {
// Post extraction // Post extraction
if (baton->topOffsetPost != -1) { if (baton->topOffsetPost != -1) {
VipsImage *extractedPost; VipsImage *extractedPost;
if (vips_extract_area(image, &extractedPost, baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost, NULL)) { if (vips_extract_area(image, &extractedPost,
baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost, NULL
)) {
return Error(baton, hook); return Error(baton, hook);
} }
vips_object_local(hook, extractedPost); vips_object_local(hook, extractedPost);
@@ -502,10 +592,10 @@ class ResizeWorker : public NanAsyncWorker {
} }
// Blur // Blur
if (baton->blurRadius != 0) { if (baton->blurSigma != 0.0) {
VipsImage *blurred; VipsImage *blurred;
if (baton->blurRadius == -1) { if (baton->blurSigma < 0.0) {
// Fast, mild blur // Fast, mild blur - averages neighbouring pixels
VipsImage *blur = vips_image_new_matrixv(3, 3, VipsImage *blur = vips_image_new_matrixv(3, 3,
1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
@@ -517,7 +607,14 @@ class ResizeWorker : public NanAsyncWorker {
} }
} else { } else {
// Slower, accurate Gaussian blur // Slower, accurate Gaussian blur
if (vips_gaussblur(image, &blurred, baton->blurRadius, NULL)) { // Create Gaussian function for standard deviation
VipsImage *gaussian;
if (vips_gaussmat(&gaussian, baton->blurSigma, 0.2, "separable", TRUE, "integer", TRUE, NULL)) {
return Error(baton, hook);
}
vips_object_local(hook, gaussian);
// Apply Gaussian function
if (vips_convsep(image, &blurred, gaussian, "precision", VIPS_PRECISION_INTEGER, NULL)) {
return Error(baton, hook); return Error(baton, hook);
} }
} }
@@ -559,17 +656,27 @@ class ResizeWorker : public NanAsyncWorker {
image = gammaDecoded; image = gammaDecoded;
} }
// Convert to sRGB colour space, if not already // Convert image to sRGB, if not already
if (image->Type != VIPS_INTERPRETATION_sRGB) { if (image->Type != VIPS_INTERPRETATION_sRGB) {
VipsImage *colourspaced; // Switch intrepretation to sRGB
if (vips_colourspace(image, &colourspaced, VIPS_INTERPRETATION_sRGB, NULL)) { VipsImage *rgb;
if (vips_colourspace(image, &rgb, VIPS_INTERPRETATION_sRGB, NULL)) {
return Error(baton, hook); return Error(baton, hook);
} }
vips_object_local(hook, colourspaced); vips_object_local(hook, rgb);
image = colourspaced; image = rgb;
// Tranform colours from embedded profile to sRGB profile
if (baton->withMetadata && HasProfile(image)) {
VipsImage *profiled;
if (vips_icc_transform(image, &profiled, srgbProfile.c_str(), "embedded", TRUE, NULL)) {
return Error(baton, hook);
}
vips_object_local(hook, profiled);
image = profiled;
}
} }
#if !(VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 40 && VIPS_MINOR_VERSION >= 5) #if !(VIPS_MAJOR_VERSION >= 8 || (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 40 && VIPS_MINOR_VERSION >= 5))
// Generate image tile cache when interlace output is required - no longer required as of libvips 7.40.5+ // Generate image tile cache when interlace output is required - no longer required as of libvips 7.40.5+
if (baton->progressive) { if (baton->progressive) {
VipsImage *cached; VipsImage *cached;
@@ -585,12 +692,13 @@ class ResizeWorker : public NanAsyncWorker {
if (baton->output == "__jpeg" || (baton->output == "__input" && inputImageType == ImageType::JPEG)) { if (baton->output == "__jpeg" || (baton->output == "__input" && inputImageType == ImageType::JPEG)) {
// Write JPEG to buffer // Write JPEG to buffer
if (vips_jpegsave_buffer(image, &baton->bufferOut, &baton->bufferOutLength, "strip", !baton->withMetadata, if (vips_jpegsave_buffer(image, &baton->bufferOut, &baton->bufferOutLength, "strip", !baton->withMetadata,
"Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progressive, NULL)) { "Q", baton->quality, "optimize_coding", TRUE, "no_subsample", baton->withoutChromaSubsampling,
"interlace", baton->progressive, NULL)) {
return Error(baton, hook); return Error(baton, hook);
} }
baton->outputFormat = "jpeg"; baton->outputFormat = "jpeg";
} else if (baton->output == "__png" || (baton->output == "__input" && inputImageType == ImageType::PNG)) { } else if (baton->output == "__png" || (baton->output == "__input" && inputImageType == ImageType::PNG)) {
#if (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 41) #if (VIPS_MAJOR_VERSION >= 8 || (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 42))
// Select PNG row filter // Select PNG row filter
int filter = baton->withoutAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_NONE : VIPS_FOREIGN_PNG_FILTER_ALL; int filter = baton->withoutAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_NONE : VIPS_FOREIGN_PNG_FILTER_ALL;
// Write PNG to buffer // Write PNG to buffer
@@ -613,6 +721,35 @@ class ResizeWorker : public NanAsyncWorker {
return Error(baton, hook); return Error(baton, hook);
} }
baton->outputFormat = "webp"; baton->outputFormat = "webp";
#if (VIPS_MAJOR_VERSION >= 8 || (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 42))
} else if (baton->output == "__raw") {
// Write raw, uncompressed image data to buffer
if (baton->greyscale) {
// Extract first band for greyscale image
VipsImage *grey;
if (vips_extract_band(image, &grey, 1, NULL)) {
return Error(baton, hook);
}
vips_object_local(hook, grey);
image = grey;
}
if (image->BandFmt != VIPS_FORMAT_UCHAR) {
// Cast pixels to uint8 (unsigned char)
VipsImage *uchar;
if (vips_cast(image, &uchar, VIPS_FORMAT_UCHAR, NULL)) {
return Error(baton, hook);
}
vips_object_local(hook, uchar);
image = uchar;
}
// Get raw image data
baton->bufferOut = vips_image_write_to_memory(image, &baton->bufferOutLength);
if (baton->bufferOut == NULL) {
(baton->err).append("Could not allocate enough memory for raw output");
return Error(baton, hook);
}
baton->outputFormat = "raw";
#endif
} else { } else {
bool outputJpeg = IsJpeg(baton->output); bool outputJpeg = IsJpeg(baton->output);
bool outputPng = IsPng(baton->output); bool outputPng = IsPng(baton->output);
@@ -622,12 +759,13 @@ class ResizeWorker : public NanAsyncWorker {
if (outputJpeg || (matchInput && inputImageType == ImageType::JPEG)) { if (outputJpeg || (matchInput && inputImageType == ImageType::JPEG)) {
// Write JPEG to file // Write JPEG to file
if (vips_jpegsave(image, baton->output.c_str(), "strip", !baton->withMetadata, if (vips_jpegsave(image, baton->output.c_str(), "strip", !baton->withMetadata,
"Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progressive, NULL)) { "Q", baton->quality, "optimize_coding", TRUE, "no_subsample", baton->withoutChromaSubsampling,
"interlace", baton->progressive, NULL)) {
return Error(baton, hook); return Error(baton, hook);
} }
baton->outputFormat = "jpeg"; baton->outputFormat = "jpeg";
} else if (outputPng || (matchInput && inputImageType == ImageType::PNG)) { } else if (outputPng || (matchInput && inputImageType == ImageType::PNG)) {
#if (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 41) #if (VIPS_MAJOR_VERSION >= 8 || (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 42))
// Select PNG row filter // Select PNG row filter
int filter = baton->withoutAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_NONE : VIPS_FOREIGN_PNG_FILTER_ALL; int filter = baton->withoutAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_NONE : VIPS_FOREIGN_PNG_FILTER_ALL;
// Write PNG to file // Write PNG to file
@@ -690,16 +828,22 @@ class ResizeWorker : public NanAsyncWorker {
// Info Object // Info Object
Local<Object> info = NanNew<Object>(); Local<Object> info = NanNew<Object>();
info->Set(NanNew<String>("format"), NanNew<String>(baton->outputFormat)); info->Set(NanNew<String>("format"), NanNew<String>(baton->outputFormat));
info->Set(NanNew<String>("width"), NanNew<Number>(width)); info->Set(NanNew<String>("width"), NanNew<Integer>(width));
info->Set(NanNew<String>("height"), NanNew<Number>(height)); info->Set(NanNew<String>("height"), NanNew<Integer>(height));
if (baton->bufferOutLength > 0) { if (baton->bufferOutLength > 0) {
// Buffer // Copy data to new Buffer
argv[1] = NanNewBufferHandle(static_cast<char*>(baton->bufferOut), baton->bufferOutLength); argv[1] = NanNewBufferHandle(static_cast<char*>(baton->bufferOut), baton->bufferOutLength);
// bufferOut was allocated via g_malloc
g_free(baton->bufferOut); g_free(baton->bufferOut);
// Add buffer size to info
info->Set(NanNew<String>("size"), NanNew<Uint32>(static_cast<uint32_t>(baton->bufferOutLength)));
argv[2] = info; argv[2] = info;
} else { } else {
// File // Add file size to info
struct stat st;
g_stat(baton->output.c_str(), &st);
info->Set(NanNew<String>("size"), NanNew<Uint32>(static_cast<uint32_t>(st.st_size)));
argv[1] = info; argv[1] = info;
} }
} }
@@ -810,11 +954,16 @@ NAN_METHOD(resize) {
// Input Buffer object // Input Buffer object
if (options->Get(NanNew<String>("bufferIn"))->IsObject()) { if (options->Get(NanNew<String>("bufferIn"))->IsObject()) {
Local<Object> buffer = options->Get(NanNew<String>("bufferIn"))->ToObject(); Local<Object> buffer = options->Get(NanNew<String>("bufferIn"))->ToObject();
// Take a copy of the input Buffer to avoid problems with V8 heap compaction
baton->bufferInLength = node::Buffer::Length(buffer); baton->bufferInLength = node::Buffer::Length(buffer);
baton->bufferIn = node::Buffer::Data(buffer); baton->bufferIn = new char[baton->bufferInLength];
memcpy(baton->bufferIn, node::Buffer::Data(buffer), baton->bufferInLength);
options->Set(NanNew<String>("bufferIn"), NanNull());
} }
// ICC profile to use when input CMYK image has no embedded profile // ICC profile to use when input CMYK image has no embedded profile
baton->iccProfileCmyk = *String::Utf8Value(options->Get(NanNew<String>("iccProfileCmyk"))->ToString()); baton->iccProfilePath = *String::Utf8Value(options->Get(NanNew<String>("iccProfilePath"))->ToString());
// Limit input images to a given number of pixels, where pixels = width * height
baton->limitInputPixels = options->Get(NanNew<String>("limitInputPixels"))->Int32Value();
// Extract image options // Extract image options
baton->topOffsetPre = options->Get(NanNew<String>("topOffsetPre"))->Int32Value(); baton->topOffsetPre = options->Get(NanNew<String>("topOffsetPre"))->Int32Value();
baton->leftOffsetPre = options->Get(NanNew<String>("leftOffsetPre"))->Int32Value(); baton->leftOffsetPre = options->Get(NanNew<String>("leftOffsetPre"))->Int32Value();
@@ -829,12 +978,14 @@ NAN_METHOD(resize) {
baton->height = options->Get(NanNew<String>("height"))->Int32Value(); baton->height = options->Get(NanNew<String>("height"))->Int32Value();
// Canvas option // Canvas option
Local<String> canvas = options->Get(NanNew<String>("canvas"))->ToString(); Local<String> canvas = options->Get(NanNew<String>("canvas"))->ToString();
if (canvas->Equals(NanNew<String>("c"))) { if (canvas->Equals(NanNew<String>("crop"))) {
baton->canvas = Canvas::CROP; baton->canvas = Canvas::CROP;
} else if (canvas->Equals(NanNew<String>("m"))) { } else if (canvas->Equals(NanNew<String>("embed"))) {
baton->canvas = Canvas::MAX;
} else if (canvas->Equals(NanNew<String>("e"))) {
baton->canvas = Canvas::EMBED; baton->canvas = Canvas::EMBED;
} else if (canvas->Equals(NanNew<String>("max"))) {
baton->canvas = Canvas::MAX;
} else if (canvas->Equals(NanNew<String>("min"))) {
baton->canvas = Canvas::MIN;
} }
// Background colour // Background colour
Local<Array> background = Local<Array>::Cast(options->Get(NanNew<String>("background"))); Local<Array> background = Local<Array>::Cast(options->Get(NanNew<String>("background")));
@@ -847,13 +998,14 @@ NAN_METHOD(resize) {
baton->interpolator = *String::Utf8Value(options->Get(NanNew<String>("interpolator"))->ToString()); baton->interpolator = *String::Utf8Value(options->Get(NanNew<String>("interpolator"))->ToString());
// Operators // Operators
baton->flatten = options->Get(NanNew<String>("flatten"))->BooleanValue(); baton->flatten = options->Get(NanNew<String>("flatten"))->BooleanValue();
baton->blurRadius = options->Get(NanNew<String>("blurRadius"))->Int32Value(); baton->blurSigma = options->Get(NanNew<String>("blurSigma"))->NumberValue();
baton->sharpenRadius = options->Get(NanNew<String>("sharpenRadius"))->Int32Value(); baton->sharpenRadius = options->Get(NanNew<String>("sharpenRadius"))->Int32Value();
baton->sharpenFlat = options->Get(NanNew<String>("sharpenFlat"))->NumberValue(); baton->sharpenFlat = options->Get(NanNew<String>("sharpenFlat"))->NumberValue();
baton->sharpenJagged = options->Get(NanNew<String>("sharpenJagged"))->NumberValue(); baton->sharpenJagged = options->Get(NanNew<String>("sharpenJagged"))->NumberValue();
baton->gamma = options->Get(NanNew<String>("gamma"))->NumberValue(); baton->gamma = options->Get(NanNew<String>("gamma"))->NumberValue();
baton->greyscale = options->Get(NanNew<String>("greyscale"))->BooleanValue(); baton->greyscale = options->Get(NanNew<String>("greyscale"))->BooleanValue();
baton->angle = options->Get(NanNew<String>("angle"))->Int32Value(); baton->angle = options->Get(NanNew<String>("angle"))->Int32Value();
baton->rotateBeforePreExtract = options->Get(NanNew<String>("rotateBeforePreExtract"))->BooleanValue();
baton->flip = options->Get(NanNew<String>("flip"))->BooleanValue(); baton->flip = options->Get(NanNew<String>("flip"))->BooleanValue();
baton->flop = options->Get(NanNew<String>("flop"))->BooleanValue(); baton->flop = options->Get(NanNew<String>("flop"))->BooleanValue();
// Output options // Output options
@@ -861,12 +1013,13 @@ NAN_METHOD(resize) {
baton->quality = options->Get(NanNew<String>("quality"))->Int32Value(); baton->quality = options->Get(NanNew<String>("quality"))->Int32Value();
baton->compressionLevel = options->Get(NanNew<String>("compressionLevel"))->Int32Value(); baton->compressionLevel = options->Get(NanNew<String>("compressionLevel"))->Int32Value();
baton->withoutAdaptiveFiltering = options->Get(NanNew<String>("withoutAdaptiveFiltering"))->BooleanValue(); baton->withoutAdaptiveFiltering = options->Get(NanNew<String>("withoutAdaptiveFiltering"))->BooleanValue();
baton->withoutChromaSubsampling = options->Get(NanNew<String>("withoutChromaSubsampling"))->BooleanValue();
baton->withMetadata = options->Get(NanNew<String>("withMetadata"))->BooleanValue(); baton->withMetadata = options->Get(NanNew<String>("withMetadata"))->BooleanValue();
// Output filename or __format for Buffer // Output filename or __format for Buffer
baton->output = *String::Utf8Value(options->Get(NanNew<String>("output"))->ToString()); baton->output = *String::Utf8Value(options->Get(NanNew<String>("output"))->ToString());
// Join queue for worker thread // Join queue for worker thread
NanCallback *callback = new NanCallback(args[1].As<v8::Function>()); NanCallback *callback = new NanCallback(args[1].As<Function>());
NanAsyncQueueWorker(new ResizeWorker(callback, baton)); NanAsyncQueueWorker(new ResizeWorker(callback, baton));
// Increment queued task counter // Increment queued task counter

View File

@@ -1,8 +1,8 @@
#ifndef SHARP_RESIZE_H #ifndef SRC_RESIZE_H_
#define SHARP_RESIZE_H #define SRC_RESIZE_H_
#include "nan.h" #include "nan.h"
NAN_METHOD(resize); NAN_METHOD(resize);
#endif #endif // SRC_RESIZE_H_

View File

@@ -8,25 +8,14 @@
#include "resize.h" #include "resize.h"
#include "utilities.h" #include "utilities.h"
using namespace v8; extern "C" void init(v8::Handle<v8::Object> target) {
static void at_exit(void* arg) {
NanScope();
vips_shutdown();
}
extern "C" void init(Handle<Object> target) {
NanScope(); NanScope();
vips_init("sharp"); vips_init("sharp");
node::AtExit(at_exit);
// Set libvips operation cache limits // Set libvips operation cache limits
vips_cache_set_max_mem(100 * 1048576); // 100 MB vips_cache_set_max_mem(100 * 1024 * 1024); // 100 MB
vips_cache_set_max(500); // 500 operations 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 // Methods available to JavaScript
NODE_SET_METHOD(target, "metadata", metadata); NODE_SET_METHOD(target, "metadata", metadata);
NODE_SET_METHOD(target, "resize", resize); NODE_SET_METHOD(target, "resize", resize);
@@ -34,6 +23,7 @@ extern "C" void init(Handle<Object> target) {
NODE_SET_METHOD(target, "concurrency", concurrency); NODE_SET_METHOD(target, "concurrency", concurrency);
NODE_SET_METHOD(target, "counters", counters); NODE_SET_METHOD(target, "counters", counters);
NODE_SET_METHOD(target, "libvipsVersion", libvipsVersion); NODE_SET_METHOD(target, "libvipsVersion", libvipsVersion);
NODE_SET_METHOD(target, "format", format);
} }
NODE_MODULE(sharp, init) NODE_MODULE(sharp, init)

View File

@@ -6,8 +6,14 @@
#include "common.h" #include "common.h"
#include "utilities.h" #include "utilities.h"
using namespace v8; using v8::Local;
using namespace sharp; using v8::Object;
using v8::Number;
using v8::String;
using v8::Boolean;
using sharp::counterQueue;
using sharp::counterProcess;
/* /*
Get and set cache memory and item limits Get and set cache memory and item limits
@@ -70,6 +76,69 @@ NAN_METHOD(counters) {
NAN_METHOD(libvipsVersion) { NAN_METHOD(libvipsVersion) {
NanScope(); NanScope();
char version[9]; char version[9];
snprintf(version, 9, "%d.%d.%d", vips_version(0), vips_version(1), vips_version(2)); snprintf(version, sizeof(version), "%d.%d.%d", vips_version(0), vips_version(1), vips_version(2));
NanReturnValue(NanNew<String>(version)); NanReturnValue(NanNew<String>(version));
} }
/*
Get available input/output file/buffer/stream formats
*/
NAN_METHOD(format) {
NanScope();
// Attribute names
Local<String> attrId = NanNew<String>("id");
Local<String> attrInput = NanNew<String>("input");
Local<String> attrOutput = NanNew<String>("output");
Local<String> attrFile = NanNew<String>("file");
Local<String> attrBuffer = NanNew<String>("buffer");
Local<String> attrStream = NanNew<String>("stream");
// Which load/save operations are available for each compressed format?
Local<Object> format = NanNew<Object>();
for (std::string f : {"jpeg", "png", "webp", "tiff", "magick", "openslide", "dz"}) {
// Input
Local<Object> input = NanNew<Object>();
input->Set(attrFile, NanNew<Boolean>(
vips_type_find("VipsOperation", (f + "load").c_str())));
input->Set(attrBuffer, NanNew<Boolean>(
vips_type_find("VipsOperation", (f + "load_buffer").c_str())));
input->Set(attrStream, input->Get(attrBuffer));
// Output
Local<Object> output = NanNew<Object>();
output->Set(attrFile, NanNew<Boolean>(
vips_type_find("VipsOperation", (f + "save").c_str())));
output->Set(attrBuffer, NanNew<Boolean>(
vips_type_find("VipsOperation", (f + "save_buffer").c_str())));
output->Set(attrStream, output->Get(attrBuffer));
// Other attributes
Local<Object> container = NanNew<Object>();
Local<String> formatId = NanNew<String>(f);
container->Set(attrId, formatId);
container->Set(attrInput, input);
container->Set(attrOutput, output);
// Add to set of formats
format->Set(formatId, container);
}
// Raw, uncompressed data
Local<Object> raw = NanNew<Object>();
raw->Set(attrId, NanNew<String>("raw"));
format->Set(NanNew<String>("raw"), raw);
// No support for raw input yet, so always false
Local<Boolean> unsupported = NanNew<Boolean>(false);
Local<Object> rawInput = NanNew<Object>();
rawInput->Set(attrFile, unsupported);
rawInput->Set(attrBuffer, unsupported);
rawInput->Set(attrStream, unsupported);
raw->Set(attrInput, rawInput);
// Raw output via Buffer/Stream is available in libvips >= 7.42.0
Local<Boolean> supportsRawOutput = NanNew<Boolean>(vips_version(0) >= 8 || (vips_version(0) == 7 && vips_version(1) >= 42));
Local<Object> rawOutput = NanNew<Object>();
rawOutput->Set(attrFile, unsupported);
rawOutput->Set(attrBuffer, supportsRawOutput);
rawOutput->Set(attrStream, supportsRawOutput);
raw->Set(attrOutput, rawOutput);
NanReturnValue(format);
}

View File

@@ -1,5 +1,5 @@
#ifndef SHARP_UTILITIES_H #ifndef SRC_UTILITIES_H_
#define SHARP_UTILITIES_H #define SRC_UTILITIES_H_
#include "nan.h" #include "nan.h"
@@ -7,5 +7,6 @@ NAN_METHOD(cache);
NAN_METHOD(concurrency); NAN_METHOD(concurrency);
NAN_METHOD(counters); NAN_METHOD(counters);
NAN_METHOD(libvipsVersion); NAN_METHOD(libvipsVersion);
NAN_METHOD(format);
#endif #endif // SRC_UTILITIES_H_

View File

@@ -9,10 +9,10 @@
}, },
"devDependencies": { "devDependencies": {
"imagemagick": "^0.1.3", "imagemagick": "^0.1.3",
"imagemagick-native": "^1.5.0", "imagemagick-native": "^1.7.0",
"gm": "^1.17.0", "gm": "^1.17.0",
"async": "^0.9.0", "async": "^0.9.0",
"semver": "^4.1.0", "semver": "^4.3.0",
"benchmark": "^1.0.0" "benchmark": "^1.0.0"
}, },
"license": "Apache 2.0", "license": "Apache 2.0",

View File

@@ -17,6 +17,9 @@ var fixtures = require('../fixtures');
var width = 720; var width = 720;
var height = 480; var height = 480;
// Approximately equivalent to fast bilinear
var magickFilter = 'Triangle';
// Disable libvips cache to ensure tests are as fair as they can be // Disable libvips cache to ensure tests are as fair as they can be
sharp.cache(0); sharp.cache(0);
@@ -31,7 +34,9 @@ async.series({
dstPath: fixtures.outputJpg, dstPath: fixtures.outputJpg,
quality: 0.8, quality: 0.8,
width: width, width: width,
height: height height: height,
format: 'jpg',
filter: magickFilter
}, function(err) { }, function(err) {
if (err) { if (err) {
throw err; throw err;
@@ -48,55 +53,78 @@ async.series({
quality: 80, quality: 80,
width: width, width: width,
height: height, height: height,
format: 'JPEG' format: 'JPEG',
filter: magickFilter
}, function (err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
}); });
deferred.resolve();
} }
}).add('gm-buffer-file', { }).add('gm-buffer-file', {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
gm(inputJpgBuffer).resize(width, height).quality(80).write(fixtures.outputJpg, function (err) { gm(inputJpgBuffer)
if (err) { .resize(width, height)
throw err; .filter(magickFilter)
} else { .quality(80)
deferred.resolve(); .write(fixtures.outputJpg, function (err) {
} if (err) {
}); throw err;
} else {
deferred.resolve();
}
});
} }
}).add('gm-buffer-buffer', { }).add('gm-buffer-buffer', {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
gm(inputJpgBuffer).resize(width, height).quality(80).toBuffer(function (err, buffer) { gm(inputJpgBuffer)
if (err) { .resize(width, height)
throw err; .filter(magickFilter)
} else { .quality(80)
assert.notStrictEqual(null, buffer); .toBuffer(function (err, buffer) {
deferred.resolve(); if (err) {
} throw err;
}); } else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
} }
}).add('gm-file-file', { }).add('gm-file-file', {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
gm(fixtures.inputJpg).resize(width, height).quality(80).write(fixtures.outputJpg, function (err) { gm(fixtures.inputJpg)
if (err) { .resize(width, height)
throw err; .filter(magickFilter)
} else { .quality(80)
deferred.resolve(); .write(fixtures.outputJpg, function (err) {
} if (err) {
}); throw err;
} else {
deferred.resolve();
}
});
} }
}).add('gm-file-buffer', { }).add('gm-file-buffer', {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
gm(fixtures.inputJpg).resize(width, height).quality(80).toBuffer(function (err, buffer) { gm(fixtures.inputJpg)
if (err) { .resize(width, height)
throw err; .filter(magickFilter)
} else { .quality(80)
assert.notStrictEqual(null, buffer); .toBuffer(function (err, buffer) {
deferred.resolve(); if (err) {
} throw err;
}); } else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
} }
}).add('sharp-buffer-file', { }).add('sharp-buffer-file', {
defer: true, defer: true,
@@ -319,6 +347,18 @@ async.series({
} }
}); });
} }
}).add('sharp-without-chroma-subsampling', {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer).resize(width, height).withoutChromaSubsampling().toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-rotate', { }).add('sharp-rotate', {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
@@ -359,7 +399,9 @@ async.series({
srcPath: fixtures.inputPng, srcPath: fixtures.inputPng,
dstPath: fixtures.outputPng, dstPath: fixtures.outputPng,
width: width, width: width,
height: height height: height,
format: 'jpg',
filter: magickFilter
}, function(err) { }, function(err) {
if (err) { if (err) {
throw err; throw err;
@@ -375,32 +417,39 @@ async.series({
srcData: inputPngBuffer, srcData: inputPngBuffer,
width: width, width: width,
height: height, height: height,
format: 'PNG' format: 'PNG',
filter: magickFilter
}); });
deferred.resolve(); deferred.resolve();
} }
}).add('gm-file-file', { }).add('gm-file-file', {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
gm(fixtures.inputPng).resize(width, height).write(fixtures.outputPng, function (err) { gm(fixtures.inputPng)
if (err) { .resize(width, height)
throw err; .filter(magickFilter)
} else { .write(fixtures.outputPng, function (err) {
deferred.resolve(); if (err) {
} throw err;
}); } else {
deferred.resolve();
}
});
} }
}).add('gm-file-buffer', { }).add('gm-file-buffer', {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
gm(fixtures.inputPng).resize(width, height).quality(80).toBuffer(function (err, buffer) { gm(fixtures.inputPng)
if (err) { .resize(width, height)
throw err; .filter(magickFilter)
} else { .toBuffer(function (err, buffer) {
assert.notStrictEqual(null, buffer); if (err) {
deferred.resolve(); throw err;
} } else {
}); assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
} }
}).add('sharp-buffer-file', { }).add('sharp-buffer-file', {
defer: true, defer: true,

View File

@@ -11,8 +11,11 @@ var fixtures = require('../fixtures');
var min = 320; var min = 320;
var max = 960; var max = 960;
// Nearest equivalent to bilinear
var magickFilter = 'Triangle';
var randomDimension = function() { var randomDimension = function() {
return Math.random() * (max - min) + min; return Math.ceil(Math.random() * (max - min) + min);
}; };
new Benchmark.Suite('random').add('imagemagick', { new Benchmark.Suite('random').add('imagemagick', {
@@ -23,7 +26,9 @@ new Benchmark.Suite('random').add('imagemagick', {
dstPath: fixtures.outputJpg, dstPath: fixtures.outputJpg,
quality: 0.8, quality: 0.8,
width: randomDimension(), width: randomDimension(),
height: randomDimension() height: randomDimension(),
format: 'jpg',
filter: magickFilter
}, function(err) { }, function(err) {
if (err) { if (err) {
throw err; throw err;
@@ -35,14 +40,18 @@ new Benchmark.Suite('random').add('imagemagick', {
}).add('gm', { }).add('gm', {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
gm(fixtures.inputJpg).resize(randomDimension(), randomDimension()).quality(80).toBuffer(function (err, buffer) { gm(fixtures.inputJpg)
if (err) { .resize(randomDimension(), randomDimension())
throw err; .filter(magickFilter)
} else { .quality(80)
assert.notStrictEqual(null, buffer); .toBuffer(function (err, buffer) {
deferred.resolve(); if (err) {
} throw err;
}); } else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
} }
}).add('sharp', { }).add('sharp', {
defer: true, defer: true,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 813 KiB

After

Width:  |  Height:  |  Size: 810 KiB

17
test/fixtures/Wikimedia-logo.svg vendored Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" standalone="yes"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"
id="Wikimedia logo"
viewBox="-599 -599 1198 1198" width="1024" height="1024">
<defs>
<clipPath id="mask">
<path d="M 47.5,-87.5 v 425 h -95 v -425 l -552,-552 v 1250 h 1199 v -1250 z" />
</clipPath>
</defs>
<g clip-path="url(#mask)">
<circle id="green parts" fill="#396" r="336.5"/>
<circle id="blue arc" fill="none" stroke="#069" r="480.25" stroke-width="135.5" />
</g>
<circle fill="#900" cy="-379.5" r="184.5" id="red circle"/>
</svg>

After

Width:  |  Height:  |  Size: 692 B

BIN
test/fixtures/corrupt-header.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
test/fixtures/free-gearhead-pack.psd vendored Normal file

Binary file not shown.

View File

@@ -14,6 +14,7 @@ module.exports = {
inputJpgWithGammaHoliness: getPath('gamma_dalai_lama_gray.jpg'), // http://www.4p8.com/eric.brasseur/gamma.html 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 inputJpgWithCmykProfile: getPath('Channel_digital_image_CMYK_color.jpg'), // http://en.wikipedia.org/wiki/File:Channel_digital_image_CMYK_color.jpg
inputJpgWithCmykNoProfile: getPath('Channel_digital_image_CMYK_color_no_profile.jpg'), inputJpgWithCmykNoProfile: getPath('Channel_digital_image_CMYK_color_no_profile.jpg'),
inputJpgWithCorruptHeader: getPath('corrupt-header.jpg'),
inputPng: getPath('50020484-00001.png'), // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png inputPng: getPath('50020484-00001.png'), // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png
inputPngWithTransparency: getPath('blackbug.png'), // public domain inputPngWithTransparency: getPath('blackbug.png'), // public domain
@@ -21,6 +22,8 @@ module.exports = {
inputWebP: getPath('4.webp'), // http://www.gstatic.com/webp/gallery/4.webp 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 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 inputGif: getPath('Crash_test.gif'), // http://upload.wikimedia.org/wikipedia/commons/e/e3/Crash_test.gif
inputSvg: getPath('Wikimedia-logo.svg'), // http://commons.wikimedia.org/wiki/File:Wikimedia-logo.svg
inputPsd: getPath('free-gearhead-pack.psd'), // https://dribbble.com/shots/1624241-Free-Gearhead-Vector-Pack
outputJpg: getPath('output.jpg'), outputJpg: getPath('output.jpg'),
outputPng: getPath('output.png'), outputPng: getPath('output.png'),

View File

@@ -35,11 +35,11 @@ describe('Blur', function() {
}); });
}); });
it('specific radius 100', function(done) { it('specific radius 0.3', function(done) {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.resize(320, 240) .resize(320, 240)
.blur(100) .blur(0.3)
.toFile(fixtures.path('output.blur-100.jpg'), function(err, info) { .toFile(fixtures.path('output.blur-0.3.jpg'), function(err, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual('jpeg', info.format); assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width); assert.strictEqual(320, info.width);
@@ -64,7 +64,7 @@ describe('Blur', function() {
it('invalid radius', function(done) { it('invalid radius', function(done) {
var isValid = true; var isValid = true;
try { try {
sharp(fixtures.inputJpg).blur(1.5); sharp(fixtures.inputJpg).blur(0.1);
} catch (err) { } catch (err) {
isValid = false; isValid = false;
} }

46
test/unit/cpplint.js Executable file
View File

@@ -0,0 +1,46 @@
'use strict';
var fs = require('fs');
var path = require('path');
var assert = require('assert');
var cpplint = require('node-cpplint/lib/');
describe('cpplint', function() {
// List C++ source files
fs.readdirSync(path.join(__dirname, '..', '..', 'src')).forEach(function (source) {
var file = path.join('src', source);
it(file, function(done) {
// Lint each source file
cpplint({
files: [file],
linelength: 140,
filters: {
legal: {
copyright: false
},
build: {
include: false,
include_order: false
},
whitespace: {
blank_line: false,
comments: false,
parens: false
}
}
}, function(err, report) {
if (err) {
throw err;
}
var expected = {};
expected[file] = [];
assert.deepEqual(expected, report);
done();
});
});
});
});

View File

@@ -92,4 +92,86 @@ describe('Partial image extraction', function() {
}); });
}); });
it('Extract then rotate', function(done) {
sharp(fixtures.inputJpg)
.extract(10, 10, 100, 100)
.rotate(90)
.toFile(fixtures.path('output.extract.extract-then-rotate.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(100, info.width);
assert.strictEqual(100, info.height);
done();
});
});
it('Rotate then extract', function(done) {
sharp(fixtures.inputJpg)
.rotate(90)
.extract(10, 10, 100, 100)
.toFile(fixtures.path('output.extract.rotate-then-extract.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(100, info.width);
assert.strictEqual(100, info.height);
done();
});
});
describe('Invalid parameters', function() {
it('Undefined', function(done) {
var isValid = true;
try {
sharp(fixtures.inputJpg).extract();
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
it('String top', function(done) {
var isValid = true;
try {
sharp(fixtures.inputJpg).extract('spoons', 10, 10, 10);
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
it('Non-integral left', function(done) {
var isValid = true;
try {
sharp(fixtures.inputJpg).extract(10, 10.2, 10, 10);
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
it('Negative width - negative', function(done) {
var isValid = true;
try {
sharp(fixtures.inputJpg).extract(10, 10, -10, 10);
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
it('Null height', function(done) {
var isValid = true;
try {
sharp(fixtures.inputJpg).extract(10, 10, 10, null);
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
});
}); });

View File

@@ -18,6 +18,7 @@ describe('Input/output', function() {
sharp(fixtures.outputJpg).toBuffer(function(err, data, info) { sharp(fixtures.outputJpg).toBuffer(function(err, data, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, data.length > 0); assert.strictEqual(true, data.length > 0);
assert.strictEqual(data.length, info.size);
assert.strictEqual('jpeg', info.format); assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width); assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height); assert.strictEqual(240, info.height);
@@ -35,6 +36,7 @@ describe('Input/output', function() {
sharp(fixtures.outputJpg).toBuffer(function(err, data, info) { sharp(fixtures.outputJpg).toBuffer(function(err, data, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, data.length > 0); assert.strictEqual(true, data.length > 0);
assert.strictEqual(data.length, info.size);
assert.strictEqual('jpeg', info.format); assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width); assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height); assert.strictEqual(240, info.height);
@@ -49,6 +51,7 @@ describe('Input/output', function() {
var readable = fs.createReadStream(fixtures.inputJpg); var readable = fs.createReadStream(fixtures.inputJpg);
var pipeline = sharp().resize(320, 240).toFile(fixtures.outputJpg, function(err, info) { var pipeline = sharp().resize(320, 240).toFile(fixtures.outputJpg, function(err, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, info.size > 0);
assert.strictEqual('jpeg', info.format); assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width); assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height); assert.strictEqual(240, info.height);
@@ -63,6 +66,7 @@ describe('Input/output', function() {
var pipeline = sharp().resize(320, 240).toBuffer(function(err, data, info) { var pipeline = sharp().resize(320, 240).toBuffer(function(err, data, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, data.length > 0); assert.strictEqual(true, data.length > 0);
assert.strictEqual(data.length, info.size);
assert.strictEqual('jpeg', info.format); assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width); assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height); assert.strictEqual(240, info.height);
@@ -90,6 +94,7 @@ describe('Input/output', function() {
sharp(fixtures.outputJpg).toBuffer(function(err, data, info) { sharp(fixtures.outputJpg).toBuffer(function(err, data, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, data.length > 0); assert.strictEqual(true, data.length > 0);
assert.strictEqual(data.length, info.size);
assert.strictEqual('jpeg', info.format); assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width); assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height); assert.strictEqual(240, info.height);
@@ -130,13 +135,15 @@ describe('Input/output', function() {
readableButNotAnImage.pipe(writable); readableButNotAnImage.pipe(writable);
}); });
it('Sequential read', function(done) { it('Sequential read, force JPEG', function(done) {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.sequentialRead() .sequentialRead()
.resize(320, 240) .resize(320, 240)
.toFormat(sharp.format.jpeg)
.toBuffer(function(err, data, info) { .toBuffer(function(err, data, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, data.length > 0); assert.strictEqual(true, data.length > 0);
assert.strictEqual(data.length, info.size);
assert.strictEqual('jpeg', info.format); assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width); assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height); assert.strictEqual(240, info.height);
@@ -144,13 +151,15 @@ describe('Input/output', function() {
}); });
}); });
it('Not sequential read', function(done) { it('Not sequential read, force JPEG', function(done) {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.sequentialRead(false) .sequentialRead(false)
.resize(320, 240) .resize(320, 240)
.toFormat('jpeg')
.toBuffer(function(err, data, info) { .toBuffer(function(err, data, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, data.length > 0); assert.strictEqual(true, data.length > 0);
assert.strictEqual(data.length, info.size);
assert.strictEqual('jpeg', info.format); assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width); assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height); assert.strictEqual(240, info.height);
@@ -221,6 +230,7 @@ describe('Input/output', function() {
sharp(data).toBuffer(function(err, data, info) { sharp(data).toBuffer(function(err, data, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, data.length > 0); assert.strictEqual(true, data.length > 0);
assert.strictEqual(data.length, info.size);
assert.strictEqual('jpeg', info.format); assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width); assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height); assert.strictEqual(240, info.height);
@@ -262,31 +272,74 @@ describe('Input/output', function() {
.resize(320, 240) .resize(320, 240)
.png() .png()
.progressive(false) .progressive(false)
.toBuffer(function(err, nonProgressive, info) { .toBuffer(function(err, nonProgressiveData, nonProgressiveInfo) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, nonProgressive.length > 0); assert.strictEqual(true, nonProgressiveData.length > 0);
assert.strictEqual('png', info.format); assert.strictEqual(nonProgressiveData.length, nonProgressiveInfo.size);
assert.strictEqual(320, info.width); assert.strictEqual('png', nonProgressiveInfo.format);
assert.strictEqual(240, info.height); assert.strictEqual(320, nonProgressiveInfo.width);
sharp(nonProgressive) assert.strictEqual(240, nonProgressiveInfo.height);
sharp(nonProgressiveData)
.progressive() .progressive()
.toBuffer(function(err, progressive, info) { .toBuffer(function(err, progressiveData, progressiveInfo) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, progressive.length > 0); assert.strictEqual(true, progressiveData.length > 0);
assert.strictEqual(true, progressive.length > nonProgressive.length); assert.strictEqual(progressiveData.length, progressiveInfo.size);
assert.strictEqual('png', info.format); assert.strictEqual(true, progressiveData.length > nonProgressiveData.length);
assert.strictEqual(320, info.width); assert.strictEqual('png', progressiveInfo.format);
assert.strictEqual(240, info.height); assert.strictEqual(320, progressiveInfo.width);
assert.strictEqual(240, progressiveInfo.height);
done(); done();
}); });
}); });
}); });
it('WebP output', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.toFormat(sharp.format.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);
done();
});
});
it('Invalid output format', function(done) {
var isValid = false;
try {
sharp().toFormat('zoinks');
isValid = true;
} catch (e) {}
assert(!isValid);
done();
});
it('File input with corrupt header fails gracefully', function(done) {
sharp(fixtures.inputJpgWithCorruptHeader)
.toBuffer(function(err) {
assert.strictEqual(true, !!err);
done();
});
});
it('Buffer input with corrupt header fails gracefully', function(done) {
sharp(fs.readFileSync(fixtures.inputJpgWithCorruptHeader))
.toBuffer(function(err) {
assert.strictEqual(true, !!err);
done();
});
});
describe('Output filename without extension uses input format', function() { describe('Output filename without extension uses input format', function() {
it('JPEG', function(done) { it('JPEG', function(done) {
sharp(fixtures.inputJpg).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) { sharp(fixtures.inputJpg).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, info.size > 0);
assert.strictEqual('jpeg', info.format); assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width); assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height); assert.strictEqual(80, info.height);
@@ -298,6 +351,7 @@ describe('Input/output', function() {
it('PNG', function(done) { it('PNG', function(done) {
sharp(fixtures.inputPng).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) { sharp(fixtures.inputPng).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, info.size > 0);
assert.strictEqual('png', info.format); assert.strictEqual('png', info.format);
assert.strictEqual(320, info.width); assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height); assert.strictEqual(80, info.height);
@@ -309,6 +363,7 @@ describe('Input/output', function() {
it('Transparent PNG', function(done) { it('Transparent PNG', function(done) {
sharp(fixtures.inputPngWithTransparency).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) { sharp(fixtures.inputPngWithTransparency).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, info.size > 0);
assert.strictEqual('png', info.format); assert.strictEqual('png', info.format);
assert.strictEqual(320, info.width); assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height); assert.strictEqual(80, info.height);
@@ -319,6 +374,7 @@ describe('Input/output', function() {
it('WebP', function(done) { it('WebP', function(done) {
sharp(fixtures.inputWebP).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) { sharp(fixtures.inputWebP).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, info.size > 0);
assert.strictEqual('webp', info.format); assert.strictEqual('webp', info.format);
assert.strictEqual(320, info.width); assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height); assert.strictEqual(80, info.height);
@@ -330,6 +386,7 @@ describe('Input/output', function() {
it('TIFF', function(done) { it('TIFF', function(done) {
sharp(fixtures.inputTiff).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) { sharp(fixtures.inputTiff).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, info.size > 0);
assert.strictEqual('tiff', info.format); assert.strictEqual('tiff', info.format);
assert.strictEqual(320, info.width); assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height); assert.strictEqual(80, info.height);
@@ -369,29 +426,31 @@ describe('Input/output', function() {
done(); done();
}); });
if (semver.gte(sharp.libvipsVersion(), '7.41.0')) { if (semver.gte(sharp.libvipsVersion(), '7.42.0')) {
it('withoutAdaptiveFiltering generates smaller file [libvips 7.41.0+]', function(done) { it('withoutAdaptiveFiltering generates smaller file [libvips ' + sharp.libvipsVersion() + '>=7.42.0]', function(done) {
// First generate with adaptive filtering // First generate with adaptive filtering
sharp(fixtures.inputPng) sharp(fixtures.inputPng)
.resize(320, 240) .resize(320, 240)
.withoutAdaptiveFiltering(false) .withoutAdaptiveFiltering(false)
.toBuffer(function(err, dataAdaptive, info) { .toBuffer(function(err, adaptiveData, adaptiveInfo) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, dataAdaptive.length > 0); assert.strictEqual(true, adaptiveData.length > 0);
assert.strictEqual('png', info.format); assert.strictEqual(adaptiveData.length, adaptiveInfo.size);
assert.strictEqual(320, info.width); assert.strictEqual('png', adaptiveInfo.format);
assert.strictEqual(240, info.height); assert.strictEqual(320, adaptiveInfo.width);
assert.strictEqual(240, adaptiveInfo.height);
// Then generate without // Then generate without
sharp(fixtures.inputPng) sharp(fixtures.inputPng)
.resize(320, 240) .resize(320, 240)
.withoutAdaptiveFiltering() .withoutAdaptiveFiltering()
.toBuffer(function(err, dataWithoutAdaptive, info) { .toBuffer(function(err, withoutAdaptiveData, withoutAdaptiveInfo) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, dataWithoutAdaptive.length > 0); assert.strictEqual(true, withoutAdaptiveData.length > 0);
assert.strictEqual('png', info.format); assert.strictEqual(withoutAdaptiveData.length, withoutAdaptiveInfo.size);
assert.strictEqual(320, info.width); assert.strictEqual('png', withoutAdaptiveInfo.format);
assert.strictEqual(240, info.height); assert.strictEqual(320, withoutAdaptiveInfo.width);
assert.strictEqual(true, dataWithoutAdaptive.length < dataAdaptive.length); assert.strictEqual(240, withoutAdaptiveInfo.height);
assert.strictEqual(true, withoutAdaptiveData.length < adaptiveData.length);
done(); done();
}); });
}); });
@@ -400,8 +459,72 @@ describe('Input/output', function() {
}); });
if (semver.gte(sharp.libvipsVersion(), '7.40.0')) { it('Without chroma subsampling generates larger file', function(done) {
it('Load TIFF from Buffer [libvips 7.40.0+]', function(done) { // First generate with chroma subsampling (default)
sharp(fixtures.inputJpg)
.resize(320, 240)
.withoutChromaSubsampling(false)
.toBuffer(function(err, withChromaSubsamplingData, withChromaSubsamplingInfo) {
if (err) throw err;
assert.strictEqual(true, withChromaSubsamplingData.length > 0);
assert.strictEqual(withChromaSubsamplingData.length, withChromaSubsamplingInfo.size);
assert.strictEqual('jpeg', withChromaSubsamplingInfo.format);
assert.strictEqual(320, withChromaSubsamplingInfo.width);
assert.strictEqual(240, withChromaSubsamplingInfo.height);
// Then generate without
sharp(fixtures.inputJpg)
.resize(320, 240)
.withoutChromaSubsampling()
.toBuffer(function(err, withoutChromaSubsamplingData, withoutChromaSubsamplingInfo) {
if (err) throw err;
assert.strictEqual(true, withoutChromaSubsamplingData.length > 0);
assert.strictEqual(withoutChromaSubsamplingData.length, withoutChromaSubsamplingInfo.size);
assert.strictEqual('jpeg', withoutChromaSubsamplingInfo.format);
assert.strictEqual(320, withoutChromaSubsamplingInfo.width);
assert.strictEqual(240, withoutChromaSubsamplingInfo.height);
assert.strictEqual(true, withChromaSubsamplingData.length < withoutChromaSubsamplingData.length);
done();
});
});
});
if (sharp.format.magick.input.file) {
it('Convert SVG, if supported, to PNG', function(done) {
sharp(fixtures.inputSvg)
.resize(100, 100)
.toFormat('png')
.toFile(fixtures.path('output.svg.png'), function(err, info) {
if (err) {
assert.strictEqual('Input file is of an unsupported image format', err.message);
} else {
assert.strictEqual(true, info.size > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(100, info.width);
assert.strictEqual(100, info.height);
}
done();
});
});
}
if (sharp.format.magick.input.file) {
it('Convert PSD to PNG', function(done) {
sharp(fixtures.inputPsd)
.resize(320, 240)
.toFormat(sharp.format.png)
.toFile(fixtures.path('output.psd.png'), function(err, info) {
if (err) throw err;
assert.strictEqual(true, info.size > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
}
if (sharp.format.tiff.input.buffer) {
it('Load TIFF from Buffer', function(done) {
var inputTiffBuffer = fs.readFileSync(fixtures.inputTiff); var inputTiffBuffer = fs.readFileSync(fixtures.inputTiff);
sharp(inputTiffBuffer) sharp(inputTiffBuffer)
.resize(320, 240) .resize(320, 240)
@@ -409,6 +532,7 @@ describe('Input/output', function() {
.toBuffer(function(err, data, info) { .toBuffer(function(err, data, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, data.length > 0); assert.strictEqual(true, data.length > 0);
assert.strictEqual(data.length, info.size);
assert.strictEqual('jpeg', info.format); assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width); assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height); assert.strictEqual(240, info.height);
@@ -417,4 +541,135 @@ describe('Input/output', function() {
}); });
} }
if (sharp.format.magick.input.buffer) {
it('Load GIF from Buffer', function(done) {
var inputGifBuffer = fs.readFileSync(fixtures.inputGif);
sharp(inputGifBuffer)
.resize(320, 240)
.jpeg()
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual(data.length, info.size);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
}
if (sharp.format.raw.output.buffer) {
describe('Ouput raw, uncompressed image data', function() {
it('1 channel greyscale image', function(done) {
sharp(fixtures.inputJpg)
.greyscale()
.resize(32, 24)
.raw()
.toBuffer(function(err, data, info) {
assert.strictEqual(32 * 24 * 1, info.size);
assert.strictEqual(data.length, info.size);
assert.strictEqual('raw', info.format);
assert.strictEqual(32, info.width);
assert.strictEqual(24, info.height);
done();
});
});
it('3 channel colour image without transparency', function(done) {
sharp(fixtures.inputJpg)
.resize(32, 24)
.toFormat('raw')
.toBuffer(function(err, data, info) {
assert.strictEqual(32 * 24 * 3, info.size);
assert.strictEqual(data.length, info.size);
assert.strictEqual('raw', info.format);
assert.strictEqual(32, info.width);
assert.strictEqual(24, info.height);
done();
});
});
it('4 channel colour image with transparency', function(done) {
sharp(fixtures.inputPngWithTransparency)
.resize(32, 24)
.toFormat(sharp.format.raw)
.toBuffer(function(err, data, info) {
assert.strictEqual(32 * 24 * 4, info.size);
assert.strictEqual(data.length, info.size);
assert.strictEqual('raw', info.format);
assert.strictEqual(32, info.width);
assert.strictEqual(24, info.height);
done();
});
});
});
}
describe('Limit pixel count of input image', function() {
it('Invalid fails - negative', function(done) {
var isValid = false;
try {
sharp().limitInputPixels(-1);
isValid = true;
} catch (e) {}
assert(!isValid);
done();
});
it('Invalid fails - float', function(done) {
var isValid = false;
try {
sharp().limitInputPixels(12.3);
isValid = true;
} catch (e) {}
assert(!isValid);
done();
});
it('Invalid fails - huge', function(done) {
var isValid = false;
try {
sharp().limitInputPixels(Math.pow(0x3FFF, 2) + 1);
isValid = true;
} catch (e) {}
assert(!isValid);
done();
});
it('Invalid fails - string', function(done) {
var isValid = false;
try {
sharp().limitInputPixels('fail');
isValid = true;
} catch (e) {}
assert(!isValid);
done();
});
it('Same size as input works', function(done) {
sharp(fixtures.inputJpg).metadata(function(err, metadata) {
if (err) throw err;
sharp(fixtures.inputJpg)
.limitInputPixels(metadata.width * metadata.height)
.toBuffer(function(err) {
assert.strictEqual(true, !err);
done();
});
});
});
it('Smaller than input fails', function(done) {
sharp(fixtures.inputJpg).metadata(function(err, metadata) {
if (err) throw err;
sharp(fixtures.inputJpg)
.limitInputPixels(metadata.width * metadata.height - 1)
.toBuffer(function(err) {
assert.strictEqual(true, !!err);
done();
});
});
});
});
}); });

View File

@@ -18,6 +18,8 @@ describe('Image metadata', function() {
assert.strictEqual(2225, metadata.height); assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space); assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels); assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasProfile);
assert.strictEqual(false, metadata.hasAlpha);
assert.strictEqual('undefined', typeof metadata.orientation); assert.strictEqual('undefined', typeof metadata.orientation);
done(); done();
}); });
@@ -31,6 +33,7 @@ describe('Image metadata', function() {
assert.strictEqual(600, metadata.height); assert.strictEqual(600, metadata.height);
assert.strictEqual('srgb', metadata.space); assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels); assert.strictEqual(3, metadata.channels);
assert.strictEqual(true, metadata.hasProfile);
assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual(false, metadata.hasAlpha);
assert.strictEqual(8, metadata.orientation); assert.strictEqual(8, metadata.orientation);
done(); done();
@@ -45,6 +48,7 @@ describe('Image metadata', function() {
assert.strictEqual(3248, metadata.height); assert.strictEqual(3248, metadata.height);
assert.strictEqual('b-w', metadata.space); assert.strictEqual('b-w', metadata.space);
assert.strictEqual(1, metadata.channels); assert.strictEqual(1, metadata.channels);
assert.strictEqual(false, metadata.hasProfile);
assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual(false, metadata.hasAlpha);
done(); done();
}); });
@@ -58,6 +62,7 @@ describe('Image metadata', function() {
assert.strictEqual(2074, metadata.height); assert.strictEqual(2074, metadata.height);
assert.strictEqual('b-w', metadata.space); assert.strictEqual('b-w', metadata.space);
assert.strictEqual(1, metadata.channels); assert.strictEqual(1, metadata.channels);
assert.strictEqual(false, metadata.hasProfile);
assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual(false, metadata.hasAlpha);
done(); done();
}); });
@@ -71,6 +76,7 @@ describe('Image metadata', function() {
assert.strictEqual(1536, metadata.height); assert.strictEqual(1536, metadata.height);
assert.strictEqual('srgb', metadata.space); assert.strictEqual('srgb', metadata.space);
assert.strictEqual(4, metadata.channels); assert.strictEqual(4, metadata.channels);
assert.strictEqual(false, metadata.hasProfile);
assert.strictEqual(true, metadata.hasAlpha); assert.strictEqual(true, metadata.hasAlpha);
done(); done();
}); });
@@ -84,6 +90,7 @@ describe('Image metadata', function() {
assert.strictEqual(772, metadata.height); assert.strictEqual(772, metadata.height);
assert.strictEqual('srgb', metadata.space); assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels); assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasProfile);
assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual(false, metadata.hasAlpha);
done(); done();
}); });
@@ -96,6 +103,7 @@ describe('Image metadata', function() {
assert.strictEqual(800, metadata.width); assert.strictEqual(800, metadata.width);
assert.strictEqual(533, metadata.height); assert.strictEqual(533, metadata.height);
assert.strictEqual(3, metadata.channels); assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasProfile);
assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual(false, metadata.hasAlpha);
done(); done();
}); });
@@ -108,11 +116,21 @@ describe('Image metadata', function() {
assert.strictEqual(2225, metadata.height); assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space); assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels); assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasProfile);
assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual(false, metadata.hasAlpha);
done(); done();
}); });
}); });
it('Non-existent file in, Promise out', function(done) {
sharp('fail').metadata().then(function(metadata) {
throw new Error('Non-existent file');
}, function (err) {
assert.ok(!!err);
done();
});
});
it('Stream in, Promise out', function(done) { it('Stream in, Promise out', function(done) {
var readable = fs.createReadStream(fixtures.inputJpg); var readable = fs.createReadStream(fixtures.inputJpg);
var pipeline = sharp(); var pipeline = sharp();
@@ -122,6 +140,7 @@ describe('Image metadata', function() {
assert.strictEqual(2225, metadata.height); assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space); assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels); assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasProfile);
assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual(false, metadata.hasAlpha);
done(); done();
}).catch(function(err) { }).catch(function(err) {
@@ -139,6 +158,7 @@ describe('Image metadata', function() {
assert.strictEqual(2225, metadata.height); assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space); assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels); assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasProfile);
assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual(false, metadata.hasAlpha);
done(); done();
}); });
@@ -154,8 +174,9 @@ describe('Image metadata', function() {
assert.strictEqual(2225, metadata.height); assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space); assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels); assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasProfile);
assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual(false, metadata.hasAlpha);
image.resize(metadata.width / 2).toBuffer(function(err, data, info) { image.resize(Math.floor(metadata.width / 2)).toBuffer(function(err, data, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, data.length > 0); assert.strictEqual(true, data.length > 0);
assert.strictEqual(1362, info.width); assert.strictEqual(1362, info.width);
@@ -166,25 +187,49 @@ describe('Image metadata', function() {
}); });
it('Keep EXIF metadata after a resize', function(done) { it('Keep EXIF metadata after a resize', function(done) {
sharp(fixtures.inputJpgWithExif).resize(320, 240).withMetadata().toBuffer(function(err, buffer) { sharp(fixtures.inputJpgWithExif)
if (err) throw err; .resize(320, 240)
sharp(buffer).metadata(function(err, metadata) { .withMetadata()
.toBuffer(function(err, buffer) {
if (err) throw err; if (err) throw err;
assert.strictEqual(8, metadata.orientation); sharp(buffer).metadata(function(err, metadata) {
done(); if (err) throw err;
assert.strictEqual(true, metadata.hasProfile);
assert.strictEqual(8, metadata.orientation);
done();
});
}); });
});
}); });
it('Remove EXIF metadata after a resize', function(done) { it('Remove EXIF metadata after a resize', function(done) {
sharp(fixtures.inputJpgWithExif).resize(320, 240).withMetadata(false).toBuffer(function(err, buffer) { sharp(fixtures.inputJpgWithExif)
if (err) throw err; .resize(320, 240)
sharp(buffer).metadata(function(err, metadata) { .withMetadata(false)
.toBuffer(function(err, buffer) {
if (err) throw err; if (err) throw err;
assert.strictEqual('undefined', typeof metadata.orientation); sharp(buffer).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual(false, metadata.hasProfile);
assert.strictEqual('undefined', typeof metadata.orientation);
done();
});
});
});
it('File input with corrupt header fails gracefully', function(done) {
sharp(fixtures.inputJpgWithCorruptHeader)
.metadata(function(err) {
assert.strictEqual(true, !!err);
done();
});
});
it('Buffer input with corrupt header fails gracefully', function(done) {
sharp(fs.readFileSync(fixtures.inputJpgWithCorruptHeader))
.metadata(function(err) {
assert.strictEqual(true, !!err);
done(); done();
}); });
});
}); });
}); });

View File

@@ -64,7 +64,7 @@ describe('Resize dimensions', function() {
}); });
}); });
it('Invalid width', function(done) { it('Invalid width - NaN', function(done) {
var isValid = true; var isValid = true;
try { try {
sharp(fixtures.inputJpg).resize('spoons', 240); sharp(fixtures.inputJpg).resize('spoons', 240);
@@ -75,7 +75,7 @@ describe('Resize dimensions', function() {
done(); done();
}); });
it('Invalid height', function(done) { it('Invalid height - NaN', function(done) {
var isValid = true; var isValid = true;
try { try {
sharp(fixtures.inputJpg).resize(320, 'spoons'); sharp(fixtures.inputJpg).resize(320, 'spoons');
@@ -86,6 +86,50 @@ describe('Resize dimensions', function() {
done(); done();
}); });
it('Invalid width - float', function(done) {
var isValid = true;
try {
sharp(fixtures.inputJpg).resize(1.5, 240);
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
it('Invalid height - float', function(done) {
var isValid = true;
try {
sharp(fixtures.inputJpg).resize(320, 1.5);
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
it('Invalid width - too large', function(done) {
var isValid = true;
try {
sharp(fixtures.inputJpg).resize(0x4000, 240);
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
it('Invalid height - too large', function(done) {
var isValid = true;
try {
sharp(fixtures.inputJpg).resize(320, 0x4000);
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
it('TIFF embed known to cause rounding errors', function(done) { it('TIFF embed known to cause rounding errors', function(done) {
sharp(fixtures.inputTiff).resize(240, 320).embed().jpeg().toBuffer(function(err, data, info) { sharp(fixtures.inputTiff).resize(240, 320).embed().jpeg().toBuffer(function(err, data, info) {
if (err) throw err; if (err) throw err;
@@ -141,6 +185,39 @@ describe('Resize dimensions', function() {
}); });
}); });
it('Min width or height considering ratio (landscape)', function(done) {
sharp(fixtures.inputJpg).resize(320, 320).min().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(392, info.width);
assert.strictEqual(320, info.height);
done();
});
});
it('Min width or height considering ratio (portrait)', function(done) {
sharp(fixtures.inputTiff).resize(320, 320).min().jpeg().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(422, info.height);
done();
});
});
it('Provide only one dimension with min, should default to crop', function(done) {
sharp(fixtures.inputJpg).resize(320).min().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) { it('Do not enlarge when input width is already less than output width', function(done) {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.resize(2800) .resize(2800)

View File

@@ -50,4 +50,25 @@ describe('Utilities', function() {
}); });
}); });
describe('Format', function() {
it('Contains expected attributes', function() {
assert.strictEqual('object', typeof sharp.format);
Object.keys(sharp.format).forEach(function(format) {
assert.strictEqual(true, 'id' in sharp.format[format]);
assert.strictEqual(format, sharp.format[format].id);
['input', 'output'].forEach(function(direction) {
assert.strictEqual(true, direction in sharp.format[format]);
assert.strictEqual('object', typeof sharp.format[format][direction]);
assert.strictEqual(3, Object.keys(sharp.format[format][direction]).length);
assert.strictEqual(true, 'file' in sharp.format[format][direction]);
assert.strictEqual(true, 'buffer' in sharp.format[format][direction]);
assert.strictEqual(true, 'stream' in sharp.format[format][direction]);
assert.strictEqual('boolean', typeof sharp.format[format][direction].file);
assert.strictEqual('boolean', typeof sharp.format[format][direction].buffer);
assert.strictEqual('boolean', typeof sharp.format[format][direction].stream);
});
});
});
});
}); });