Compare commits

...

28 Commits

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

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

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

```text
Package libgsf-1 was not found in the pkg-config search path.
Perhaps you should add the directory containing `libgsf-1.pc'
to the PKG_CONFIG_PATH environment variable
Package 'libgsf-1', required by 'vips', not found
gyp: Call to 'PKG_CONFIG_PATH=":$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig:/usr/lib/pkgconfig" pkg-config --libs vips' returned exit status 1. while trying to load binding.gyp
```
2015-02-26 13:44:00 +01:00
Lovell Fuller
ef54e327b7 Document GIF input via Buffer and Stream
Ensure @mcuelenaere is credited
2015-02-16 13:51:47 +00:00
Lovell Fuller
d8d0158774 Version bump of libvips to 7.42.3
Rename version vars to major, minor and patch
2015-02-16 13:49:22 +00:00
Lovell Fuller
4d75f27a25 Merge branch 'mcuelenaere-imagemagick-buffer' 2015-02-16 13:27:51 +00:00
Lovell Fuller
f89e9d726d Add support for libvips v8.0.0 2015-02-16 13:27:22 +00:00
Lovell Fuller
5194b37460 Merge branch 'imagemagick-buffer' of https://github.com/mcuelenaere/sharp into mcuelenaere-imagemagick-buffer 2015-02-16 12:02:27 +00:00
Lovell Fuller
55ea432711 Add assumeyes flag for Fedora #167 2015-02-16 11:18:42 +00:00
Maurus Cuelenaere
ab7408c96f Add support for loading images through ImageMagick as a buffer 2015-02-16 10:12:59 +01:00
Lovell Fuller
1f7e80e581 Add chroma subsampling options for JPEG output 2015-02-13 09:41:42 +00:00
Lovell Fuller
0e91ca90d6 Remove lingering NanAdjustExternalMemory
Should have been removed in fe34548b
2015-02-12 12:15:56 +00:00
Lovell Fuller
8f41fed9c2 Add toFormat convenience method #137 2015-02-12 11:37:56 +00:00
Lovell Fuller
96dd40cee1 Add Node.js 0.12 stable to CI build
Replaces 0.11 unstable
2015-02-09 17:09:37 +00:00
Lovell Fuller
62767d072b Version bumps 2015-02-09 09:52:27 +00:00
Lovell Fuller
33880ce19e Merge pull request #161 from ide/nan
Change nan dependency back to ^1.6.2
2015-02-06 21:32:27 +00:00
James Ide
988176846d Change nan dependency back to ^1.6.2
The issue with nan on io.js was fixed in https://github.com/rvagg/nan/pull/273. `npm install` on io.js 1.1.0 works now.
2015-02-06 12:30:15 -08:00
Lovell Fuller
657d436a0f Merge pull request #160 from jo/patch-1
Adjust comment in interpolation example
2015-02-06 14:28:59 +00:00
Johannes Jörg Schmidt
e5549e3063 Adjust comment in interpolation example 2015-02-06 15:17:10 +01:00
Lovell Fuller
0b2fb967b8 Add iojs to CI test matrix
Specific version of nan required
2015-02-06 09:36:45 +00:00
Lovell Fuller
f57478c1aa Update bench to latest imagemagick-native
Use 'Triangle' filter as bilinear equiv.
2015-02-02 16:16:45 +00:00
17 changed files with 487 additions and 112 deletions

View File

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

View File

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

View File

@@ -31,8 +31,9 @@ This module is powered by the blazingly fast [libvips](https://github.com/jcupit
### Prerequisites
* Node.js v0.10+
* Node.js v0.10+ or io.js
* [libvips](https://github.com/jcupitt/libvips) v7.40.0+ (7.42.0+ recommended)
* C++11 compatible compiler such as gcc 4.6+ or clang 3.0+
To install the most suitable version of libvips on the following Operating Systems:
@@ -187,7 +188,7 @@ sharp(inputBuffer)
.toFile('output.tiff')
.then(function() {
// output.tiff is a 200 pixels wide and 300 pixels high image
// containing a bicubic scaled version, embedded on a white canvas,
// containing a nohalo scaled version, embedded on a white canvas,
// of the image data in inputBuffer
});
```
@@ -197,7 +198,7 @@ sharp('input.gif')
.resize(200, 300)
.background({r: 0, g: 0, b: 0, a: 0})
.embed()
.webp()
.toFormat(sharp.format.webp)
.toBuffer(function(err, outputBuffer) {
if (err) {
throw err;
@@ -211,30 +212,66 @@ sharp('input.gif')
sharp(inputBuffer)
.resize(200, 200)
.max()
.jpeg()
.toFormat('jpeg')
.toBuffer().then(function(outputBuffer) {
// outputBuffer contains JPEG image data no wider than 200 pixels and no higher
// than 200 pixels regardless of the inputBuffer image dimensions
});
```
```javascript
// Runtime discovery of available formats
console.dir(sharp.format);
```
## API
### Attributes
#### format
An Object containing nested boolean values
representing the available input and output formats/methods,
for example:
```javascript
{ jpeg: { id: 'jpeg',
input: { file: true, buffer: true, stream: true },
output: { file: true, buffer: true, stream: true } },
png: { id: 'png',
input: { file: true, buffer: true, stream: true },
output: { file: true, buffer: true, stream: true } },
webp: { id: 'webp',
input: { file: true, buffer: true, stream: true },
output: { file: true, buffer: true, stream: true } },
tiff: { id: 'tiff',
input: { file: true, buffer: true, stream: true },
output: { file: true, buffer: false, stream: false } },
magick: { id: 'magick',
input: { file: true, buffer: true, stream: true },
output: { file: false, buffer: false, stream: false } },
raw: { id: 'raw',
input: { file: false, buffer: false, stream: false },
output: { file: false, buffer: true, stream: true } } }
```
### Input methods
#### sharp([input])
Constructor to which further methods are chained. `input`, if present, can be one of:
* Buffer containing JPEG, PNG, WebP or TIFF image data, or
* Buffer containing JPEG, PNG, WebP, GIF* or TIFF image data, or
* String containing the filename of an image, with most major formats supported.
The object returned implements the [stream.Duplex](http://nodejs.org/api/stream.html#stream_class_stream_duplex) class.
JPEG, PNG, WebP or TIFF format image data can be streamed into the object when `input` is not provided.
JPEG, PNG, WebP, GIF* or TIFF format image data can be streamed into the object when `input` is not provided.
JPEG, PNG or WebP format image data can be streamed out from this object.
\* libvips 8.0.0+ is required for Buffer/Stream input of GIF and other `magick` formats.
#### metadata([callback])
Fast access to image metadata without decoding any compressed image data.
@@ -292,7 +329,19 @@ Possible values are `north`, `east`, `south`, `west`, `center` and `centre`. The
#### max()
Preserving aspect ratio, resize the image to the maximum `width` or `height` specified.
Preserving aspect ratio,
resize the image to be as large as possible
while ensuring its dimensions are less than or equal to
the `width` and `height` specified.
Both `width` and `height` must be provided via `resize` otherwise the behaviour will default to `crop`.
#### min()
Preserving aspect ratio,
resize the image to be as small as possible
while ensuring its dimensions are greater than or equal to
the `width` and `height` specified.
Both `width` and `height` must be provided via `resize` otherwise the behaviour will default to `crop`.
@@ -415,6 +464,13 @@ The number of channels depends on the input image and selected options.
* 3 channels for colour images without alpha transparency, with bytes ordered \[red, green, blue, red, green, blue, etc.\]).
* 4 channels for colour images with alpha transparency, with bytes ordered \[red, green, blue, alpha, red, green, blue, alpha, etc.\].
#### toFormat(format)
Convenience method for the above output format methods, where `format` is either:
* an attribute of the `sharp.format` Object e.g. `sharp.format.jpeg`, or
* a String containing `jpeg`, `png`, `webp` or `raw`.
#### quality(quality)
The output quality to use for lossy JPEG, WebP and TIFF output formats. The default quality is `80`.
@@ -431,6 +487,15 @@ Include all metadata (EXIF, XMP, IPTC) from the input image in the output image.
The default behaviour is to strip all metadata and convert to the device-independent sRGB colour space.
#### withoutChromaSubsampling()
Disable the use of [chroma subsampling](http://en.wikipedia.org/wiki/Chroma_subsampling) with JPEG output (4:4:4).
This can improve colour representation at higher quality settings (90+),
but usually increases output file size and typically reduces performance by 25%.
The default behaviour is to use chroma subsampling (4:2:0).
#### compressionLevel(compressionLevel)
An advanced setting for the _zlib_ compression level of the lossless PNG output format. The default level is `6`.
@@ -611,6 +676,7 @@ This module would never have been possible without the help and code contributio
* [Amit Pitaru](https://github.com/apitaru)
* [Brandon Aaron](https://github.com/brandonaaron)
* [Andreas Lind](https://github.com/papandreou)
* [Maurus Cuelenaere](https://github.com/mcuelenaere)
Thank you!

View File

@@ -41,7 +41,7 @@ var Sharp = function(input) {
heightPost: -1,
width: -1,
height: -1,
canvas: 'c',
canvas: 'crop',
gravity: 0,
angle: 0,
rotateBeforePreExtract: false,
@@ -64,6 +64,7 @@ var Sharp = function(input) {
quality: 80,
compressionLevel: 6,
withoutAdaptiveFiltering: false,
withoutChromaSubsampling: false,
streamOut: false,
withMetadata: false
};
@@ -82,11 +83,13 @@ var Sharp = function(input) {
(input[0] === 0x52 && input[1] === 0x49) ||
// TIFF
(input[0] === 0x4D && input[1] === 0x4D && input[2] === 0x00 && (input[3] === 0x2A || input[3] === 0x2B)) ||
(input[0] === 0x49 && input[1] === 0x49 && (input[2] === 0x2A || input[2] === 0x2B) && input[3] === 0x00)
(input[0] === 0x49 && input[1] === 0x49 && (input[2] === 0x2A || input[2] === 0x2B) && input[3] === 0x00) ||
// GIF
(input[0] === 0x47 && input[1] === 0x49 && input[2] === 0x46 && input[3] === 0x38 && (input[4] === 0x37 || input[4] === 0x39) && input[5] === 0x61)
) {
this.options.bufferIn = input;
} else {
throw new Error('Buffer contains an unsupported image format. JPEG, PNG, WebP and TIFF are currently supported.');
throw new Error('Buffer contains an unsupported image format. JPEG, PNG, WebP, TIFF and GIF are currently supported.');
}
} else {
// input=stream
@@ -97,6 +100,11 @@ var Sharp = function(input) {
module.exports = Sharp;
util.inherits(Sharp, stream.Duplex);
/*
Supported image formats
*/
module.exports.format = sharp.format();
/*
Handle incoming chunk on Writable Stream
*/
@@ -128,7 +136,7 @@ Sharp.prototype._write = function(chunk, encoding, callback) {
module.exports.gravity = {'center': 0, 'centre': 0, 'north': 1, 'east': 2, 'south': 3, 'west': 4};
Sharp.prototype.crop = function(gravity) {
this.options.canvas = 'c';
this.options.canvas = 'crop';
if (typeof gravity === 'number' && !Number.isNaN(gravity) && gravity >= 0 && gravity <= 4) {
this.options.gravity = gravity;
} else {
@@ -168,12 +176,17 @@ Sharp.prototype.background = function(rgba) {
};
Sharp.prototype.embed = function() {
this.options.canvas = 'e';
this.options.canvas = 'embed';
return this;
};
Sharp.prototype.max = function() {
this.options.canvas = 'm';
this.options.canvas = 'max';
return this;
};
Sharp.prototype.min = function() {
this.options.canvas = 'min';
return this;
};
@@ -368,6 +381,14 @@ Sharp.prototype.withoutAdaptiveFiltering = function(withoutAdaptiveFiltering) {
return this;
};
/*
Disable the use of chroma subsampling for JPEG output
*/
Sharp.prototype.withoutChromaSubsampling = function(withoutChromaSubsampling) {
this.options.withoutChromaSubsampling = (typeof withoutChromaSubsampling === 'boolean') ? withoutChromaSubsampling : true;
return this;
};
Sharp.prototype.withMetadata = function(withMetadata) {
this.options.withMetadata = (typeof withMetadata === 'boolean') ? withMetadata : true;
return this;
@@ -435,27 +456,43 @@ Sharp.prototype.toFile = function(output, callback) {
return this;
};
/*
Write output to a Buffer
*/
Sharp.prototype.toBuffer = function(callback) {
return this._sharp(callback);
};
/*
Force JPEG output
*/
Sharp.prototype.jpeg = function() {
this.options.output = '__jpeg';
return this;
};
/*
Force PNG output
*/
Sharp.prototype.png = function() {
this.options.output = '__png';
return this;
};
/*
Force WebP output
*/
Sharp.prototype.webp = function() {
this.options.output = '__webp';
return this;
};
/*
Force raw, uint8 output
*/
Sharp.prototype.raw = function() {
if (semver.gte(libvipsVersion, '7.42.0')) {
var supportsRawOutput = module.exports.format.raw.output;
if (supportsRawOutput.file || supportsRawOutput.buffer || supportsRawOutput.stream) {
this.options.output = '__raw';
} else {
console.error('Raw output requires libvips 7.42.0+');
@@ -463,6 +500,23 @@ Sharp.prototype.raw = function() {
return this;
};
/*
Force output to a given format
@param format is either the id as a String or an Object with an 'id' attribute
*/
Sharp.prototype.toFormat = function(format) {
var id = format;
if (typeof format === 'object') {
id = format.id;
}
if (typeof id === 'string' && typeof module.exports.format[id] === 'object' && typeof this[id] === 'function') {
this[id]();
} else {
throw new Error('Unsupported format ' + format);
}
return this;
};
/*
Used by a Writable Stream to notify that it is ready for data
*/

14
package.json Executable file → Normal file
View File

@@ -1,6 +1,6 @@
{
"name": "sharp",
"version": "0.9.1",
"version": "0.9.3",
"author": "Lovell Fuller <npm@lovell.info>",
"contributors": [
"Pierre Inglebert <pierre.inglebert@gmail.com>",
@@ -11,7 +11,9 @@
"Julian Walker <julian@fiftythree.com>",
"Amit Pitaru <pitaru.amit@gmail.com>",
"Brandon Aaron <hello.brandon@aaron.sh>",
"Andreas Lind <andreas@one.com>"
"Andreas Lind <andreas@one.com>",
"Maurus Cuelenaere <mcuelenaere@gmail.com>",
"Linus Unnebäck <linus@folkdatorn.se>"
],
"description": "High performance Node.js module to resize JPEG, PNG, WebP and TIFF images using the libvips library",
"scripts": {
@@ -34,15 +36,15 @@
"vips"
],
"dependencies": {
"bluebird": "^2.9.3",
"bluebird": "^2.9.12",
"color": "^0.7.3",
"nan": "^1.5.1",
"semver": "^4.2.0"
"nan": "^1.6.2",
"semver": "^4.3.1"
},
"devDependencies": {
"mocha": "^2.1.0",
"mocha-jshint": "^0.0.9",
"istanbul": "^0.3.5",
"istanbul": "^0.3.6",
"coveralls": "^2.11.2",
"node-cpplint": "^0.4.0"
},

View File

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

View File

@@ -43,6 +43,15 @@ namespace sharp {
);
}
static bool buffer_is_gif(char *buffer, size_t len) {
return (
len >= 6 && (
(buffer[0] == 'G' && buffer[1] == 'I' && buffer[2] == 'F' &&
buffer[3] == '8' && (buffer[4] == '7' || buffer[4] == '9') && buffer[5] == 'a')
)
);
}
/*
Determine image format of a buffer.
*/
@@ -57,6 +66,8 @@ namespace sharp {
imageType = ImageType::WEBP;
} else if (buffer_is_tiff(static_cast<char*>(buffer), length)) {
imageType = ImageType::TIFF;
} else if (buffer_is_gif(static_cast<char*>(buffer), length)) {
imageType = ImageType::MAGICK;
}
}
return imageType;
@@ -75,6 +86,10 @@ namespace sharp {
vips_webpload_buffer(buffer, length, &image, "access", access, NULL);
} else if (imageType == ImageType::TIFF) {
vips_tiffload_buffer(buffer, length, &image, "access", access, NULL);
#if (VIPS_MAJOR_VERSION >= 8)
} else if (imageType == ImageType::MAGICK) {
vips_magickload_buffer(buffer, length, &image, "access", access, NULL);
#endif
}
return image;
}

View File

@@ -37,8 +37,9 @@ using sharp::counterQueue;
enum class Canvas {
CROP,
EMBED,
MAX,
EMBED
MIN
};
enum class Angle {
@@ -90,6 +91,7 @@ struct ResizeBaton {
int quality;
int compressionLevel;
bool withoutAdaptiveFiltering;
bool withoutChromaSubsampling;
std::string err;
bool withMetadata;
@@ -117,6 +119,7 @@ struct ResizeBaton {
quality(80),
compressionLevel(6),
withoutAdaptiveFiltering(false),
withoutChromaSubsampling(false),
withMetadata(false) {
background[0] = 0.0;
background[1] = 0.0;
@@ -245,19 +248,34 @@ class ResizeWorker : public NanAsyncWorker {
int interpolatorWindowSize = InterpolatorWindowSize(baton->interpolator.c_str());
// Scaling calculations
double factor;
double factor = 1.0;
if (baton->width > 0 && baton->height > 0) {
// Fixed width and height
double xfactor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
double yfactor = static_cast<double>(inputHeight) / static_cast<double>(baton->height);
factor = (baton->canvas == Canvas::CROP) ? std::min(xfactor, yfactor) : std::max(xfactor, yfactor);
// if max is set, we need to compute the real size of the thumb image
if (baton->canvas == Canvas::MAX) {
if (xfactor > yfactor) {
baton->height = round(static_cast<double>(inputHeight) / xfactor);
} else {
baton->width = round(static_cast<double>(inputWidth) / yfactor);
}
switch (baton->canvas) {
case Canvas::CROP:
factor = std::min(xfactor, yfactor);
break;
case Canvas::EMBED:
factor = std::max(xfactor, yfactor);
break;
case Canvas::MAX:
factor = std::max(xfactor, yfactor);
if (xfactor > yfactor) {
baton->height = round(static_cast<double>(inputHeight) / xfactor);
} else {
baton->width = round(static_cast<double>(inputWidth) / yfactor);
}
break;
case Canvas::MIN:
factor = std::min(xfactor, yfactor);
if (xfactor < yfactor) {
baton->height = round(static_cast<double>(inputHeight) / xfactor);
} else {
baton->width = round(static_cast<double>(inputWidth) / yfactor);
}
break;
}
} else if (baton->width > 0) {
// Fixed width, auto height
@@ -269,7 +287,6 @@ class ResizeWorker : public NanAsyncWorker {
baton->width = floor(static_cast<double>(inputWidth) / factor);
} else {
// Identity transform
factor = 1;
baton->width = inputWidth;
baton->height = inputHeight;
}
@@ -547,7 +564,7 @@ class ResizeWorker : public NanAsyncWorker {
vips_object_local(hook, embedded);
image = embedded;
} else {
// Crop/max
// Crop/max/min
int left;
int top;
std::tie(left, top) = CalculateCrop(image->Xsize, image->Ysize, baton->width, baton->height, baton->gravity);
@@ -659,7 +676,7 @@ class ResizeWorker : public NanAsyncWorker {
}
}
#if !(VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 40 && VIPS_MINOR_VERSION >= 5)
#if !(VIPS_MAJOR_VERSION >= 8 || (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 40 && VIPS_MINOR_VERSION >= 5))
// Generate image tile cache when interlace output is required - no longer required as of libvips 7.40.5+
if (baton->progressive) {
VipsImage *cached;
@@ -675,12 +692,13 @@ class ResizeWorker : public NanAsyncWorker {
if (baton->output == "__jpeg" || (baton->output == "__input" && inputImageType == ImageType::JPEG)) {
// Write JPEG to buffer
if (vips_jpegsave_buffer(image, &baton->bufferOut, &baton->bufferOutLength, "strip", !baton->withMetadata,
"Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progressive, NULL)) {
"Q", baton->quality, "optimize_coding", TRUE, "no_subsample", baton->withoutChromaSubsampling,
"interlace", baton->progressive, NULL)) {
return Error(baton, hook);
}
baton->outputFormat = "jpeg";
} else if (baton->output == "__png" || (baton->output == "__input" && inputImageType == ImageType::PNG)) {
#if (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 42)
#if (VIPS_MAJOR_VERSION >= 8 || (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 42))
// Select PNG row filter
int filter = baton->withoutAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_NONE : VIPS_FOREIGN_PNG_FILTER_ALL;
// Write PNG to buffer
@@ -703,7 +721,7 @@ class ResizeWorker : public NanAsyncWorker {
return Error(baton, hook);
}
baton->outputFormat = "webp";
#if (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 42)
#if (VIPS_MAJOR_VERSION >= 8 || (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 42))
} else if (baton->output == "__raw") {
// Write raw, uncompressed image data to buffer
if (baton->greyscale) {
@@ -741,12 +759,13 @@ class ResizeWorker : public NanAsyncWorker {
if (outputJpeg || (matchInput && inputImageType == ImageType::JPEG)) {
// Write JPEG to file
if (vips_jpegsave(image, baton->output.c_str(), "strip", !baton->withMetadata,
"Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progressive, NULL)) {
"Q", baton->quality, "optimize_coding", TRUE, "no_subsample", baton->withoutChromaSubsampling,
"interlace", baton->progressive, NULL)) {
return Error(baton, hook);
}
baton->outputFormat = "jpeg";
} else if (outputPng || (matchInput && inputImageType == ImageType::PNG)) {
#if (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 42)
#if (VIPS_MAJOR_VERSION >= 8 || (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 42))
// Select PNG row filter
int filter = baton->withoutAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_NONE : VIPS_FOREIGN_PNG_FILTER_ALL;
// Write PNG to file
@@ -959,12 +978,14 @@ NAN_METHOD(resize) {
baton->height = options->Get(NanNew<String>("height"))->Int32Value();
// Canvas option
Local<String> canvas = options->Get(NanNew<String>("canvas"))->ToString();
if (canvas->Equals(NanNew<String>("c"))) {
if (canvas->Equals(NanNew<String>("crop"))) {
baton->canvas = Canvas::CROP;
} else if (canvas->Equals(NanNew<String>("m"))) {
baton->canvas = Canvas::MAX;
} else if (canvas->Equals(NanNew<String>("e"))) {
} else if (canvas->Equals(NanNew<String>("embed"))) {
baton->canvas = Canvas::EMBED;
} else if (canvas->Equals(NanNew<String>("max"))) {
baton->canvas = Canvas::MAX;
} else if (canvas->Equals(NanNew<String>("min"))) {
baton->canvas = Canvas::MIN;
}
// Background colour
Local<Array> background = Local<Array>::Cast(options->Get(NanNew<String>("background")));
@@ -992,6 +1013,7 @@ NAN_METHOD(resize) {
baton->quality = options->Get(NanNew<String>("quality"))->Int32Value();
baton->compressionLevel = options->Get(NanNew<String>("compressionLevel"))->Int32Value();
baton->withoutAdaptiveFiltering = options->Get(NanNew<String>("withoutAdaptiveFiltering"))->BooleanValue();
baton->withoutChromaSubsampling = options->Get(NanNew<String>("withoutChromaSubsampling"))->BooleanValue();
baton->withMetadata = options->Get(NanNew<String>("withMetadata"))->BooleanValue();
// Output filename or __format for Buffer
baton->output = *String::Utf8Value(options->Get(NanNew<String>("output"))->ToString());

View File

@@ -13,12 +13,9 @@ extern "C" void init(v8::Handle<v8::Object> target) {
vips_init("sharp");
// Set libvips operation cache limits
vips_cache_set_max_mem(100 * 1048576); // 100 MB
vips_cache_set_max_mem(100 * 1024 * 1024); // 100 MB
vips_cache_set_max(500); // 500 operations
// Notify the V8 garbage collector of max cache size
NanAdjustExternalMemory(vips_cache_get_max_mem());
// Methods available to JavaScript
NODE_SET_METHOD(target, "metadata", metadata);
NODE_SET_METHOD(target, "resize", resize);
@@ -26,6 +23,7 @@ extern "C" void init(v8::Handle<v8::Object> target) {
NODE_SET_METHOD(target, "concurrency", concurrency);
NODE_SET_METHOD(target, "counters", counters);
NODE_SET_METHOD(target, "libvipsVersion", libvipsVersion);
NODE_SET_METHOD(target, "format", format);
}
NODE_MODULE(sharp, init)

View File

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

View File

@@ -7,5 +7,6 @@ NAN_METHOD(cache);
NAN_METHOD(concurrency);
NAN_METHOD(counters);
NAN_METHOD(libvipsVersion);
NAN_METHOD(format);
#endif // SRC_UTILITIES_H_

View File

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

View File

@@ -347,6 +347,18 @@ async.series({
}
});
}
}).add('sharp-without-chroma-subsampling', {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer).resize(width, height).withoutChromaSubsampling().toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-rotate', {
defer: true,
fn: function(deferred) {

View File

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

View File

@@ -135,10 +135,11 @@ describe('Input/output', function() {
readableButNotAnImage.pipe(writable);
});
it('Sequential read', function(done) {
it('Sequential read, force JPEG', function(done) {
sharp(fixtures.inputJpg)
.sequentialRead()
.resize(320, 240)
.toFormat(sharp.format.jpeg)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
@@ -150,10 +151,11 @@ describe('Input/output', function() {
});
});
it('Not sequential read', function(done) {
it('Not sequential read, force JPEG', function(done) {
sharp(fixtures.inputJpg)
.sequentialRead(false)
.resize(320, 240)
.toFormat('jpeg')
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
@@ -292,6 +294,30 @@ describe('Input/output', function() {
});
});
it('WebP output', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.toFormat(sharp.format.webp)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('webp', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
it('Invalid output format', function(done) {
var isValid = false;
try {
sharp().toFormat('zoinks');
isValid = true;
} catch (e) {}
assert(!isValid);
done();
});
it('File input with corrupt header fails gracefully', function(done) {
sharp(fixtures.inputJpgWithCorruptHeader)
.toBuffer(function(err) {
@@ -433,39 +459,72 @@ describe('Input/output', function() {
});
it('Convert SVG, if supported, to PNG', function(done) {
sharp(fixtures.inputSvg)
.resize(100, 100)
.png()
.toFile(fixtures.path('output.svg.png'), function(err, info) {
if (err) {
assert.strictEqual('Input file is of an unsupported image format', err.message);
} else {
it('Without chroma subsampling generates larger file', function(done) {
// First generate with chroma subsampling (default)
sharp(fixtures.inputJpg)
.resize(320, 240)
.withoutChromaSubsampling(false)
.toBuffer(function(err, withChromaSubsamplingData, withChromaSubsamplingInfo) {
if (err) throw err;
assert.strictEqual(true, withChromaSubsamplingData.length > 0);
assert.strictEqual(withChromaSubsamplingData.length, withChromaSubsamplingInfo.size);
assert.strictEqual('jpeg', withChromaSubsamplingInfo.format);
assert.strictEqual(320, withChromaSubsamplingInfo.width);
assert.strictEqual(240, withChromaSubsamplingInfo.height);
// Then generate without
sharp(fixtures.inputJpg)
.resize(320, 240)
.withoutChromaSubsampling()
.toBuffer(function(err, withoutChromaSubsamplingData, withoutChromaSubsamplingInfo) {
if (err) throw err;
assert.strictEqual(true, withoutChromaSubsamplingData.length > 0);
assert.strictEqual(withoutChromaSubsamplingData.length, withoutChromaSubsamplingInfo.size);
assert.strictEqual('jpeg', withoutChromaSubsamplingInfo.format);
assert.strictEqual(320, withoutChromaSubsamplingInfo.width);
assert.strictEqual(240, withoutChromaSubsamplingInfo.height);
assert.strictEqual(true, withChromaSubsamplingData.length < withoutChromaSubsamplingData.length);
done();
});
});
});
if (sharp.format.magick.input.file) {
it('Convert SVG, if supported, to PNG', function(done) {
sharp(fixtures.inputSvg)
.resize(100, 100)
.toFormat('png')
.toFile(fixtures.path('output.svg.png'), function(err, info) {
if (err) {
assert.strictEqual('Input file is of an unsupported image format', err.message);
} else {
assert.strictEqual(true, info.size > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(100, info.width);
assert.strictEqual(100, info.height);
}
done();
});
});
}
if (sharp.format.magick.input.file) {
it('Convert PSD to PNG', function(done) {
sharp(fixtures.inputPsd)
.resize(320, 240)
.toFormat(sharp.format.png)
.toFile(fixtures.path('output.psd.png'), function(err, info) {
if (err) throw err;
assert.strictEqual(true, info.size > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(100, info.width);
assert.strictEqual(100, info.height);
}
done();
});
});
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
}
it('Convert PSD to PNG', function(done) {
sharp(fixtures.inputPsd)
.resize(320, 240)
.png()
.toFile(fixtures.path('output.psd.png'), function(err, info) {
if (err) throw err;
assert.strictEqual(true, info.size > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
if (semver.gte(sharp.libvipsVersion(), '7.40.0')) {
it('Load TIFF from Buffer [libvips ' + sharp.libvipsVersion() + '>=7.40.0]', function(done) {
if (sharp.format.tiff.input.buffer) {
it('Load TIFF from Buffer', function(done) {
var inputTiffBuffer = fs.readFileSync(fixtures.inputTiff);
sharp(inputTiffBuffer)
.resize(320, 240)
@@ -482,8 +541,26 @@ describe('Input/output', function() {
});
}
if (semver.gte(sharp.libvipsVersion(), '7.42.0')) {
describe('Ouput raw, uncompressed image data [libvips ' + sharp.libvipsVersion() + '>=7.42.0]', function() {
if (sharp.format.magick.input.buffer) {
it('Load GIF from Buffer', function(done) {
var inputGifBuffer = fs.readFileSync(fixtures.inputGif);
sharp(inputGifBuffer)
.resize(320, 240)
.jpeg()
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual(data.length, info.size);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
}
if (sharp.format.raw.output.buffer) {
describe('Ouput raw, uncompressed image data', function() {
it('1 channel greyscale image', function(done) {
sharp(fixtures.inputJpg)
.greyscale()
@@ -501,7 +578,7 @@ describe('Input/output', function() {
it('3 channel colour image without transparency', function(done) {
sharp(fixtures.inputJpg)
.resize(32, 24)
.raw()
.toFormat('raw')
.toBuffer(function(err, data, info) {
assert.strictEqual(32 * 24 * 3, info.size);
assert.strictEqual(data.length, info.size);
@@ -514,7 +591,7 @@ describe('Input/output', function() {
it('4 channel colour image with transparency', function(done) {
sharp(fixtures.inputPngWithTransparency)
.resize(32, 24)
.raw()
.toFormat(sharp.format.raw)
.toBuffer(function(err, data, info) {
assert.strictEqual(32 * 24 * 4, info.size);
assert.strictEqual(data.length, info.size);

View File

@@ -185,6 +185,39 @@ describe('Resize dimensions', function() {
});
});
it('Min width or height considering ratio (landscape)', function(done) {
sharp(fixtures.inputJpg).resize(320, 320).min().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(392, info.width);
assert.strictEqual(320, info.height);
done();
});
});
it('Min width or height considering ratio (portrait)', function(done) {
sharp(fixtures.inputTiff).resize(320, 320).min().jpeg().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(422, info.height);
done();
});
});
it('Provide only one dimension with min, should default to crop', function(done) {
sharp(fixtures.inputJpg).resize(320).min().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(261, info.height);
done();
});
});
it('Do not enlarge when input width is already less than output width', function(done) {
sharp(fixtures.inputJpg)
.resize(2800)

View File

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