Compare commits

...

79 Commits

Author SHA1 Message Date
Lovell Fuller
75d72cfded Version bumps ahead of v0.10.1 2015-06-01 09:50:49 +01:00
Lovell Fuller
21b0d8c7f7 Version bumps ahead of v0.10.1
Use SPDX format in licence field
2015-05-31 19:35:24 +01:00
Lovell Fuller
fa8f06f07d Include C standard library for 'atoi' #228
Xcode 6.3 appears to no longer do this
2015-05-31 19:05:18 +01:00
Lovell Fuller
e07a105b7c Test availability of __has_feature macro 2015-05-11 11:54:53 +01:00
Lovell Fuller
4f72dcbf54 Verify platform/compiler compatibility #178 #214 2015-05-11 10:46:47 +01:00
Lovell Fuller
b77877c83d Add Amazon Linux 2015.03
Update to libvips version 8.0.2
2015-05-07 15:50:14 +01:00
Lovell Fuller
8fd3520257 Correct use of Windows CI env variable 2015-05-05 11:15:32 +01:00
Lovell Fuller
f15e64039c Windows CI does not yet have io.js v2 2015-05-05 10:46:23 +01:00
Lovell Fuller
3ffe2ba17f Windows CI matrix and version bumps 2015-05-05 10:15:20 +01:00
Lovell Fuller
33782d3c83 Embed alpha image on non-transparent background #204 2015-04-29 20:14:45 +01:00
Lovell Fuller
783826aa26 Reverse Openslide if/else for Debian 8 #203 2015-04-27 18:44:53 +01:00
Lovell Fuller
c2ef16eac2 Don't publish AppVeyor config to npm 2015-04-27 18:44:11 +01:00
Lovell Fuller
e999fb6e30 Dependency version bumps ahead of v0.10.0 2015-04-23 14:29:14 +01:00
Lovell Fuller
d1fc0591a5 Silence Windows compiler warnings #19 2015-04-21 14:39:37 +01:00
Lovell Fuller
fb1c9cf3d3 Less strict assert for unordered events 2015-04-21 13:57:24 +01:00
Lovell Fuller
21ba1dfc26 Wrap flapping event test in nextTick 2015-04-21 13:22:29 +01:00
Lovell Fuller
dacd62428e Fix Windows CI binding config 2015-04-21 12:37:45 +01:00
Lovell Fuller
1e52c2dbe6 Windows compatibility #19
Hide WebP format and normalise option

Separate test runners for node and iojs
2015-04-21 12:13:19 +01:00
Lovell Fuller
8926ebc56c Add Appveyor config for Windows CI
Silence 'possible loss of data' warning
2015-04-20 19:00:22 +01:00
Lovell Fuller
9da87ce868 Fix typo in conditional introduced in 8ac33aa 2015-04-20 17:50:47 +01:00
Lovell Fuller
46cc45c186 Fail fast for unknown interpolator 2015-04-20 11:22:21 +01:00
Lovell Fuller
54f2243386 Merge pull request #198 from bkw/unknownInterpolator
Runtime guard against unknown interpolator class, avoids segfault.
2015-04-20 10:17:44 +01:00
Bernhard K. Weisshuhn
8ac33aad69 avoid segfault with unknown interpolator 2015-04-20 00:24:03 +02:00
Lovell Fuller
6fc62d39c9 Update thank you list
Add perf test for normalise
2015-04-19 21:09:19 +01:00
Lovell Fuller
a0655806de No need to remove dzi file extension
libvips handles this - ensures filenames containing . work
2015-04-19 21:08:15 +01:00
Lovell Fuller
3614d14f83 A few small fixes to the test scripts 2015-04-19 16:15:40 +01:00
Lovell Fuller
f6fd45cc90 Expose libjpeg extension param features
Trellis quantisation, overshoot deringing and scan optimisation
2015-04-19 16:15:40 +01:00
Lovell Fuller
be39297f3b Merge pull request #194 from bkw/normalize
Add normalize() to use full luminance range.
2015-04-19 16:05:20 +01:00
Bernhard K. Weisshuhn
dce36e0074 Add normalize() for simple histogram stretching
Available as normalize() or normalise().
Normalization takes place in LAB place and thus should not change any
colors.

Existing alpha channels are preserved untouched by normalization.
2015-04-18 12:55:04 +02:00
Lovell Fuller
ba034a8164 Add docs for new ignoreAspectRatio option 2015-04-16 18:28:30 +01:00
Lovell Fuller
3dfc7bea3a Merge pull request #192 from skedastik/judgement
Add support for ignoreAspectRatio option when resizing
2015-04-16 15:53:43 +01:00
Alaric Holloway
f72435c750 Support resize without preserving aspect ratio #118 2015-04-16 06:50:47 -07:00
Lovell Fuller
3810f642d3 Add small cache before convolution for seq access 2015-04-14 21:31:20 +01:00
Lovell Fuller
ae968142ee Soften limitInputPixels upper limit #146
Default limit of 14-bit dimensions remains
2015-04-12 14:23:36 +01:00
Lovell Fuller
ccb7887cb9 Use libjpeg-dev metapackage for Debian variants #190 2015-04-09 11:56:36 +01:00
Lovell Fuller
f1ad1216ca Add support for Windows #19
Requires VIPS_HOME environment variable
2015-04-05 22:42:14 +01:00
Lovell Fuller
ce6813329b Improve code portability ahead of Windows support 2015-04-05 21:57:53 +01:00
Lovell Fuller
7ad7193b1e Add test for progressive JPEG output 2015-03-30 11:36:14 +01:00
Lovell Fuller
bd96a49de6 Add usage example of Stream error handling #182 2015-03-24 10:35:05 +00:00
Lovell Fuller
81c710eaa3 Add EventEmitter for queue length changes
Remove unnecessary params from Error handler
2015-03-20 15:44:18 +00:00
Lovell Fuller
711f0fefb6 Disable unused static library and introspection components 2015-03-20 14:57:13 +00:00
Lovell Fuller
33ca86e4f2 Add example of Deep Zoom output
Clean up preinstall and prereqs docs
2015-03-12 17:33:06 +00:00
Lovell Fuller
9b5229f2dd Support old and new Magick loader class names
See jcupitt/libvips@99b4bcb
2015-03-12 17:12:06 +00:00
Lovell Fuller
5781a23a4d Combine new tile* API methods
Use v7.40.0+ libvips loader methods

Separate Openslide as input vs Deep Zoom as output

Split tile-based tests into new file

Added assertions for generated tile size
2015-03-12 15:39:27 +00:00
Victor Mateevitsi
2d1e6f2644 Added Deep Zoom support.
Added OpenSuse 13.1 and 13.2 support in preinstall.sh script.
Added OpenSlide support in preinstall script.
Added unit tests for Deep Zoom and OpenSlide.
2015-03-10 14:00:27 +00:00
Lovell Fuller
5240eeb518 Merge pull request #166 from mcuelenaere/no-custom-image-header-checking
Let libvips check whether we received a valid image or not
2015-03-01 15:41:37 +00:00
Maurus Cuelenaere
125ee836fe Let libvips check whether we received a valid image or not
This removes the custom image fingerprinting code and uses the libvips
is_a_buffer() infrastructure instead.
2015-03-01 11:53:17 +01:00
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
37 changed files with 2184 additions and 452 deletions

2
.gitignore vendored
View File

@@ -2,7 +2,7 @@ build
node_modules node_modules
coverage coverage
test/bench/node_modules test/bench/node_modules
test/fixtures/output.* test/fixtures/output*
test/leak/libvips.supp test/leak/libvips.supp
# Mac OS X # Mac OS X

View File

@@ -2,7 +2,7 @@
"strict": true, "strict": true,
"node": true, "node": true,
"maxparams": 4, "maxparams": 4,
"maxcomplexity": 11, "maxcomplexity": 13,
"globals": { "globals": {
"describe": true, "describe": true,
"it": true "it": true

View File

@@ -6,3 +6,4 @@ coverage
.gitignore .gitignore
test test
.travis.yml .travis.yml
appveyor.yml

View File

@@ -1,7 +1,9 @@
language: node_js language: node_js
node_js: node_js:
- "0.10" - "0.10"
- "0.11" - "0.12"
- "iojs-v1"
- "iojs-v2"
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:

184
README.md
View File

@@ -12,7 +12,11 @@
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.
This module supports reading and writing JPEG, PNG and WebP images to and from Streams, Buffer objects and the filesystem. 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. It also supports reading images of many other formats from the filesystem via libmagick, libgraphicsmagick or [OpenSlide](http://openslide.org/) if present.
Deep Zoom image pyramids can be generated, suitable for use with "slippy map" tile viewers like
[OpenSeadragon](https://github.com/openseadragon/openseadragon) and [Leaflet](https://github.com/turban/Leaflet.Zoomify).
Colour spaces, embedded ICC profiles and alpha transparency channels are all handled correctly. Colour spaces, embedded ICC profiles and alpha transparency channels are all handled correctly.
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. 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.
@@ -31,8 +35,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+, clang 3.0+ or MSVC 2013
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:
@@ -47,6 +52,8 @@ To install the most suitable version of libvips on the following Operating Syste
* RHEL/Centos/Scientific 6, 7 * RHEL/Centos/Scientific 6, 7
* Fedora 21, 22 * Fedora 21, 22
* Amazon Linux 2014.09 * Amazon Linux 2014.09
* OpenSuse Linux
* OpenSuse 13.1, 13.2
run the following as a user with `sudo` access: run the following as a user with `sudo` access:
@@ -58,6 +65,10 @@ or run the following as `root`:
The [preinstall.sh](https://github.com/lovell/sharp/blob/master/preinstall.sh) script requires `curl` and `pkg-config`. The [preinstall.sh](https://github.com/lovell/sharp/blob/master/preinstall.sh) script requires `curl` and `pkg-config`.
Add `--with-openslide` to enable OpenSlide support:
curl -s https://raw.githubusercontent.com/lovell/sharp/master/preinstall.sh | sudo bash -s -- --with-openslide
### Mac OS tips ### Mac OS tips
Manual install via homebrew: Manual install via homebrew:
@@ -72,6 +83,18 @@ The _gettext_ dependency of _libvips_ [can lead](https://github.com/lovell/sharp
brew link gettext --force brew link gettext --force
### Windows
Requires x86 32-bit Node.js or io.js (use `iojs.exe` rather than `node.exe`).
The WebP format is currently unsupported.
1. Ensure the [node-gyp prerequisites](https://github.com/TooTallNate/node-gyp#installation) are met.
2. [Download](http://www.vips.ecs.soton.ac.uk/supported/current/win32/) and unzip `vips-dev.x.y.z.zip`.
3. Set the `VIPS_HOME` environment variable to the full path of the `vips-dev-x.y.z` directory.
4. Add `vips-dev-x.y.z\bin` to `PATH`.
Versions of MSVC more recent than 2013 may require the use of `npm install --arch=ia32 --msvs_version=2013`.
### 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.
@@ -105,9 +128,14 @@ sharp('input.jpg').resize(300, 200).toFile('output.jpg', function(err) {
``` ```
```javascript ```javascript
var transformer = sharp().resize(300, 200).crop(sharp.gravity.north); var transformer = sharp()
readableStream.pipe(transformer).pipe(writableStream); .resize(300, 200)
.crop(sharp.gravity.north)
.on('error', function(err) {
console.log(err);
});
// Read image data from readableStream, resize and write image data to writableStream // Read image data from readableStream, resize and write image data to writableStream
readableStream.pipe(transformer).pipe(writableStream);
``` ```
```javascript ```javascript
@@ -187,7 +215,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
}); });
``` ```
@@ -197,7 +225,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;
@@ -211,37 +239,93 @@ 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
sharp('input.tiff').tile(256).toFile('output.dzi', function(err, info) {
// The output.dzi file is the XML format Deep Zoom definition
// The output_files directory contains 256x256 pixel tiles grouped by zoom level
});
```
```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 } } }
```
#### queue
An EventEmitter that emits a `change` event when a task is either:
* queued, waiting for _libuv_ to provide a worker thread
* complete
```javascript
sharp.queue.on('change', function(queueLength) {
console.log('Queue contains ' + queueLength + ' task(s)');
});
```
### 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.
`callback`, if present, gets the arguments `(err, metadata)` where `metadata` has the attributes: `callback`, if present, gets the arguments `(err, metadata)` where `metadata` has the attributes:
* `format`: Name of decoder to be used to decompress image data e.g. `jpeg`, `png`, `webp` (for file-based input additionally `tiff` and `magick`) * `format`: Name of decoder to be used to decompress image data e.g. `jpeg`, `png`, `webp` (for file-based input additionally `tiff`, `magick` and `openslide`)
* `width`: Number of pixels wide * `width`: Number of pixels wide
* `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)
@@ -292,10 +376,26 @@ 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`. 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`.
#### ignoreAspectRatio()
Ignoring the aspect ratio of the input, stretch the image to the exact `width` and/or `height` provided via `resize`.
#### background(rgba) #### background(rgba)
Set the background for the `embed` and `flatten` operations. Set the background for the `embed` and `flatten` operations.
@@ -389,6 +489,10 @@ This is a linear operation. If the input image is in a non-linear colour space s
The output image will still be web-friendly sRGB and contain three (identical) channels. The output image will still be web-friendly sRGB and contain three (identical) channels.
#### normalize() / normalise()
Enhance output image contrast by stretching its luminance to cover the full dynamic range. This typically reduces performance by 30%.
### Output options ### Output options
#### jpeg() #### jpeg()
@@ -415,6 +519,13 @@ The number of channels depends on the input image and selected options.
* 3 channels for colour images without alpha transparency, with bytes ordered \[red, green, blue, red, green, blue, etc.\]). * 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.\]. * 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`.
@@ -431,6 +542,46 @@ Include all metadata (EXIF, XMP, IPTC) from the input image in the output image.
The default behaviour is to strip all metadata and convert to the device-independent sRGB colour space. The default behaviour is to strip all metadata and convert to the device-independent sRGB colour space.
#### tile([size], [overlap])
The size and overlap, in pixels, of square Deep Zoom image pyramid tiles.
* `size` is an integral Number between 1 and 8192. The default value is 256 pixels.
* `overlap` is an integral Number between 0 and 8192. The default value is 0 pixels.
#### 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).
#### trellisQuantisation() / trellisQuantization()
_Requires libvips 8.0.0+ compiled against mozjpeg 3.0+_
An advanced setting to apply the use of
[trellis quantisation](http://en.wikipedia.org/wiki/Trellis_quantization) with JPEG output.
Reduces file size and slightly increases relative quality at the cost of increased compression time.
#### overshootDeringing()
_Requires libvips 8.0.0+ compiled against mozjpeg 3.0+_
An advanced setting to reduce the effects of
[ringing](http://en.wikipedia.org/wiki/Ringing_%28signal%29) in JPEG output,
in particular where black text appears on a white background (or vice versa).
#### optimiseScans() / optimizeScans()
_Requires libvips 8.0.0+ compiled against mozjpeg 3.0+_
An advanced setting for progressive (interlace) JPEG output.
Calculates which spectrum of DCT coefficients uses the fewest bits.
Usually reduces file size at the cost of increased compression time.
#### compressionLevel(compressionLevel) #### compressionLevel(compressionLevel)
An advanced setting for the _zlib_ compression level of the lossless PNG output format. The default level is `6`. An advanced setting for the _zlib_ compression level of the lossless PNG output format. The default level is `6`.
@@ -447,7 +598,7 @@ An advanced setting to disable adaptive row filtering for the lossless PNG outpu
#### toFile(filename, [callback]) #### toFile(filename, [callback])
`filename` is a String containing the filename to write the image data to. The format is inferred from the extension, with JPEG, PNG, WebP and TIFF supported. `filename` is a String containing the filename to write the image data to. The format is inferred from the extension, with JPEG, PNG, WebP, TIFF and DZI supported.
`callback`, if present, is called with two arguments `(err, info)` where: `callback`, if present, is called with two arguments `(err, info)` where:
@@ -530,6 +681,10 @@ A [guide for contributors](https://github.com/lovell/sharp/blob/master/CONTRIBUT
[![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)
#### Windows Server 2012
[![Windows Server 2012 Build Status](https://ci.appveyor.com/api/projects/status/pgtul704nkhhg6sg)](https://ci.appveyor.com/project/lovell/sharp)
### Benchmark tests ### Benchmark tests
``` ```
@@ -611,6 +766,11 @@ 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)
* [Linus Unneb<65>ck](https://github.com/LinusU)
* [Victor Mateevitsi](https://github.com/mvictoras)
* [Alaric Holloway](https://github.com/skedastik)
* [Bernhard K. Weisshuhn](https://github.com/bkw)
Thank you! Thank you!

25
appveyor.yml Normal file
View File

@@ -0,0 +1,25 @@
os: Visual Studio 2014 CTP4
platform: Win32
configuration: Release
environment:
VIPS_VERSION_MAJOR_MINOR: 8.0
VIPS_VERSION_PATCH: 2
VIPS_WARNING: 0
matrix:
- nodejs_version: "0.10"
nodejs_exec: "node"
- nodejs_version: "0.12"
nodejs_exec: "node"
- nodejs_version: "1.0"
nodejs_exec: "iojs"
install:
- ps: $env:VIPS_VERSION = "$env:VIPS_VERSION_MAJOR_MINOR.$env:VIPS_VERSION_PATCH"
- ps: Write-Output "Fetching http://www.vips.ecs.soton.ac.uk/supported/$env:VIPS_VERSION_MAJOR_MINOR/win32/vips-dev-$env:VIPS_VERSION.zip"
- ps: Start-FileDownload http://www.vips.ecs.soton.ac.uk/supported/$env:VIPS_VERSION_MAJOR_MINOR/win32/vips-dev-$env:VIPS_VERSION.zip -FileName c:\vips-dev-$env:VIPS_VERSION.zip
- ps: Invoke-Expression "& 7z -y x c:\vips-dev-$env:VIPS_VERSION.zip -oc:\ | FIND /V `"ing `""
- ps: $env:VIPS_HOME = "c:\vips-dev-$env:VIPS_VERSION"
- ps: $env:PATH = "$env:VIPS_HOME\bin;$env:PATH"
- ps: Install-Product node $env:nodejs_version x86
- npm install --arch=ia32 --msvs_version=2013
test_script:
- npm run-script test-win32-%nodejs_exec%

View File

@@ -8,15 +8,50 @@
'src/resize.cc', 'src/resize.cc',
'src/sharp.cc' 'src/sharp.cc'
], ],
'variables': { 'conditions': [
'PKG_CONFIG_PATH': '<!(which brew >/dev/null 2>&1 && eval $(brew --env) && echo $PKG_CONFIG_LIBDIR || true):$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig:/usr/lib/pkgconfig' ['OS=="win"', {
}, 'library_dirs': [
'libraries': [ '$(VIPS_HOME)/lib'
'<!(PKG_CONFIG_PATH="<(PKG_CONFIG_PATH)" pkg-config --libs vips)' ],
], 'libraries': [
'include_dirs': [ 'libvips.dll.a',
'<!(PKG_CONFIG_PATH="<(PKG_CONFIG_PATH)" pkg-config --cflags vips glib-2.0)', 'glib-2.0.lib',
'<!(node -e "require(\'nan\')")' 'gobject-2.0.lib',
'gthread-2.0.lib',
'gmodule-2.0.lib',
'liblcms2.dll.a',
'libxml2.lib',
'intl.lib',
'libjpeg.dll.a',
'libexif.dll.a',
'libpng.lib',
'libtiff.dll.a',
'libMagickWand-6.Q16.dll.a',
'libMagickCore-6.Q16.dll.a',
'pango-1.0.lib',
'pangoft2-1.0.lib',
'libgsf-1.dll.a',
'libopenslide.dll.a',
'libfftw3.dll.a'
],
'include_dirs': [
'$(VIPS_HOME)/include',
'$(VIPS_HOME)/include/glib-2.0',
'$(VIPS_HOME)/lib/glib-2.0/include',
'<!(node -e "require(\'nan\')")'
]
}, {
'variables': {
'PKG_CONFIG_PATH': '<!(which brew >/dev/null 2>&1 && eval $(brew --env) && echo $PKG_CONFIG_LIBDIR || true):$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig:/usr/lib/pkgconfig'
},
'libraries': [
'<!(PKG_CONFIG_PATH="<(PKG_CONFIG_PATH)" pkg-config --libs vips)'
],
'include_dirs': [
'<!(PKG_CONFIG_PATH="<(PKG_CONFIG_PATH)" pkg-config --cflags vips glib-2.0)',
'<!(node -e "require(\'nan\')")'
]
}]
], ],
'cflags_cc': [ 'cflags_cc': [
'-std=c++0x', '-std=c++0x',
@@ -33,6 +68,11 @@
'-O3' '-O3'
], ],
'MACOSX_DEPLOYMENT_TARGET': '10.7' 'MACOSX_DEPLOYMENT_TARGET': '10.7'
},
'msvs_settings': {
'VCCLCompilerTool': {
'ExceptionHandling': 1 # /EHsc
}
} }
}] }]
} }

210
index.js
View File

@@ -3,6 +3,7 @@
var path = require('path'); var path = require('path');
var util = require('util'); var util = require('util');
var stream = require('stream'); var stream = require('stream');
var events = require('events');
var semver = require('semver'); var semver = require('semver');
var color = require('color'); var color = require('color');
@@ -41,7 +42,7 @@ 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, rotateBeforePreExtract: false,
@@ -58,36 +59,32 @@ var Sharp = function(input) {
sharpenJagged: 2, sharpenJagged: 2,
gamma: 0, gamma: 0,
greyscale: false, greyscale: false,
normalize: 0,
// output options // output options
output: '__input', output: '__input',
progressive: false, progressive: false,
quality: 80, quality: 80,
compressionLevel: 6, compressionLevel: 6,
withoutAdaptiveFiltering: false, withoutAdaptiveFiltering: false,
withoutChromaSubsampling: false,
trellisQuantisation: false,
overshootDeringing: false,
optimiseScans: false,
streamOut: false, streamOut: false,
withMetadata: false withMetadata: false,
tileSize: 256,
tileOverlap: 0,
// Function to notify of queue length changes
queueListener: function(queueLength) {
module.exports.queue.emit('change', queueLength);
}
}; };
if (typeof input === 'string') { if (typeof input === 'string') {
// input=file // input=file
this.options.fileIn = input; this.options.fileIn = input;
} else if (typeof input === 'object' && input instanceof Buffer) { } else if (typeof input === 'object' && input instanceof Buffer) {
// input=buffer // input=buffer
if ( this.options.bufferIn = input;
(input.length > 3) &&
// JPEG
(input[0] === 0xFF && input[1] === 0xD8) ||
// PNG
(input[0] === 0x89 && input[1] === 0x50) ||
// WebP
(input[0] === 0x52 && input[1] === 0x49) ||
// TIFF
(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)
) {
this.options.bufferIn = input;
} else {
throw new Error('Buffer contains an unsupported image format. JPEG, PNG, WebP and TIFF are currently supported.');
}
} else { } else {
// input=stream // input=stream
this.options.streamIn = true; this.options.streamIn = true;
@@ -97,6 +94,16 @@ var Sharp = function(input) {
module.exports = Sharp; module.exports = Sharp;
util.inherits(Sharp, stream.Duplex); util.inherits(Sharp, stream.Duplex);
/*
EventEmitter singleton emits queue length 'change' events
*/
module.exports.queue = new events.EventEmitter();
/*
Supported image formats
*/
module.exports.format = sharp.format();
/* /*
Handle incoming chunk on Writable Stream Handle incoming chunk on Writable Stream
*/ */
@@ -128,7 +135,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 {
@@ -142,7 +149,11 @@ 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 // Ensure existing rotation occurs before pre-resize extraction
if (suffix === 'Pre' && this.options.angle !== 0) { if (suffix === 'Pre' && this.options.angle !== 0) {
@@ -164,12 +175,26 @@ 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;
};
/*
Ignoring the aspect ratio of the input, stretch the image to
the exact width and/or height provided via the resize method.
*/
Sharp.prototype.ignoreAspectRatio = function() {
this.options.canvas = 'ignore_aspect';
return this; return this;
}; };
@@ -292,7 +317,18 @@ module.exports.interpolator = {
vertexSplitQuadraticBasisSpline: 'vsqbs' vertexSplitQuadraticBasisSpline: 'vsqbs'
}; };
Sharp.prototype.interpolateWith = function(interpolator) { Sharp.prototype.interpolateWith = function(interpolator) {
this.options.interpolator = interpolator; var isValid = false;
for (var key in module.exports.interpolator) {
if (module.exports.interpolator[key] === interpolator) {
isValid = true;
break;
}
}
if (isValid) {
this.options.interpolator = interpolator;
} else {
throw new Error('Invalid interpolator ' + interpolator);
}
return this; return this;
}; };
@@ -312,6 +348,19 @@ Sharp.prototype.gamma = function(gamma) {
return this; return this;
}; };
/*
Enhance output image contrast by stretching its luminance to cover the full dynamic range
*/
Sharp.prototype.normalize = function(normalize) {
if (process.platform !== 'win32') {
this.options.normalize = (typeof normalize === 'boolean') ? normalize : true;
} else {
console.error('normalize unavailable on win32 platform');
}
return this;
};
Sharp.prototype.normalise = Sharp.prototype.normalize;
/* /*
Convert to greyscale Convert to greyscale
*/ */
@@ -364,11 +413,89 @@ 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;
};
/*
Apply trellis quantisation to JPEG output - requires libvips 8.0.0+ compiled against mozjpeg 3.0+
*/
Sharp.prototype.trellisQuantisation = function(trellisQuantisation) {
if (semver.gte(libvipsVersion, '8.0.0')) {
this.options.trellisQuantisation = (typeof trellisQuantisation === 'boolean') ? trellisQuantisation : true;
} else {
console.error('trellisQuantisation requires libvips 8.0.0+');
}
return this;
};
Sharp.prototype.trellisQuantization = Sharp.prototype.trellisQuantisation;
/*
Apply overshoot deringing to JPEG output - requires libvips 8.0.0+ compiled against mozjpeg 3.0+
*/
Sharp.prototype.overshootDeringing = function(overshootDeringing) {
if (semver.gte(libvipsVersion, '8.0.0')) {
this.options.overshootDeringing = (typeof overshootDeringing === 'boolean') ? overshootDeringing : true;
} else {
console.error('overshootDeringing requires libvips 8.0.0+');
}
return this;
};
/*
Optimise scans in progressive JPEG output - requires libvips 8.0.0+ compiled against mozjpeg 3.0+
*/
Sharp.prototype.optimiseScans = function(optimiseScans) {
if (semver.gte(libvipsVersion, '8.0.0')) {
this.options.optimiseScans = (typeof optimiseScans === 'boolean') ? optimiseScans : true;
if (this.options.optimiseScans) {
this.progressive();
}
} else {
console.error('optimiseScans requires libvips 8.0.0+');
}
return this;
};
Sharp.prototype.optimizeScans = Sharp.prototype.optimiseScans;
/*
Include all metadata (EXIF, XMP, IPTC) from the input image in the output image
*/
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;
}; };
/*
Tile size and overlap for Deep Zoom output
*/
Sharp.prototype.tile = function(size, overlap) {
// Size of square tiles, in pixels
if (typeof size !== 'undefined' && size !== null) {
if (!Number.isNaN(size) && size % 1 === 0 && size >= 1 && size <= 8192) {
this.options.tileSize = size;
} else {
throw new Error('Invalid tile size (1 to 8192) ' + size);
}
}
// Overlap of tiles, in pixels
if (typeof overlap !== 'undefined' && overlap !== null) {
if (!Number.isNaN(overlap) && overlap % 1 === 0 && overlap >=0 && overlap <= 8192) {
if (overlap > this.options.tileSize) {
throw new Error('Tile overlap ' + overlap + ' cannot be larger than tile size ' + this.options.tileSize);
}
this.options.tileOverlap = overlap;
} else {
throw new Error('Invalid tile overlap (0 to 8192) ' + overlap);
}
}
return this;
};
Sharp.prototype.resize = function(width, height) { Sharp.prototype.resize = function(width, height) {
if (!width) { if (!width) {
this.options.width = -1; this.options.width = -1;
@@ -396,7 +523,7 @@ Sharp.prototype.resize = function(width, height) {
Assumes the image dimensions contained in the file header can be trusted Assumes the image dimensions contained in the file header can be trusted
*/ */
Sharp.prototype.limitInputPixels = function(limit) { Sharp.prototype.limitInputPixels = function(limit) {
if (typeof limit === 'number' && !Number.isNaN(limit) && limit % 1 === 0 && limit > 0 && limit <= maximum.pixels) { if (typeof limit === 'number' && !Number.isNaN(limit) && limit % 1 === 0 && limit > 0) {
this.options.limitInputPixels = limit; this.options.limitInputPixels = limit;
} else { } else {
throw new Error('Invalid pixel limit (1 to ' + maximum.pixels + ') ' + limit); throw new Error('Invalid pixel limit (1 to ' + maximum.pixels + ') ' + limit);
@@ -431,27 +558,43 @@ 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() { Sharp.prototype.raw = function() {
if (semver.gte(libvipsVersion, '7.42.0')) { var supportsRawOutput = module.exports.format.raw.output;
if (supportsRawOutput.file || supportsRawOutput.buffer || supportsRawOutput.stream) {
this.options.output = '__raw'; this.options.output = '__raw';
} else { } else {
console.error('Raw output requires libvips 7.42.0+'); console.error('Raw output requires libvips 7.42.0+');
@@ -459,6 +602,23 @@ Sharp.prototype.raw = function() {
return this; 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
*/ */

View File

@@ -1,6 +1,6 @@
{ {
"name": "sharp", "name": "sharp",
"version": "0.9.0", "version": "0.10.1",
"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,18 @@
"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>",
"Victor Mateevitsi <mvictoras@gmail.com>",
"Alaric Holloway <alaric.holloway@gmail.com>",
"Bernhard K. Weisshuhn <bkw@codingforce.com>"
], ],
"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": "VIPS_WARNING=0 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=20000 ./test/unit/*.js",
"test-win32-node": "node ./node_modules/mocha/bin/mocha --slow=5000 --timeout=20000 ./test/unit/*.js",
"test-win32-iojs": "iojs ./node_modules/mocha/bin/mocha --slow=5000 --timeout=20000 ./test/unit/*.js"
}, },
"main": "index.js", "main": "index.js",
"repository": { "repository": {
@@ -27,6 +34,7 @@
"png", "png",
"webp", "webp",
"tiff", "tiff",
"dzi",
"resize", "resize",
"thumbnail", "thumbnail",
"crop", "crop",
@@ -34,19 +42,21 @@
"vips" "vips"
], ],
"dependencies": { "dependencies": {
"bluebird": "^2.8.2", "bluebird": "^2.9.27",
"color": "^0.7.3", "color": "^0.8.0",
"nan": "^1.6.1", "nan": "^1.8.4",
"semver": "^4.2.0" "semver": "^4.3.6"
}, },
"devDependencies": { "devDependencies": {
"mocha": "^2.1.0", "async": "^1.1.0",
"mocha-jshint": "^0.0.9",
"istanbul": "^0.3.5",
"coveralls": "^2.11.2", "coveralls": "^2.11.2",
"node-cpplint": "^0.4.0" "istanbul": "^0.3.14",
"mocha": "^2.2.5",
"mocha-jshint": "^2.2.3",
"node-cpplint": "^0.4.0",
"rimraf": "^2.3.4"
}, },
"license": "Apache 2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": ">=0.10" "node": ">=0.10"
} }

View File

@@ -13,50 +13,104 @@
# * Amazon Linux 2014.09 # * Amazon Linux 2014.09
vips_version_minimum=7.40.0 vips_version_minimum=7.40.0
vips_version_latest_major=7.42 vips_version_latest_major_minor=8.0
vips_version_latest_minor=1 vips_version_latest_patch=2
openslide_version_minimum=3.4.0
openslide_version_latest_major_minor=3.4
openslide_version_latest_patch=1
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 --without-gsf $1 ./configure --disable-debug --disable-docs --disable-static --disable-introspection --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"
}
install_libopenslide_from_source() {
echo "Compiling openslide $openslide_version_latest_major_minor.$openslide_version_latest_patch from source"
curl -O -L https://github.com/openslide/openslide/releases/download/v$openslide_version_latest_major_minor.$openslide_version_latest_patch/openslide-$openslide_version_latest_major_minor.$openslide_version_latest_patch.tar.gz
tar xzvf openslide-$openslide_version_latest_major_minor.$openslide_version_latest_patch.tar.gz
cd openslide-$openslide_version_latest_major_minor.$openslide_version_latest_patch
PKG_CONFIG_PATH=$pkg_config_path ./configure $1
make
make install
cd ..
rm -rf openslide-$openslide_version_latest_major_minor.$openslide_version_latest_patch
rm openslide-$openslide_version_latest_major_minor.$openslide_version_latest_patch.tar.gz
ldconfig
echo "Installed libopenslide $openslide_version_latest_major_minor.$openslide_version_latest_patch"
} }
sorry() { sorry() {
echo "Sorry, I don't yet know how to install libvips on $1" echo "Sorry, I don't yet know how to install lib$1 on $2"
exit 1 exit 1
} }
# Is libvips already installed, and is it at least the minimum required version?
if ! type pkg-config >/dev/null; then
sorry "a system without pkg-config"
fi
pkg_config_path_homebrew=`which brew >/dev/null 2>&1 && eval $(brew --env) && echo $PKG_CONFIG_LIBDIR || true` pkg_config_path_homebrew=`which brew >/dev/null 2>&1 && eval $(brew --env) && echo $PKG_CONFIG_LIBDIR || true`
pkg_config_path="$pkg_config_path_homebrew:$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig:/usr/lib/pkgconfig" pkg_config_path="$pkg_config_path_homebrew:$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig:/usr/lib/pkgconfig"
PKG_CONFIG_PATH=$pkg_config_path pkg-config --exists vips check_if_library_exists() {
if [ $? -eq 0 ]; then PKG_CONFIG_PATH=$pkg_config_path pkg-config --exists $1
vips_version_found=$(PKG_CONFIG_PATH=$pkg_config_path pkg-config --modversion vips)
pkg-config --atleast-version=$vips_version_minimum vips
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
# Found suitable version of libvips version_found=$(PKG_CONFIG_PATH=$pkg_config_path pkg-config --modversion $1)
echo "Found libvips $vips_version_found" PKG_CONFIG_PATH=$pkg_config_path pkg-config --atleast-version=$2 $1
exit 0 if [ $? -eq 0 ]; then
# Found suitable version of libvips
echo "Found lib$1 $version_found"
return 1
fi
echo "Found lib$1 $version_found but require $2"
else
echo "Could not find lib$1 using a PKG_CONFIG_PATH of '$pkg_config_path'"
fi fi
echo "Found libvips $vips_version_found but require $vips_version_minimum" return 0
else }
echo "Could not find libvips using a PKG_CONFIG_PATH of '$pkg_config_path'"
enable_openslide=0
# Is libvips already installed, and is it at least the minimum required version?
if [ $# -eq 1 ]; then
if [ "$1" = "--with-openslide" ]; then
echo "Installing vips with openslide support"
enable_openslide=1
else
echo "Sorry, $1 is not supported. Did you mean --with-openslide?"
exit 1
fi
fi
if ! type pkg-config >/dev/null; then
sorry "vips" "a system without pkg-config"
fi
openslide_exists=0
if [ $enable_openslide -eq 1 ]; then
check_if_library_exists "openslide" "$openslide_version_minimum"
openslide_exists=$?
fi
check_if_library_exists "vips" "$vips_version_minimum"
vips_exists=$?
if [ $vips_exists -eq 1 ] && [ $enable_openslide -eq 1 ]; then
if [ $openslide_exists -eq 1 ]; then
# Check if vips compiled with openslide support
vips_with_openslide=`vips list classes | grep -i opensli`
if [ -z $vips_with_openslide ]; then
echo "Vips compiled without openslide support."
else
exit 0
fi
fi
elif [ $vips_exists -eq 1 ] && [ $enable_openslide -eq 0 ]; then
exit 0
fi fi
# Verify root/sudo access # Verify root/sudo access
@@ -65,6 +119,108 @@ if [ "$(id -u)" -ne "0" ]; then
exit 1 exit 1
fi fi
# OS-specific installations of libopenslide follows
# Either openslide does not exist, or vips is installed without openslide support
if [ $enable_openslide -eq 1 ] && [ -z $vips_with_openslide ] && [ $openslide_exists -eq 0 ]; then
case $(uname -s) in
*[Dd]arwin*)
# Mac OS
echo "Detected Mac OS"
if type "brew" > /dev/null; then
echo "Installing libopenslide via homebrew"
brew install openslide
elif type "port" > /dev/null; then
echo "Installing libopenslide via MacPorts"
port install openslide
else
sorry "openslide" "Mac OS without homebrew or MacPorts"
fi
;;
*)
if [ -f /etc/debian_version ]; then
# Debian Linux
DISTRO=$(lsb_release -c -s)
echo "Detected Debian Linux '$DISTRO'"
case "$DISTRO" in
jessie|vivid)
# Debian 8, Ubuntu 15
echo "Installing libopenslide via apt-get"
apt-get install -y libopenslide-dev
;;
trusty|utopic|qiana|rebecca)
# Ubuntu 14, Mint 17
echo "Installing libopenslide dependencies via apt-get"
apt-get install -y automake build-essential curl zlib1g-dev libopenjpeg-dev libpng12-dev libjpeg-dev libtiff5-dev libgdk-pixbuf2.0-dev libxml2-dev libsqlite3-dev libcairo2-dev libglib2.0-dev sqlite3 libsqlite3-dev
install_libopenslide_from_source
;;
precise|wheezy|maya)
# Debian 7, Ubuntu 12.04, Mint 13
echo "Installing libopenslide dependencies via apt-get"
apt-get install -y automake build-essential curl zlib1g-dev libopenjpeg-dev libpng12-dev libjpeg-dev libtiff5-dev libgdk-pixbuf2.0-dev libxml2-dev libsqlite3-dev libcairo2-dev libglib2.0-dev sqlite3 libsqlite3-dev
install_libopenslide_from_source
;;
*)
# Unsupported Debian-based OS
sorry "openslide" "Debian-based $DISTRO"
;;
esac
elif [ -f /etc/redhat-release ]; then
# Red Hat Linux
RELEASE=$(cat /etc/redhat-release)
echo "Detected Red Hat Linux '$RELEASE'"
case $RELEASE in
"Red Hat Enterprise Linux release 7."*|"CentOS Linux release 7."*|"Scientific Linux release 7."*)
# RHEL/CentOS 7
echo "Installing libopenslide dependencies via yum"
yum groupinstall -y "Development Tools"
yum install -y tar curl libpng-devel libjpeg-devel libxml2-devel zlib-devel openjpeg-devel libtiff-devel gdk-pixbuf2-devel sqlite-devel cairo-devel glib2-devel
install_libopenslide_from_source "--prefix=/usr"
;;
"Red Hat Enterprise Linux release 6."*|"CentOS release 6."*|"Scientific Linux release 6."*)
# RHEL/CentOS 6
echo "Installing libopenslide dependencies via yum"
yum groupinstall -y "Development Tools"
yum install -y tar curl libpng-devel libjpeg-devel libxml2-devel zlib-devel openjpeg-devel libtiff-devel gdk-pixbuf2-devel sqlite-devel cairo-devel glib2-devel
install_libopenslide_from_source "--prefix=/usr"
;;
"Fedora release 21 "*|"Fedora release 22 "*)
# Fedora 21, 22
echo "Installing libopenslide via yum"
yum install -y openslide-devel
;;
*)
# Unsupported RHEL-based OS
sorry "openslide" "$RELEASE"
;;
esac
elif [ -f /etc/os-release ]; then
RELEASE=$(cat /etc/os-release | grep VERSION)
echo "Detected OpenSuse Linux '$RELEASE'"
case $RELEASE in
*"13.2"*)
echo "Installing libopenslide via zypper"
zypper --gpg-auto-import-keys install -y libopenslide-devel
;;
esac
elif [ -f /etc/SuSE-brand ]; then
RELEASE=$(cat /etc/SuSE-brand | grep VERSION)
echo "Detected OpenSuse Linux '$RELEASE'"
case $RELEASE in
*"13.1")
echo "Installing libopenslide dependencies via zypper"
zypper --gpg-auto-import-keys install -y --type pattern devel_basis
zypper --gpg-auto-import-keys install -y tar curl libpng16-devel libjpeg-turbo libjpeg8-devel libxml2-devel zlib-devel openjpeg-devel libtiff-devel libgdk_pixbuf-2_0-0 sqlite3-devel cairo-devel glib2-devel
install_libopenslide_from_source
;;
esac
else
# Unsupported OS
sorry "openslide" "$(uname -a)"
fi
;;
esac
fi
# OS-specific installations of libvips follows # OS-specific installations of libvips follows
case $(uname -s) in case $(uname -s) in
@@ -73,12 +229,16 @@ case $(uname -s) in
echo "Detected Mac OS" echo "Detected Mac OS"
if type "brew" > /dev/null; then if type "brew" > /dev/null; then
echo "Installing libvips via homebrew" echo "Installing libvips via homebrew"
brew install homebrew/science/vips --with-webp --with-graphicsmagick if [ $enable_openslide -eq 1 ]; then
brew install homebrew/science/vips --with-webp --with-graphicsmagick --with-openslide
else
brew install homebrew/science/vips --with-webp --with-graphicsmagick
fi
elif type "port" > /dev/null; then elif type "port" > /dev/null; then
echo "Installing libvips via MacPorts" echo "Installing libvips via MacPorts"
port install vips port install vips
else else
sorry "Mac OS without homebrew or MacPorts" sorry "vips" "Mac OS without homebrew or MacPorts"
fi fi
;; ;;
*) *)
@@ -89,13 +249,18 @@ case $(uname -s) in
case "$DISTRO" in case "$DISTRO" in
jessie|vivid) jessie|vivid)
# Debian 8, Ubuntu 15 # Debian 8, Ubuntu 15
echo "Installing libvips via apt-get" if [ $enable_openslide -eq 1 ]; then
apt-get install -y libvips-dev echo "Recompiling vips with openslide support"
install_libvips_from_source
else
echo "Installing libvips via apt-get"
apt-get install -y libvips-dev libgsf-1-dev
fi
;; ;;
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 liblcms2-dev libxml2-dev swig libmagickcore-dev curl apt-get install -y automake build-essential gobject-introspection gtk-doc-tools libglib2.0-dev libjpeg-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)
@@ -103,12 +268,12 @@ 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 liblcms2-dev libxml2-dev swig libmagickcore-dev curl apt-get install -y automake build-essential gobject-introspection gtk-doc-tools libglib2.0-dev libjpeg-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
;; ;;
*) *)
# Unsupported Debian-based OS # Unsupported Debian-based OS
sorry "Debian-based $DISTRO" sorry "vips" "Debian-based $DISTRO"
;; ;;
esac esac
elif [ -f /etc/redhat-release ]; then elif [ -f /etc/redhat-release ]; then
@@ -120,14 +285,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 lcms-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 lcms-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,30 +301,64 @@ 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" if [ $enable_openslide -eq 1 ]; then
yum install vips-devel echo "Installing libvips dependencies via yum"
yum groupinstall -y "Development Tools"
yum install -y gcc-c++ gtk-doc libxml2-devel libjpeg-turbo-devel libpng-devel libtiff-devel libexif-devel lcms-devel ImageMagick-devel gobject-introspection-devel libwebp-devel curl
echo "Compiling vips with openslide support"
install_libvips_from_source "--prefix=/usr"
else
echo "Installing libvips via yum"
yum install -y vips-devel
fi
;; ;;
*) *)
# Unsupported RHEL-based OS # Unsupported RHEL-based OS
sorry "$RELEASE" sorry "vips" "$RELEASE"
;; ;;
esac esac
elif [ -f /etc/system-release ]; then elif [ -f /etc/system-release ]; then
# Probably Amazon Linux # Probably Amazon Linux
RELEASE=$(cat /etc/system-release) RELEASE=$(cat /etc/system-release)
case $RELEASE in case $RELEASE in
"Amazon Linux AMI release 2014.09") "Amazon Linux AMI release 2014.09"|"Amazon Linux AMI release 2015.03")
# Amazon Linux # Amazon Linux
echo "Detected '$RELEASE'" echo "Detected '$RELEASE'"
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 lcms-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"
;; ;;
*)
# Unsupported Amazon Linux version
sorry "vips" "$RELEASE"
;;
esac
elif [ -f /etc/os-release ]; then
RELEASE=$(cat /etc/os-release | grep VERSION)
echo "Detected OpenSuse Linux '$RELEASE'"
case $RELEASE in
*"13.2"*)
echo "Installing libvips dependencies via zypper"
zypper --gpg-auto-import-keys install -y --type pattern devel_basis
zypper --gpg-auto-import-keys install -y tar curl gtk-doc libxml2-devel libjpeg-turbo libjpeg8-devel libpng16-devel libtiff-devel libexif-devel liblcms2-devel ImageMagick-devel gobject-introspection-devel libwebp-devel
install_libvips_from_source
;;
esac
elif [ -f /etc/SuSE-brand ]; then
RELEASE=$(cat /etc/SuSE-brand | grep VERSION)
echo "Detected OpenSuse Linux '$RELEASE'"
case $RELEASE in
*"13.1")
echo "Installing libvips dependencies via zypper"
zypper --gpg-auto-import-keys install -y --type pattern devel_basis
zypper --gpg-auto-import-keys install -y tar curl gtk-doc libxml2-devel libjpeg-turbo libjpeg8-devel libpng16-devel libtiff-devel libexif-devel liblcms2-devel ImageMagick-devel gobject-introspection-devel libwebp-devel
install_libvips_from_source
;;
esac esac
else else
# Unsupported OS # Unsupported OS
sorry "$(uname -a)" sorry "vips" "$(uname -a)"
fi fi
;; ;;
esac esac

View File

@@ -1,9 +1,26 @@
#include <cstdlib>
#include <string> #include <string>
#include <string.h> #include <string.h>
#include <vips/vips.h> #include <vips/vips.h>
#include "common.h" #include "common.h"
// Verify platform and compiler compatibility
#ifdef _WIN64
#error Windows 64-bit is currently unsupported - see https://github.com/lovell/sharp#windows
#endif
#if ((!defined(__clang__)) && defined(__GNUC__) && (__GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 6)))
#error GCC version 4.6+ is required for C++11 features - see https://github.com/lovell/sharp#prerequisites
#endif
#if (defined(__clang__) && defined(__has_feature))
#if (!__has_feature(cxx_range_for))
#error clang version 3.0+ is required for C++11 features - see https://github.com/lovell/sharp#prerequisites
#endif
#endif
namespace sharp { namespace sharp {
// How many tasks are in the queue? // How many tasks are in the queue?
@@ -28,19 +45,8 @@ namespace sharp {
bool IsTiff(std::string const &str) { bool IsTiff(std::string const &str) {
return EndsWith(str, ".tif") || EndsWith(str, ".tiff") || EndsWith(str, ".TIF") || EndsWith(str, ".TIFF"); return EndsWith(str, ".tif") || EndsWith(str, ".tiff") || EndsWith(str, ".TIF") || EndsWith(str, ".TIFF");
} }
bool IsDz(std::string const &str) {
// Buffer content checkers return EndsWith(str, ".dzi") || EndsWith(str, ".DZI");
unsigned char const MARKER_JPEG[] = {0xff, 0xd8};
unsigned char const MARKER_PNG[] = {0x89, 0x50};
unsigned char const MARKER_WEBP[] = {0x52, 0x49};
static bool buffer_is_tiff(char *buffer, size_t len) {
return (
len >= 4 && (
(buffer[0] == 'M' && buffer[1] == 'M' && buffer[2] == '\0' && (buffer[3] == '*' || buffer[3] == '+')) ||
(buffer[0] == 'I' && buffer[1] == 'I' && (buffer[2] == '*' || buffer[2] == '+') && buffer[3] == '\0')
)
);
} }
/* /*
@@ -48,15 +54,19 @@ namespace sharp {
*/ */
ImageType DetermineImageType(void *buffer, size_t const length) { ImageType DetermineImageType(void *buffer, size_t const length) {
ImageType imageType = ImageType::UNKNOWN; ImageType imageType = ImageType::UNKNOWN;
if (length >= 4) { char const *load = vips_foreign_find_load_buffer(buffer, length);
if (memcmp(MARKER_JPEG, buffer, 2) == 0) { if (load != NULL) {
std::string loader = load;
if (EndsWith(loader, "JpegBuffer")) {
imageType = ImageType::JPEG; imageType = ImageType::JPEG;
} else if (memcmp(MARKER_PNG, buffer, 2) == 0) { } else if (EndsWith(loader, "PngBuffer")) {
imageType = ImageType::PNG; imageType = ImageType::PNG;
} else if (memcmp(MARKER_WEBP, buffer, 2) == 0) { } else if (EndsWith(loader, "WebpBuffer")) {
imageType = ImageType::WEBP; imageType = ImageType::WEBP;
} else if (buffer_is_tiff(static_cast<char*>(buffer), length)) { } else if (EndsWith(loader, "TiffBuffer")) {
imageType = ImageType::TIFF; imageType = ImageType::TIFF;
} else if (EndsWith(loader, "MagickBuffer")) {
imageType = ImageType::MAGICK;
} }
} }
return imageType; return imageType;
@@ -65,35 +75,31 @@ namespace sharp {
/* /*
Initialise and return a VipsImage from a buffer. Supports JPEG, PNG, WebP and TIFF. Initialise and return a VipsImage from a buffer. Supports JPEG, PNG, WebP and TIFF.
*/ */
VipsImage* InitImage(ImageType imageType, void *buffer, size_t const length, VipsAccess const access) { VipsImage* InitImage(void *buffer, size_t const length, VipsAccess const access) {
VipsImage *image = NULL; return vips_image_new_from_buffer(buffer, length, NULL, "access", access, NULL);
if (imageType == ImageType::JPEG) {
vips_jpegload_buffer(buffer, length, &image, "access", access, NULL);
} else if (imageType == ImageType::PNG) {
vips_pngload_buffer(buffer, length, &image, "access", access, NULL);
} else if (imageType == ImageType::WEBP) {
vips_webpload_buffer(buffer, length, &image, "access", access, NULL);
} else if (imageType == ImageType::TIFF) {
vips_tiffload_buffer(buffer, length, &image, "access", access, NULL);
}
return image;
} }
/* /*
Inpect the first 2-4 bytes of a file to determine image format Determine image format, reads the first few bytes of the file
*/ */
ImageType DetermineImageType(char const *file) { ImageType DetermineImageType(char const *file) {
ImageType imageType = ImageType::UNKNOWN; ImageType imageType = ImageType::UNKNOWN;
if (vips_foreign_is_a("jpegload", file)) { char const *load = vips_foreign_find_load(file);
imageType = ImageType::JPEG; if (load != NULL) {
} else if (vips_foreign_is_a("pngload", file)) { std::string loader = load;
imageType = ImageType::PNG; if (EndsWith(loader, "JpegFile")) {
} else if (vips_foreign_is_a("webpload", file)) { imageType = ImageType::JPEG;
imageType = ImageType::WEBP; } else if (EndsWith(loader, "Png")) {
} else if (vips_foreign_is_a("tiffload", file)) { imageType = ImageType::PNG;
imageType = ImageType::TIFF; } else if (EndsWith(loader, "WebpFile")) {
} else if(vips_foreign_is_a("magickload", file)) { imageType = ImageType::WEBP;
imageType = ImageType::MAGICK; } else if (EndsWith(loader, "Openslide")) {
imageType = ImageType::OPENSLIDE;
} else if (EndsWith(loader, "TiffFile")) {
imageType = ImageType::TIFF;
} else if (EndsWith(loader, "Magick") || EndsWith(loader, "MagickFile")) {
imageType = ImageType::MAGICK;
}
} }
return imageType; return imageType;
} }
@@ -101,20 +107,8 @@ namespace sharp {
/* /*
Initialise and return a VipsImage from a file. Initialise and return a VipsImage from a file.
*/ */
VipsImage* InitImage(ImageType imageType, char const *file, VipsAccess const access) { VipsImage* InitImage(char const *file, VipsAccess const access) {
VipsImage *image = NULL; return vips_image_new_from_file(file, "access", access, NULL);
if (imageType == ImageType::JPEG) {
vips_jpegload(file, &image, "access", access, NULL);
} else if (imageType == ImageType::PNG) {
vips_pngload(file, &image, "access", access, NULL);
} else if (imageType == ImageType::WEBP) {
vips_webpload(file, &image, "access", access, NULL);
} else if (imageType == ImageType::TIFF) {
vips_tiffload(file, &image, "access", access, NULL);
} else if (imageType == ImageType::MAGICK) {
vips_magickload(file, &image, "access", access, NULL);
}
return image;
} }
/* /*
@@ -157,6 +151,9 @@ namespace sharp {
*/ */
int InterpolatorWindowSize(char const *name) { int InterpolatorWindowSize(char const *name) {
VipsInterpolate *interpolator = vips_interpolate_new(name); VipsInterpolate *interpolator = vips_interpolate_new(name);
if (interpolator == NULL) {
return -1;
}
int window_size = vips_interpolate_get_window_size(interpolator); int window_size = vips_interpolate_get_window_size(interpolator);
g_object_unref(interpolator); g_object_unref(interpolator);
return window_size; return window_size;

View File

@@ -9,7 +9,8 @@ namespace sharp {
PNG, PNG,
WEBP, WEBP,
TIFF, TIFF,
MAGICK MAGICK,
OPENSLIDE
}; };
// How many tasks are in the queue? // How many tasks are in the queue?
@@ -23,6 +24,7 @@ namespace sharp {
bool IsPng(std::string const &str); bool IsPng(std::string const &str);
bool IsWebp(std::string const &str); bool IsWebp(std::string const &str);
bool IsTiff(std::string const &str); bool IsTiff(std::string const &str);
bool IsDz(std::string const &str);
/* /*
Determine image format of a buffer. Determine image format of a buffer.
@@ -37,12 +39,12 @@ namespace sharp {
/* /*
Initialise and return a VipsImage from a buffer. Supports JPEG, PNG, WebP and TIFF. Initialise and return a VipsImage from a buffer. Supports JPEG, PNG, WebP and TIFF.
*/ */
VipsImage* InitImage(ImageType imageType, void *buffer, size_t const length, VipsAccess const access); VipsImage* InitImage(void *buffer, size_t const length, VipsAccess const access);
/* /*
Initialise and return a VipsImage from a file. Initialise and return a VipsImage from a file.
*/ */
VipsImage* InitImage(ImageType imageType, char const *file, VipsAccess const access); VipsImage* InitImage(char const *file, VipsAccess const access);
/* /*
Does this image have an embedded profile? Does this image have an embedded profile?

View File

@@ -61,7 +61,7 @@ class MetadataWorker : public NanAsyncWorker {
// From buffer // From buffer
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(baton->bufferIn, baton->bufferInLength, VIPS_ACCESS_RANDOM);
if (image == NULL) { if (image == NULL) {
(baton->err).append("Input buffer has corrupt header"); (baton->err).append("Input buffer has corrupt header");
imageType = ImageType::UNKNOWN; imageType = ImageType::UNKNOWN;
@@ -73,7 +73,7 @@ class MetadataWorker : public NanAsyncWorker {
// From file // From file
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(baton->fileIn.c_str(), VIPS_ACCESS_RANDOM);
if (image == NULL) { if (image == NULL) {
(baton->err).append("Input file has corrupt header"); (baton->err).append("Input file has corrupt header");
imageType = ImageType::UNKNOWN; imageType = ImageType::UNKNOWN;
@@ -90,6 +90,7 @@ class MetadataWorker : public NanAsyncWorker {
case ImageType::WEBP: baton->format = "webp"; break; case ImageType::WEBP: baton->format = "webp"; break;
case ImageType::TIFF: baton->format = "tiff"; break; case ImageType::TIFF: baton->format = "tiff"; break;
case ImageType::MAGICK: baton->format = "magick"; break; case ImageType::MAGICK: baton->format = "magick"; break;
case ImageType::OPENSLIDE: baton->format = "openslide"; break;
case ImageType::UNKNOWN: break; case ImageType::UNKNOWN: break;
} }
// VipsImage attributes // VipsImage attributes

View File

@@ -32,13 +32,16 @@ using sharp::IsJpeg;
using sharp::IsPng; using sharp::IsPng;
using sharp::IsWebp; using sharp::IsWebp;
using sharp::IsTiff; using sharp::IsTiff;
using sharp::IsDz;
using sharp::counterProcess; using sharp::counterProcess;
using sharp::counterQueue; using sharp::counterQueue;
enum class Canvas { enum class Canvas {
CROP, CROP,
EMBED,
MAX, MAX,
EMBED MIN,
IGNORE_ASPECT
}; };
enum class Angle { enum class Angle {
@@ -80,6 +83,7 @@ struct ResizeBaton {
double sharpenJagged; double sharpenJagged;
double gamma; double gamma;
bool greyscale; bool greyscale;
bool normalize;
int angle; int angle;
bool rotateBeforePreExtract; bool rotateBeforePreExtract;
bool flip; bool flip;
@@ -90,8 +94,14 @@ struct ResizeBaton {
int quality; int quality;
int compressionLevel; int compressionLevel;
bool withoutAdaptiveFiltering; bool withoutAdaptiveFiltering;
bool withoutChromaSubsampling;
bool trellisQuantisation;
bool overshootDeringing;
bool optimiseScans;
std::string err; std::string err;
bool withMetadata; bool withMetadata;
int tileSize;
int tileOverlap;
ResizeBaton(): ResizeBaton():
bufferInLength(0), bufferInLength(0),
@@ -109,6 +119,7 @@ struct ResizeBaton {
sharpenJagged(2.0), sharpenJagged(2.0),
gamma(0.0), gamma(0.0),
greyscale(false), greyscale(false),
normalize(false),
angle(0), angle(0),
flip(false), flip(false),
flop(false), flop(false),
@@ -117,7 +128,13 @@ struct ResizeBaton {
quality(80), quality(80),
compressionLevel(6), compressionLevel(6),
withoutAdaptiveFiltering(false), withoutAdaptiveFiltering(false),
withMetadata(false) { withoutChromaSubsampling(false),
trellisQuantisation(false),
overshootDeringing(false),
optimiseScans(false),
withMetadata(false),
tileSize(256),
tileOverlap(0) {
background[0] = 0.0; background[0] = 0.0;
background[1] = 0.0; background[1] = 0.0;
background[2] = 0.0; background[2] = 0.0;
@@ -138,7 +155,8 @@ static void DeleteBuffer(VipsObject *object, char *buffer) {
class ResizeWorker : public NanAsyncWorker { class ResizeWorker : public NanAsyncWorker {
public: public:
ResizeWorker(NanCallback *callback, ResizeBaton *baton) : NanAsyncWorker(callback), baton(baton) {} ResizeWorker(NanCallback *callback, ResizeBaton *baton, NanCallback *queueListener) :
NanAsyncWorker(callback), baton(baton), queueListener(queueListener) {}
~ResizeWorker() {} ~ResizeWorker() {}
/* /*
@@ -154,8 +172,8 @@ class ResizeWorker : public NanAsyncWorker {
// Latest v2 sRGB ICC profile // Latest v2 sRGB ICC profile
std::string srgbProfile = baton->iccProfilePath + "sRGB_IEC61966-2-1_black_scaled.icc"; std::string srgbProfile = baton->iccProfilePath + "sRGB_IEC61966-2-1_black_scaled.icc";
// Hang image references from this hook object // Create "hook" VipsObject to hang image references from
VipsObject *hook = reinterpret_cast<VipsObject*>(vips_image_new()); hook = reinterpret_cast<VipsObject*>(vips_image_new());
// Input // Input
ImageType inputImageType = ImageType::UNKNOWN; ImageType inputImageType = ImageType::UNKNOWN;
@@ -164,7 +182,7 @@ class ResizeWorker : public NanAsyncWorker {
// 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(baton->bufferIn, baton->bufferInLength, baton->accessMethod);
if (image != NULL) { if (image != NULL) {
// Listen for "postclose" signal to delete input buffer // Listen for "postclose" signal to delete input buffer
g_signal_connect(image, "postclose", G_CALLBACK(DeleteBuffer), baton->bufferIn); g_signal_connect(image, "postclose", G_CALLBACK(DeleteBuffer), baton->bufferIn);
@@ -182,7 +200,7 @@ class ResizeWorker : public NanAsyncWorker {
// 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(baton->fileIn.c_str(), baton->accessMethod);
if (image == NULL) { if (image == NULL) {
(baton->err).append("Input file has corrupt header"); (baton->err).append("Input file has corrupt header");
inputImageType = ImageType::UNKNOWN; inputImageType = ImageType::UNKNOWN;
@@ -192,14 +210,14 @@ class ResizeWorker : public NanAsyncWorker {
} }
} }
if (image == NULL || inputImageType == ImageType::UNKNOWN) { if (image == NULL || inputImageType == ImageType::UNKNOWN) {
return Error(baton, hook); return Error();
} }
vips_object_local(hook, image); vips_object_local(hook, image);
// Limit input images to a given number of pixels, where pixels = width * height // Limit input images to a given number of pixels, where pixels = width * height
if (image->Xsize * image->Ysize > baton->limitInputPixels) { if (image->Xsize * image->Ysize > baton->limitInputPixels) {
(baton->err).append("Input image exceeds pixel limit"); (baton->err).append("Input image exceeds pixel limit");
return Error(baton, hook); return Error();
} }
// Calculate angle of rotation // Calculate angle of rotation
@@ -215,7 +233,7 @@ class ResizeWorker : public NanAsyncWorker {
if (baton->rotateBeforePreExtract && 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();
} }
vips_object_local(hook, rotated); vips_object_local(hook, rotated);
image = rotated; image = rotated;
@@ -225,7 +243,7 @@ class ResizeWorker : public NanAsyncWorker {
if (baton->topOffsetPre != -1) { if (baton->topOffsetPre != -1) {
VipsImage *extractedPre; VipsImage *extractedPre;
if (vips_extract_area(image, &extractedPre, baton->leftOffsetPre, baton->topOffsetPre, baton->widthPre, baton->heightPre, NULL)) { if (vips_extract_area(image, &extractedPre, baton->leftOffsetPre, baton->topOffsetPre, baton->widthPre, baton->heightPre, NULL)) {
return Error(baton, hook); return Error();
} }
vips_object_local(hook, extractedPre); vips_object_local(hook, extractedPre);
image = extractedPre; image = extractedPre;
@@ -243,95 +261,130 @@ class ResizeWorker : public NanAsyncWorker {
// 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());
if (interpolatorWindowSize < 0) {
return Error();
}
// Scaling calculations // Scaling calculations
double factor; double xfactor = 1.0;
double yfactor = 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); xfactor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
double yfactor = static_cast<double>(inputHeight) / static_cast<double>(baton->height); 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) { xfactor = std::min(xfactor, yfactor);
if (xfactor > yfactor) { yfactor = xfactor;
baton->height = round(static_cast<double>(inputHeight) / xfactor); break;
} else { case Canvas::EMBED:
baton->width = round(static_cast<double>(inputWidth) / yfactor); xfactor = std::max(xfactor, yfactor);
} yfactor = xfactor;
break;
case Canvas::MAX:
if (xfactor > yfactor) {
baton->height = static_cast<int>(round(static_cast<double>(inputHeight) / xfactor));
yfactor = xfactor;
} else {
baton->width = static_cast<int>(round(static_cast<double>(inputWidth) / yfactor));
xfactor = yfactor;
}
break;
case Canvas::MIN:
if (xfactor < yfactor) {
baton->height = static_cast<int>(round(static_cast<double>(inputHeight) / xfactor));
yfactor = xfactor;
} else {
baton->width = static_cast<int>(round(static_cast<double>(inputWidth) / yfactor));
xfactor = yfactor;
}
break;
case Canvas::IGNORE_ASPECT:
// xfactor, yfactor OK!
break;
} }
} else if (baton->width > 0) { } else if (baton->width > 0) {
// Fixed width, auto height // Fixed width
factor = static_cast<double>(inputWidth) / static_cast<double>(baton->width); xfactor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
baton->height = floor(static_cast<double>(inputHeight) / factor); if (baton->canvas == Canvas::IGNORE_ASPECT) {
baton->height = inputHeight;
} else {
// Auto height
yfactor = xfactor;
baton->height = static_cast<int>(floor(static_cast<double>(inputHeight) / yfactor));
}
} else if (baton->height > 0) { } else if (baton->height > 0) {
// Fixed height, auto width // Fixed height
factor = static_cast<double>(inputHeight) / static_cast<double>(baton->height); yfactor = static_cast<double>(inputHeight) / static_cast<double>(baton->height);
baton->width = floor(static_cast<double>(inputWidth) / factor); if (baton->canvas == Canvas::IGNORE_ASPECT) {
baton->width = inputWidth;
} else {
// Auto width
xfactor = yfactor;
baton->width = static_cast<int>(floor(static_cast<double>(inputWidth) / xfactor));
}
} else { } else {
// Identity transform // Identity transform
factor = 1;
baton->width = inputWidth; baton->width = inputWidth;
baton->height = inputHeight; baton->height = inputHeight;
} }
// Calculate integral box shrink // Calculate integral box shrink
int shrink = 1; int xshrink = CalculateShrink(xfactor, interpolatorWindowSize);
if (factor >= 2 && interpolatorWindowSize > 3) { int yshrink = CalculateShrink(yfactor, interpolatorWindowSize);
// Shrink less, affine more with interpolators that use at least 4x4 pixel window, e.g. bicubic
shrink = floor(factor * 3.0 / interpolatorWindowSize);
} else {
shrink = floor(factor);
}
if (shrink < 1) {
shrink = 1;
}
// Calculate residual float affine transformation // Calculate residual float affine transformation
double residual = static_cast<double>(shrink) / factor; double xresidual = CalculateResidual(xshrink, xfactor);
double yresidual = CalculateResidual(yshrink, yfactor);
// Do not enlarge the output if the input width *or* height are already less than the required dimensions // Do not enlarge the output if the input width *or* height are already less than the required dimensions
if (baton->withoutEnlargement) { if (baton->withoutEnlargement) {
if (inputWidth < baton->width || inputHeight < baton->height) { if (inputWidth < baton->width || inputHeight < baton->height) {
factor = 1; xfactor = 1;
shrink = 1; yfactor = 1;
residual = 0; xshrink = 1;
yshrink = 1;
xresidual = 0;
yresidual = 0;
baton->width = inputWidth; baton->width = inputWidth;
baton->height = inputHeight; baton->height = inputHeight;
} }
} }
// Try to use libjpeg shrink-on-load, but not when applying gamma correction or pre-resize extract // If integral x and y shrink are equal, try to use libjpeg shrink-on-load, but not when applying gamma correction or pre-resize extract
int shrink_on_load = 1; int shrink_on_load = 1;
if (inputImageType == ImageType::JPEG && shrink >= 2 && baton->gamma == 0 && baton->topOffsetPre == -1) { if (xshrink == yshrink && inputImageType == ImageType::JPEG && xshrink >= 2 && baton->gamma == 0 && baton->topOffsetPre == -1) {
if (shrink >= 8) { if (xshrink >= 8) {
factor = factor / 8; xfactor = xfactor / 8;
yfactor = yfactor / 8;
shrink_on_load = 8; shrink_on_load = 8;
} else if (shrink >= 4) { } else if (xshrink >= 4) {
factor = factor / 4; xfactor = xfactor / 4;
yfactor = yfactor / 4;
shrink_on_load = 4; shrink_on_load = 4;
} else if (shrink >= 2) { } else if (xshrink >= 2) {
factor = factor / 2; xfactor = xfactor / 2;
yfactor = yfactor / 2;
shrink_on_load = 2; shrink_on_load = 2;
} }
} }
if (shrink_on_load > 1) { if (shrink_on_load > 1) {
// Recalculate integral shrink and double residual // Recalculate integral shrink and double residual
factor = std::max(factor, 1.0); xfactor = std::max(xfactor, 1.0);
if (factor >= 2 && interpolatorWindowSize > 3) { yfactor = std::max(yfactor, 1.0);
shrink = floor(factor * 3.0 / interpolatorWindowSize); xshrink = CalculateShrink(xfactor, interpolatorWindowSize);
} else { yshrink = CalculateShrink(yfactor, interpolatorWindowSize);
shrink = floor(factor); xresidual = CalculateResidual(xshrink, xfactor);
} yresidual = CalculateResidual(yshrink, yfactor);
residual = static_cast<double>(shrink) / factor;
// Reload input using shrink-on-load // Reload input using shrink-on-load
VipsImage *shrunkOnLoad; VipsImage *shrunkOnLoad;
if (baton->bufferInLength > 1) { if (baton->bufferInLength > 1) {
if (vips_jpegload_buffer(baton->bufferIn, baton->bufferInLength, &shrunkOnLoad, "shrink", shrink_on_load, NULL)) { if (vips_jpegload_buffer(baton->bufferIn, baton->bufferInLength, &shrunkOnLoad, "shrink", shrink_on_load, NULL)) {
return Error(baton, hook); return Error();
} }
} else { } else {
if (vips_jpegload((baton->fileIn).c_str(), &shrunkOnLoad, "shrink", shrink_on_load, NULL)) { if (vips_jpegload((baton->fileIn).c_str(), &shrunkOnLoad, "shrink", shrink_on_load, NULL)) {
return Error(baton, hook); return Error();
} }
} }
vips_object_local(hook, shrunkOnLoad); vips_object_local(hook, shrunkOnLoad);
@@ -352,7 +405,7 @@ class ResizeWorker : public NanAsyncWorker {
std::string cmykProfile = baton->iccProfilePath + "USWebCoatedSWOP.icc"; std::string cmykProfile = baton->iccProfilePath + "USWebCoatedSWOP.icc";
VipsImage *transformed; VipsImage *transformed;
if (vips_icc_transform(image, &transformed, srgbProfile.c_str(), "input_profile", cmykProfile.c_str(), NULL)) { if (vips_icc_transform(image, &transformed, srgbProfile.c_str(), "input_profile", cmykProfile.c_str(), NULL)) {
return Error(baton, hook); return Error();
} }
vips_object_local(hook, transformed); vips_object_local(hook, transformed);
image = transformed; image = transformed;
@@ -370,7 +423,7 @@ class ResizeWorker : public NanAsyncWorker {
VipsImage *flattened; VipsImage *flattened;
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();
} }
vips_area_unref(reinterpret_cast<VipsArea*>(background)); vips_area_unref(reinterpret_cast<VipsArea*>(background));
vips_object_local(hook, flattened); vips_object_local(hook, flattened);
@@ -381,7 +434,7 @@ class ResizeWorker : public NanAsyncWorker {
if (baton->gamma >= 1 && baton->gamma <= 3) { if (baton->gamma >= 1 && baton->gamma <= 3) {
VipsImage *gammaEncoded; VipsImage *gammaEncoded;
if (vips_gamma(image, &gammaEncoded, "exponent", 1.0 / baton->gamma, NULL)) { if (vips_gamma(image, &gammaEncoded, "exponent", 1.0 / baton->gamma, NULL)) {
return Error(baton, hook); return Error();
} }
vips_object_local(hook, gammaEncoded); vips_object_local(hook, gammaEncoded);
image = gammaEncoded; image = gammaEncoded;
@@ -391,40 +444,44 @@ class ResizeWorker : public NanAsyncWorker {
if (baton->greyscale) { if (baton->greyscale) {
VipsImage *greyscale; VipsImage *greyscale;
if (vips_colourspace(image, &greyscale, VIPS_INTERPRETATION_B_W, NULL)) { if (vips_colourspace(image, &greyscale, VIPS_INTERPRETATION_B_W, NULL)) {
return Error(baton, hook); return Error();
} }
vips_object_local(hook, greyscale); vips_object_local(hook, greyscale);
image = greyscale; image = greyscale;
} }
if (shrink > 1) { if (xshrink > 1 || yshrink > 1) {
VipsImage *shrunk; VipsImage *shrunk;
// Use vips_shrink with the integral reduction // Use vips_shrink with the integral reduction
if (vips_shrink(image, &shrunk, shrink, shrink, NULL)) { if (vips_shrink(image, &shrunk, xshrink, yshrink, NULL)) {
return Error(baton, hook); return Error();
} }
vips_object_local(hook, shrunk); vips_object_local(hook, shrunk);
image = shrunk; image = shrunk;
// Recalculate residual float based on dimensions of required vs shrunk images // Recalculate residual float based on dimensions of required vs shrunk images
double shrunkWidth = shrunk->Xsize; int shrunkWidth = shrunk->Xsize;
double shrunkHeight = shrunk->Ysize; int shrunkHeight = shrunk->Ysize;
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 = shrunkWidth; int swap = shrunkWidth;
shrunkWidth = shrunkHeight; shrunkWidth = shrunkHeight;
shrunkHeight = swap; shrunkHeight = swap;
} }
double residualx = static_cast<double>(baton->width) / static_cast<double>(shrunkWidth); xresidual = static_cast<double>(baton->width) / static_cast<double>(shrunkWidth);
double residualy = static_cast<double>(baton->height) / static_cast<double>(shrunkHeight); yresidual = static_cast<double>(baton->height) / static_cast<double>(shrunkHeight);
if (baton->canvas == Canvas::EMBED) { if (baton->canvas == Canvas::EMBED) {
residual = std::min(residualx, residualy); xresidual = std::min(xresidual, yresidual);
} else { yresidual = xresidual;
residual = std::max(residualx, residualy); } else if (baton->canvas != Canvas::IGNORE_ASPECT) {
xresidual = std::max(xresidual, yresidual);
yresidual = xresidual;
} }
} }
// Use vips_affine with the remaining float part // Use vips_affine with the remaining float part
if (residual != 0.0) { if (xresidual != 0.0 || yresidual != 0.0) {
// Use average of x and y residuals to compute sigma for Gaussian blur
double residual = (xresidual + yresidual) / 2.0;
// Apply Gaussian blur before large affine reductions // Apply Gaussian blur before large affine reductions
if (residual < 1.0) { if (residual < 1.0) {
// Calculate standard deviation // Calculate standard deviation
@@ -433,13 +490,22 @@ class ResizeWorker : public NanAsyncWorker {
// Create Gaussian function for standard deviation // Create Gaussian function for standard deviation
VipsImage *gaussian; VipsImage *gaussian;
if (vips_gaussmat(&gaussian, sigma, 0.2, "separable", TRUE, "integer", TRUE, NULL)) { if (vips_gaussmat(&gaussian, sigma, 0.2, "separable", TRUE, "integer", TRUE, NULL)) {
return Error(baton, hook); return Error();
} }
vips_object_local(hook, gaussian); vips_object_local(hook, gaussian);
// Sequential input requires a small linecache before use of convolution
if (baton->accessMethod == VIPS_ACCESS_SEQUENTIAL) {
VipsImage *lineCached;
if (vips_linecache(image, &lineCached, "access", VIPS_ACCESS_SEQUENTIAL, "tile_height", 1, "threaded", TRUE, NULL)) {
return Error();
}
vips_object_local(hook, lineCached);
image = lineCached;
}
// Apply Gaussian function // Apply Gaussian function
VipsImage *blurred; VipsImage *blurred;
if (vips_convsep(image, &blurred, gaussian, "precision", VIPS_PRECISION_INTEGER, NULL)) { if (vips_convsep(image, &blurred, gaussian, "precision", VIPS_PRECISION_INTEGER, NULL)) {
return Error(baton, hook); return Error();
} }
vips_object_local(hook, blurred); vips_object_local(hook, blurred);
image = blurred; image = blurred;
@@ -447,11 +513,14 @@ class ResizeWorker : public NanAsyncWorker {
} }
// 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());
if (interpolator == NULL) {
return Error();
}
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, 0.0, residual, "interpolate", interpolator, NULL)) { if (vips_affine(image, &affined, xresidual, 0.0, 0.0, yresidual, "interpolate", interpolator, NULL)) {
return Error(baton, hook); return Error();
} }
vips_object_local(hook, affined); vips_object_local(hook, affined);
image = affined; image = affined;
@@ -461,7 +530,7 @@ class ResizeWorker : public NanAsyncWorker {
if (!baton->rotateBeforePreExtract && 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();
} }
vips_object_local(hook, rotated); vips_object_local(hook, rotated);
image = rotated; image = rotated;
@@ -471,7 +540,7 @@ class ResizeWorker : public NanAsyncWorker {
if (baton->flip) { if (baton->flip) {
VipsImage *flipped; VipsImage *flipped;
if (vips_flip(image, &flipped, VIPS_DIRECTION_VERTICAL, NULL)) { if (vips_flip(image, &flipped, VIPS_DIRECTION_VERTICAL, NULL)) {
return Error(baton, hook); return Error();
} }
vips_object_local(hook, flipped); vips_object_local(hook, flipped);
image = flipped; image = flipped;
@@ -481,7 +550,7 @@ class ResizeWorker : public NanAsyncWorker {
if (baton->flop) { if (baton->flop) {
VipsImage *flopped; VipsImage *flopped;
if (vips_flip(image, &flopped, VIPS_DIRECTION_HORIZONTAL, NULL)) { if (vips_flip(image, &flopped, VIPS_DIRECTION_HORIZONTAL, NULL)) {
return Error(baton, hook); return Error();
} }
vips_object_local(hook, flopped); vips_object_local(hook, flopped);
image = flopped; image = flopped;
@@ -495,7 +564,7 @@ class ResizeWorker : public NanAsyncWorker {
// Convert to sRGB colour space // Convert to sRGB colour space
VipsImage *colourspaced; VipsImage *colourspaced;
if (vips_colourspace(image, &colourspaced, VIPS_INTERPRETATION_sRGB, NULL)) { if (vips_colourspace(image, &colourspaced, VIPS_INTERPRETATION_sRGB, NULL)) {
return Error(baton, hook); return Error();
} }
vips_object_local(hook, colourspaced); vips_object_local(hook, colourspaced);
image = colourspaced; image = colourspaced;
@@ -505,26 +574,26 @@ class ResizeWorker : public NanAsyncWorker {
// Create single-channel transparency // Create single-channel transparency
VipsImage *black; VipsImage *black;
if (vips_black(&black, image->Xsize, image->Ysize, "bands", 1, NULL)) { if (vips_black(&black, image->Xsize, image->Ysize, "bands", 1, NULL)) {
return Error(baton, hook); return Error();
} }
vips_object_local(hook, black); vips_object_local(hook, black);
// Invert to become non-transparent // Invert to become non-transparent
VipsImage *alpha; VipsImage *alpha;
if (vips_invert(black, &alpha, NULL)) { if (vips_invert(black, &alpha, NULL)) {
return Error(baton, hook); return Error();
} }
vips_object_local(hook, alpha); vips_object_local(hook, alpha);
// Append alpha channel to existing image // Append alpha channel to existing image
VipsImage *joined; VipsImage *joined;
if (vips_bandjoin2(image, alpha, &joined, NULL)) { if (vips_bandjoin2(image, alpha, &joined, NULL)) {
return Error(baton, hook); return Error();
} }
vips_object_local(hook, joined); vips_object_local(hook, joined);
image = joined; image = joined;
} }
// Create background // Create background
VipsArrayDouble *background; VipsArrayDouble *background;
if (baton->background[3] < 255.0) { if (baton->background[3] < 255.0 || HasAlpha(image)) {
background = vips_array_double_newv( background = vips_array_double_newv(
4, baton->background[0], baton->background[1], baton->background[2], baton->background[3] 4, baton->background[0], baton->background[1], baton->background[2], baton->background[3]
); );
@@ -541,13 +610,13 @@ class ResizeWorker : public NanAsyncWorker {
"extend", VIPS_EXTEND_BACKGROUND, "background", background, NULL "extend", VIPS_EXTEND_BACKGROUND, "background", background, NULL
)) { )) {
vips_area_unref(reinterpret_cast<VipsArea*>(background)); vips_area_unref(reinterpret_cast<VipsArea*>(background));
return Error(baton, hook); return Error();
} }
vips_area_unref(reinterpret_cast<VipsArea*>(background)); vips_area_unref(reinterpret_cast<VipsArea*>(background));
vips_object_local(hook, embedded); vips_object_local(hook, embedded);
image = embedded; image = embedded;
} else { } else if (baton->canvas != Canvas::IGNORE_ASPECT) {
// 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);
@@ -555,7 +624,7 @@ class ResizeWorker : public NanAsyncWorker {
int height = std::min(image->Ysize, baton->height); int height = std::min(image->Ysize, baton->height);
VipsImage *extracted; VipsImage *extracted;
if (vips_extract_area(image, &extracted, left, top, width, height, NULL)) { if (vips_extract_area(image, &extracted, left, top, width, height, NULL)) {
return Error(baton, hook); return Error();
} }
vips_object_local(hook, extracted); vips_object_local(hook, extracted);
image = extracted; image = extracted;
@@ -568,7 +637,7 @@ class ResizeWorker : public NanAsyncWorker {
if (vips_extract_area(image, &extractedPost, if (vips_extract_area(image, &extractedPost,
baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost, NULL baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost, NULL
)) { )) {
return Error(baton, hook); return Error();
} }
vips_object_local(hook, extractedPost); vips_object_local(hook, extractedPost);
image = extractedPost; image = extractedPost;
@@ -586,19 +655,19 @@ class ResizeWorker : public NanAsyncWorker {
vips_image_set_double(blur, "scale", 9); vips_image_set_double(blur, "scale", 9);
vips_object_local(hook, blur); vips_object_local(hook, blur);
if (vips_conv(image, &blurred, blur, NULL)) { if (vips_conv(image, &blurred, blur, NULL)) {
return Error(baton, hook); return Error();
} }
} else { } else {
// Slower, accurate Gaussian blur // Slower, accurate Gaussian blur
// Create Gaussian function for standard deviation // Create Gaussian function for standard deviation
VipsImage *gaussian; VipsImage *gaussian;
if (vips_gaussmat(&gaussian, baton->blurSigma, 0.2, "separable", TRUE, "integer", TRUE, NULL)) { if (vips_gaussmat(&gaussian, baton->blurSigma, 0.2, "separable", TRUE, "integer", TRUE, NULL)) {
return Error(baton, hook); return Error();
} }
vips_object_local(hook, gaussian); vips_object_local(hook, gaussian);
// Apply Gaussian function // Apply Gaussian function
if (vips_convsep(image, &blurred, gaussian, "precision", VIPS_PRECISION_INTEGER, NULL)) { if (vips_convsep(image, &blurred, gaussian, "precision", VIPS_PRECISION_INTEGER, NULL)) {
return Error(baton, hook); return Error();
} }
} }
vips_object_local(hook, blurred); vips_object_local(hook, blurred);
@@ -617,12 +686,12 @@ class ResizeWorker : public NanAsyncWorker {
vips_image_set_double(sharpen, "scale", 24); vips_image_set_double(sharpen, "scale", 24);
vips_object_local(hook, sharpen); vips_object_local(hook, sharpen);
if (vips_conv(image, &sharpened, sharpen, NULL)) { if (vips_conv(image, &sharpened, sharpen, NULL)) {
return Error(baton, hook); return Error();
} }
} else { } else {
// Slow, accurate sharpen in LAB colour space, with control over flat vs jagged areas // Slow, accurate sharpen in LAB colour space, with control over flat vs jagged areas
if (vips_sharpen(image, &sharpened, "radius", baton->sharpenRadius, "m1", baton->sharpenFlat, "m2", baton->sharpenJagged, NULL)) { if (vips_sharpen(image, &sharpened, "radius", baton->sharpenRadius, "m1", baton->sharpenFlat, "m2", baton->sharpenJagged, NULL)) {
return Error(baton, hook); return Error();
} }
} }
vips_object_local(hook, sharpened); vips_object_local(hook, sharpened);
@@ -633,18 +702,100 @@ class ResizeWorker : public NanAsyncWorker {
if (baton->gamma >= 1 && baton->gamma <= 3) { if (baton->gamma >= 1 && baton->gamma <= 3) {
VipsImage *gammaDecoded; VipsImage *gammaDecoded;
if (vips_gamma(image, &gammaDecoded, "exponent", baton->gamma, NULL)) { if (vips_gamma(image, &gammaDecoded, "exponent", baton->gamma, NULL)) {
return Error(baton, hook); return Error();
} }
vips_object_local(hook, gammaDecoded); vips_object_local(hook, gammaDecoded);
image = gammaDecoded; image = gammaDecoded;
} }
#ifndef _WIN32
// Apply normalization
if (baton->normalize) {
VipsInterpretation typeBeforeNormalize = image->Type;
if (typeBeforeNormalize == VIPS_INTERPRETATION_RGB) {
typeBeforeNormalize = VIPS_INTERPRETATION_sRGB;
}
// normalize the luminance band in LAB space:
VipsImage *lab;
if (vips_colourspace(image, &lab, VIPS_INTERPRETATION_LAB, NULL)) {
return Error();
}
vips_object_local(hook, lab);
VipsImage *luminance;
if (vips_extract_band(lab, &luminance, 0, "n", 1, NULL)) {
return Error();
}
vips_object_local(hook, luminance);
VipsImage *chroma;
if (vips_extract_band(lab, &chroma, 1, "n", 2, NULL)) {
return Error();
}
vips_object_local(hook, chroma);
VipsImage *stats;
if (vips_stats(luminance, &stats, NULL)) {
return Error();
}
vips_object_local(hook, stats);
double min = *VIPS_MATRIX(stats, 0, 0);
double max = *VIPS_MATRIX(stats, 1, 0);
VipsImage *normalized;
if (min == max) {
// Range of zero: create black image
if (vips_black(&normalized, image->Xsize, image->Ysize, "bands", 1, NULL )) {
return Error();
}
vips_object_local(hook, normalized);
} else {
double f = 100.0 / (max - min);
double a = -(min * f);
VipsImage *luminance100;
if (vips_linear1(luminance, &luminance100, f, a, NULL)) {
return Error();
}
vips_object_local(hook, luminance100);
VipsImage *normalizedLab;
if (vips_bandjoin2(luminance100, chroma, &normalizedLab, NULL)) {
return Error();
}
vips_object_local(hook, normalizedLab);
if (vips_colourspace(normalizedLab, &normalized, typeBeforeNormalize, NULL)) {
return Error();
}
vips_object_local(hook, normalized);
}
if (HasAlpha(image)) {
VipsImage *alpha;
if (vips_extract_band(image, &alpha, image->Bands - 1, "n", 1, NULL)) {
return Error();
}
vips_object_local(hook, alpha);
VipsImage *normalizedAlpha;
if (vips_bandjoin2(normalized, alpha, &normalizedAlpha, NULL)) {
return Error();
}
vips_object_local(hook, normalizedAlpha);
image = normalizedAlpha;
} else {
image = normalized;
}
}
#endif
// Convert image to sRGB, if not already // Convert image to sRGB, if not already
if (image->Type != VIPS_INTERPRETATION_sRGB) { if (image->Type != VIPS_INTERPRETATION_sRGB) {
// Switch intrepretation to sRGB // Switch intrepretation to sRGB
VipsImage *rgb; VipsImage *rgb;
if (vips_colourspace(image, &rgb, VIPS_INTERPRETATION_sRGB, NULL)) { if (vips_colourspace(image, &rgb, VIPS_INTERPRETATION_sRGB, NULL)) {
return Error(baton, hook); return Error();
} }
vips_object_local(hook, rgb); vips_object_local(hook, rgb);
image = rgb; image = rgb;
@@ -652,19 +803,19 @@ class ResizeWorker : public NanAsyncWorker {
if (baton->withMetadata && HasProfile(image)) { if (baton->withMetadata && HasProfile(image)) {
VipsImage *profiled; VipsImage *profiled;
if (vips_icc_transform(image, &profiled, srgbProfile.c_str(), "embedded", TRUE, NULL)) { if (vips_icc_transform(image, &profiled, srgbProfile.c_str(), "embedded", TRUE, NULL)) {
return Error(baton, hook); return Error();
} }
vips_object_local(hook, profiled); vips_object_local(hook, profiled);
image = 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;
if (vips_tilecache(image, &cached, "threaded", TRUE, "persistent", TRUE, "max_tiles", -1, NULL)) { if (vips_tilecache(image, &cached, "threaded", TRUE, "persistent", TRUE, "max_tiles", -1, NULL)) {
return Error(baton, hook); return Error();
} }
vips_object_local(hook, cached); vips_object_local(hook, cached);
image = cached; image = cached;
@@ -675,24 +826,30 @@ 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,
return Error(baton, hook); #if (VIPS_MAJOR_VERSION >= 8)
"trellis_quant", baton->trellisQuantisation,
"overshoot_deringing", baton->overshootDeringing,
"optimize_scans", baton->optimiseScans,
#endif
"interlace", baton->progressive, NULL)) {
return Error();
} }
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 >= 42) #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
if (vips_pngsave_buffer(image, &baton->bufferOut, &baton->bufferOutLength, "strip", !baton->withMetadata, if (vips_pngsave_buffer(image, &baton->bufferOut, &baton->bufferOutLength, "strip", !baton->withMetadata,
"compression", baton->compressionLevel, "interlace", baton->progressive, "filter", filter, NULL)) { "compression", baton->compressionLevel, "interlace", baton->progressive, "filter", filter, NULL)) {
return Error(baton, hook); return Error();
} }
#else #else
// Write PNG to buffer // Write PNG to buffer
if (vips_pngsave_buffer(image, &baton->bufferOut, &baton->bufferOutLength, "strip", !baton->withMetadata, if (vips_pngsave_buffer(image, &baton->bufferOut, &baton->bufferOutLength, "strip", !baton->withMetadata,
"compression", baton->compressionLevel, "interlace", baton->progressive, NULL)) { "compression", baton->compressionLevel, "interlace", baton->progressive, NULL)) {
return Error(baton, hook); return Error();
} }
#endif #endif
baton->outputFormat = "png"; baton->outputFormat = "png";
@@ -700,17 +857,17 @@ class ResizeWorker : public NanAsyncWorker {
// Write WEBP to buffer // Write WEBP to buffer
if (vips_webpsave_buffer(image, &baton->bufferOut, &baton->bufferOutLength, "strip", !baton->withMetadata, if (vips_webpsave_buffer(image, &baton->bufferOut, &baton->bufferOutLength, "strip", !baton->withMetadata,
"Q", baton->quality, NULL)) { "Q", baton->quality, NULL)) {
return Error(baton, hook); return Error();
} }
baton->outputFormat = "webp"; baton->outputFormat = "webp";
#if (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 42) #if (VIPS_MAJOR_VERSION >= 8 || (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 42))
} else if (baton->output == "__raw") { } else if (baton->output == "__raw") {
// Write raw, uncompressed image data to buffer // Write raw, uncompressed image data to buffer
if (baton->greyscale) { if (baton->greyscale || image->Type == VIPS_INTERPRETATION_B_W) {
// Extract first band for greyscale image // Extract first band for greyscale image
VipsImage *grey; VipsImage *grey;
if (vips_extract_band(image, &grey, 1, NULL)) { if (vips_extract_band(image, &grey, 0, NULL)) {
return Error(baton, hook); return Error();
} }
vips_object_local(hook, grey); vips_object_local(hook, grey);
image = grey; image = grey;
@@ -719,7 +876,7 @@ class ResizeWorker : public NanAsyncWorker {
// Cast pixels to uint8 (unsigned char) // Cast pixels to uint8 (unsigned char)
VipsImage *uchar; VipsImage *uchar;
if (vips_cast(image, &uchar, VIPS_FORMAT_UCHAR, NULL)) { if (vips_cast(image, &uchar, VIPS_FORMAT_UCHAR, NULL)) {
return Error(baton, hook); return Error();
} }
vips_object_local(hook, uchar); vips_object_local(hook, uchar);
image = uchar; image = uchar;
@@ -728,7 +885,7 @@ class ResizeWorker : public NanAsyncWorker {
baton->bufferOut = vips_image_write_to_memory(image, &baton->bufferOutLength); baton->bufferOut = vips_image_write_to_memory(image, &baton->bufferOutLength);
if (baton->bufferOut == NULL) { if (baton->bufferOut == NULL) {
(baton->err).append("Could not allocate enough memory for raw output"); (baton->err).append("Could not allocate enough memory for raw output");
return Error(baton, hook); return Error();
} }
baton->outputFormat = "raw"; baton->outputFormat = "raw";
#endif #endif
@@ -737,28 +894,35 @@ class ResizeWorker : public NanAsyncWorker {
bool outputPng = IsPng(baton->output); bool outputPng = IsPng(baton->output);
bool outputWebp = IsWebp(baton->output); bool outputWebp = IsWebp(baton->output);
bool outputTiff = IsTiff(baton->output); bool outputTiff = IsTiff(baton->output);
bool matchInput = !(outputJpeg || outputPng || outputWebp || outputTiff); bool outputDz = IsDz(baton->output);
bool matchInput = !(outputJpeg || outputPng || outputWebp || outputTiff || outputDz);
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,
return Error(baton, hook); #if (VIPS_MAJOR_VERSION >= 8)
"trellis_quant", baton->trellisQuantisation,
"overshoot_deringing", baton->overshootDeringing,
"optimize_scans", baton->optimiseScans,
#endif
"interlace", baton->progressive, NULL)) {
return Error();
} }
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 >= 42) #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
if (vips_pngsave(image, baton->output.c_str(), "strip", !baton->withMetadata, if (vips_pngsave(image, baton->output.c_str(), "strip", !baton->withMetadata,
"compression", baton->compressionLevel, "interlace", baton->progressive, "filter", filter, NULL)) { "compression", baton->compressionLevel, "interlace", baton->progressive, "filter", filter, NULL)) {
return Error(baton, hook); return Error();
} }
#else #else
// Write PNG to file // Write PNG to file
if (vips_pngsave(image, baton->output.c_str(), "strip", !baton->withMetadata, if (vips_pngsave(image, baton->output.c_str(), "strip", !baton->withMetadata,
"compression", baton->compressionLevel, "interlace", baton->progressive, NULL)) { "compression", baton->compressionLevel, "interlace", baton->progressive, NULL)) {
return Error(baton, hook); return Error();
} }
#endif #endif
baton->outputFormat = "png"; baton->outputFormat = "png";
@@ -766,19 +930,26 @@ class ResizeWorker : public NanAsyncWorker {
// Write WEBP to file // Write WEBP to file
if (vips_webpsave(image, baton->output.c_str(), "strip", !baton->withMetadata, if (vips_webpsave(image, baton->output.c_str(), "strip", !baton->withMetadata,
"Q", baton->quality, NULL)) { "Q", baton->quality, NULL)) {
return Error(baton, hook); return Error();
} }
baton->outputFormat = "webp"; baton->outputFormat = "webp";
} else if (outputTiff || (matchInput && inputImageType == ImageType::TIFF)) { } else if (outputTiff || (matchInput && inputImageType == ImageType::TIFF)) {
// Write TIFF to file // Write TIFF to file
if (vips_tiffsave(image, baton->output.c_str(), "strip", !baton->withMetadata, if (vips_tiffsave(image, baton->output.c_str(), "strip", !baton->withMetadata,
"compression", VIPS_FOREIGN_TIFF_COMPRESSION_JPEG, "Q", baton->quality, NULL)) { "compression", VIPS_FOREIGN_TIFF_COMPRESSION_JPEG, "Q", baton->quality, NULL)) {
return Error(baton, hook); return Error();
} }
baton->outputFormat = "tiff"; baton->outputFormat = "tiff";
} else if (outputDz) {
// Write DZ to file
if (vips_dzsave(image, baton->output.c_str(), "strip", !baton->withMetadata,
"tile_size", baton->tileSize, "overlap", baton->tileOverlap, NULL)) {
return Error();
}
baton->outputFormat = "dz";
} else { } else {
(baton->err).append("Unsupported output " + baton->output); (baton->err).append("Unsupported output " + baton->output);
return Error(baton, hook); return Error();
} }
} }
// Clean up any dangling image references // Clean up any dangling image references
@@ -809,8 +980,8 @@ 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<Integer>(width)); info->Set(NanNew<String>("width"), NanNew<Uint32>(static_cast<uint32_t>(width)));
info->Set(NanNew<String>("height"), NanNew<Integer>(height)); info->Set(NanNew<String>("height"), NanNew<Uint32>(static_cast<uint32_t>(height)));
if (baton->bufferOutLength > 0) { if (baton->bufferOutLength > 0) {
// Copy data to new Buffer // Copy data to new Buffer
@@ -822,7 +993,7 @@ class ResizeWorker : public NanAsyncWorker {
argv[2] = info; argv[2] = info;
} else { } else {
// Add file size to info // Add file size to info
struct stat st; GStatBuf st;
g_stat(baton->output.c_str(), &st); g_stat(baton->output.c_str(), &st);
info->Set(NanNew<String>("size"), NanNew<Uint32>(static_cast<uint32_t>(st.st_size))); info->Set(NanNew<String>("size"), NanNew<Uint32>(static_cast<uint32_t>(st.st_size)));
argv[1] = info; argv[1] = info;
@@ -832,13 +1003,18 @@ class ResizeWorker : public NanAsyncWorker {
// Decrement processing task counter // Decrement processing task counter
g_atomic_int_dec_and_test(&counterProcess); g_atomic_int_dec_and_test(&counterProcess);
Handle<Value> queueLength[1] = { NanNew<Uint32>(counterQueue) };
queueListener->Call(1, queueLength);
delete queueListener;
// Return to JavaScript // Return to JavaScript
callback->Call(3, argv); callback->Call(3, argv);
} }
private: private:
ResizeBaton* baton; ResizeBaton *baton;
NanCallback *queueListener;
VipsObject *hook;
/* /*
Calculate the angle of rotation and need-to-flip for the output image. Calculate the angle of rotation and need-to-flip for the output image.
@@ -903,12 +1079,36 @@ class ResizeWorker : public NanAsyncWorker {
return std::make_tuple(left, top); return std::make_tuple(left, top);
} }
/*
Calculate integral shrink given factor and interpolator window size
*/
int CalculateShrink(double factor, int interpolatorWindowSize) {
int shrink = 1;
if (factor >= 2 && interpolatorWindowSize > 3) {
// Shrink less, affine more with interpolators that use at least 4x4 pixel window, e.g. bicubic
shrink = static_cast<int>(floor(factor * 3.0 / interpolatorWindowSize));
} else {
shrink = static_cast<int>(floor(factor));
}
if (shrink < 1) {
shrink = 1;
}
return shrink;
}
/*
Calculate residual given shrink and factor
*/
double CalculateResidual(int shrink, double factor) {
return static_cast<double>(shrink) / factor;
}
/* /*
Copy then clear the error message. Copy then clear the error message.
Unref all transitional images on the hook. Unref all transitional images on the hook.
Clear all thread-local data. Clear all thread-local data.
*/ */
void Error(ResizeBaton *baton, VipsObject *hook) { void Error() {
// Get libvips' error message // Get libvips' error message
(baton->err).append(vips_error_buffer()); (baton->err).append(vips_error_buffer());
// Clean up any dangling image references // Clean up any dangling image references
@@ -959,12 +1159,16 @@ 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;
} else if (canvas->Equals(NanNew<String>("ignore_aspect"))) {
baton->canvas = Canvas::IGNORE_ASPECT;
} }
// 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")));
@@ -983,6 +1187,7 @@ NAN_METHOD(resize) {
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->normalize = options->Get(NanNew<String>("normalize"))->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->rotateBeforePreExtract = options->Get(NanNew<String>("rotateBeforePreExtract"))->BooleanValue();
baton->flip = options->Get(NanNew<String>("flip"))->BooleanValue(); baton->flip = options->Get(NanNew<String>("flip"))->BooleanValue();
@@ -992,16 +1197,26 @@ 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->trellisQuantisation = options->Get(NanNew<String>("trellisQuantisation"))->BooleanValue();
baton->overshootDeringing = options->Get(NanNew<String>("overshootDeringing"))->BooleanValue();
baton->optimiseScans = options->Get(NanNew<String>("optimiseScans"))->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());
baton->tileSize = options->Get(NanNew<String>("tileSize"))->Int32Value();
baton->tileOverlap = options->Get(NanNew<String>("tileOverlap"))->Int32Value();
// Function to notify of queue length changes
NanCallback *queueListener = new NanCallback(Handle<Function>::Cast(options->Get(NanNew<String>("queueListener"))));
// Join queue for worker thread // Join queue for worker thread
NanCallback *callback = new NanCallback(args[1].As<Function>()); NanCallback *callback = new NanCallback(args[1].As<Function>());
NanAsyncQueueWorker(new ResizeWorker(callback, baton)); NanAsyncQueueWorker(new ResizeWorker(callback, baton, queueListener));
// Increment queued task counter // Increment queued task counter
g_atomic_int_inc(&counterQueue); g_atomic_int_inc(&counterQueue);
Handle<Value> queueLength[1] = { NanNew<Uint32>(counterQueue) };
queueListener->Call(1, queueLength);
NanReturnUndefined(); NanReturnUndefined();
} }

View File

@@ -13,12 +13,9 @@ extern "C" void init(v8::Handle<v8::Object> target) {
vips_init("sharp"); vips_init("sharp");
// 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);
@@ -26,6 +23,7 @@ extern "C" void init(v8::Handle<v8::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

@@ -10,6 +10,7 @@ using v8::Local;
using v8::Object; using v8::Object;
using v8::Number; using v8::Number;
using v8::String; using v8::String;
using v8::Boolean;
using sharp::counterQueue; using sharp::counterQueue;
using sharp::counterProcess; using sharp::counterProcess;
@@ -75,6 +76,69 @@ NAN_METHOD(counters) {
NAN_METHOD(libvipsVersion) { NAN_METHOD(libvipsVersion) {
NanScope(); NanScope();
char version[9]; char version[9];
snprintf(version, sizeof(version), "%d.%d.%d", vips_version(0), vips_version(1), vips_version(2)); g_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

@@ -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 // SRC_UTILITIES_H_ #endif // SRC_UTILITIES_H_

View File

@@ -9,10 +9,10 @@
}, },
"devDependencies": { "devDependencies": {
"imagemagick": "^0.1.3", "imagemagick": "^0.1.3",
"imagemagick-native": "^1.6.0", "imagemagick-native": "mash/node-imagemagick-native",
"gm": "^1.17.0", "gm": "^1.17.0",
"async": "^0.9.0", "async": "^0.9.0",
"semver": "^4.2.0", "semver": "^4.3.0",
"benchmark": "^1.0.0" "benchmark": "^1.0.0"
}, },
"license": "Apache 2.0", "license": "Apache 2.0",

View File

@@ -311,6 +311,18 @@ async.series({
} }
}); });
} }
}).add('sharp-normalise', {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer).resize(width, height).normalise().toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-greyscale', { }).add('sharp-greyscale', {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
@@ -347,6 +359,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) {

View File

@@ -11,6 +11,9 @@ 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.ceil(Math.random() * (max - min) + min); return Math.ceil(Math.random() * (max - min) + min);
}; };
@@ -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,

BIN
test/fixtures/2x2_fdcce6.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 B

BIN
test/fixtures/CMU-1-Small-Region.svs vendored Normal file

Binary file not shown.

BIN
test/fixtures/grey-8bit-alpha.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -15,9 +15,12 @@ module.exports = {
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'), inputJpgWithCorruptHeader: getPath('corrupt-header.jpg'),
inputJpgWithLowContrast: getPath('low-contrast.jpg'), // http://www.flickr.com/photos/grizdave/2569067123/
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
inputPngWithGreyAlpha: getPath('grey-8bit-alpha.png'),
inputPngWithOneColor: getPath('2x2_fdcce6.png'),
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
@@ -25,6 +28,8 @@ module.exports = {
inputSvg: getPath('Wikimedia-logo.svg'), // http://commons.wikimedia.org/wiki/File:Wikimedia-logo.svg 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 inputPsd: getPath('free-gearhead-pack.psd'), // https://dribbble.com/shots/1624241-Free-Gearhead-Vector-Pack
inputSvs: getPath('CMU-1-Small-Region.svs'), // http://openslide.cs.cmu.edu/download/openslide-testdata/Aperio/CMU-1-Small-Region.svs
outputJpg: getPath('output.jpg'), outputJpg: getPath('output.jpg'),
outputPng: getPath('output.png'), outputPng: getPath('output.png'),
outputWebP: getPath('output.webp'), outputWebP: getPath('output.webp'),

BIN
test/fixtures/low-contrast.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -3,6 +3,6 @@ if ! type valgrind >/dev/null; then
exit 1 exit 1
fi fi
curl -O https://raw.githubusercontent.com/jcupitt/libvips/master/libvips.supp test/leak/libvips.supp curl -O https://raw.githubusercontent.com/jcupitt/libvips/master/libvips.supp
cd ../../ cd ../../
G_SLICE=always-malloc G_DEBUG=gc-friendly valgrind --suppressions=test/leak/libvips.supp --suppressions=test/leak/sharp.supp --leak-check=full --show-leak-kinds=definite,indirect,possible --num-callers=20 --trace-children=yes npm test G_SLICE=always-malloc G_DEBUG=gc-friendly valgrind --suppressions=test/leak/libvips.supp --suppressions=test/leak/sharp.supp --leak-check=full --show-leak-kinds=definite,indirect,possible --num-callers=20 --trace-children=yes npm test

View File

@@ -31,16 +31,18 @@ describe('Colour space conversion', function() {
.toFile(fixtures.path('output.greyscale-not.jpg'), done); .toFile(fixtures.path('output.greyscale-not.jpg'), done);
}); });
it('From 1-bit TIFF to sRGB WebP [slow]', function(done) { if (sharp.format.webp.output.buffer) {
sharp(fixtures.inputTiff) it('From 1-bit TIFF to sRGB WebP [slow]', function(done) {
.webp() sharp(fixtures.inputTiff)
.toBuffer(function(err, data, info) { .webp()
if (err) throw err; .toBuffer(function(err, data, info) {
assert.strictEqual(true, data.length > 0); if (err) throw err;
assert.strictEqual('webp', info.format); assert.strictEqual(true, data.length > 0);
done(); assert.strictEqual('webp', info.format);
}); done();
}); });
});
}
it('From CMYK to sRGB', function(done) { it('From CMYK to sRGB', function(done) {
sharp(fixtures.inputJpgWithCmykProfile) sharp(fixtures.inputJpgWithCmykProfile)

View File

@@ -8,39 +8,42 @@ var cpplint = require('node-cpplint/lib/');
describe('cpplint', function() { describe('cpplint', function() {
// List C++ source files // Ignore cpplint failures, possibly newline-related, on Windows
fs.readdirSync(path.join(__dirname, '..', '..', 'src')).forEach(function (source) { if (process.platform !== 'win32') {
var file = path.join('src', source); // List C++ source files
it(file, function(done) { fs.readdirSync(path.join(__dirname, '..', '..', 'src')).forEach(function (source) {
// Lint each source file var file = path.join('src', source);
cpplint({ it(file, function(done) {
files: [file], // Lint each source file
linelength: 140, cpplint({
filters: { files: [file],
legal: { linelength: 140,
copyright: false filters: {
}, legal: {
build: { copyright: false
include: false, },
include_order: false build: {
}, include: false,
whitespace: { include_order: false
blank_line: false, },
comments: false, whitespace: {
parens: false blank_line: false,
comments: false,
parens: false
}
} }
} }, function(err, report) {
}, function(err, report) { if (err) {
if (err) { throw err;
throw err; }
} var expected = {};
var expected = {}; expected[file] = [];
expected[file] = []; assert.deepEqual(expected, report);
assert.deepEqual(expected, report); done();
done(); });
}); });
});
}); });
}
}); });

View File

@@ -28,18 +28,38 @@ describe('Embed', function() {
}); });
}); });
it('JPEG within WebP, to include alpha channel', function(done) { if (sharp.format.webp.output.buffer) {
sharp(fixtures.inputJpg) it('JPEG within WebP, to include alpha channel', function(done) {
.resize(320, 240) sharp(fixtures.inputJpg)
.background({r: 0, g: 0, b: 0, a: 0}) .resize(320, 240)
.background({r: 0, g: 0, b: 0, a: 0})
.embed()
.webp()
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('webp', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
sharp(data).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual(4, metadata.channels);
done();
});
});
});
}
it('PNG with alpha channel', function(done) {
sharp(fixtures.inputPngWithTransparency)
.resize(50, 50)
.embed() .embed()
.webp()
.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('webp', info.format); assert.strictEqual('png', info.format);
assert.strictEqual(320, info.width); assert.strictEqual(50, info.width);
assert.strictEqual(240, info.height); assert.strictEqual(50, info.height);
sharp(data).metadata(function(err, metadata) { sharp(data).metadata(function(err, metadata) {
if (err) throw err; if (err) throw err;
assert.strictEqual(4, metadata.channels); assert.strictEqual(4, metadata.channels);

View File

@@ -31,16 +31,18 @@ describe('Partial image extraction', function() {
}); });
}); });
it('WebP', function(done) { if (sharp.format.webp.output.file) {
sharp(fixtures.inputWebP) it('WebP', function(done) {
.extract(50, 100, 125, 200) sharp(fixtures.inputWebP)
.toFile(fixtures.path('output.extract.webp'), function(err, info) { .extract(50, 100, 125, 200)
if (err) throw err; .toFile(fixtures.path('output.extract.webp'), function(err, info) {
assert.strictEqual(125, info.width); if (err) throw err;
assert.strictEqual(200, info.height); assert.strictEqual(125, info.width);
done(); assert.strictEqual(200, info.height);
}); done();
}); });
});
}
it('TIFF', function(done) { it('TIFF', function(done) {
sharp(fixtures.inputTiff) sharp(fixtures.inputTiff)
@@ -116,4 +118,62 @@ describe('Partial image extraction', function() {
}); });
}); });
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

@@ -93,4 +93,14 @@ describe('Interpolation', function() {
}); });
}); });
it('unknown interpolator throws', function(done) {
var isValid = false;
try {
sharp().interpolateWith('nonexistant');
isValid = true;
} catch (e) {}
assert(!isValid);
done();
});
}); });

View File

@@ -135,10 +135,11 @@ 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);
@@ -150,10 +151,11 @@ 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);
@@ -200,27 +202,23 @@ describe('Input/output', function() {
}); });
it('Fail when input is empty Buffer', function(done) { it('Fail when input is empty Buffer', function(done) {
var failed = true; sharp(new Buffer(0)).toBuffer().then(function () {
try { assert(false);
sharp(new Buffer(0)); done();
failed = false; }).catch(function (err) {
} catch (err) {
assert(err instanceof Error); assert(err instanceof Error);
} done();
assert(failed); });
done();
}); });
it('Fail when input is invalid Buffer', function(done) { it('Fail when input is invalid Buffer', function(done) {
var failed = true; sharp(new Buffer([0x1, 0x2, 0x3, 0x4])).toBuffer().then(function () {
try { assert(false);
sharp(new Buffer([0x1, 0x2, 0x3, 0x4])); done();
failed = false; }).catch(function (err) {
} catch (err) {
assert(err instanceof Error); assert(err instanceof Error);
} done();
assert(failed); });
done();
}); });
it('Promises/A+', function(done) { it('Promises/A+', function(done) {
@@ -265,7 +263,34 @@ describe('Input/output', function() {
done(); done();
}); });
it('Progressive image', function(done) { it('Progressive JPEG image', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.progressive(false)
.toBuffer(function(err, nonProgressiveData, nonProgressiveInfo) {
if (err) throw err;
assert.strictEqual(true, nonProgressiveData.length > 0);
assert.strictEqual(nonProgressiveData.length, nonProgressiveInfo.size);
assert.strictEqual('jpeg', nonProgressiveInfo.format);
assert.strictEqual(320, nonProgressiveInfo.width);
assert.strictEqual(240, nonProgressiveInfo.height);
sharp(fixtures.inputJpg)
.resize(320, 240)
.progressive()
.toBuffer(function(err, progressiveData, progressiveInfo) {
if (err) throw err;
assert.strictEqual(true, progressiveData.length > 0);
assert.strictEqual(progressiveData.length, progressiveInfo.size);
assert.strictEqual(false, progressiveData.length === nonProgressiveData.length);
assert.strictEqual('jpeg', progressiveInfo.format);
assert.strictEqual(320, progressiveInfo.width);
assert.strictEqual(240, progressiveInfo.height);
done();
});
});
});
it('Progressive PNG image', function(done) {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.resize(320, 240) .resize(320, 240)
.png() .png()
@@ -292,6 +317,32 @@ describe('Input/output', function() {
}); });
}); });
if (sharp.format.webp.output.buffer) {
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) { it('File input with corrupt header fails gracefully', function(done) {
sharp(fixtures.inputJpgWithCorruptHeader) sharp(fixtures.inputJpgWithCorruptHeader)
.toBuffer(function(err) { .toBuffer(function(err) {
@@ -345,17 +396,19 @@ describe('Input/output', function() {
}); });
}); });
it('WebP', function(done) { if (sharp.format.webp.input.file) {
sharp(fixtures.inputWebP).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) { it('WebP', function(done) {
if (err) throw err; sharp(fixtures.inputWebP).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) {
assert.strictEqual(true, info.size > 0); if (err) throw err;
assert.strictEqual('webp', info.format); assert.strictEqual(true, info.size > 0);
assert.strictEqual(320, info.width); assert.strictEqual('webp', info.format);
assert.strictEqual(80, info.height); assert.strictEqual(320, info.width);
fs.unlinkSync(fixtures.outputZoinks); assert.strictEqual(80, info.height);
done(); fs.unlinkSync(fixtures.outputZoinks);
done();
});
}); });
}); }
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) {
@@ -433,36 +486,160 @@ describe('Input/output', function() {
}); });
it('Convert SVG to PNG', function(done) { it('Without chroma subsampling generates larger file', function(done) {
sharp(fixtures.inputSvg) // First generate with chroma subsampling (default)
.resize(100, 100) sharp(fixtures.inputJpg)
.png()
.toFile(fixtures.path('output.svg.png'), function(err, info) {
if (err) throw err;
assert.strictEqual(true, info.size > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(100, info.width);
assert.strictEqual(100, info.height);
done();
});
});
it('Convert PSD to PNG', function(done) {
sharp(fixtures.inputPsd)
.resize(320, 240) .resize(320, 240)
.png() .withoutChromaSubsampling(false)
.toFile(fixtures.path('output.psd.png'), function(err, info) { .toBuffer(function(err, withChromaSubsamplingData, withChromaSubsamplingInfo) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, info.size > 0); assert.strictEqual(true, withChromaSubsamplingData.length > 0);
assert.strictEqual('png', info.format); assert.strictEqual(withChromaSubsamplingData.length, withChromaSubsamplingInfo.size);
assert.strictEqual(320, info.width); assert.strictEqual('jpeg', withChromaSubsamplingInfo.format);
assert.strictEqual(240, info.height); assert.strictEqual(320, withChromaSubsamplingInfo.width);
done(); 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 (semver.gte(sharp.libvipsVersion(), '7.40.0')) { if (semver.gte(sharp.libvipsVersion(), '8.0.0')) {
it('Load TIFF from Buffer [libvips ' + sharp.libvipsVersion() + '>=7.40.0]', function(done) { it('Trellis quantisation [libvips ' + sharp.libvipsVersion() + '>=8.0.0]', function(done) {
// First generate without
sharp(fixtures.inputJpg)
.resize(320, 240)
.trellisQuantisation(false)
.toBuffer(function(err, withoutData, withoutInfo) {
if (err) throw err;
assert.strictEqual(true, withoutData.length > 0);
assert.strictEqual(withoutData.length, withoutInfo.size);
assert.strictEqual('jpeg', withoutInfo.format);
assert.strictEqual(320, withoutInfo.width);
assert.strictEqual(240, withoutInfo.height);
// Then generate with
sharp(fixtures.inputJpg)
.resize(320, 240)
.trellisQuantization()
.toBuffer(function(err, withData, withInfo) {
if (err) throw err;
assert.strictEqual(true, withData.length > 0);
assert.strictEqual(withData.length, withInfo.size);
assert.strictEqual('jpeg', withInfo.format);
assert.strictEqual(320, withInfo.width);
assert.strictEqual(240, withInfo.height);
// Verify image is same (as mozjpeg may not be present) size or less
assert.strictEqual(true, withData.length <= withoutData.length);
done();
});
});
});
it('Overshoot deringing [libvips ' + sharp.libvipsVersion() + '>=8.0.0]', function(done) {
// First generate without
sharp(fixtures.inputJpg)
.resize(320, 240)
.overshootDeringing(false)
.toBuffer(function(err, withoutData, withoutInfo) {
if (err) throw err;
assert.strictEqual(true, withoutData.length > 0);
assert.strictEqual(withoutData.length, withoutInfo.size);
assert.strictEqual('jpeg', withoutInfo.format);
assert.strictEqual(320, withoutInfo.width);
assert.strictEqual(240, withoutInfo.height);
// Then generate with
sharp(fixtures.inputJpg)
.resize(320, 240)
.overshootDeringing()
.toBuffer(function(err, withData, withInfo) {
if (err) throw err;
assert.strictEqual(true, withData.length > 0);
assert.strictEqual(withData.length, withInfo.size);
assert.strictEqual('jpeg', withInfo.format);
assert.strictEqual(320, withInfo.width);
assert.strictEqual(240, withInfo.height);
done();
});
});
});
it('Optimise scans [libvips ' + sharp.libvipsVersion() + '>=8.0.0]', function(done) {
// First generate without
sharp(fixtures.inputJpg)
.resize(320, 240)
.optimiseScans(false)
.toBuffer(function(err, withoutData, withoutInfo) {
if (err) throw err;
assert.strictEqual(true, withoutData.length > 0);
assert.strictEqual(withoutData.length, withoutInfo.size);
assert.strictEqual('jpeg', withoutInfo.format);
assert.strictEqual(320, withoutInfo.width);
assert.strictEqual(240, withoutInfo.height);
// Then generate with
sharp(fixtures.inputJpg)
.resize(320, 240)
.optimizeScans()
.toBuffer(function(err, withData, withInfo) {
if (err) throw err;
assert.strictEqual(true, withData.length > 0);
assert.strictEqual(withData.length, withInfo.size);
assert.strictEqual('jpeg', withInfo.format);
assert.strictEqual(320, withInfo.width);
assert.strictEqual(240, withInfo.height);
// Verify image is of a different size (progressive output even without mozjpeg)
assert.strictEqual(true, withData.length != withoutData.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(0, err.message.indexOf('Input file is of an unsupported image format'));
} 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)
@@ -479,8 +656,42 @@ describe('Input/output', function() {
}); });
} }
if (semver.gte(sharp.libvipsVersion(), '7.42.0')) { if (sharp.format.magick.input.buffer) {
describe('Ouput raw, uncompressed image data [libvips ' + sharp.libvipsVersion() + '>=7.42.0]', function() { 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.openslide.input.file) {
it('Load Aperio SVS file via Openslide', function(done) {
sharp(fixtures.inputSvs)
.resize(320, 240)
.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(240, info.height);
done();
});
});
}
if (sharp.format.raw.output.buffer) {
describe('Ouput raw, uncompressed image data', function() {
it('1 channel greyscale image', function(done) { it('1 channel greyscale image', function(done) {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.greyscale() .greyscale()
@@ -498,7 +709,7 @@ describe('Input/output', function() {
it('3 channel colour image without transparency', function(done) { it('3 channel colour image without transparency', function(done) {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.resize(32, 24) .resize(32, 24)
.raw() .toFormat('raw')
.toBuffer(function(err, data, info) { .toBuffer(function(err, data, info) {
assert.strictEqual(32 * 24 * 3, info.size); assert.strictEqual(32 * 24 * 3, info.size);
assert.strictEqual(data.length, info.size); assert.strictEqual(data.length, info.size);
@@ -511,7 +722,7 @@ describe('Input/output', function() {
it('4 channel colour image with transparency', function(done) { it('4 channel colour image with transparency', function(done) {
sharp(fixtures.inputPngWithTransparency) sharp(fixtures.inputPngWithTransparency)
.resize(32, 24) .resize(32, 24)
.raw() .toFormat(sharp.format.raw)
.toBuffer(function(err, data, info) { .toBuffer(function(err, data, info) {
assert.strictEqual(32 * 24 * 4, info.size); assert.strictEqual(32 * 24 * 4, info.size);
assert.strictEqual(data.length, info.size); assert.strictEqual(data.length, info.size);
@@ -546,16 +757,6 @@ describe('Input/output', function() {
done(); 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) { it('Invalid fails - string', function(done) {
var isValid = false; var isValid = false;
try { try {
@@ -592,4 +793,23 @@ describe('Input/output', function() {
}); });
it('Queue length change events', function(done) {
var eventCounter = 0;
var queueListener = function(queueLength) {
assert.strictEqual(true, queueLength === 0 || queueLength === 1);
eventCounter++;
};
sharp.queue.on('change', queueListener);
sharp(fixtures.inputJpg)
.resize(320, 240)
.toBuffer(function(err) {
process.nextTick(function() {
sharp.queue.removeListener('change', queueListener);
if (err) throw err;
assert.strictEqual(2, eventCounter);
done();
});
});
});
}); });

View File

@@ -82,19 +82,21 @@ describe('Image metadata', function() {
}); });
}); });
it('WebP', function(done) { if (sharp.format.webp.input.file) {
sharp(fixtures.inputWebP).metadata(function(err, metadata) { it('WebP', function(done) {
if (err) throw err; sharp(fixtures.inputWebP).metadata(function(err, metadata) {
assert.strictEqual('webp', metadata.format); if (err) throw err;
assert.strictEqual(1024, metadata.width); assert.strictEqual('webp', metadata.format);
assert.strictEqual(772, metadata.height); assert.strictEqual(1024, metadata.width);
assert.strictEqual('srgb', metadata.space); assert.strictEqual(772, metadata.height);
assert.strictEqual(3, metadata.channels); assert.strictEqual('srgb', metadata.space);
assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual(false, metadata.hasProfile);
done(); assert.strictEqual(false, metadata.hasAlpha);
done();
});
}); });
}); }
it('GIF via libmagick', function(done) { it('GIF via libmagick', function(done) {
sharp(fixtures.inputGif).metadata(function(err, metadata) { sharp(fixtures.inputGif).metadata(function(err, metadata) {
@@ -109,6 +111,22 @@ describe('Image metadata', function() {
}); });
}); });
if (sharp.format.openslide.input.file) {
it('Aperio SVS via openslide', function(done) {
sharp(fixtures.inputSvs).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('openslide', metadata.format);
assert.strictEqual(2220, metadata.width);
assert.strictEqual(2967, metadata.height);
assert.strictEqual(4, metadata.channels);
assert.strictEqual('rgb', metadata.space);
assert.strictEqual(false, metadata.hasProfile);
assert.strictEqual(true, metadata.hasAlpha);
done();
});
});
}
it('File in, Promise out', function(done) { it('File in, Promise out', function(done) {
sharp(fixtures.inputJpg).metadata().then(function(metadata) { sharp(fixtures.inputJpg).metadata().then(function(metadata) {
assert.strictEqual('jpeg', metadata.format); assert.strictEqual('jpeg', metadata.format);

129
test/unit/normalize.js Executable file
View File

@@ -0,0 +1,129 @@
'use strict';
var assert = require('assert');
var sharp = require('../../index');
var fixtures = require('../fixtures');
sharp.cache(0);
describe('Normalization', function () {
it('uses the same prototype for both spellings', function () {
assert.strictEqual(sharp.prototype.normalize, sharp.prototype.normalise);
});
// Normalize is currently unavailable on Windows
if (process.platform !== 'win32') {
it('spreads rgb image values between 0 and 255', function(done) {
sharp(fixtures.inputJpgWithLowContrast)
.normalize()
.raw()
.toBuffer(function (err, data, info) {
if (err) throw err;
var min = 255, max = 0, i;
for (i = 0; i < data.length; i += 3) {
min = Math.min(min, data[i], data[i + 1], data[i + 2]);
max = Math.max(max, data[i], data[i + 1], data[i + 2]);
}
assert.strictEqual(0, min);
assert.strictEqual(255, max);
return done();
});
});
it('spreads grayscaled image values between 0 and 255', function(done) {
sharp(fixtures.inputJpgWithLowContrast)
.gamma()
.greyscale()
.normalize(true)
.raw()
.toBuffer(function (err, data, info) {
if (err) throw err;
var min = 255, max = 0, i;
for (i = 0; i < data.length; i++) {
min = Math.min(min, data[i]);
max = Math.max(max, data[i]);
}
assert.strictEqual(0, min);
assert.strictEqual(255, max);
return done();
});
});
it('stretches greyscale images with alpha channel', function (done) {
sharp(fixtures.inputPngWithGreyAlpha)
.normalize()
.raw()
.toBuffer(function (err, data, info) {
var min = 255, max = 0, i;
for (i = 0; i < data.length; i++) {
min = Math.min(min, data[i]);
max = Math.max(max, data[i]);
}
assert.strictEqual(0, min);
assert.strictEqual(255, max);
return done();
});
});
it('keeps an existing alpha channel', function (done) {
sharp(fixtures.inputPngWithTransparency)
.normalize()
.toBuffer(function (err, data, info) {
sharp(data)
.metadata()
.then(function (metadata) {
assert.strictEqual(4, metadata.channels);
assert.strictEqual(true, metadata.hasAlpha);
assert.strictEqual('srgb', metadata.space);
})
.finally(done);
});
});
it('keeps the alpha channel of greyscale images intact', function (done) {
sharp(fixtures.inputPngWithGreyAlpha)
.normalize()
.toBuffer(function (err, data, info) {
sharp(data)
.metadata()
.then(function (metadata) {
assert.strictEqual(true, metadata.hasAlpha);
assert.strictEqual(4, metadata.channels);
assert.strictEqual('srgb', metadata.space);
})
.finally(done);
});
});
it('returns a black image for images with only one color', function (done) {
sharp(fixtures.inputPngWithOneColor)
.normalize()
.toBuffer()
.bind({})
.then(function (imageData) {
this.imageData = imageData;
return sharp(imageData)
.metadata();
})
.then(function (metadata) {
assert.strictEqual(false, metadata.hasAlpha);
assert.strictEqual(3, metadata.channels);
assert.strictEqual('srgb', metadata.space);
})
.then(function () {
return sharp(this.imageData)
.raw()
.toBuffer();
})
.then(function (rawData) {
var blackBuffer = new Buffer([0,0,0, 0,0,0, 0,0,0, 0,0,0]);
assert.strictEqual(blackBuffer.toString(), rawData.toString());
})
.finally(done);
});
}
});

View File

@@ -185,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)
@@ -226,5 +259,104 @@ describe('Resize dimensions', function() {
done(); done();
}); });
}); });
it('Downscale width and height, ignoring aspect ratio', function(done) {
sharp(fixtures.inputJpg).resize(320, 320).ignoreAspectRatio().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(320, info.height);
done();
});
});
it('Downscale width, ignoring aspect ratio', function(done) {
sharp(fixtures.inputJpg).resize(320).ignoreAspectRatio().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(2225, info.height);
done();
});
});
it('Downscale height, ignoring aspect ratio', function(done) {
sharp(fixtures.inputJpg).resize(null, 320).ignoreAspectRatio().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(320, info.height);
done();
});
});
it('Upscale width and height, ignoring aspect ratio', function(done) {
sharp(fixtures.inputJpg).resize(3000, 3000).ignoreAspectRatio().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3000, info.width);
assert.strictEqual(3000, info.height);
done();
});
});
it('Upscale width, ignoring aspect ratio', function(done) {
sharp(fixtures.inputJpg).resize(3000).ignoreAspectRatio().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3000, info.width);
assert.strictEqual(2225, info.height);
done();
});
});
it('Upscale height, ignoring aspect ratio', function(done) {
sharp(fixtures.inputJpg).resize(null, 3000).ignoreAspectRatio().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(3000, info.height);
done();
});
});
it('Downscale width, upscale height, ignoring aspect ratio', function(done) {
sharp(fixtures.inputJpg).resize(320, 3000).ignoreAspectRatio().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(3000, info.height);
done();
});
});
it('Upscale width, downscale height, ignoring aspect ratio', function(done) {
sharp(fixtures.inputJpg).resize(3000, 320).ignoreAspectRatio().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3000, info.width);
assert.strictEqual(320, info.height);
done();
});
});
it('Identity transform, ignoring aspect ratio', function(done) {
sharp(fixtures.inputJpg).ignoreAspectRatio().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(2225, info.height);
done();
});
});
}); });

204
test/unit/tile.js Executable file
View File

@@ -0,0 +1,204 @@
'use strict';
var fs = require('fs');
var path = require('path');
var assert = require('assert');
var async = require('async');
var rimraf = require('rimraf');
var sharp = require('../../index');
var fixtures = require('../fixtures');
sharp.cache(0);
// Verifies all tiles in a given dz output directory are <= size
var assertDeepZoomTiles = function(directory, expectedSize, expectedLevels, done) {
// Get levels
var levels = fs.readdirSync(directory);
assert.strictEqual(expectedLevels, levels.length);
// Get tiles
var tiles = [];
levels.forEach(function(level) {
// Verify level directory name
assert.strictEqual(true, /^[0-9]+$/.test(level));
fs.readdirSync(path.join(directory, level)).forEach(function(tile) {
// Verify tile file name
assert.strictEqual(true, /^[0-9]+_[0-9]+\.jpeg$/.test(tile));
tiles.push(path.join(directory, level, tile));
});
});
// Verify each tile is <= expectedSize
async.eachSeries(tiles, function(tile, done) {
sharp(tile).metadata(function(err, metadata) {
if (err) {
done(err);
} else {
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasProfile);
assert.strictEqual(false, metadata.hasAlpha);
assert.strictEqual(true, metadata.width <= expectedSize);
assert.strictEqual(true, metadata.height <= expectedSize);
done();
}
});
}, done);
};
describe('Tile', function() {
describe('Invalid tile values', function() {
it('size - NaN', function(done) {
var isValid = true;
try {
sharp().tile('zoinks');
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
it('size - float', function(done) {
var isValid = true;
try {
sharp().tile(1.1);
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
it('size - negative', function(done) {
var isValid = true;
try {
sharp().tile(-1);
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
it('size - zero', function(done) {
var isValid = true;
try {
sharp().tile(0);
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
it('size - too large', function(done) {
var isValid = true;
try {
sharp().tile(8193);
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
it('overlap - NaN', function(done) {
var isValid = true;
try {
sharp().tile(null, 'zoinks');
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
it('overlap - float', function(done) {
var isValid = true;
try {
sharp().tile(null, 1.1);
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
it('overlap - negative', function(done) {
var isValid = true;
try {
sharp().tile(null, -1);
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
it('overlap - too large', function(done) {
var isValid = true;
try {
sharp().tile(null, 8193);
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
it('overlap - larger than default size', function(done) {
var isValid = true;
try {
sharp().tile(null, 257);
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
it('overlap - larger than provided size', function(done) {
var isValid = true;
try {
sharp().tile(512, 513);
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
});
if (sharp.format.dz.output.file) {
describe('Deep Zoom output', function() {
it('Tile size - 256px default', function(done) {
var directory = fixtures.path('output.256_files');
rimraf(directory, function() {
sharp(fixtures.inputJpg).toFile(fixtures.path('output.256.dzi'), function(err, info) {
if (err) throw err;
assert.strictEqual('dz', info.format);
assertDeepZoomTiles(directory, 256, 13, done);
});
});
});
it('Tile size/overlap - 512/16px', function(done) {
var directory = fixtures.path('output.512_files');
rimraf(directory, function() {
sharp(fixtures.inputJpg).tile(512, 16).toFile(fixtures.path('output.512.dzi'), function(err, info) {
if (err) throw err;
assert.strictEqual('dz', info.format);
assertDeepZoomTiles(directory, 512 + 2 * 16, 13, done);
});
});
});
});
}
});

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);
});
});
});
});
}); });