Add joinChannel and toColourspace/toColorspace operations (#513)
28
docs/api.md
@ -60,7 +60,7 @@ Fast access to image metadata without decoding any compressed image data.
|
|||||||
* `format`: Name of decoder used to decompress image data e.g. `jpeg`, `png`, `webp`, `gif`, `svg`
|
* `format`: Name of decoder used to decompress image data e.g. `jpeg`, `png`, `webp`, `gif`, `svg`
|
||||||
* `width`: Number of pixels wide
|
* `width`: Number of pixels wide
|
||||||
* `height`: Number of pixels high
|
* `height`: Number of pixels high
|
||||||
* `space`: Name of colour space interpretation e.g. `srgb`, `rgb`, `scrgb`, `cmyk`, `lab`, `xyz`, `b-w` [...](https://github.com/jcupitt/libvips/blob/master/libvips/iofuncs/enumtypes.c#L522)
|
* `space`: Name of colour space interpretation e.g. `srgb`, `rgb`, `scrgb`, `cmyk`, `lab`, `xyz`, `b-w` [...](https://github.com/jcupitt/libvips/blob/master/libvips/iofuncs/enumtypes.c#L568)
|
||||||
* `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
|
* `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
|
||||||
@ -445,7 +445,7 @@ Convert to 8-bit greyscale; 256 shades of grey.
|
|||||||
|
|
||||||
This is a linear operation. If the input image is in a non-linear colour space such as sRGB, use `gamma()` with `greyscale()` for the best results.
|
This is a linear operation. If the input image is in a non-linear colour space such as sRGB, use `gamma()` with `greyscale()` for the best results.
|
||||||
|
|
||||||
The output image will still be web-friendly sRGB and contain three (identical) channels.
|
By default the output image will be web-friendly sRGB and contain three (identical) color channels. This may be overridden by other sharp operations such as `toColourspace('b-w')`, which will produce an output image containing one color channel. An alpha channel may be present, and will be unchanged by the operation.
|
||||||
|
|
||||||
#### normalize() / normalise()
|
#### normalize() / normalise()
|
||||||
|
|
||||||
@ -490,11 +490,17 @@ sharp('input.png')
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### toColourspace(colourspace) / toColorspace(colorspace)
|
||||||
|
|
||||||
|
Set the output colourspace. By default output image will be web-friendly sRGB, with additional channels interpreted as alpha channels.
|
||||||
|
|
||||||
|
`colourspace` is a string or `sharp.colourspace` enum that identifies an output colourspace. String arguments comprise vips colour space interpretation names e.g. `srgb`, `rgb`, `scrgb`, `cmyk`, `lab`, `xyz`, `b-w` [...](https://github.com/jcupitt/libvips/blob/master/libvips/iofuncs/enumtypes.c#L568)
|
||||||
|
|
||||||
#### extractChannel(channel)
|
#### extractChannel(channel)
|
||||||
|
|
||||||
Extract a single channel from a multi-channel image.
|
Extract a single channel from a multi-channel image.
|
||||||
|
|
||||||
* `channel` is a zero-indexed integral Number representing the band number to extract. `red`, `green` or `blue` are also accepted as an alternative to `0`, `1` or `2` respectively.
|
`channel` is a zero-indexed integral Number representing the band number to extract. `red`, `green` or `blue` are also accepted as an alternative to `0`, `1` or `2` respectively.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
sharp(input)
|
sharp(input)
|
||||||
@ -505,6 +511,22 @@ sharp(input)
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### joinChannel(channels, [options])
|
||||||
|
|
||||||
|
Join a data channel to the image. The meaning of the added channels depends on the output colourspace, set with `toColourspace()`. By default the output image will be web-friendly sRGB, with additional channels interpreted as alpha channels.
|
||||||
|
|
||||||
|
`channels` is one of
|
||||||
|
* a single file path
|
||||||
|
* an array of file paths
|
||||||
|
* a single buffer
|
||||||
|
* an array of buffers
|
||||||
|
|
||||||
|
Note that channel ordering follows vips convention:
|
||||||
|
* sRGB: 0: Red, 1: Green, 2: Blue, 3: Alpha
|
||||||
|
* CMYK: 0: Magenta, 1: Cyan, 2: Yellow, 3: Black, 4: Alpha
|
||||||
|
|
||||||
|
Buffers may be any of the image formats supported by sharp: JPEG, PNG, WebP, GIF, SVG, TIFF or raw pixel image data. In the case of a RAW buffer, the `options` object should contain a `raw` attribute, which follows the format of the attribute of the same name in the `sharp()` constructor. See `sharp()` for details. See `raw()` for pixel ordering.
|
||||||
|
|
||||||
#### bandbool(operation)
|
#### bandbool(operation)
|
||||||
|
|
||||||
Perform a bitwise boolean operation on all input image channels (bands) to produce a single channel output image.
|
Perform a bitwise boolean operation on all input image channels (bands) to produce a single channel output image.
|
||||||
|
37
index.js
@ -86,6 +86,7 @@ var Sharp = function(input, options) {
|
|||||||
normalize: 0,
|
normalize: 0,
|
||||||
booleanBufferIn: null,
|
booleanBufferIn: null,
|
||||||
booleanFileIn: '',
|
booleanFileIn: '',
|
||||||
|
joinChannelIn: [],
|
||||||
// overlay
|
// overlay
|
||||||
overlayGravity: 0,
|
overlayGravity: 0,
|
||||||
overlayXOffset : -1,
|
overlayXOffset : -1,
|
||||||
@ -109,6 +110,7 @@ var Sharp = function(input, options) {
|
|||||||
tileSize: 256,
|
tileSize: 256,
|
||||||
tileOverlap: 0,
|
tileOverlap: 0,
|
||||||
extractChannel: -1,
|
extractChannel: -1,
|
||||||
|
colourspace: 'srgb',
|
||||||
// Function to notify of queue length changes
|
// Function to notify of queue length changes
|
||||||
queueListener: function(queueLength) {
|
queueListener: function(queueLength) {
|
||||||
module.exports.queue.emit('change', queueLength);
|
module.exports.queue.emit('change', queueLength);
|
||||||
@ -421,6 +423,20 @@ Sharp.prototype.overlayWith = function(overlay, options) {
|
|||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add another color channel to the image
|
||||||
|
*/
|
||||||
|
Sharp.prototype.joinChannel = function(images, options) {
|
||||||
|
if (Array.isArray(images)) {
|
||||||
|
images.forEach(function(image) {
|
||||||
|
this.options.joinChannelIn.push(this._createInputDescriptor(image, options));
|
||||||
|
}, this);
|
||||||
|
} else {
|
||||||
|
this.options.joinChannelIn.push(this._createInputDescriptor(images, options));
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Rotate output image by 0, 90, 180 or 270 degrees
|
Rotate output image by 0, 90, 180 or 270 degrees
|
||||||
Auto-rotation based on the EXIF Orientation tag is represented by an angle of -1
|
Auto-rotation based on the EXIF Orientation tag is represented by an angle of -1
|
||||||
@ -629,6 +645,18 @@ Sharp.prototype.greyscale = function(greyscale) {
|
|||||||
};
|
};
|
||||||
Sharp.prototype.grayscale = Sharp.prototype.greyscale;
|
Sharp.prototype.grayscale = Sharp.prototype.greyscale;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Set output colourspace
|
||||||
|
*/
|
||||||
|
Sharp.prototype.toColourspace = function(colourspace) {
|
||||||
|
if (!isString(colourspace) ) {
|
||||||
|
throw new Error('Invalid output colourspace ' + colourspace);
|
||||||
|
}
|
||||||
|
this.options.colourspace = colourspace;
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
Sharp.prototype.toColorspace = Sharp.prototype.toColourspace;
|
||||||
|
|
||||||
Sharp.prototype.progressive = function(progressive) {
|
Sharp.prototype.progressive = function(progressive) {
|
||||||
this.options.progressive = isBoolean(progressive) ? progressive : true;
|
this.options.progressive = isBoolean(progressive) ? progressive : true;
|
||||||
return this;
|
return this;
|
||||||
@ -817,6 +845,15 @@ module.exports.bool = {
|
|||||||
or: 'or',
|
or: 'or',
|
||||||
eor: 'eor'
|
eor: 'eor'
|
||||||
};
|
};
|
||||||
|
// Colourspaces
|
||||||
|
module.exports.colourspace = {
|
||||||
|
multiband: 'multiband',
|
||||||
|
'b-w': 'b-w',
|
||||||
|
bw: 'b-w',
|
||||||
|
cmyk: 'cmyk',
|
||||||
|
srgb: 'srgb'
|
||||||
|
};
|
||||||
|
module.exports.colorspace = module.exports.colourspace;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Resize image to width x height pixels
|
Resize image to width x height pixels
|
||||||
|
@ -430,4 +430,13 @@ namespace sharp {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Get interpretation type from string
|
||||||
|
*/
|
||||||
|
VipsInterpretation GetInterpretation(std::string const typeStr) {
|
||||||
|
return static_cast<VipsInterpretation>(
|
||||||
|
vips_enum_from_nick(nullptr, VIPS_TYPE_INTERPRETATION, typeStr.data())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace sharp
|
} // namespace sharp
|
||||||
|
@ -196,6 +196,11 @@ namespace sharp {
|
|||||||
*/
|
*/
|
||||||
VipsOperationBoolean GetBooleanOperation(std::string const opStr);
|
VipsOperationBoolean GetBooleanOperation(std::string const opStr);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Get interpretation type from string
|
||||||
|
*/
|
||||||
|
VipsInterpretation GetInterpretation(std::string const typeStr);
|
||||||
|
|
||||||
} // namespace sharp
|
} // namespace sharp
|
||||||
|
|
||||||
#endif // SRC_COMMON_H_
|
#endif // SRC_COMMON_H_
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
|
#include <map>
|
||||||
|
|
||||||
#include <vips/vips8>
|
#include <vips/vips8>
|
||||||
#include <node.h>
|
#include <node.h>
|
||||||
@ -39,8 +40,15 @@ class PipelineWorker : public Nan::AsyncWorker {
|
|||||||
// Increment processing task counter
|
// Increment processing task counter
|
||||||
g_atomic_int_inc(&sharp::counterProcess);
|
g_atomic_int_inc(&sharp::counterProcess);
|
||||||
|
|
||||||
|
std::map<VipsInterpretation, std::string> profileMap;
|
||||||
// Default sRGB ICC profile from https://packages.debian.org/sid/all/icc-profiles-free/filelist
|
// Default sRGB ICC profile from https://packages.debian.org/sid/all/icc-profiles-free/filelist
|
||||||
std::string srgbProfile = baton->iccProfilePath + "sRGB.icc";
|
profileMap.insert(
|
||||||
|
std::pair<VipsInterpretation, std::string>(VIPS_INTERPRETATION_sRGB,
|
||||||
|
baton->iccProfilePath + "sRGB.icc"));
|
||||||
|
// Convert to sRGB using default CMYK profile from http://www.argyllcms.com/cmyk.icm
|
||||||
|
profileMap.insert(
|
||||||
|
std::pair<VipsInterpretation, std::string>(VIPS_INTERPRETATION_CMYK,
|
||||||
|
baton->iccProfilePath + "cmyk.icm"));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Open input
|
// Open input
|
||||||
@ -266,7 +274,8 @@ class PipelineWorker : public Nan::AsyncWorker {
|
|||||||
if (sharp::HasProfile(image)) {
|
if (sharp::HasProfile(image)) {
|
||||||
// Convert to sRGB using embedded profile
|
// Convert to sRGB using embedded profile
|
||||||
try {
|
try {
|
||||||
image = image.icc_transform(const_cast<char*>(srgbProfile.data()), VImage::option()
|
image = image.icc_transform(
|
||||||
|
const_cast<char*>(profileMap[VIPS_INTERPRETATION_sRGB].data()), VImage::option()
|
||||||
->set("embedded", TRUE)
|
->set("embedded", TRUE)
|
||||||
->set("intent", VIPS_INTENT_PERCEPTUAL)
|
->set("intent", VIPS_INTENT_PERCEPTUAL)
|
||||||
);
|
);
|
||||||
@ -274,10 +283,9 @@ class PipelineWorker : public Nan::AsyncWorker {
|
|||||||
// Ignore failure of embedded profile
|
// Ignore failure of embedded profile
|
||||||
}
|
}
|
||||||
} else if (image.interpretation() == VIPS_INTERPRETATION_CMYK) {
|
} else if (image.interpretation() == VIPS_INTERPRETATION_CMYK) {
|
||||||
// Convert to sRGB using default CMYK profile from http://www.argyllcms.com/cmyk.icm
|
image = image.icc_transform(
|
||||||
std::string cmykProfile = baton->iccProfilePath + "cmyk.icm";
|
const_cast<char*>(profileMap[VIPS_INTERPRETATION_sRGB].data()), VImage::option()
|
||||||
image = image.icc_transform(const_cast<char*>(srgbProfile.data()), VImage::option()
|
->set("input_profile", profileMap[VIPS_INTERPRETATION_CMYK].data())
|
||||||
->set("input_profile", cmykProfile.data())
|
|
||||||
->set("intent", VIPS_INTENT_PERCEPTUAL)
|
->set("intent", VIPS_INTENT_PERCEPTUAL)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -420,6 +428,19 @@ class PipelineWorker : public Nan::AsyncWorker {
|
|||||||
sharp::RemoveExifOrientation(image);
|
sharp::RemoveExifOrientation(image);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Join additional color channels to the image
|
||||||
|
if(baton->joinChannelIn.size() > 0) {
|
||||||
|
VImage joinImage;
|
||||||
|
ImageType joinImageType = ImageType::UNKNOWN;
|
||||||
|
|
||||||
|
for(unsigned int i = 0; i < baton->joinChannelIn.size(); i++) {
|
||||||
|
std::tie(joinImage, joinImageType) = sharp::OpenInput(baton->joinChannelIn[i], baton->accessMethod);
|
||||||
|
|
||||||
|
image = image.bandjoin(joinImage);
|
||||||
|
}
|
||||||
|
image = image.copy(VImage::option()->set("interpretation", baton->colourspace));
|
||||||
|
}
|
||||||
|
|
||||||
// Crop/embed
|
// Crop/embed
|
||||||
if (image.width() != baton->width || image.height() != baton->height) {
|
if (image.width() != baton->width || image.height() != baton->height) {
|
||||||
if (baton->canvas == Canvas::EMBED) {
|
if (baton->canvas == Canvas::EMBED) {
|
||||||
@ -654,12 +675,15 @@ class PipelineWorker : public Nan::AsyncWorker {
|
|||||||
if (sharp::Is16Bit(image.interpretation())) {
|
if (sharp::Is16Bit(image.interpretation())) {
|
||||||
image = image.cast(VIPS_FORMAT_USHORT);
|
image = image.cast(VIPS_FORMAT_USHORT);
|
||||||
}
|
}
|
||||||
if (image.interpretation() != VIPS_INTERPRETATION_sRGB) {
|
if (image.interpretation() != baton->colourspace) {
|
||||||
image = image.colourspace(VIPS_INTERPRETATION_sRGB);
|
// Need to convert image
|
||||||
// Transform colours from embedded profile to sRGB profile
|
image = image.colourspace(baton->colourspace);
|
||||||
if (baton->withMetadata && sharp::HasProfile(image)) {
|
// Transform colours from embedded profile to output profile
|
||||||
image = image.icc_transform(const_cast<char*>(srgbProfile.data()), VImage::option()
|
if (baton->withMetadata &&
|
||||||
->set("embedded", TRUE)
|
sharp::HasProfile(image) &&
|
||||||
|
profileMap[baton->colourspace] != std::string()) {
|
||||||
|
image = image.icc_transform(const_cast<char*>(profileMap[baton->colourspace].data()),
|
||||||
|
VImage::option()->set("embedded", TRUE)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -693,7 +717,11 @@ class PipelineWorker : public Nan::AsyncWorker {
|
|||||||
area->free_fn = nullptr;
|
area->free_fn = nullptr;
|
||||||
vips_area_unref(area);
|
vips_area_unref(area);
|
||||||
baton->formatOut = "jpeg";
|
baton->formatOut = "jpeg";
|
||||||
|
if(baton->colourspace == VIPS_INTERPRETATION_CMYK) {
|
||||||
|
baton->channels = std::min(baton->channels, 4);
|
||||||
|
} else {
|
||||||
baton->channels = std::min(baton->channels, 3);
|
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)) {
|
||||||
// Strip profile
|
// Strip profile
|
||||||
if (!baton->withMetadata) {
|
if (!baton->withMetadata) {
|
||||||
@ -1021,6 +1049,19 @@ NAN_METHOD(pipeline) {
|
|||||||
baton->crop = AttrTo<int32_t>(options, "crop");
|
baton->crop = AttrTo<int32_t>(options, "crop");
|
||||||
baton->kernel = AttrAsStr(options, "kernel");
|
baton->kernel = AttrAsStr(options, "kernel");
|
||||||
baton->interpolator = AttrAsStr(options, "interpolator");
|
baton->interpolator = AttrAsStr(options, "interpolator");
|
||||||
|
// Join Channel Options
|
||||||
|
if(HasAttr(options, "joinChannelIn")) {
|
||||||
|
v8::Local<v8::Object> joinChannelObject = Nan::Get(options, Nan::New("joinChannelIn").ToLocalChecked())
|
||||||
|
.ToLocalChecked().As<v8::Object>();
|
||||||
|
v8::Local<v8::Array> joinChannelArray = joinChannelObject.As<v8::Array>();
|
||||||
|
int joinChannelArrayLength = AttrTo<int32_t>(joinChannelObject, "length");
|
||||||
|
for(int i = 0; i < joinChannelArrayLength; i++) {
|
||||||
|
baton->joinChannelIn.push_back(
|
||||||
|
CreateInputDescriptor(
|
||||||
|
Nan::Get(joinChannelArray, i).ToLocalChecked().As<v8::Object>(),
|
||||||
|
buffersToPersist));
|
||||||
|
}
|
||||||
|
}
|
||||||
// Operators
|
// Operators
|
||||||
baton->flatten = AttrTo<bool>(options, "flatten");
|
baton->flatten = AttrTo<bool>(options, "flatten");
|
||||||
baton->negate = AttrTo<bool>(options, "negate");
|
baton->negate = AttrTo<bool>(options, "negate");
|
||||||
@ -1077,6 +1118,9 @@ NAN_METHOD(pipeline) {
|
|||||||
baton->optimiseScans = AttrTo<bool>(options, "optimiseScans");
|
baton->optimiseScans = AttrTo<bool>(options, "optimiseScans");
|
||||||
baton->withMetadata = AttrTo<bool>(options, "withMetadata");
|
baton->withMetadata = AttrTo<bool>(options, "withMetadata");
|
||||||
baton->withMetadataOrientation = AttrTo<uint32_t>(options, "withMetadataOrientation");
|
baton->withMetadataOrientation = AttrTo<uint32_t>(options, "withMetadataOrientation");
|
||||||
|
baton->colourspace = sharp::GetInterpretation(AttrAsStr(options, "colourspace"));
|
||||||
|
if(baton->colourspace == VIPS_INTERPRETATION_ERROR)
|
||||||
|
baton->colourspace = VIPS_INTERPRETATION_sRGB;
|
||||||
// Output
|
// Output
|
||||||
baton->formatOut = AttrAsStr(options, "formatOut");
|
baton->formatOut = AttrAsStr(options, "formatOut");
|
||||||
baton->fileOut = AttrAsStr(options, "fileOut");
|
baton->fileOut = AttrAsStr(options, "fileOut");
|
||||||
|
@ -32,6 +32,7 @@ struct PipelineBaton {
|
|||||||
int overlayYOffset;
|
int overlayYOffset;
|
||||||
bool overlayTile;
|
bool overlayTile;
|
||||||
bool overlayCutout;
|
bool overlayCutout;
|
||||||
|
std::vector<sharp::InputDescriptor *> joinChannelIn;
|
||||||
int topOffsetPre;
|
int topOffsetPre;
|
||||||
int leftOffsetPre;
|
int leftOffsetPre;
|
||||||
int widthPre;
|
int widthPre;
|
||||||
@ -90,6 +91,7 @@ struct PipelineBaton {
|
|||||||
VipsOperationBoolean booleanOp;
|
VipsOperationBoolean booleanOp;
|
||||||
VipsOperationBoolean bandBoolOp;
|
VipsOperationBoolean bandBoolOp;
|
||||||
int extractChannel;
|
int extractChannel;
|
||||||
|
VipsInterpretation colourspace;
|
||||||
int tileSize;
|
int tileSize;
|
||||||
int tileOverlap;
|
int tileOverlap;
|
||||||
VipsForeignDzContainer tileContainer;
|
VipsForeignDzContainer tileContainer;
|
||||||
@ -148,6 +150,7 @@ struct PipelineBaton {
|
|||||||
booleanOp(VIPS_OPERATION_BOOLEAN_LAST),
|
booleanOp(VIPS_OPERATION_BOOLEAN_LAST),
|
||||||
bandBoolOp(VIPS_OPERATION_BOOLEAN_LAST),
|
bandBoolOp(VIPS_OPERATION_BOOLEAN_LAST),
|
||||||
extractChannel(-1),
|
extractChannel(-1),
|
||||||
|
colourspace(VIPS_INTERPRETATION_LAST),
|
||||||
tileSize(256),
|
tileSize(256),
|
||||||
tileOverlap(0),
|
tileOverlap(0),
|
||||||
tileContainer(VIPS_FOREIGN_DZ_CONTAINER_FS),
|
tileContainer(VIPS_FOREIGN_DZ_CONTAINER_FS),
|
||||||
|
BIN
test/fixtures/expected/joinChannel-cmyk.jpg
vendored
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
test/fixtures/expected/joinChannel-rgb.jpg
vendored
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
test/fixtures/expected/joinChannel-rgba.png
vendored
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
test/fixtures/expected/output.greyscale-single.jpg
vendored
Normal file
After Width: | Height: | Size: 12 KiB |
1
test/fixtures/index.js
vendored
@ -80,6 +80,7 @@ module.exports = {
|
|||||||
inputPngAlphaPremultiplicationSmall: getPath('alpha-premultiply-1024x768-paper.png'),
|
inputPngAlphaPremultiplicationSmall: getPath('alpha-premultiply-1024x768-paper.png'),
|
||||||
inputPngAlphaPremultiplicationLarge: getPath('alpha-premultiply-2048x1536-paper.png'),
|
inputPngAlphaPremultiplicationLarge: getPath('alpha-premultiply-2048x1536-paper.png'),
|
||||||
inputPngBooleanNoAlpha: getPath('bandbool.png'),
|
inputPngBooleanNoAlpha: getPath('bandbool.png'),
|
||||||
|
inputPngTestJoinChannel: getPath('testJoinChannel.png'),
|
||||||
|
|
||||||
inputWebP: getPath('4.webp'), // http://www.gstatic.com/webp/gallery/4.webp
|
inputWebP: getPath('4.webp'), // http://www.gstatic.com/webp/gallery/4.webp
|
||||||
inputWebPWithTransparency: getPath('5_webp_a.webp'), // http://www.gstatic.com/webp/gallery3/5_webp_a.webp
|
inputWebPWithTransparency: getPath('5_webp_a.webp'), // http://www.gstatic.com/webp/gallery3/5_webp_a.webp
|
||||||
|
BIN
test/fixtures/stripesH.png
vendored
Before Width: | Height: | Size: 502 B After Width: | Height: | Size: 338 B |
BIN
test/fixtures/stripesV.png
vendored
Before Width: | Height: | Size: 624 B After Width: | Height: | Size: 418 B |
BIN
test/fixtures/testJoinChannel.png
vendored
Normal file
After Width: | Height: | Size: 756 B |
@ -15,13 +15,12 @@ describe('Bandbool per-channel boolean operations', function() {
|
|||||||
it(op + ' operation', function(done) {
|
it(op + ' operation', function(done) {
|
||||||
sharp(fixtures.inputPngBooleanNoAlpha)
|
sharp(fixtures.inputPngBooleanNoAlpha)
|
||||||
.bandbool(op)
|
.bandbool(op)
|
||||||
|
.toColourspace('b-w')
|
||||||
.toBuffer(function(err, data, info) {
|
.toBuffer(function(err, data, info) {
|
||||||
// should use .toColourspace('b-w') here to get 1 channel output, when it is merged
|
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
assert.strictEqual(200, info.width);
|
assert.strictEqual(200, info.width);
|
||||||
assert.strictEqual(200, info.height);
|
assert.strictEqual(200, info.height);
|
||||||
//assert.strictEqual(1, info.channels);
|
assert.strictEqual(1, info.channels);
|
||||||
assert.strictEqual(3, info.channels);
|
|
||||||
fixtures.assertSimilar(fixtures.expected('bandbool_' + op + '_result.png'), data, done);
|
fixtures.assertSimilar(fixtures.expected('bandbool_' + op + '_result.png'), data, done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -29,6 +29,20 @@ describe('Colour space conversion', function() {
|
|||||||
.toFile(fixtures.path('output.greyscale-not.jpg'), done);
|
.toFile(fixtures.path('output.greyscale-not.jpg'), done);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Greyscale with single channel output', function(done) {
|
||||||
|
sharp(fixtures.inputJpg)
|
||||||
|
.resize(320, 240)
|
||||||
|
.greyscale()
|
||||||
|
.toColourspace('b-w')
|
||||||
|
.toBuffer(function(err, data, info) {
|
||||||
|
if (err) throw err;
|
||||||
|
assert.strictEqual(1, info.channels);
|
||||||
|
assert.strictEqual(320, info.width);
|
||||||
|
assert.strictEqual(240, info.height);
|
||||||
|
fixtures.assertSimilar(fixtures.expected('output.greyscale-single.jpg'), data, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
if (sharp.format.tiff.input.file && sharp.format.webp.output.buffer) {
|
if (sharp.format.tiff.input.file && sharp.format.webp.output.buffer) {
|
||||||
it('From 1-bit TIFF to sRGB WebP [slow]', function(done) {
|
it('From 1-bit TIFF to sRGB WebP [slow]', function(done) {
|
||||||
sharp(fixtures.inputTiff)
|
sharp(fixtures.inputTiff)
|
||||||
@ -79,4 +93,10 @@ describe('Colour space conversion', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Invalid input', function() {
|
||||||
|
assert.throws(function() {
|
||||||
|
sharp(fixtures.inputJpg)
|
||||||
|
.toColourspace(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
151
test/unit/joinChannel.js
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var assert = require('assert');
|
||||||
|
var fs = require('fs');
|
||||||
|
var sharp = require('../../index');
|
||||||
|
var fixtures = require('../fixtures');
|
||||||
|
var BluebirdPromise = require('bluebird');
|
||||||
|
|
||||||
|
describe('Image channel insertion', function() {
|
||||||
|
|
||||||
|
it('Grayscale to RGB, buffer', function(done) {
|
||||||
|
sharp(fixtures.inputPng) // gray -> red
|
||||||
|
.resize(320, 240)
|
||||||
|
.joinChannel(fixtures.inputPngTestJoinChannel) // new green channel
|
||||||
|
.joinChannel(fixtures.inputPngStripesH) // new blue channel
|
||||||
|
.toBuffer(function(err, data, info) {
|
||||||
|
if (err) throw err;
|
||||||
|
assert.strictEqual(320, info.width);
|
||||||
|
assert.strictEqual(240, info.height);
|
||||||
|
assert.strictEqual(3, info.channels);
|
||||||
|
fixtures.assertSimilar(fixtures.expected('joinChannel-rgb.jpg'), data, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Grayscale to RGB, file', function(done) {
|
||||||
|
sharp(fixtures.inputPng) // gray -> red
|
||||||
|
.resize(320, 240)
|
||||||
|
.joinChannel(fs.readFileSync(fixtures.inputPngTestJoinChannel)) // new green channel
|
||||||
|
.joinChannel(fs.readFileSync(fixtures.inputPngStripesH)) // new blue channel
|
||||||
|
.toBuffer(function(err, data, info) {
|
||||||
|
if (err) throw err;
|
||||||
|
assert.strictEqual(320, info.width);
|
||||||
|
assert.strictEqual(240, info.height);
|
||||||
|
assert.strictEqual(3, info.channels);
|
||||||
|
fixtures.assertSimilar(fixtures.expected('joinChannel-rgb.jpg'), data, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Grayscale to RGBA, buffer', function(done) {
|
||||||
|
sharp(fixtures.inputPng) // gray -> red
|
||||||
|
.resize(320, 240)
|
||||||
|
.joinChannel([fixtures.inputPngTestJoinChannel,
|
||||||
|
fixtures.inputPngStripesH,
|
||||||
|
fixtures.inputPngStripesV]) // new green + blue + alpha channel
|
||||||
|
.toColourspace(sharp.colourspace.srgb)
|
||||||
|
.toBuffer(function(err, data, info) {
|
||||||
|
if (err) throw err;
|
||||||
|
assert.strictEqual(320, info.width);
|
||||||
|
assert.strictEqual(240, info.height);
|
||||||
|
assert.strictEqual(4, info.channels);
|
||||||
|
fixtures.assertSimilar(fixtures.expected('joinChannel-rgba.png'), data, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Grayscale to RGBA, file', function(done) {
|
||||||
|
sharp(fixtures.inputPng) // gray -> red
|
||||||
|
.resize(320, 240)
|
||||||
|
.joinChannel([fs.readFileSync(fixtures.inputPngTestJoinChannel), // new green channel
|
||||||
|
fs.readFileSync(fixtures.inputPngStripesH), // new blue channel
|
||||||
|
fs.readFileSync(fixtures.inputPngStripesV)]) // new alpha channel
|
||||||
|
.toColourspace('srgb')
|
||||||
|
.toBuffer(function(err, data, info) {
|
||||||
|
if (err) throw err;
|
||||||
|
assert.strictEqual(320, info.width);
|
||||||
|
assert.strictEqual(240, info.height);
|
||||||
|
assert.strictEqual(4, info.channels);
|
||||||
|
fixtures.assertSimilar(fixtures.expected('joinChannel-rgba.png'), data, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Grayscale to CMYK, buffers', function(done) {
|
||||||
|
sharp(fixtures.inputPng) // gray -> magenta
|
||||||
|
.resize(320, 240)
|
||||||
|
.joinChannel([fs.readFileSync(fixtures.inputPngTestJoinChannel), // new cyan channel
|
||||||
|
fs.readFileSync(fixtures.inputPngStripesH), // new yellow channel
|
||||||
|
fs.readFileSync(fixtures.inputPngStripesV)]) // new black channel
|
||||||
|
.toColorspace('cmyk')
|
||||||
|
.toFormat('jpeg')
|
||||||
|
.toBuffer(function(err, data, info) {
|
||||||
|
if (err) throw err;
|
||||||
|
assert.strictEqual(320, info.width);
|
||||||
|
assert.strictEqual(240, info.height);
|
||||||
|
assert.strictEqual(4, info.channels);
|
||||||
|
fixtures.assertSimilar(fixtures.expected('joinChannel-cmyk.jpg'), data, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Join raw buffers to RGB', function(done) {
|
||||||
|
BluebirdPromise.all([
|
||||||
|
sharp(fixtures.inputPngTestJoinChannel).toColourspace('b-w').raw().toBuffer(),
|
||||||
|
sharp(fixtures.inputPngStripesH).toColourspace('b-w').raw().toBuffer()
|
||||||
|
])
|
||||||
|
.then(function(buffers) {
|
||||||
|
sharp(fixtures.inputPng)
|
||||||
|
.resize(320, 240)
|
||||||
|
.joinChannel(buffers,
|
||||||
|
{ raw: {
|
||||||
|
width: 320,
|
||||||
|
height: 240,
|
||||||
|
channels: 1
|
||||||
|
}})
|
||||||
|
.toBuffer(function(err, data, info) {
|
||||||
|
if (err) throw err;
|
||||||
|
assert.strictEqual(320, info.width);
|
||||||
|
assert.strictEqual(240, info.height);
|
||||||
|
assert.strictEqual(3, info.channels);
|
||||||
|
fixtures.assertSimilar(fixtures.expected('joinChannel-rgb.jpg'), data, done);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Grayscale to RGBA, files, two arrays', function(done) {
|
||||||
|
sharp(fixtures.inputPng) // gray -> red
|
||||||
|
.resize(320, 240)
|
||||||
|
.joinChannel([fs.readFileSync(fixtures.inputPngTestJoinChannel)]) // new green channel
|
||||||
|
.joinChannel([fs.readFileSync(fixtures.inputPngStripesH), // new blue channel
|
||||||
|
fs.readFileSync(fixtures.inputPngStripesV)]) // new alpha channel
|
||||||
|
.toColourspace('srgb')
|
||||||
|
.toBuffer(function(err, data, info) {
|
||||||
|
if (err) throw err;
|
||||||
|
assert.strictEqual(320, info.width);
|
||||||
|
assert.strictEqual(240, info.height);
|
||||||
|
assert.strictEqual(4, info.channels);
|
||||||
|
fixtures.assertSimilar(fixtures.expected('joinChannel-rgba.png'), data, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Invalid raw buffer description', function() {
|
||||||
|
assert.throws(function() {
|
||||||
|
sharp().joinChannel(fs.readFileSync(fixtures.inputPng),{raw:{}});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Invalid input', function() {
|
||||||
|
assert.throws(function() {
|
||||||
|
sharp(fixtures.inputJpg)
|
||||||
|
.joinChannel(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('No arguments', function() {
|
||||||
|
assert.throws(function() {
|
||||||
|
sharp(fixtures.inputJpg)
|
||||||
|
.joinChannel();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|