Merge branch 'needle'

This commit is contained in:
Lovell Fuller 2016-03-22 09:52:25 +00:00
commit fe2eccef39
38 changed files with 1130 additions and 503 deletions

View File

@ -221,7 +221,7 @@
'<(module_root_dir)/lib/libintl-8.dll', '<(module_root_dir)/lib/libintl-8.dll',
'<(module_root_dir)/lib/libjpeg-62.dll', '<(module_root_dir)/lib/libjpeg-62.dll',
'<(module_root_dir)/lib/liblcms2-2.dll', '<(module_root_dir)/lib/liblcms2-2.dll',
'<(module_root_dir)/lib/libopenjpeg-1.dll', '<(module_root_dir)/lib/libopenjp2.dll',
'<(module_root_dir)/lib/libopenslide-0.dll', '<(module_root_dir)/lib/libopenslide-0.dll',
'<(module_root_dir)/lib/libpango-1.0-0.dll', '<(module_root_dir)/lib/libpango-1.0-0.dll',
'<(module_root_dir)/lib/libpangocairo-1.0-0.dll', '<(module_root_dir)/lib/libpangocairo-1.0-0.dll',

View File

@ -27,6 +27,7 @@ The object returned by the constructor implements the
[stream.Duplex](http://nodejs.org/api/stream.html#stream_class_stream_duplex) class. [stream.Duplex](http://nodejs.org/api/stream.html#stream_class_stream_duplex) class.
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.
When using Stream based output, derived attributes are available from the `info` event.
```javascript ```javascript
sharp('input.jpg') sharp('input.jpg')
@ -37,6 +38,19 @@ sharp('input.jpg')
}); });
``` ```
```javascript
// Read image data from readableStream,
// resize to 300 pixels wide,
// emit an 'info' event with calculated dimensions
// and finally write image data to writableStream
var transformer = sharp()
.resize(300)
.on('info', function(info) {
console.log('Image height is ' + info.height);
});
readableStream.pipe(transformer).pipe(writableStream);
```
#### 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.
@ -48,6 +62,7 @@ Fast access to image metadata without decoding any compressed image data.
* `height`: Number of pixels high * `height`: Number of pixels high
* `space`: Name of colour space interpretation e.g. `srgb`, `rgb`, `scrgb`, `cmyk`, `lab`, `xyz`, `b-w` [...](https://github.com/jcupitt/libvips/blob/master/libvips/iofuncs/enumtypes.c#L522) * `space`: Name of colour space interpretation e.g. `srgb`, `rgb`, `scrgb`, `cmyk`, `lab`, `xyz`, `b-w` [...](https://github.com/jcupitt/libvips/blob/master/libvips/iofuncs/enumtypes.c#L522)
* `channels`: Number of bands e.g. `3` for sRGB, `4` for CMYK * `channels`: Number of bands e.g. `3` for sRGB, `4` for CMYK
* `density`: Number of pixels per inch (DPI), if present
* `hasProfile`: Boolean indicating the presence of an embedded ICC profile * `hasProfile`: Boolean indicating the presence of an embedded ICC profile
* `hasAlpha`: Boolean indicating the presence of an alpha transparency channel * `hasAlpha`: Boolean indicating the presence of an alpha transparency channel
* `orientation`: Number value of the EXIF Orientation header, if present * `orientation`: Number value of the EXIF Orientation header, if present
@ -110,23 +125,37 @@ Scale output to `width` x `height`. By default, the resized image is cropped to
`height` is the integral Number of pixels high the resultant image should be, between 1 and 16383. Use `null` or `undefined` to auto-scale the height to match the width. `height` is the integral Number of pixels high the resultant image should be, between 1 and 16383. Use `null` or `undefined` to auto-scale the height to match the width.
#### crop([gravity]) #### crop([option])
Crop the resized image to the exact size specified, the default behaviour. Crop the resized image to the exact size specified, the default behaviour.
`gravity`, if present, is a String or an attribute of the `sharp.gravity` Object e.g. `sharp.gravity.north`. `option`, if present, is an attribute of:
Possible values are `north`, `northeast`, `east`, `southeast`, `south`, `southwest`, `west`, `northwest`, `center` and `centre`. * `sharp.gravity` e.g. `sharp.gravity.north`, to crop to an edge or corner, or
The default gravity is `center`/`centre`. * `sharp.strategy` e.g. `sharp.strategy.entropy`, to crop dynamically.
Possible attributes of `sharp.gravity` are
`north`, `northeast`, `east`, `southeast`, `south`,
`southwest`, `west`, `northwest`, `center` and `centre`.
Possible attributes of the experimental `sharp.strategy` are:
* `entropy`: resize so one dimension is at its target size
then repeatedly remove pixels from the edge with the lowest
[Shannon entropy](https://en.wikipedia.org/wiki/Entropy_%28information_theory%29)
until it too reaches the target size.
The default crop option is a `center`/`centre` gravity.
```javascript ```javascript
var transformer = sharp() var transformer = sharp()
.resize(300, 200) .resize(200, 200)
.crop(sharp.gravity.north) .crop(sharp.strategy.entropy)
.on('error', function(err) { .on('error', function(err) {
console.log(err); console.log(err);
}); });
// Read image data from readableStream, resize and write image data to writableStream // Read image data from readableStream
// Write 200px square auto-cropped image data to writableStream
readableStream.pipe(transformer).pipe(writableStream); readableStream.pipe(transformer).pipe(writableStream);
``` ```
@ -265,7 +294,7 @@ sharp(input)
#### background(rgba) #### background(rgba)
Set the background for the `embed` and `flatten` operations. Set the background for the `embed`, `flatten` and `extend` operations.
`rgba` is parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha. `rgba` is parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha.
@ -277,6 +306,25 @@ The default background is `{r: 0, g: 0, b: 0, a: 1}`, black without transparency
Merge alpha transparency channel, if any, with `background`. Merge alpha transparency channel, if any, with `background`.
#### extend(extension)
Extends/pads the edges of the image with `background`, where `extension` is one of:
* a Number representing the pixel count to add to each edge, or
* an Object containing `top`, `left`, `bottom` and `right` attributes, each a Number of pixels to add to that edge.
This operation will always occur after resizing and extraction, if any.
```javascript
// Resize to 140 pixels wide, then add 10 transparent pixels
// to the top, left and right edges and 20 to the bottom edge
sharp(input)
.resize(140)
.background({r: 0, g: 0, b: 0, a: 0})
.extend({top: 10, bottom: 20, left: 10, right: 10})
...
```
#### negate() #### negate()
Produces the "negative" of the image. White => Black, Black => White, Blue => Yellow, etc. Produces the "negative" of the image. White => Black, Black => White, Blue => Yellow, etc.
@ -365,13 +413,18 @@ The output image will still be web-friendly sRGB and contain three (identical) c
Enhance output image contrast by stretching its luminance to cover the full dynamic range. This typically reduces performance by 30%. Enhance output image contrast by stretching its luminance to cover the full dynamic range. This typically reduces performance by 30%.
#### overlayWith(path) #### overlayWith(image, [options])
_Experimental_ Overlay (composite) a image containing an alpha channel over the processed (resized, extracted etc.) image.
Alpha composite image at `path` over the processed (resized, extracted) image. The dimensions of the two images must match. `image` is one of the following, and must be the same size or smaller than the processed image:
* `path` is a String containing the path to an image file with an alpha channel. * Buffer containing PNG, WebP, GIF or SVG image data, or
* String containing the path to an image file, with most major transparency formats supported.
`options`, if present, is an Object with the following optional attributes:
* `gravity` is a String or an attribute of the `sharp.gravity` Object e.g. `sharp.gravity.north` at which to place the overlay, defaulting to `center`/`centre`.
```javascript ```javascript
sharp('input.png') sharp('input.png')
@ -379,7 +432,7 @@ sharp('input.png')
.resize(300) .resize(300)
.flatten() .flatten()
.background('#ff6600') .background('#ff6600')
.overlayWith('overlay.png') .overlayWith('overlay.png', { gravity: sharp.gravity.southeast } )
.sharpen() .sharpen()
.withMetadata() .withMetadata()
.quality(90) .quality(90)
@ -387,8 +440,8 @@ sharp('input.png')
.toBuffer() .toBuffer()
.then(function(outputBuffer) { .then(function(outputBuffer) {
// outputBuffer contains upside down, 300px wide, alpha channel flattened // outputBuffer contains upside down, 300px wide, alpha channel flattened
// onto orange background, composited with overlay.png, sharpened, // onto orange background, composited with overlay.png with SE gravity,
// with metadata, 90% quality WebP image data. Phew! // sharpened, with metadata, 90% quality WebP image data. Phew!
}); });
``` ```
@ -471,17 +524,24 @@ This has no effect if the input image does not have an EXIF `Orientation` tag.
The default behaviour, when `withMetadata` is not used, is to strip all metadata and convert to the device-independent sRGB colour space. The default behaviour, when `withMetadata` is not used, is to strip all metadata and convert to the device-independent sRGB colour space.
#### tile([size], [overlap]) #### tile(options)
The size and overlap, in pixels, of square Deep Zoom image pyramid tiles. The size, overlap and directory layout to use when generating square Deep Zoom image pyramid tiles.
`options` is an Object with one or more of the following attributes:
* `size` is an integral Number between 1 and 8192. The default value is 256 pixels. * `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. * `overlap` is an integral Number between 0 and 8192. The default value is 0 pixels.
* `layout` is a String, with value `dz`, `zoomify` or `google`. The default value is `dz`.
```javascript ```javascript
sharp('input.tiff').tile(256).toFile('output.dzi', function(err, info) { sharp('input.tiff')
// The output.dzi file is the XML format Deep Zoom definition .tile({
// The output_files directory contains 256x256 pixel tiles grouped by zoom level size: 512
})
.toFile('output.dzi', function(err, info) {
// output.dzi is the Deep Zoom XML definition
// output_files contains 512x512 tiles grouped by zoom level
}); });
``` ```

View File

@ -1,5 +1,31 @@
# Changelog # Changelog
### v0.14 - "*needle*"
* Add ability to extend (pad) the edges of an image.
[#128](https://github.com/lovell/sharp/issues/128)
[@blowsie](https://github.com/blowsie)
* Add support for Zoomify and Google tile layouts. Breaks existing tile API.
[#223](https://github.com/lovell/sharp/issues/223)
[@bdunnette](https://github.com/bdunnette)
* Improvements to overlayWith: differing sizes/formats, gravity, buffer input.
[#239](https://github.com/lovell/sharp/issues/239)
[@chrisriley](https://github.com/chrisriley)
* Add entropy-based crop strategy to remove least interesting edges.
[#295](https://github.com/lovell/sharp/issues/295)
[@rightaway](https://github.com/rightaway)
* Expose density metadata; set density of images from vector input.
[#338](https://github.com/lovell/sharp/issues/338)
[@lookfirst](https://github.com/lookfirst)
* Emit post-processing 'info' event for Stream output.
[#367](https://github.com/lovell/sharp/issues/367)
[@salzhrani](https://github.com/salzhrani)
### v0.13 - "*mind*" ### v0.13 - "*mind*"
#### v0.13.1 - 27<sup>th</sup> February 2016 #### v0.13.1 - 27<sup>th</sup> February 2016

182
index.js
View File

@ -46,7 +46,7 @@ var Sharp = function(input, options) {
streamIn: false, streamIn: false,
sequentialRead: false, sequentialRead: false,
limitInputPixels: maximum.pixels, limitInputPixels: maximum.pixels,
density: '72', density: 72,
rawWidth: 0, rawWidth: 0,
rawHeight: 0, rawHeight: 0,
rawChannels: 0, rawChannels: 0,
@ -64,11 +64,15 @@ var Sharp = function(input, options) {
width: -1, width: -1,
height: -1, height: -1,
canvas: 'crop', canvas: 'crop',
gravity: 0, crop: 0,
angle: 0, angle: 0,
rotateBeforePreExtract: false, rotateBeforePreExtract: false,
flip: false, flip: false,
flop: false, flop: false,
extendTop: 0,
extendBottom: 0,
extendLeft: 0,
extendRight: 0,
withoutEnlargement: false, withoutEnlargement: false,
interpolator: 'bicubic', interpolator: 'bicubic',
// operations // operations
@ -84,7 +88,9 @@ var Sharp = function(input, options) {
greyscale: false, greyscale: false,
normalize: 0, normalize: 0,
// overlay // overlay
overlayPath: '', overlayFileIn: '',
overlayBufferIn: null,
overlayGravity: 0,
// output options // output options
formatOut: 'input', formatOut: 'input',
fileOut: '', fileOut: '',
@ -106,13 +112,13 @@ var Sharp = function(input, options) {
module.exports.queue.emit('change', queueLength); module.exports.queue.emit('change', queueLength);
} }
}; };
if (typeof input === 'string') { if (isString(input)) {
// input=file // input=file
this.options.fileIn = input; this.options.fileIn = input;
} else if (typeof input === 'object' && input instanceof Buffer) { } else if (isBuffer(input)) {
// input=buffer // input=buffer
this.options.bufferIn = input; this.options.bufferIn = input;
} else if (typeof input === 'undefined' || input === null) { } else if (!isDefined(input)) {
// input=stream // input=stream
this.options.streamIn = true; this.options.streamIn = true;
} else { } else {
@ -148,12 +154,21 @@ var isDefined = function(val) {
var isObject = function(val) { var isObject = function(val) {
return typeof val === 'object'; return typeof val === 'object';
}; };
var isBuffer = function(val) {
return typeof val === 'object' && val instanceof Buffer;
};
var isString = function(val) {
return typeof val === 'string' && val.length > 0;
};
var isInteger = function(val) { var isInteger = function(val) {
return typeof val === 'number' && !Number.isNaN(val) && val % 1 === 0; return typeof val === 'number' && !Number.isNaN(val) && val % 1 === 0;
}; };
var inRange = function(val, min, max) { var inRange = function(val, min, max) {
return val >= min && val <= max; return val >= min && val <= max;
}; };
var contains = function(val, list) {
return list.indexOf(val) !== -1;
};
/* /*
Set input-related options Set input-related options
@ -164,7 +179,7 @@ Sharp.prototype._inputOptions = function(options) {
// Density // Density
if (isDefined(options.density)) { if (isDefined(options.density)) {
if (isInteger(options.density) && inRange(options.density, 1, 2400)) { if (isInteger(options.density) && inRange(options.density, 1, 2400)) {
this.options.density = options.density.toString(); this.options.density = options.density;
} else { } else {
throw new Error('Invalid density (1 to 2400) ' + options.density); throw new Error('Invalid density (1 to 2400) ' + options.density);
} }
@ -216,48 +231,53 @@ Sharp.prototype._write = function(chunk, encoding, callback) {
} }
}; };
// Crop this part of the resized image (Center/Centre, North, East, South, West) // Weighting to apply to image crop
module.exports.gravity = { module.exports.gravity = {
'center': 0, center: 0,
'centre': 0, centre: 0,
'north': 1, north: 1,
'east': 2, east: 2,
'south': 3, south: 3,
'west': 4, west: 4,
'northeast': 5, northeast: 5,
'southeast': 6, southeast: 6,
'southwest': 7, southwest: 7,
'northwest': 8 northwest: 8
}; };
Sharp.prototype.crop = function(gravity) { // Strategies for automagic behaviour
module.exports.strategy = {
entropy: 16
};
/*
What part of the image should be retained when cropping?
*/
Sharp.prototype.crop = function(crop) {
this.options.canvas = 'crop'; this.options.canvas = 'crop';
if (typeof gravity === 'undefined') { if (!isDefined(crop)) {
this.options.gravity = module.exports.gravity.center; // Default
} else if (typeof gravity === 'number' && !Number.isNaN(gravity) && gravity >= 0 && gravity <= 8) { this.options.crop = module.exports.gravity.center;
this.options.gravity = gravity; } else if (isInteger(crop) && inRange(crop, 0, 8)) {
} else if (typeof gravity === 'string' && typeof module.exports.gravity[gravity] === 'number') { // Gravity (numeric)
this.options.gravity = module.exports.gravity[gravity]; this.options.crop = crop;
} else if (isString(crop) && isInteger(module.exports.gravity[crop])) {
// Gravity (string)
this.options.crop = module.exports.gravity[crop];
} else if (isInteger(crop) && crop === module.exports.strategy.entropy) {
// Strategy
this.options.crop = crop;
} else { } else {
throw new Error('Unsupported crop gravity ' + gravity); throw new Error('Unsupported crop ' + crop);
} }
return this; return this;
}; };
Sharp.prototype.extract = function(options) { Sharp.prototype.extract = function(options) {
if (!options || typeof options !== 'object') {
// Legacy extract(top,left,width,height) syntax
options = {
left: arguments[1],
top: arguments[0],
width: arguments[2],
height: arguments[3]
};
}
var suffix = this.options.width === -1 && this.options.height === -1 ? 'Pre' : 'Post'; var suffix = this.options.width === -1 && this.options.height === -1 ? 'Pre' : 'Post';
['left', 'top', 'width', 'height'].forEach(function (name) { ['left', 'top', 'width', 'height'].forEach(function (name) {
var value = options[name]; var value = options[name];
if (typeof value === 'number' && !Number.isNaN(value) && value % 1 === 0 && value >= 0) { if (isInteger(value) && value >= 0) {
this.options[name + (name === 'left' || name === 'top' ? 'Offset' : '') + suffix] = value; this.options[name + (name === 'left' || name === 'top' ? 'Offset' : '') + suffix] = value;
} else { } else {
throw new Error('Non-integer value for ' + name + ' of ' + value); throw new Error('Non-integer value for ' + name + ' of ' + value);
@ -316,14 +336,26 @@ Sharp.prototype.negate = function(negate) {
return this; return this;
}; };
Sharp.prototype.overlayWith = function(overlayPath) { /*
if (typeof overlayPath !== 'string') { Overlay with another image, using an optional gravity
throw new Error('The overlay path must be a string'); */
Sharp.prototype.overlayWith = function(overlay, options) {
if (isString(overlay)) {
this.options.overlayFileIn = overlay;
} else if (isBuffer(overlay)) {
this.options.overlayBufferIn = overlay;
} else {
throw new Error('Unsupported overlay ' + typeof overlay);
}
if (isObject(options)) {
if (isInteger(options.gravity) && inRange(options.gravity, 0, 8)) {
this.options.overlayGravity = options.gravity;
} else if (isString(options.gravity) && isInteger(module.exports.gravity[options.gravity])) {
this.options.overlayGravity = module.exports.gravity[options.gravity];
} else if (isDefined(options.gravity)) {
throw new Error('Unsupported overlay gravity ' + options.gravity);
} }
if (overlayPath === '') {
throw new Error('The overlay path cannot be empty');
} }
this.options.overlayPath = overlayPath;
return this; return this;
}; };
@ -605,28 +637,64 @@ Sharp.prototype.withMetadata = function(withMetadata) {
}; };
/* /*
Tile size and overlap for Deep Zoom output Tile-based deep zoom output options: size, overlap, layout
*/ */
Sharp.prototype.tile = function(size, overlap) { Sharp.prototype.tile = function(tile) {
if (isObject(tile)) {
// Size of square tiles, in pixels // Size of square tiles, in pixels
if (typeof size !== 'undefined' && size !== null) { if (isDefined(tile.size)) {
if (!Number.isNaN(size) && size % 1 === 0 && size >= 1 && size <= 8192) { if (isInteger(tile.size) && inRange(tile.size, 1, 8192)) {
this.options.tileSize = size; this.options.tileSize = tile.size;
} else { } else {
throw new Error('Invalid tile size (1 to 8192) ' + size); throw new Error('Invalid tile size (1 to 8192) ' + tile.size);
} }
} }
// Overlap of tiles, in pixels // Overlap of tiles, in pixels
if (typeof overlap !== 'undefined' && overlap !== null) { if (isDefined(tile.overlap)) {
if (!Number.isNaN(overlap) && overlap % 1 === 0 && overlap >= 0 && overlap <= 8192) { if (isInteger(tile.overlap) && inRange(tile.overlap, 0, 8192)) {
if (overlap > this.options.tileSize) { if (tile.overlap > this.options.tileSize) {
throw new Error('Tile overlap ' + overlap + ' cannot be larger than tile size ' + this.options.tileSize); throw new Error('Tile overlap ' + tile.overlap + ' cannot be larger than tile size ' + this.options.tileSize);
} }
this.options.tileOverlap = overlap; this.options.tileOverlap = tile.overlap;
} else { } else {
throw new Error('Invalid tile overlap (0 to 8192) ' + overlap); throw new Error('Invalid tile overlap (0 to 8192) ' + tile.overlap);
} }
} }
// Layout
if (isDefined(tile.layout)) {
if (isString(tile.layout) && contains(tile.layout, ['dz', 'google', 'zoomify'])) {
this.options.tileLayout = tile.layout;
} else {
throw new Error('Invalid tile layout ' + tile.layout);
}
}
}
return this;
};
/*
Extend edges
*/
Sharp.prototype.extend = function(extend) {
if (isInteger(extend) && extend > 0) {
this.options.extendTop = extend;
this.options.extendBottom = extend;
this.options.extendLeft = extend;
this.options.extendRight = extend;
} else if (
isObject(extend) &&
isInteger(extend.top) && extend.top >= 0 &&
isInteger(extend.bottom) && extend.bottom >= 0 &&
isInteger(extend.left) && extend.left >= 0 &&
isInteger(extend.right) && extend.right >= 0
) {
this.options.extendTop = extend.top;
this.options.extendBottom = extend.bottom;
this.options.extendLeft = extend.left;
this.options.extendRight = extend.right;
} else {
throw new Error('Invalid edge extension ' + extend);
}
return this; return this;
}; };
@ -783,10 +851,11 @@ Sharp.prototype._pipeline = function(callback) {
if (this.options.streamIn) { if (this.options.streamIn) {
// output=stream, input=stream // output=stream, input=stream
this.on('finish', function() { this.on('finish', function() {
sharp.pipeline(that.options, function(err, data) { sharp.pipeline(that.options, function(err, data, info) {
if (err) { if (err) {
that.emit('error', err); that.emit('error', err);
} else { } else {
that.emit('info', info);
that.push(data); that.push(data);
} }
that.push(null); that.push(null);
@ -794,10 +863,11 @@ Sharp.prototype._pipeline = function(callback) {
}); });
} else { } else {
// output=stream, input=file/buffer // output=stream, input=file/buffer
sharp.pipeline(this.options, function(err, data) { sharp.pipeline(this.options, function(err, data, info) {
if (err) { if (err) {
that.emit('error', err); that.emit('error', err);
} else { } else {
that.emit('info', info);
that.push(data); that.push(data);
} }
that.push(null); that.push(null);

View File

@ -1,6 +1,6 @@
{ {
"name": "sharp", "name": "sharp",
"version": "0.13.1", "version": "0.14.0",
"author": "Lovell Fuller <npm@lovell.info>", "author": "Lovell Fuller <npm@lovell.info>",
"contributors": [ "contributors": [
"Pierre Inglebert <pierre.inglebert@gmail.com>", "Pierre Inglebert <pierre.inglebert@gmail.com>",
@ -47,7 +47,7 @@
"vips" "vips"
], ],
"dependencies": { "dependencies": {
"bluebird": "^3.3.3", "bluebird": "^3.3.4",
"color": "^0.11.1", "color": "^0.11.1",
"nan": "^2.2.0", "nan": "^2.2.0",
"semver": "^5.1.0", "semver": "^5.1.0",
@ -68,7 +68,7 @@
}, },
"license": "Apache-2.0", "license": "Apache-2.0",
"config": { "config": {
"libvips": "8.2.2" "libvips": "8.2.3"
}, },
"engines": { "engines": {
"node": ">=0.10" "node": ">=0.10"

View File

@ -19,7 +19,7 @@ export CXXFLAGS="-O3"
# Dependency version numbers # Dependency version numbers
VERSION_ZLIB=1.2.8 VERSION_ZLIB=1.2.8
VERSION_FFI=3.2.1 VERSION_FFI=3.2.1
VERSION_GLIB=2.47.5 VERSION_GLIB=2.47.6
VERSION_XML2=2.9.3 VERSION_XML2=2.9.3
VERSION_GSF=1.14.34 VERSION_GSF=1.14.34
VERSION_EXIF=0.6.21 VERSION_EXIF=0.6.21
@ -29,8 +29,8 @@ VERSION_JPEG=1.4.2
VERSION_PNG16=1.6.21 VERSION_PNG16=1.6.21
VERSION_WEBP=0.5.0 VERSION_WEBP=0.5.0
VERSION_TIFF=4.0.6 VERSION_TIFF=4.0.6
VERSION_ORC=0.4.24 VERSION_ORC=0.4.25
VERSION_VIPS=8.2.2 VERSION_VIPS=8.2.3
mkdir ${DEPS}/zlib mkdir ${DEPS}/zlib
curl -Ls http://zlib.net/zlib-${VERSION_ZLIB}.tar.xz | tar xJC ${DEPS}/zlib --strip-components=1 curl -Ls http://zlib.net/zlib-${VERSION_ZLIB}.tar.xz | tar xJC ${DEPS}/zlib --strip-components=1

View File

@ -1,5 +1,7 @@
#!/bin/sh #!/bin/sh
VERSION_VIPS=8.2.3
# Is docker available? # Is docker available?
if ! type docker >/dev/null; then if ! type docker >/dev/null; then
@ -13,15 +15,15 @@ fi
docker build -t vips-dev-win win docker build -t vips-dev-win win
WIN_CONTAINER_ID=$(docker run -d vips-dev-win) WIN_CONTAINER_ID=$(docker run -d vips-dev-win)
docker cp $WIN_CONTAINER_ID:/libvips-8.2.2-win.tar.gz . docker cp "${WIN_CONTAINER_ID}:/libvips-${VERSION_VIPS}-win.tar.gz" .
docker rm $WIN_CONTAINER_ID docker rm "${WIN_CONTAINER_ID}"
# Linux # Linux
docker build -t vips-dev-lin lin docker build -t vips-dev-lin lin
LIN_CONTAINER_ID=$(docker run -d vips-dev-lin) LIN_CONTAINER_ID=$(docker run -d vips-dev-lin)
docker cp $LIN_CONTAINER_ID:/libvips-8.2.2-lin.tar.gz . docker cp "${LIN_CONTAINER_ID}:/libvips-${VERSION_VIPS}-lin.tar.gz" .
docker rm $LIN_CONTAINER_ID docker rm "${LIN_CONTAINER_ID}"
# Checksums # Checksums

View File

@ -20,7 +20,7 @@ ENV PKG_CONFIG_PATH=${PKG_CONFIG_PATH}:${TARGET}/lib/pkgconfig \
# Dependency version numbers # Dependency version numbers
ENV VERSION_ZLIB=1.2.8 \ ENV VERSION_ZLIB=1.2.8 \
VERSION_FFI=3.2.1 \ VERSION_FFI=3.2.1 \
VERSION_GLIB=2.47.5 \ VERSION_GLIB=2.47.6 \
VERSION_XML2=2.9.3 \ VERSION_XML2=2.9.3 \
VERSION_GSF=1.14.34 \ VERSION_GSF=1.14.34 \
VERSION_EXIF=0.6.21 \ VERSION_EXIF=0.6.21 \
@ -30,8 +30,8 @@ ENV VERSION_ZLIB=1.2.8 \
VERSION_PNG16=1.6.21 \ VERSION_PNG16=1.6.21 \
VERSION_WEBP=0.5.0 \ VERSION_WEBP=0.5.0 \
VERSION_TIFF=4.0.6 \ VERSION_TIFF=4.0.6 \
VERSION_ORC=0.4.24 \ VERSION_ORC=0.4.25 \
VERSION_VIPS=8.2.2 VERSION_VIPS=8.2.3
RUN mkdir ${DEPS}/zlib RUN mkdir ${DEPS}/zlib
RUN curl -Ls http://zlib.net/zlib-${VERSION_ZLIB}.tar.xz | tar xJC ${DEPS}/zlib --strip-components=1 RUN curl -Ls http://zlib.net/zlib-${VERSION_ZLIB}.tar.xz | tar xJC ${DEPS}/zlib --strip-components=1

View File

@ -3,11 +3,13 @@ MAINTAINER Lovell Fuller <npm@lovell.info>
RUN apt-get update && apt-get install -y curl zip RUN apt-get update && apt-get install -y curl zip
ENV VERSION_VIPS=8.2.3
# Fetch and unzip # Fetch and unzip
RUN mkdir /vips RUN mkdir /vips
WORKDIR /vips WORKDIR /vips
RUN curl -O http://www.vips.ecs.soton.ac.uk/supported/8.2/win32/vips-dev-w64-8.2.2.zip RUN curl -O http://www.vips.ecs.soton.ac.uk/supported/8.2/win32/vips-dev-w64-8.2.zip
RUN unzip vips-dev-w64-8.2.2.zip RUN unzip vips-dev-w64-8.2.zip
# Clean and zip # Clean and zip
WORKDIR /vips/vips-dev-8.2 WORKDIR /vips/vips-dev-8.2
@ -15,4 +17,4 @@ RUN rm bin/libvipsCC-42.dll bin/libvips-cpp-42.dll bin/libgsf-win32-1-114.dll bi
RUN cp bin/*.dll lib/ RUN cp bin/*.dll lib/
RUN cp -r lib64/* lib/ RUN cp -r lib64/* lib/
RUN GZIP=-9 tar czf /libvips-8.2.2-win.tar.gz include lib/glib-2.0 lib/libvips.lib lib/libglib-2.0.lib lib/libgobject-2.0.lib lib/*.dll RUN GZIP=-9 tar czf /libvips-${VERSION_VIPS}-win.tar.gz include lib/glib-2.0 lib/libvips.lib lib/libglib-2.0.lib lib/libgobject-2.0.lib lib/*.dll

View File

@ -176,6 +176,30 @@ namespace sharp {
SetExifOrientation(image, 0); SetExifOrientation(image, 0);
} }
/*
Does this image have a non-default density?
*/
bool HasDensity(VImage image) {
return image.xres() > 1.0;
}
/*
Get pixels/mm resolution as pixels/inch density.
*/
int GetDensity(VImage image) {
return static_cast<int>(round(image.xres() * 25.4));
}
/*
Set pixels/mm resolution based on a pixels/inch density.
*/
void SetDensity(VImage image, const int density) {
const double pixelsPerMm = static_cast<double>(density) / 25.4;
image.set("Xres", pixelsPerMm);
image.set("Yres", pixelsPerMm);
image.set(VIPS_META_RESOLUTION_UNIT, "in");
}
/* /*
Called when a Buffer undergoes GC, required to support mixed runtime libraries in Windows Called when a Buffer undergoes GC, required to support mixed runtime libraries in Windows
*/ */
@ -185,4 +209,45 @@ namespace sharp {
} }
} }
/*
Calculate the (left, top) coordinates of the output image
within the input image, applying the given gravity.
*/
std::tuple<int, int> CalculateCrop(int const inWidth, int const inHeight,
int const outWidth, int const outHeight, int const gravity) {
int left = 0;
int top = 0;
switch (gravity) {
case 1: // North
left = (inWidth - outWidth + 1) / 2;
break;
case 2: // East
left = inWidth - outWidth;
top = (inHeight - outHeight + 1) / 2;
break;
case 3: // South
left = (inWidth - outWidth + 1) / 2;
top = inHeight - outHeight;
break;
case 4: // West
top = (inHeight - outHeight + 1) / 2;
break;
case 5: // Northeast
left = inWidth - outWidth;
break;
case 6: // Southeast
left = inWidth - outWidth;
top = inHeight - outHeight;
case 7: // Southwest
top = inHeight - outHeight;
case 8: // Northwest
break;
default: // Centre
left = (inWidth - outWidth + 1) / 2;
top = (inHeight - outHeight + 1) / 2;
}
return std::make_tuple(left, top);
}
} // namespace sharp } // namespace sharp

View File

@ -2,6 +2,8 @@
#define SRC_COMMON_H_ #define SRC_COMMON_H_
#include <string> #include <string>
#include <tuple>
#include <vips/vips8> #include <vips/vips8>
using vips::VImage; using vips::VImage;
@ -75,11 +77,33 @@ namespace sharp {
*/ */
void RemoveExifOrientation(VImage image); void RemoveExifOrientation(VImage image);
/*
Does this image have a non-default density?
*/
bool HasDensity(VImage image);
/*
Get pixels/mm resolution as pixels/inch density.
*/
int GetDensity(VImage image);
/*
Set pixels/mm resolution based on a pixels/inch density.
*/
void SetDensity(VImage image, const int density);
/* /*
Called when a Buffer undergoes GC, required to support mixed runtime libraries in Windows Called when a Buffer undergoes GC, required to support mixed runtime libraries in Windows
*/ */
void FreeCallback(char* data, void* hint); void FreeCallback(char* data, void* hint);
/*
Calculate the (left, top) coordinates of the output image
within the input image, applying the given gravity.
*/
std::tuple<int, int> CalculateCrop(int const inWidth, int const inHeight,
int const outWidth, int const outHeight, int const gravity);
} // namespace sharp } // namespace sharp
#endif // SRC_COMMON_H_ #endif // SRC_COMMON_H_

View File

@ -38,6 +38,8 @@ using sharp::DetermineImageType;
using sharp::HasProfile; using sharp::HasProfile;
using sharp::HasAlpha; using sharp::HasAlpha;
using sharp::ExifOrientation; using sharp::ExifOrientation;
using sharp::HasDensity;
using sharp::GetDensity;
using sharp::FreeCallback; using sharp::FreeCallback;
using sharp::counterQueue; using sharp::counterQueue;
@ -52,6 +54,7 @@ struct MetadataBaton {
int height; int height;
std::string space; std::string space;
int channels; int channels;
int density;
bool hasProfile; bool hasProfile;
bool hasAlpha; bool hasAlpha;
int orientation; int orientation;
@ -63,6 +66,7 @@ struct MetadataBaton {
MetadataBaton(): MetadataBaton():
bufferInLength(0), bufferInLength(0),
density(0),
orientation(0), orientation(0),
exifLength(0), exifLength(0),
iccLength(0) {} iccLength(0) {}
@ -120,6 +124,9 @@ class MetadataWorker : public AsyncWorker {
baton->height = image.height(); baton->height = image.height();
baton->space = vips_enum_nick(VIPS_TYPE_INTERPRETATION, image.interpretation()); baton->space = vips_enum_nick(VIPS_TYPE_INTERPRETATION, image.interpretation());
baton->channels = image.bands(); baton->channels = image.bands();
if (HasDensity(image)) {
baton->density = GetDensity(image);
}
baton->hasProfile = HasProfile(image); baton->hasProfile = HasProfile(image);
// Derived attributes // Derived attributes
baton->hasAlpha = HasAlpha(image); baton->hasAlpha = HasAlpha(image);
@ -161,6 +168,9 @@ class MetadataWorker : public AsyncWorker {
Set(info, New("height").ToLocalChecked(), New<Number>(baton->height)); Set(info, New("height").ToLocalChecked(), New<Number>(baton->height));
Set(info, New("space").ToLocalChecked(), New<String>(baton->space).ToLocalChecked()); Set(info, New("space").ToLocalChecked(), New<String>(baton->space).ToLocalChecked());
Set(info, New("channels").ToLocalChecked(), New<Number>(baton->channels)); Set(info, New("channels").ToLocalChecked(), New<Number>(baton->channels));
if (baton->density > 0) {
Set(info, New("density").ToLocalChecked(), New<Number>(baton->density));
}
Set(info, New("hasProfile").ToLocalChecked(), New<Boolean>(baton->hasProfile)); Set(info, New("hasProfile").ToLocalChecked(), New<Boolean>(baton->hasProfile));
Set(info, New("hasAlpha").ToLocalChecked(), New<Boolean>(baton->hasAlpha)); Set(info, New("hasAlpha").ToLocalChecked(), New<Boolean>(baton->hasAlpha));
if (baton->orientation > 0) { if (baton->orientation > 0) {

View File

@ -1,37 +1,54 @@
#include <algorithm>
#include <tuple>
#include <vips/vips8> #include <vips/vips8>
#include "common.h" #include "common.h"
#include "operations.h" #include "operations.h"
using vips::VImage; using vips::VImage;
using vips::VError;
namespace sharp { namespace sharp {
/* /*
Alpha composite src over dst Alpha composite src over dst with given gravity.
Assumes alpha channels are already premultiplied and will be unpremultiplied after Assumes alpha channels are already premultiplied and will be unpremultiplied after.
*/ */
VImage Composite(VImage src, VImage dst) { VImage Composite(VImage src, VImage dst, const int gravity) {
using sharp::CalculateCrop;
using sharp::HasAlpha; using sharp::HasAlpha;
// Split src into non-alpha and alpha if (!HasAlpha(src)) {
throw VError("Overlay image must have an alpha channel");
}
if (!HasAlpha(dst)) {
throw VError("Image to be overlaid must have an alpha channel");
}
if (src.width() > dst.width() || src.height() > dst.height()) {
throw VError("Overlay image must have same dimensions or smaller");
}
// Enlarge overlay src, if required
if (src.width() < dst.width() || src.height() < dst.height()) {
// Calculate the (left, top) coordinates of the output image within the input image, applying the given gravity.
int left;
int top;
std::tie(left, top) = CalculateCrop(dst.width(), dst.height(), src.width(), src.height(), gravity);
// Embed onto transparent background
std::vector<double> background { 0.0, 0.0, 0.0, 0.0 };
src = src.embed(left, top, dst.width(), dst.height(), VImage::option()
->set("extend", VIPS_EXTEND_BACKGROUND)
->set("background", background)
);
}
// Split src into non-alpha and alpha channels
VImage srcWithoutAlpha = src.extract_band(0, VImage::option()->set("n", src.bands() - 1)); VImage srcWithoutAlpha = src.extract_band(0, VImage::option()->set("n", src.bands() - 1));
VImage srcAlpha = src[src.bands() - 1] * (1.0 / 255.0); VImage srcAlpha = src[src.bands() - 1] * (1.0 / 255.0);
// Split dst into non-alpha and alpha channels // Split dst into non-alpha and alpha channels
VImage dstWithoutAlpha; VImage dstWithoutAlpha = dst.extract_band(0, VImage::option()->set("n", dst.bands() - 1));
VImage dstAlpha; VImage dstAlpha = dst[dst.bands() - 1] * (1.0 / 255.0);
if (HasAlpha(dst)) {
// Non-alpha: extract all-but-last channel
dstWithoutAlpha = dst.extract_band(0, VImage::option()->set("n", dst.bands() - 1));
// Alpha: Extract last channel
dstAlpha = dst[dst.bands() - 1] * (1.0 / 255.0);
} else {
// Non-alpha: Copy reference
dstWithoutAlpha = dst;
// Alpha: Use blank, opaque (0xFF) image
dstAlpha = VImage::black(dst.width(), dst.height()).invert();
}
// //
// Compute normalized output alpha channel: // Compute normalized output alpha channel:
@ -154,4 +171,83 @@ namespace sharp {
); );
} }
} }
/*
Calculate crop area based on image entropy
*/
std::tuple<int, int> EntropyCrop(VImage image, int const outWidth, int const outHeight) {
int left = 0;
int top = 0;
int const inWidth = image.width();
int const inHeight = image.height();
if (inWidth > outWidth) {
// Reduce width by repeated removing slices from edge with lowest entropy
int width = inWidth;
double leftEntropy = 0.0;
double rightEntropy = 0.0;
// Max width of each slice
int const maxSliceWidth = static_cast<int>(ceil((inWidth - outWidth) / 8.0));
while (width > outWidth) {
// Width of current slice
int const slice = std::min(width - outWidth, maxSliceWidth);
if (leftEntropy == 0.0) {
// Update entropy of left slice
leftEntropy = Entropy(image.extract_area(left, 0, slice, inHeight));
}
if (rightEntropy == 0.0) {
// Update entropy of right slice
rightEntropy = Entropy(image.extract_area(width - slice - 1, 0, slice, inHeight));
}
// Keep slice with highest entropy
if (leftEntropy >= rightEntropy) {
// Discard right slice
rightEntropy = 0.0;
} else {
// Discard left slice
leftEntropy = 0.0;
left = left + slice;
}
width = width - slice;
}
}
if (inHeight > outHeight) {
// Reduce height by repeated removing slices from edge with lowest entropy
int height = inHeight;
double topEntropy = 0.0;
double bottomEntropy = 0.0;
// Max height of each slice
int const maxSliceHeight = static_cast<int>(ceil((inHeight - outHeight) / 8.0));
while (height > outHeight) {
// Height of current slice
int const slice = std::min(height - outHeight, maxSliceHeight);
if (topEntropy == 0.0) {
// Update entropy of top slice
topEntropy = Entropy(image.extract_area(0, top, inWidth, slice));
}
if (bottomEntropy == 0.0) {
// Update entropy of bottom slice
bottomEntropy = Entropy(image.extract_area(0, height - slice - 1, inWidth, slice));
}
// Keep slice with highest entropy
if (topEntropy >= bottomEntropy) {
// Discard bottom slice
bottomEntropy = 0.0;
} else {
// Discard top slice
topEntropy = 0.0;
top = top + slice;
}
height = height - slice;
}
}
return std::make_tuple(left, top);
}
/*
Calculate the Shannon entropy for an image
*/
double Entropy(VImage image) {
return image.hist_find().hist_entropy();
}
} // namespace sharp } // namespace sharp

View File

@ -1,6 +1,7 @@
#ifndef SRC_OPERATIONS_H_ #ifndef SRC_OPERATIONS_H_
#define SRC_OPERATIONS_H_ #define SRC_OPERATIONS_H_
#include <tuple>
#include <vips/vips8> #include <vips/vips8>
using vips::VImage; using vips::VImage;
@ -8,10 +9,10 @@ using vips::VImage;
namespace sharp { namespace sharp {
/* /*
Composite images `src` and `dst` with premultiplied alpha channel and output Alpha composite src over dst with given gravity.
image with premultiplied alpha. Assumes alpha channels are already premultiplied and will be unpremultiplied after.
*/ */
VImage Composite(VImage src, VImage dst); VImage Composite(VImage src, VImage dst, const int gravity);
/* /*
* Stretch luminance to cover full dynamic range. * Stretch luminance to cover full dynamic range.
@ -32,6 +33,17 @@ namespace sharp {
* Sharpen flat and jagged areas. Use radius of -1 for fast sharpen. * Sharpen flat and jagged areas. Use radius of -1 for fast sharpen.
*/ */
VImage Sharpen(VImage image, int const radius, double const flat, double const jagged); VImage Sharpen(VImage image, int const radius, double const flat, double const jagged);
/*
Calculate crop area based on image entropy
*/
std::tuple<int, int> EntropyCrop(VImage image, int const outWidth, int const outHeight);
/*
Calculate the Shannon entropy for an image
*/
double Entropy(VImage image);
} // namespace sharp } // namespace sharp
#endif // SRC_OPERATIONS_H_ #endif // SRC_OPERATIONS_H_

View File

@ -1,10 +1,12 @@
#include <tuple>
#include <algorithm> #include <algorithm>
#include <utility>
#include <cmath> #include <cmath>
#include <tuple>
#include <utility>
#include <vips/vips8>
#include <node.h> #include <node.h>
#include <node_buffer.h> #include <node_buffer.h>
#include <vips/vips8>
#include "nan.h" #include "nan.h"
@ -47,6 +49,7 @@ using sharp::Normalize;
using sharp::Gamma; using sharp::Gamma;
using sharp::Blur; using sharp::Blur;
using sharp::Sharpen; using sharp::Sharpen;
using sharp::EntropyCrop;
using sharp::ImageType; using sharp::ImageType;
using sharp::ImageTypeId; using sharp::ImageTypeId;
@ -56,139 +59,29 @@ using sharp::HasAlpha;
using sharp::ExifOrientation; using sharp::ExifOrientation;
using sharp::SetExifOrientation; using sharp::SetExifOrientation;
using sharp::RemoveExifOrientation; using sharp::RemoveExifOrientation;
using sharp::SetDensity;
using sharp::IsJpeg; 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::IsDz;
using sharp::FreeCallback; using sharp::FreeCallback;
using sharp::CalculateCrop;
using sharp::counterProcess; using sharp::counterProcess;
using sharp::counterQueue; using sharp::counterQueue;
enum class Canvas {
CROP,
EMBED,
MAX,
MIN,
IGNORE_ASPECT
};
struct PipelineBaton {
std::string fileIn;
char *bufferIn;
size_t bufferInLength;
std::string iccProfilePath;
int limitInputPixels;
std::string density;
int rawWidth;
int rawHeight;
int rawChannels;
std::string formatOut;
std::string fileOut;
void *bufferOut;
size_t bufferOutLength;
int topOffsetPre;
int leftOffsetPre;
int widthPre;
int heightPre;
int topOffsetPost;
int leftOffsetPost;
int widthPost;
int heightPost;
int width;
int height;
int channels;
Canvas canvas;
int gravity;
std::string interpolator;
double background[4];
bool flatten;
bool negate;
double blurSigma;
int sharpenRadius;
double sharpenFlat;
double sharpenJagged;
int threshold;
std::string overlayPath;
double gamma;
bool greyscale;
bool normalize;
int angle;
bool rotateBeforePreExtract;
bool flip;
bool flop;
bool progressive;
bool withoutEnlargement;
VipsAccess accessMethod;
int quality;
int compressionLevel;
bool withoutAdaptiveFiltering;
bool withoutChromaSubsampling;
bool trellisQuantisation;
bool overshootDeringing;
bool optimiseScans;
std::string err;
bool withMetadata;
int withMetadataOrientation;
int tileSize;
int tileOverlap;
PipelineBaton():
bufferInLength(0),
limitInputPixels(0),
density(""),
rawWidth(0),
rawHeight(0),
rawChannels(0),
formatOut(""),
fileOut(""),
bufferOutLength(0),
topOffsetPre(-1),
topOffsetPost(-1),
channels(0),
canvas(Canvas::CROP),
gravity(0),
flatten(false),
negate(false),
blurSigma(0.0),
sharpenRadius(0),
sharpenFlat(1.0),
sharpenJagged(2.0),
threshold(0),
gamma(0.0),
greyscale(false),
normalize(false),
angle(0),
flip(false),
flop(false),
progressive(false),
withoutEnlargement(false),
quality(80),
compressionLevel(6),
withoutAdaptiveFiltering(false),
withoutChromaSubsampling(false),
trellisQuantisation(false),
overshootDeringing(false),
optimiseScans(false),
withMetadata(false),
withMetadataOrientation(-1),
tileSize(256),
tileOverlap(0) {
background[0] = 0.0;
background[1] = 0.0;
background[2] = 0.0;
background[3] = 255.0;
}
};
class PipelineWorker : public AsyncWorker { class PipelineWorker : public AsyncWorker {
public: public:
PipelineWorker(Callback *callback, PipelineBaton *baton, Callback *queueListener, const Local<Object> &bufferIn) : PipelineWorker(Callback *callback, PipelineBaton *baton, Callback *queueListener,
const Local<Object> &bufferIn, const Local<Object> &overlayBufferIn) :
AsyncWorker(callback), baton(baton), queueListener(queueListener) { AsyncWorker(callback), baton(baton), queueListener(queueListener) {
if (baton->bufferInLength > 0) { if (baton->bufferInLength > 0) {
SaveToPersistent("bufferIn", bufferIn); SaveToPersistent("bufferIn", bufferIn);
} }
if (baton->overlayBufferInLength > 0) {
SaveToPersistent("overlayBufferIn", overlayBufferIn);
}
} }
~PipelineWorker() {} ~PipelineWorker() {}
@ -227,9 +120,12 @@ class PipelineWorker : public AsyncWorker {
try { try {
VOption *option = VImage::option()->set("access", baton->accessMethod); VOption *option = VImage::option()->set("access", baton->accessMethod);
if (inputImageType == ImageType::MAGICK) { if (inputImageType == ImageType::MAGICK) {
option->set("density", baton->density.data()); option->set("density", std::to_string(baton->density).data());
} }
image = VImage::new_from_buffer(baton->bufferIn, baton->bufferInLength, nullptr, option); image = VImage::new_from_buffer(baton->bufferIn, baton->bufferInLength, nullptr, option);
if (inputImageType == ImageType::MAGICK) {
SetDensity(image, baton->density);
}
} catch (...) { } catch (...) {
(baton->err).append("Input buffer has corrupt header"); (baton->err).append("Input buffer has corrupt header");
inputImageType = ImageType::UNKNOWN; inputImageType = ImageType::UNKNOWN;
@ -245,9 +141,12 @@ class PipelineWorker : public AsyncWorker {
try { try {
VOption *option = VImage::option()->set("access", baton->accessMethod); VOption *option = VImage::option()->set("access", baton->accessMethod);
if (inputImageType == ImageType::MAGICK) { if (inputImageType == ImageType::MAGICK) {
option->set("density", baton->density.data()); option->set("density", std::to_string(baton->density).data());
} }
image = VImage::new_from_file(baton->fileIn.data(), option); image = VImage::new_from_file(baton->fileIn.data(), option);
if (inputImageType == ImageType::MAGICK) {
SetDensity(image, baton->density);
}
} catch (...) { } catch (...) {
(baton->err).append("Input file has corrupt header"); (baton->err).append("Input file has corrupt header");
inputImageType = ImageType::UNKNOWN; inputImageType = ImageType::UNKNOWN;
@ -508,11 +407,19 @@ class PipelineWorker : public AsyncWorker {
} }
} }
// Ensure image has an alpha channel when there is an overlay
bool hasOverlay = baton->overlayBufferInLength > 0 || !baton->overlayFileIn.empty();
if (hasOverlay && !HasAlpha(image)) {
double multiplier = (image.interpretation() == VIPS_INTERPRETATION_RGB16) ? 256.0 : 1.0;
image = image.bandjoin(
VImage::new_matrix(image.width(), image.height()).new_from_image(255 * multiplier)
);
}
bool shouldAffineTransform = xresidual != 0.0 || yresidual != 0.0; bool shouldAffineTransform = xresidual != 0.0 || yresidual != 0.0;
bool shouldBlur = baton->blurSigma != 0.0; bool shouldBlur = baton->blurSigma != 0.0;
bool shouldSharpen = baton->sharpenRadius != 0; bool shouldSharpen = baton->sharpenRadius != 0;
bool shouldThreshold = baton->threshold != 0; bool shouldThreshold = baton->threshold != 0;
bool hasOverlay = !baton->overlayPath.empty();
bool shouldPremultiplyAlpha = HasAlpha(image) && bool shouldPremultiplyAlpha = HasAlpha(image) &&
(shouldAffineTransform || shouldBlur || shouldSharpen || hasOverlay); (shouldAffineTransform || shouldBlur || shouldSharpen || hasOverlay);
@ -600,9 +507,15 @@ class PipelineWorker : public AsyncWorker {
// Crop/max/min // Crop/max/min
int left; int left;
int top; int top;
if (baton->crop < 9) {
// Gravity-based crop
std::tie(left, top) = CalculateCrop( std::tie(left, top) = CalculateCrop(
image.width(), image.height(), baton->width, baton->height, baton->gravity image.width(), image.height(), baton->width, baton->height, baton->crop
); );
} else {
// Entropy-based crop
std::tie(left, top) = EntropyCrop(image, baton->width, baton->height);
}
int width = std::min(image.width(), baton->width); int width = std::min(image.width(), baton->width);
int height = std::min(image.height(), baton->height); int height = std::min(image.height(), baton->height);
image = image.extract_area(left, top, width, height); image = image.extract_area(left, top, width, height);
@ -616,6 +529,27 @@ class PipelineWorker : public AsyncWorker {
); );
} }
// Extend edges
if (baton->extendTop > 0 || baton->extendBottom > 0 || baton->extendLeft > 0 || baton->extendRight > 0) {
// Scale up 8-bit values to match 16-bit input image
const double multiplier = (image.interpretation() == VIPS_INTERPRETATION_RGB16) ? 256.0 : 1.0;
// Create background colour
std::vector<double> background {
baton->background[0] * multiplier,
baton->background[1] * multiplier,
baton->background[2] * multiplier
};
// Add alpha channel to background colour
if (HasAlpha(image)) {
background.push_back(baton->background[3] * multiplier);
}
// Embed
baton->width = image.width() + baton->extendLeft + baton->extendRight;
baton->height = image.height() + baton->extendTop + baton->extendBottom;
image = image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
VImage::option()->set("extend", VIPS_EXTEND_BACKGROUND)->set("background", background));
}
// Threshold - must happen before blurring, due to the utility of blurring after thresholding // Threshold - must happen before blurring, due to the utility of blurring after thresholding
if (shouldThreshold) { if (shouldThreshold) {
image = image.colourspace(VIPS_INTERPRETATION_B_W) >= baton->threshold; image = image.colourspace(VIPS_INTERPRETATION_B_W) >= baton->threshold;
@ -634,38 +568,41 @@ class PipelineWorker : public AsyncWorker {
// Composite with overlay, if present // Composite with overlay, if present
if (hasOverlay) { if (hasOverlay) {
VImage overlayImage; VImage overlayImage;
ImageType overlayImageType = DetermineImageType(baton->overlayPath.data()); ImageType overlayImageType = ImageType::UNKNOWN;
if (baton->overlayBufferInLength > 0) {
// Overlay with image from buffer
overlayImageType = DetermineImageType(baton->overlayBufferIn, baton->overlayBufferInLength);
if (overlayImageType != ImageType::UNKNOWN) { if (overlayImageType != ImageType::UNKNOWN) {
overlayImage = VImage::new_from_file( try {
baton->overlayPath.data(), overlayImage = VImage::new_from_buffer(baton->overlayBufferIn, baton->overlayBufferInLength,
VImage::option()->set("access", baton->accessMethod) nullptr, VImage::option()->set("access", baton->accessMethod));
); } catch (...) {
(baton->err).append("Overlay buffer has corrupt header");
overlayImageType = ImageType::UNKNOWN;
}
} else { } else {
(baton->err).append("Overlay image is of an unsupported image format"); (baton->err).append("Overlay buffer contains unsupported image format");
}
} else {
// Overlay with image from file
overlayImageType = DetermineImageType(baton->overlayFileIn.data());
if (overlayImageType != ImageType::UNKNOWN) {
try {
overlayImage = VImage::new_from_file(baton->overlayFileIn.data(),
VImage::option()->set("access", baton->accessMethod));
} catch (...) {
(baton->err).append("Overlay file has corrupt header");
overlayImageType = ImageType::UNKNOWN;
}
}
}
if (overlayImageType == ImageType::UNKNOWN) {
return Error(); return Error();
} }
if (image.format() != VIPS_FORMAT_UCHAR && image.format() != VIPS_FORMAT_FLOAT) { // Ensure overlay is premultiplied sRGB
(baton->err).append("Expected image band format to be uchar or float: ");
(baton->err).append(vips_enum_nick(VIPS_TYPE_BAND_FORMAT, image.format()));
return Error();
}
if (overlayImage.format() != VIPS_FORMAT_UCHAR && overlayImage.format() != VIPS_FORMAT_FLOAT) {
(baton->err).append("Expected overlay image band format to be uchar or float: ");
(baton->err).append(vips_enum_nick(VIPS_TYPE_BAND_FORMAT, overlayImage.format()));
return Error();
}
if (!HasAlpha(overlayImage)) {
(baton->err).append("Overlay image must have an alpha channel");
return Error();
}
if (overlayImage.width() != image.width() && overlayImage.height() != image.height()) {
(baton->err).append("Overlay image must have same dimensions as resized image");
return Error();
}
// Ensure overlay is sRGB and premutiplied
overlayImage = overlayImage.colourspace(VIPS_INTERPRETATION_sRGB).premultiply(); overlayImage = overlayImage.colourspace(VIPS_INTERPRETATION_sRGB).premultiply();
// Composite images with given gravity
image = Composite(overlayImage, image); image = Composite(overlayImage, image, baton->overlayGravity);
} }
// Reverse premultiplication after all transformations: // Reverse premultiplication after all transformations:
@ -708,6 +645,9 @@ class PipelineWorker : public AsyncWorker {
SetExifOrientation(image, baton->withMetadataOrientation); SetExifOrientation(image, baton->withMetadataOrientation);
} }
// Number of channels used in output image
baton->channels = image.bands();
// Output // Output
if (baton->fileOut == "") { if (baton->fileOut == "") {
// Buffer output // Buffer output
@ -728,6 +668,7 @@ class PipelineWorker : public AsyncWorker {
area->free_fn = nullptr; area->free_fn = nullptr;
vips_area_unref(area); vips_area_unref(area);
baton->formatOut = "jpeg"; baton->formatOut = "jpeg";
baton->channels = std::min(baton->channels, 3);
} else if (baton->formatOut == "png" || (baton->formatOut == "input" && inputImageType == ImageType::PNG)) { } else if (baton->formatOut == "png" || (baton->formatOut == "input" && inputImageType == ImageType::PNG)) {
// Write PNG to buffer // Write PNG to buffer
VipsArea *area = VIPS_AREA(image.pngsave_buffer(VImage::option() VipsArea *area = VIPS_AREA(image.pngsave_buffer(VImage::option()
@ -800,6 +741,7 @@ class PipelineWorker : public AsyncWorker {
->set("interlace", baton->progressive) ->set("interlace", baton->progressive)
); );
baton->formatOut = "jpeg"; baton->formatOut = "jpeg";
baton->channels = std::min(baton->channels, 3);
} else if (baton->formatOut == "png" || isPng || (matchInput && inputImageType == ImageType::PNG)) { } else if (baton->formatOut == "png" || isPng || (matchInput && inputImageType == ImageType::PNG)) {
// Write PNG to file // Write PNG to file
image.pngsave(const_cast<char*>(baton->fileOut.data()), VImage::option() image.pngsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
@ -824,12 +766,14 @@ class PipelineWorker : public AsyncWorker {
->set("compression", VIPS_FOREIGN_TIFF_COMPRESSION_JPEG) ->set("compression", VIPS_FOREIGN_TIFF_COMPRESSION_JPEG)
); );
baton->formatOut = "tiff"; baton->formatOut = "tiff";
baton->channels = std::min(baton->channels, 3);
} else if (baton->formatOut == "dz" || IsDz(baton->fileOut)) { } else if (baton->formatOut == "dz" || IsDz(baton->fileOut)) {
// Write DZ to file // Write DZ to file
image.dzsave(const_cast<char*>(baton->fileOut.data()), VImage::option() image.dzsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
->set("strip", !baton->withMetadata) ->set("strip", !baton->withMetadata)
->set("tile_size", baton->tileSize) ->set("tile_size", baton->tileSize)
->set("overlap", baton->tileOverlap) ->set("overlap", baton->tileOverlap)
->set("layout", baton->tileLayout)
); );
baton->formatOut = "dz"; baton->formatOut = "dz";
} else { } else {
@ -838,8 +782,6 @@ class PipelineWorker : public AsyncWorker {
return Error(); return Error();
} }
} }
// Number of channels used in output image
baton->channels = image.bands();
} catch (VError const &err) { } catch (VError const &err) {
(baton->err).append(err.what()); (baton->err).append(err.what());
} }
@ -890,10 +832,13 @@ class PipelineWorker : public AsyncWorker {
} }
} }
// Dispose of Persistent wrapper around input Buffer so it can be garbage collected // Dispose of Persistent wrapper around input Buffers so they can be garbage collected
if (baton->bufferInLength > 0) { if (baton->bufferInLength > 0) {
GetFromPersistent("bufferIn"); GetFromPersistent("bufferIn");
} }
if (baton->overlayBufferInLength > 0) {
GetFromPersistent("overlayBufferIn");
}
delete baton; delete baton;
// Decrement processing task counter // Decrement processing task counter
@ -944,46 +889,6 @@ class PipelineWorker : public AsyncWorker {
return std::make_tuple(rotate, flip, flop); return std::make_tuple(rotate, flip, flop);
} }
/*
Calculate the (left, top) coordinates of the output image
within the input image, applying the given gravity.
*/
std::tuple<int, int>
CalculateCrop(int const inWidth, int const inHeight, int const outWidth, int const outHeight, int const gravity) {
int left = 0;
int top = 0;
switch (gravity) {
case 1: // North
left = (inWidth - outWidth + 1) / 2;
break;
case 2: // East
left = inWidth - outWidth;
top = (inHeight - outHeight + 1) / 2;
break;
case 3: // South
left = (inWidth - outWidth + 1) / 2;
top = inHeight - outHeight;
break;
case 4: // West
top = (inHeight - outHeight + 1) / 2;
break;
case 5: // Northeast
left = inWidth - outWidth;
break;
case 6: // Southeast
left = inWidth - outWidth;
top = inHeight - outHeight;
case 7: // Southwest
top = inHeight - outHeight;
case 8: // Northwest
break;
default: // Centre
left = (inWidth - outWidth + 1) / 2;
top = (inHeight - outHeight + 1) / 2;
}
return std::make_tuple(left, top);
}
/* /*
Calculate integral shrink given factor and interpolator window size Calculate integral shrink given factor and interpolator window size
*/ */
@ -1052,7 +957,7 @@ NAN_METHOD(pipeline) {
// 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
baton->limitInputPixels = attrAs<int32_t>(options, "limitInputPixels"); baton->limitInputPixels = attrAs<int32_t>(options, "limitInputPixels");
// Density/DPI at which to load vector images via libmagick // Density/DPI at which to load vector images via libmagick
baton->density = attrAsStr(options, "density"); baton->density = attrAs<int32_t>(options, "density");
// Raw pixel input // Raw pixel input
baton->rawWidth = attrAs<int32_t>(options, "rawWidth"); baton->rawWidth = attrAs<int32_t>(options, "rawWidth");
baton->rawHeight = attrAs<int32_t>(options, "rawHeight"); baton->rawHeight = attrAs<int32_t>(options, "rawHeight");
@ -1088,10 +993,17 @@ NAN_METHOD(pipeline) {
baton->background[i] = To<int32_t>(Get(background, i).ToLocalChecked()).FromJust(); baton->background[i] = To<int32_t>(Get(background, i).ToLocalChecked()).FromJust();
} }
// Overlay options // Overlay options
baton->overlayPath = attrAsStr(options, "overlayPath"); baton->overlayFileIn = attrAsStr(options, "overlayFileIn");
Local<Object> overlayBufferIn;
if (node::Buffer::HasInstance(Get(options, New("overlayBufferIn").ToLocalChecked()).ToLocalChecked())) {
overlayBufferIn = Get(options, New("overlayBufferIn").ToLocalChecked()).ToLocalChecked().As<Object>();
baton->overlayBufferInLength = node::Buffer::Length(overlayBufferIn);
baton->overlayBufferIn = node::Buffer::Data(overlayBufferIn);
}
baton->overlayGravity = attrAs<int32_t>(options, "overlayGravity");
// Resize options // Resize options
baton->withoutEnlargement = attrAs<bool>(options, "withoutEnlargement"); baton->withoutEnlargement = attrAs<bool>(options, "withoutEnlargement");
baton->gravity = attrAs<int32_t>(options, "gravity"); baton->crop = attrAs<int32_t>(options, "crop");
baton->interpolator = attrAsStr(options, "interpolator"); baton->interpolator = attrAsStr(options, "interpolator");
// Operators // Operators
baton->flatten = attrAs<bool>(options, "flatten"); baton->flatten = attrAs<bool>(options, "flatten");
@ -1108,6 +1020,10 @@ NAN_METHOD(pipeline) {
baton->rotateBeforePreExtract = attrAs<bool>(options, "rotateBeforePreExtract"); baton->rotateBeforePreExtract = attrAs<bool>(options, "rotateBeforePreExtract");
baton->flip = attrAs<bool>(options, "flip"); baton->flip = attrAs<bool>(options, "flip");
baton->flop = attrAs<bool>(options, "flop"); baton->flop = attrAs<bool>(options, "flop");
baton->extendTop = attrAs<int32_t>(options, "extendTop");
baton->extendBottom = attrAs<int32_t>(options, "extendBottom");
baton->extendLeft = attrAs<int32_t>(options, "extendLeft");
baton->extendRight = attrAs<int32_t>(options, "extendRight");
// Output options // Output options
baton->progressive = attrAs<bool>(options, "progressive"); baton->progressive = attrAs<bool>(options, "progressive");
baton->quality = attrAs<int32_t>(options, "quality"); baton->quality = attrAs<int32_t>(options, "quality");
@ -1122,8 +1038,18 @@ NAN_METHOD(pipeline) {
// Output // Output
baton->formatOut = attrAsStr(options, "formatOut"); baton->formatOut = attrAsStr(options, "formatOut");
baton->fileOut = attrAsStr(options, "fileOut"); baton->fileOut = attrAsStr(options, "fileOut");
// Tile output
baton->tileSize = attrAs<int32_t>(options, "tileSize"); baton->tileSize = attrAs<int32_t>(options, "tileSize");
baton->tileOverlap = attrAs<int32_t>(options, "tileOverlap"); baton->tileOverlap = attrAs<int32_t>(options, "tileOverlap");
std::string tileLayout = attrAsStr(options, "tileLayout");
if (tileLayout == "google") {
baton->tileLayout = VIPS_FOREIGN_DZ_LAYOUT_GOOGLE;
} else if (tileLayout == "zoomify") {
baton->tileLayout = VIPS_FOREIGN_DZ_LAYOUT_ZOOMIFY;
} else {
baton->tileLayout = VIPS_FOREIGN_DZ_LAYOUT_DZ;
}
// Function to notify of queue length changes // Function to notify of queue length changes
Callback *queueListener = new Callback( Callback *queueListener = new Callback(
Get(options, New("queueListener").ToLocalChecked()).ToLocalChecked().As<Function>() Get(options, New("queueListener").ToLocalChecked()).ToLocalChecked().As<Function>()
@ -1131,7 +1057,7 @@ NAN_METHOD(pipeline) {
// Join queue for worker thread // Join queue for worker thread
Callback *callback = new Callback(info[1].As<Function>()); Callback *callback = new Callback(info[1].As<Function>());
AsyncQueueWorker(new PipelineWorker(callback, baton, queueListener, bufferIn)); AsyncQueueWorker(new PipelineWorker(callback, baton, queueListener, bufferIn, overlayBufferIn));
// Increment queued task counter // Increment queued task counter
g_atomic_int_inc(&counterQueue); g_atomic_int_inc(&counterQueue);

View File

@ -1,8 +1,141 @@
#ifndef SRC_PIPELINE_H_ #ifndef SRC_PIPELINE_H_
#define SRC_PIPELINE_H_ #define SRC_PIPELINE_H_
#include <vips/vips8>
#include "nan.h" #include "nan.h"
NAN_METHOD(pipeline); NAN_METHOD(pipeline);
enum class Canvas {
CROP,
EMBED,
MAX,
MIN,
IGNORE_ASPECT
};
struct PipelineBaton {
std::string fileIn;
char *bufferIn;
size_t bufferInLength;
std::string iccProfilePath;
int limitInputPixels;
int density;
int rawWidth;
int rawHeight;
int rawChannels;
std::string formatOut;
std::string fileOut;
void *bufferOut;
size_t bufferOutLength;
std::string overlayFileIn;
char *overlayBufferIn;
size_t overlayBufferInLength;
int overlayGravity;
int topOffsetPre;
int leftOffsetPre;
int widthPre;
int heightPre;
int topOffsetPost;
int leftOffsetPost;
int widthPost;
int heightPost;
int width;
int height;
int channels;
Canvas canvas;
int crop;
std::string interpolator;
double background[4];
bool flatten;
bool negate;
double blurSigma;
int sharpenRadius;
double sharpenFlat;
double sharpenJagged;
int threshold;
double gamma;
bool greyscale;
bool normalize;
int angle;
bool rotateBeforePreExtract;
bool flip;
bool flop;
int extendTop;
int extendBottom;
int extendLeft;
int extendRight;
bool progressive;
bool withoutEnlargement;
VipsAccess accessMethod;
int quality;
int compressionLevel;
bool withoutAdaptiveFiltering;
bool withoutChromaSubsampling;
bool trellisQuantisation;
bool overshootDeringing;
bool optimiseScans;
std::string err;
bool withMetadata;
int withMetadataOrientation;
int tileSize;
int tileOverlap;
VipsForeignDzLayout tileLayout;
PipelineBaton():
bufferInLength(0),
limitInputPixels(0),
density(72),
rawWidth(0),
rawHeight(0),
rawChannels(0),
formatOut(""),
fileOut(""),
bufferOutLength(0),
overlayBufferInLength(0),
overlayGravity(0),
topOffsetPre(-1),
topOffsetPost(-1),
channels(0),
canvas(Canvas::CROP),
crop(0),
flatten(false),
negate(false),
blurSigma(0.0),
sharpenRadius(0),
sharpenFlat(1.0),
sharpenJagged(2.0),
threshold(0),
gamma(0.0),
greyscale(false),
normalize(false),
angle(0),
flip(false),
flop(false),
extendTop(0),
extendBottom(0),
extendLeft(0),
extendRight(0),
progressive(false),
withoutEnlargement(false),
quality(80),
compressionLevel(6),
withoutAdaptiveFiltering(false),
withoutChromaSubsampling(false),
trellisQuantisation(false),
overshootDeringing(false),
optimiseScans(false),
withMetadata(false),
withMetadataOrientation(-1),
tileSize(256),
tileOverlap(0),
tileLayout(VIPS_FOREIGN_DZ_LAYOUT_DZ) {
background[0] = 0.0;
background[1] = 0.0;
background[2] = 0.0;
background[3] = 255.0;
}
};
#endif // SRC_PIPELINE_H_ #endif // SRC_PIPELINE_H_

BIN
test/fixtures/expected/crop-entropy.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

BIN
test/fixtures/expected/crop-entropy.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

BIN
test/fixtures/expected/extend-equal.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -248,6 +248,34 @@
... ...
fun:_ZN2v88internal12_GLOBAL__N_117CreateICUCollatorEPNS0_7IsolateERKN6icu_556LocaleENS0_6HandleINS0_8JSObjectEEE fun:_ZN2v88internal12_GLOBAL__N_117CreateICUCollatorEPNS0_7IsolateERKN6icu_556LocaleENS0_6HandleINS0_8JSObjectEEE
} }
{
leak_v8_CallInterfaceDescriptorData
Memcheck:Leak
match-leak-kinds: possible
...
fun:_ZN2v88internal27CallInterfaceDescriptorData26InitializePlatformSpecificEiPNS0_8RegisterEPNS0_27PlatformInterfaceDescriptorE
}
{
leak_v8_InitializePlatformSpecific14
Memcheck:Leak
match-leak-kinds: possible
...
fun:_ZN2v88internal14LoadDescriptor26InitializePlatformSpecificEPNS0_27CallInterfaceDescriptorDataE
}
{
leak_v8_InitializePlatformSpecific15
Memcheck:Leak
match-leak-kinds: possible
...
fun:_ZN2v88internal15StoreDescriptor26InitializePlatformSpecificEPNS0_27CallInterfaceDescriptorDataE
}
{
leak_v8_Malloced
Memcheck:Leak
match-leak-kinds: possible
...
fun:_ZN2v88internal8Malloced3NewEm
}
# vips__init warnings # vips__init warnings
{ {
@ -279,7 +307,7 @@
fun:vips__magick_read_header fun:vips__magick_read_header
} }
{ {
cond_magick_is_palette_image cond_magick_is_palette_image_get_bands
Memcheck:Cond Memcheck:Cond
fun:IsPaletteImage fun:IsPaletteImage
... ...
@ -292,6 +320,13 @@
... ...
fun:get_bands fun:get_bands
} }
{
cond_magick_is_palette_image_parse_header
Memcheck:Cond
fun:IsPaletteImage
...
fun:parse_header
}
# glib g_file_read_link # glib g_file_read_link
# https://github.com/GNOME/glib/commit/49a5d0f6f2aed99cd78f25655f137f4448e47d92 # https://github.com/GNOME/glib/commit/49a5d0f6f2aed99cd78f25655f137f4448e47d92

View File

@ -5,9 +5,9 @@ var assert = require('assert');
var sharp = require('../../index'); var sharp = require('../../index');
var fixtures = require('../fixtures'); var fixtures = require('../fixtures');
describe('Crop gravities', function() { describe('Crop', function() {
var testSettings = [ [
{ {
name: 'North', name: 'North',
width: 320, width: 320,
@ -50,6 +50,13 @@ describe('Crop gravities', function() {
gravity: sharp.gravity.centre, gravity: sharp.gravity.centre,
fixture: 'gravity-centre.jpg' fixture: 'gravity-centre.jpg'
}, },
{
name: 'Default (centre)',
width: 80,
height: 320,
gravity: undefined,
fixture: 'gravity-centre.jpg'
},
{ {
name: 'Northeast', name: 'Northeast',
width: 320, width: 320,
@ -106,10 +113,8 @@ describe('Crop gravities', function() {
gravity: sharp.gravity.northwest, gravity: sharp.gravity.northwest,
fixture: 'gravity-west.jpg' fixture: 'gravity-west.jpg'
} }
]; ].forEach(function(settings) {
it(settings.name + ' gravity', function(done) {
testSettings.forEach(function(settings) {
it(settings.name, function(done) {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.resize(settings.width, settings.height) .resize(settings.width, settings.height)
.crop(settings.gravity) .crop(settings.gravity)
@ -122,7 +127,7 @@ describe('Crop gravities', function() {
}); });
}); });
it('allows specifying the gravity as a string', function(done) { it('Allows specifying the gravity as a string', function(done) {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.resize(80, 320) .resize(80, 320)
.crop('east') .crop('east')
@ -134,36 +139,57 @@ describe('Crop gravities', function() {
}); });
}); });
it('Invalid number', function() { it('Invalid values fail', function() {
assert.throws(function() { assert.throws(function() {
sharp(fixtures.inputJpg).crop(9); sharp().crop(9);
});
assert.throws(function() {
sharp().crop(1.1);
});
assert.throws(function() {
sharp().crop(-1);
});
assert.throws(function() {
sharp().crop('zoinks');
}); });
}); });
it('Invalid string', function() { it('Uses default value when none specified', function() {
assert.throws(function() {
sharp(fixtures.inputJpg).crop('yadda');
});
});
it('does not throw if crop gravity is undefined', function() {
assert.doesNotThrow(function() { assert.doesNotThrow(function() {
sharp(fixtures.inputJpg).crop(); sharp().crop();
}); });
}); });
it('defaults crop gravity to sharp.gravity.center', function(done) { describe('Entropy-based strategy', function() {
var centerGravitySettings = testSettings.filter(function (settings) {
return settings.name === 'Center'; it('JPEG', function(done) {
})[0]; sharp(fixtures.inputJpgWithCmykProfile)
sharp(fixtures.inputJpg) .resize(80, 320)
.resize(centerGravitySettings.width, centerGravitySettings.height) .crop(sharp.strategy.entropy)
.crop()
.toBuffer(function(err, data, info) { .toBuffer(function(err, data, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual(centerGravitySettings.width, info.width); assert.strictEqual('jpeg', info.format);
assert.strictEqual(centerGravitySettings.height, info.height); assert.strictEqual(3, info.channels);
fixtures.assertSimilar(fixtures.expected(centerGravitySettings.fixture), data, done); assert.strictEqual(80, info.width);
assert.strictEqual(320, info.height);
fixtures.assertSimilar(fixtures.expected('crop-entropy.jpg'), data, done);
}); });
}); });
it('PNG', function(done) {
sharp(fixtures.inputPngWithTransparency)
.resize(320, 80)
.crop(sharp.strategy.entropy)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual('png', info.format);
assert.strictEqual(4, info.channels);
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
fixtures.assertSimilar(fixtures.expected('crop-entropy.png'), data, done);
});
});
});
}); });

52
test/unit/extend.js Normal file
View File

@ -0,0 +1,52 @@
'use strict';
var assert = require('assert');
var sharp = require('../../index');
var fixtures = require('../fixtures');
describe('Extend', function () {
it('extend all sides equally with RGB', function(done) {
sharp(fixtures.inputJpg)
.resize(120)
.background({r: 255, g: 0, b: 0})
.extend(10)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(140, info.width);
assert.strictEqual(118, info.height);
fixtures.assertSimilar(fixtures.expected('extend-equal.jpg'), data, done);
});
});
it('extend sides unequally with RGBA', function(done) {
sharp(fixtures.inputPngWithTransparency16bit)
.resize(120)
.background({r: 0, g: 0, b: 0, a: 0})
.extend({top: 50, bottom: 0, left: 10, right: 35})
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(165, info.width);
assert.strictEqual(170, info.height);
fixtures.assertSimilar(fixtures.expected('extend-unequal.png'), data, done);
});
});
it('missing parameter fails', function() {
assert.throws(function() {
sharp().extend();
});
});
it('negative fails', function() {
assert.throws(function() {
sharp().extend(-1);
});
});
it('partial object fails', function() {
assert.throws(function() {
sharp().extend({top: 1});
});
});
});

View File

@ -6,29 +6,6 @@ var sharp = require('../../index');
var fixtures = require('../fixtures'); var fixtures = require('../fixtures');
describe('Partial image extraction', function() { describe('Partial image extraction', function() {
describe('using the legacy extract(top,left,width,height) syntax', function () {
it('JPEG', function(done) {
sharp(fixtures.inputJpg)
.extract(2, 2, 20, 20)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(20, info.width);
assert.strictEqual(20, info.height);
fixtures.assertSimilar(fixtures.expected('extract.jpg'), data, done);
});
});
it('PNG', function(done) {
sharp(fixtures.inputPng)
.extract(300, 200, 400, 200)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(400, info.width);
assert.strictEqual(200, info.height);
fixtures.assertSimilar(fixtures.expected('extract.png'), data, done);
});
});
});
it('JPEG', function(done) { it('JPEG', function(done) {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)

View File

@ -109,6 +109,26 @@ describe('Input/output', function() {
readable.pipe(pipeline).pipe(writable); readable.pipe(pipeline).pipe(writable);
}); });
it('Stream should emit info event', function(done) {
var readable = fs.createReadStream(fixtures.inputJpg);
var writable = fs.createWriteStream(fixtures.outputJpg);
var pipeline = sharp().resize(320, 240);
var infoEventEmitted = false;
pipeline.on('info', function(info) {
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
assert.strictEqual(3, info.channels);
infoEventEmitted = true;
});
writable.on('finish', function() {
assert.strictEqual(true, infoEventEmitted);
fs.unlinkSync(fixtures.outputJpg);
done();
});
readable.pipe(pipeline).pipe(writable);
});
it('Handle Stream to Stream error ', function(done) { it('Handle Stream to Stream error ', function(done) {
var pipeline = sharp().resize(320, 240); var pipeline = sharp().resize(320, 240);
var anErrorWasEmitted = false; var anErrorWasEmitted = false;
@ -662,7 +682,14 @@ describe('Input/output', function() {
assert.strictEqual('png', info.format); assert.strictEqual('png', info.format);
assert.strictEqual(40, info.width); assert.strictEqual(40, info.width);
assert.strictEqual(40, info.height); assert.strictEqual(40, info.height);
fixtures.assertSimilar(fixtures.expected('svg72.png'), data, done); fixtures.assertSimilar(fixtures.expected('svg72.png'), data, function(err) {
if (err) throw err;
sharp(data).metadata(function(err, info) {
if (err) throw err;
assert.strictEqual(72, info.density);
done();
});
});
} }
}); });
}); });
@ -679,7 +706,14 @@ describe('Input/output', function() {
assert.strictEqual('png', info.format); assert.strictEqual('png', info.format);
assert.strictEqual(40, info.width); assert.strictEqual(40, info.width);
assert.strictEqual(40, info.height); assert.strictEqual(40, info.height);
fixtures.assertSimilar(fixtures.expected('svg1200.png'), data, done); fixtures.assertSimilar(fixtures.expected('svg1200.png'), data, function(err) {
if (err) throw err;
sharp(data).metadata(function(err, info) {
if (err) throw err;
assert.strictEqual(1200, info.density);
done();
});
});
} }
}); });
}); });

View File

@ -18,6 +18,7 @@ describe('Image metadata', function() {
assert.strictEqual(2225, metadata.height); assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space); assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels); assert.strictEqual(3, metadata.channels);
assert.strictEqual('undefined', typeof metadata.density);
assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasProfile);
assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual(false, metadata.hasAlpha);
assert.strictEqual('undefined', typeof metadata.orientation); assert.strictEqual('undefined', typeof metadata.orientation);
@ -35,6 +36,7 @@ describe('Image metadata', function() {
assert.strictEqual(600, metadata.height); assert.strictEqual(600, metadata.height);
assert.strictEqual('srgb', metadata.space); assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels); assert.strictEqual(3, metadata.channels);
assert.strictEqual(72, metadata.density);
assert.strictEqual(true, metadata.hasProfile); assert.strictEqual(true, metadata.hasProfile);
assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual(false, metadata.hasAlpha);
assert.strictEqual(8, metadata.orientation); assert.strictEqual(8, metadata.orientation);
@ -64,6 +66,7 @@ describe('Image metadata', function() {
assert.strictEqual(3248, metadata.height); assert.strictEqual(3248, metadata.height);
assert.strictEqual('b-w', metadata.space); assert.strictEqual('b-w', metadata.space);
assert.strictEqual(1, metadata.channels); assert.strictEqual(1, metadata.channels);
assert.strictEqual(300, metadata.density);
assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasProfile);
assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual(false, metadata.hasAlpha);
assert.strictEqual('undefined', typeof metadata.orientation); assert.strictEqual('undefined', typeof metadata.orientation);
@ -82,6 +85,7 @@ describe('Image metadata', function() {
assert.strictEqual(2074, metadata.height); assert.strictEqual(2074, metadata.height);
assert.strictEqual('b-w', metadata.space); assert.strictEqual('b-w', metadata.space);
assert.strictEqual(1, metadata.channels); assert.strictEqual(1, metadata.channels);
assert.strictEqual(300, metadata.density);
assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasProfile);
assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual(false, metadata.hasAlpha);
assert.strictEqual('undefined', typeof metadata.orientation); assert.strictEqual('undefined', typeof metadata.orientation);
@ -99,6 +103,7 @@ describe('Image metadata', function() {
assert.strictEqual(1536, metadata.height); assert.strictEqual(1536, metadata.height);
assert.strictEqual('srgb', metadata.space); assert.strictEqual('srgb', metadata.space);
assert.strictEqual(4, metadata.channels); assert.strictEqual(4, metadata.channels);
assert.strictEqual(72, metadata.density);
assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasProfile);
assert.strictEqual(true, metadata.hasAlpha); assert.strictEqual(true, metadata.hasAlpha);
assert.strictEqual('undefined', typeof metadata.orientation); assert.strictEqual('undefined', typeof metadata.orientation);
@ -117,6 +122,7 @@ describe('Image metadata', function() {
assert.strictEqual(772, metadata.height); assert.strictEqual(772, metadata.height);
assert.strictEqual('srgb', metadata.space); assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels); assert.strictEqual(3, metadata.channels);
assert.strictEqual('undefined', typeof metadata.density);
assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasProfile);
assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual(false, metadata.hasAlpha);
assert.strictEqual('undefined', typeof metadata.orientation); assert.strictEqual('undefined', typeof metadata.orientation);
@ -135,6 +141,7 @@ describe('Image metadata', function() {
assert.strictEqual(800, metadata.width); assert.strictEqual(800, metadata.width);
assert.strictEqual(533, metadata.height); assert.strictEqual(533, metadata.height);
assert.strictEqual(3, metadata.channels); assert.strictEqual(3, metadata.channels);
assert.strictEqual('undefined', typeof metadata.density);
assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasProfile);
assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual(false, metadata.hasAlpha);
assert.strictEqual('undefined', typeof metadata.orientation); assert.strictEqual('undefined', typeof metadata.orientation);
@ -153,6 +160,7 @@ describe('Image metadata', function() {
assert.strictEqual(2220, metadata.width); assert.strictEqual(2220, metadata.width);
assert.strictEqual(2967, metadata.height); assert.strictEqual(2967, metadata.height);
assert.strictEqual(4, metadata.channels); assert.strictEqual(4, metadata.channels);
assert.strictEqual('undefined', typeof metadata.density);
assert.strictEqual('rgb', metadata.space); assert.strictEqual('rgb', metadata.space);
assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasProfile);
assert.strictEqual(true, metadata.hasAlpha); assert.strictEqual(true, metadata.hasAlpha);
@ -171,6 +179,7 @@ describe('Image metadata', function() {
assert.strictEqual(2225, metadata.height); assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space); assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels); assert.strictEqual(3, metadata.channels);
assert.strictEqual('undefined', typeof metadata.density);
assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasProfile);
assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual(false, metadata.hasAlpha);
assert.strictEqual('undefined', typeof metadata.orientation); assert.strictEqual('undefined', typeof metadata.orientation);
@ -198,6 +207,7 @@ describe('Image metadata', function() {
assert.strictEqual(2225, metadata.height); assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space); assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels); assert.strictEqual(3, metadata.channels);
assert.strictEqual('undefined', typeof metadata.density);
assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasProfile);
assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual(false, metadata.hasAlpha);
assert.strictEqual('undefined', typeof metadata.orientation); assert.strictEqual('undefined', typeof metadata.orientation);
@ -219,6 +229,7 @@ describe('Image metadata', function() {
assert.strictEqual(2225, metadata.height); assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space); assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels); assert.strictEqual(3, metadata.channels);
assert.strictEqual('undefined', typeof metadata.density);
assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasProfile);
assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual(false, metadata.hasAlpha);
assert.strictEqual('undefined', typeof metadata.orientation); assert.strictEqual('undefined', typeof metadata.orientation);
@ -238,6 +249,7 @@ describe('Image metadata', function() {
assert.strictEqual(2225, metadata.height); assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space); assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels); assert.strictEqual(3, metadata.channels);
assert.strictEqual('undefined', typeof metadata.density);
assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasProfile);
assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual(false, metadata.hasAlpha);
assert.strictEqual('undefined', typeof metadata.orientation); assert.strictEqual('undefined', typeof metadata.orientation);

View File

@ -1,5 +1,6 @@
'use strict'; 'use strict';
var fs = require('fs');
var assert = require('assert'); var assert = require('assert');
var fixtures = require('../fixtures'); var fixtures = require('../fixtures');
var sharp = require('../../index'); var sharp = require('../../index');
@ -17,7 +18,7 @@ var getPaths = function(baseName, extension) {
// Test // Test
describe('Overlays', function() { describe('Overlays', function() {
it('Overlay transparent PNG on solid background', function(done) { it('Overlay transparent PNG file on solid background', function(done) {
var paths = getPaths('alpha-layer-01'); var paths = getPaths('alpha-layer-01');
sharp(fixtures.inputPngOverlayLayer0) sharp(fixtures.inputPngOverlayLayer0)
@ -29,6 +30,18 @@ describe('Overlays', function() {
}); });
}); });
it('Overlay transparent PNG Buffer on solid background', function(done) {
var paths = getPaths('alpha-layer-01');
sharp(fixtures.inputPngOverlayLayer0)
.overlayWith(fs.readFileSync(fixtures.inputPngOverlayLayer1))
.toFile(paths.actual, function (error) {
if (error) return done(error);
fixtures.assertMaxColourDistance(paths.actual, paths.expected);
done();
});
});
it('Overlay low-alpha transparent PNG on solid background', function(done) { it('Overlay low-alpha transparent PNG on solid background', function(done) {
var paths = getPaths('alpha-layer-01-low-alpha'); var paths = getPaths('alpha-layer-01-low-alpha');
@ -141,18 +154,19 @@ describe('Overlays', function() {
}); });
} }
it('Fail when compositing images with different dimensions', function(done) { it('Fail when overlay does not contain alpha channel', function(done) {
sharp(fixtures.inputJpg) sharp(fixtures.inputPngOverlayLayer1)
.overlayWith(fixtures.inputPngWithGreyAlpha) .overlayWith(fixtures.inputJpg)
.toBuffer(function(error) { .toBuffer(function(error) {
assert.strictEqual(true, error instanceof Error); assert.strictEqual(true, error instanceof Error);
done(); done();
}); });
}); });
it('Fail when compositing non-PNG image', function(done) { it('Fail when overlay is larger', function(done) {
sharp(fixtures.inputPngOverlayLayer1) sharp(fixtures.inputJpg)
.overlayWith(fixtures.inputJpg) .resize(320)
.overlayWith(fixtures.inputPngOverlayLayer1)
.toBuffer(function(error) { .toBuffer(function(error) {
assert.strictEqual(true, error instanceof Error); assert.strictEqual(true, error instanceof Error);
done(); done();
@ -170,4 +184,62 @@ describe('Overlays', function() {
sharp().overlayWith(1); sharp().overlayWith(1);
}); });
}); });
it('Fail with unsupported gravity', function() {
assert.throws(function() {
sharp()
.overlayWith(fixtures.inputPngOverlayLayer1, {
gravity: 9
});
});
});
it('Empty options', function() {
assert.doesNotThrow(function() {
sharp().overlayWith(fixtures.inputPngOverlayLayer1, {});
});
});
describe('Overlay with numeric gravity', function() {
Object.keys(sharp.gravity).forEach(function(gravity) {
it(gravity, function(done) {
var expected = fixtures.expected('overlay-gravity-' + gravity + '.jpg');
sharp(fixtures.inputJpg)
.resize(80)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
gravity: gravity
})
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(80, info.width);
assert.strictEqual(65, info.height);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(expected, data, done);
});
});
});
});
describe('Overlay with string-based gravity', function() {
Object.keys(sharp.gravity).forEach(function(gravity) {
it(gravity, function(done) {
var expected = fixtures.expected('overlay-gravity-' + gravity + '.jpg');
sharp(fixtures.inputJpg)
.resize(80)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
gravity: sharp.gravity[gravity]
})
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(80, info.width);
assert.strictEqual(65, info.height);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(expected, data, done);
});
});
});
});
}); });

View File

@ -47,137 +47,86 @@ var assertDeepZoomTiles = function(directory, expectedSize, expectedLevels, done
describe('Tile', function() { describe('Tile', function() {
describe('Invalid tile values', function() { it('Valid size values pass', function() {
it('size - NaN', function(done) { [1, 8192].forEach(function(size) {
var isValid = true; assert.doesNotThrow(function() {
try { sharp().tile({
sharp().tile('zoinks'); size: size
} catch (err) { });
isValid = false; });
} });
assert.strictEqual(false, isValid);
done();
}); });
it('size - float', function(done) { it('Invalid size values fail', function() {
var isValid = true; ['zoinks', 1.1, -1, 0, 8193].forEach(function(size) {
try { assert.throws(function() {
sharp().tile(1.1); sharp().tile({
} catch (err) { size: size
isValid = false; });
} });
assert.strictEqual(false, isValid); });
done();
}); });
it('size - negative', function(done) { it('Valid overlap values pass', function() {
var isValid = true; [0, 8192].forEach(function(overlap) {
try { assert.doesNotThrow(function() {
sharp().tile(-1); sharp().tile({
} catch (err) { size: 8192,
isValid = false; overlap: overlap
} });
assert.strictEqual(false, isValid); });
done(); });
}); });
it('size - zero', function(done) { it('Invalid overlap values fail', function() {
var isValid = true; ['zoinks', 1.1, -1, 8193].forEach(function(overlap) {
try { assert.throws(function() {
sharp().tile(0); sharp().tile({
} catch (err) { overlap: overlap
isValid = false; });
} });
assert.strictEqual(false, isValid); });
done();
}); });
it('size - too large', function(done) { it('Valid layout values pass', function() {
var isValid = true; ['dz', 'google', 'zoomify'].forEach(function(layout) {
try { assert.doesNotThrow(function() {
sharp().tile(8193); sharp().tile({
} catch (err) { layout: layout
isValid = false; });
} });
assert.strictEqual(false, isValid); });
done();
}); });
it('overlap - NaN', function(done) { it('Invalid layout values fail', function() {
var isValid = true; ['zoinks', 1].forEach(function(layout) {
try { assert.throws(function() {
sharp().tile(null, 'zoinks'); sharp().tile({
} catch (err) { layout: layout
isValid = false; });
} });
assert.strictEqual(false, isValid); });
done();
}); });
it('overlap - float', function(done) { it('Prevent larger overlap than default size', function() {
var isValid = true; assert.throws(function() {
try { sharp().tile({overlap: 257});
sharp().tile(null, 1.1); });
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
}); });
it('overlap - negative', function(done) { it('Prevent larger overlap than provided size', function() {
var isValid = true; assert.throws(function() {
try { sharp().tile({size: 512, overlap: 513});
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) { if (sharp.format.dz.output.file) {
describe('Deep Zoom output', function() {
it('Tile size - 256px default', function(done) { it('Deep Zoom layout', function(done) {
var directory = fixtures.path('output.256_files'); var directory = fixtures.path('output.dz_files');
rimraf(directory, function() { rimraf(directory, function() {
sharp(fixtures.inputJpg).toFile(fixtures.path('output.256.dzi'), function(err, info) { sharp(fixtures.inputJpg)
.toFile(fixtures.path('output.dz.dzi'), function(err, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual('dz', info.format); assert.strictEqual('dz', info.format);
assertDeepZoomTiles(directory, 256, 13, done); assertDeepZoomTiles(directory, 256, 13, done);
@ -185,10 +134,15 @@ describe('Tile', function() {
}); });
}); });
it('Tile size/overlap - 512/16px', function(done) { it('Deep Zoom layout with custom size+overlap', function(done) {
var directory = fixtures.path('output.512_files'); var directory = fixtures.path('output.dz.512_files');
rimraf(directory, function() { rimraf(directory, function() {
sharp(fixtures.inputJpg).tile(512, 16).toFile(fixtures.path('output.512.dzi'), function(err, info) { sharp(fixtures.inputJpg)
.tile({
size: 512,
overlap: 16
})
.toFile(fixtures.path('output.dz.512.dzi'), function(err, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual('dz', info.format); assert.strictEqual('dz', info.format);
assertDeepZoomTiles(directory, 512 + 2 * 16, 13, done); assertDeepZoomTiles(directory, 512 + 2 * 16, 13, done);
@ -196,7 +150,46 @@ describe('Tile', function() {
}); });
}); });
it('Zoomify layout', function(done) {
var directory = fixtures.path('output.zoomify');
rimraf(directory, function() {
sharp(fixtures.inputJpg)
.tile({
layout: 'zoomify'
})
.toFile(fixtures.path('output.zoomify.dzi'), function(err, info) {
if (err) throw err;
assert.strictEqual('dz', info.format);
fs.stat(path.join(directory, 'ImageProperties.xml'), function(err, stat) {
if (err) throw err;
assert.strictEqual(true, stat.isFile());
assert.strictEqual(true, stat.size > 0);
done();
}); });
});
});
});
it('Google layout', function(done) {
var directory = fixtures.path('output.google');
rimraf(directory, function() {
sharp(fixtures.inputJpg)
.tile({
layout: 'google'
})
.toFile(fixtures.path('output.google.dzi'), function(err, info) {
if (err) throw err;
assert.strictEqual('dz', info.format);
fs.stat(path.join(directory, '0', '0', '0.jpg'), function(err, stat) {
if (err) throw err;
assert.strictEqual(true, stat.isFile());
assert.strictEqual(true, stat.size > 0);
done();
});
});
});
});
} }
}); });