Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add joinChannel and toColourspace commands #513

Merged
merged 13 commits into from
Aug 17, 2016
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
* `width`: Number of pixels wide
* `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
* `density`: Number of pixels per inch (DPI), if present
* `hasProfile`: Boolean indicating the presence of an embedded ICC profile
Expand Down Expand Up @@ -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.

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()

Expand Down Expand Up @@ -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)

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
sharp(input)
Expand All @@ -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.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to include information that the images to be joined must be the same dimension?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They actually don't have to be of the same dimension -- vips will expand the image to fit the largest channel.

#### bandbool(operation)

Perform a bitwise boolean operation on all input image channels (bands) to produce a single channel output image.
Expand Down
37 changes: 37 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ var Sharp = function(input, options) {
normalize: 0,
booleanBufferIn: null,
booleanFileIn: '',
joinChannelIn: [],
// overlay
overlayGravity: 0,
overlayXOffset : -1,
Expand All @@ -109,6 +110,7 @@ var Sharp = function(input, options) {
tileSize: 256,
tileOverlap: 0,
extractChannel: -1,
colourspace: 'srgb',
// Function to notify of queue length changes
queueListener: function(queueLength) {
module.exports.queue.emit('change', queueLength);
Expand Down Expand Up @@ -421,6 +423,20 @@ Sharp.prototype.overlayWith = function(overlay, options) {
return this;
};

/*
Add another color channel to the image
*/
Sharp.prototype.joinChannel = function(images, options) {
if (Array.isArray(images)) {
images.forEach(function(image, index) {
this.options.joinChannelIn[index] = this._createInputDescriptor(image, options);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this use push() rather than be index based? This would allow .joinChannel(array1).joinChannel(array2).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Woops. I'll make that change.

}, this);
} else {
this.options.joinChannelIn.push(this._createInputDescriptor(images, options));
}
return this;
};

/*
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
Expand Down Expand Up @@ -629,6 +645,18 @@ Sharp.prototype.greyscale = function(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) {
this.options.progressive = isBoolean(progressive) ? progressive : true;
return this;
Expand Down Expand Up @@ -817,6 +845,15 @@ module.exports.bool = {
or: 'or',
eor: 'eor'
};
// Colourspaces
module.exports.colourspace = {
multiband: 'multiband',
'b-w': 'b-w',
bw: 'b-w',
cmyk: 'cmyk',
srgb: 'srgb'
};
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs suggest other colourspaces are available, e.g. lab. Should we include them all?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The trouble with other colorspaces supported by vips is that its hard to write images in those formats. You can always use the string nick to access them, but by not putting them in the colourspace enum it signals that they aren't nicely "supported". Happy to add them if you feel otherwise...

module.exports.colorspace = module.exports.colourspace;

/*
Resize image to width x height pixels
Expand Down
9 changes: 9 additions & 0 deletions src/common.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions src/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,11 @@ namespace sharp {
*/
VipsOperationBoolean GetBooleanOperation(std::string const opStr);

/*
Get interpretation type from string
*/
VipsInterpretation GetInterpretation(std::string const typeStr);

} // namespace sharp

#endif // SRC_COMMON_H_
70 changes: 57 additions & 13 deletions src/pipeline.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <utility>
#include <memory>
#include <numeric>
#include <map>

#include <vips/vips8>
#include <node.h>
Expand Down Expand Up @@ -39,8 +40,15 @@ class PipelineWorker : public Nan::AsyncWorker {
// Increment processing task counter
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
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 {
// Open input
Expand Down Expand Up @@ -266,18 +274,18 @@ class PipelineWorker : public Nan::AsyncWorker {
if (sharp::HasProfile(image)) {
// Convert to sRGB using embedded profile
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("intent", VIPS_INTENT_PERCEPTUAL)
);
} catch(...) {
// Ignore failure of embedded profile
}
} else if (image.interpretation() == VIPS_INTERPRETATION_CMYK) {
// Convert to sRGB using default CMYK profile from http://www.argyllcms.com/cmyk.icm
std::string cmykProfile = baton->iccProfilePath + "cmyk.icm";
image = image.icc_transform(const_cast<char*>(srgbProfile.data()), VImage::option()
->set("input_profile", cmykProfile.data())
image = image.icc_transform(
const_cast<char*>(profileMap[VIPS_INTERPRETATION_sRGB].data()), VImage::option()
->set("input_profile", profileMap[VIPS_INTERPRETATION_CMYK].data())
->set("intent", VIPS_INTENT_PERCEPTUAL)
);
}
Expand Down Expand Up @@ -420,6 +428,19 @@ class PipelineWorker : public Nan::AsyncWorker {
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
if (image.width() != baton->width || image.height() != baton->height) {
if (baton->canvas == Canvas::EMBED) {
Expand Down Expand Up @@ -654,12 +675,15 @@ class PipelineWorker : public Nan::AsyncWorker {
if (sharp::Is16Bit(image.interpretation())) {
image = image.cast(VIPS_FORMAT_USHORT);
}
if (image.interpretation() != VIPS_INTERPRETATION_sRGB) {
image = image.colourspace(VIPS_INTERPRETATION_sRGB);
// Transform colours from embedded profile to sRGB profile
if (baton->withMetadata && sharp::HasProfile(image)) {
image = image.icc_transform(const_cast<char*>(srgbProfile.data()), VImage::option()
->set("embedded", TRUE)
if (image.interpretation() != baton->colourspace) {
// Need to convert image
image = image.colourspace(baton->colourspace);
// Transform colours from embedded profile to output profile
if (baton->withMetadata &&
sharp::HasProfile(image) &&
profileMap[baton->colourspace] != std::string()) {
image = image.icc_transform(const_cast<char*>(profileMap[baton->colourspace].data()),
VImage::option()->set("embedded", TRUE)
);
}
}
Expand Down Expand Up @@ -693,7 +717,11 @@ class PipelineWorker : public Nan::AsyncWorker {
area->free_fn = nullptr;
vips_area_unref(area);
baton->formatOut = "jpeg";
baton->channels = std::min(baton->channels, 3);
if(baton->colourspace == VIPS_INTERPRETATION_CMYK) {
baton->channels = std::min(baton->channels, 4);
} else {
baton->channels = std::min(baton->channels, 3);
}
} else if (baton->formatOut == "png" || (baton->formatOut == "input" && inputImageType == ImageType::PNG)) {
// Strip profile
if (!baton->withMetadata) {
Expand Down Expand Up @@ -1021,6 +1049,19 @@ NAN_METHOD(pipeline) {
baton->crop = AttrTo<int32_t>(options, "crop");
baton->kernel = AttrAsStr(options, "kernel");
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
baton->flatten = AttrTo<bool>(options, "flatten");
baton->negate = AttrTo<bool>(options, "negate");
Expand Down Expand Up @@ -1077,6 +1118,9 @@ NAN_METHOD(pipeline) {
baton->optimiseScans = AttrTo<bool>(options, "optimiseScans");
baton->withMetadata = AttrTo<bool>(options, "withMetadata");
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
baton->formatOut = AttrAsStr(options, "formatOut");
baton->fileOut = AttrAsStr(options, "fileOut");
Expand Down
3 changes: 3 additions & 0 deletions src/pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ struct PipelineBaton {
int overlayYOffset;
bool overlayTile;
bool overlayCutout;
std::vector<sharp::InputDescriptor *> joinChannelIn;
int topOffsetPre;
int leftOffsetPre;
int widthPre;
Expand Down Expand Up @@ -90,6 +91,7 @@ struct PipelineBaton {
VipsOperationBoolean booleanOp;
VipsOperationBoolean bandBoolOp;
int extractChannel;
VipsInterpretation colourspace;
int tileSize;
int tileOverlap;
VipsForeignDzContainer tileContainer;
Expand Down Expand Up @@ -148,6 +150,7 @@ struct PipelineBaton {
booleanOp(VIPS_OPERATION_BOOLEAN_LAST),
bandBoolOp(VIPS_OPERATION_BOOLEAN_LAST),
extractChannel(-1),
colourspace(VIPS_INTERPRETATION_LAST),
tileSize(256),
tileOverlap(0),
tileContainer(VIPS_FOREIGN_DZ_CONTAINER_FS),
Expand Down
Binary file added test/fixtures/expected/joinChannel-cmyk.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/expected/joinChannel-rgb.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/expected/joinChannel-rgba.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions test/fixtures/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ module.exports = {
inputPngAlphaPremultiplicationSmall: getPath('alpha-premultiply-1024x768-paper.png'),
inputPngAlphaPremultiplicationLarge: getPath('alpha-premultiply-2048x1536-paper.png'),
inputPngBooleanNoAlpha: getPath('bandbool.png'),
inputPngTestJoinChannel: getPath('testJoinChannel.png'),

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
Expand Down
Binary file modified test/fixtures/stripesH.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/fixtures/stripesV.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/testJoinChannel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 2 additions & 3 deletions test/unit/bandbool.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,12 @@ describe('Bandbool per-channel boolean operations', function() {
it(op + ' operation', function(done) {
sharp(fixtures.inputPngBooleanNoAlpha)
.bandbool(op)
.toColourspace('b-w')
.toBuffer(function(err, data, info) {
// should use .toColourspace('b-w') here to get 1 channel output, when it is merged
if (err) throw err;
assert.strictEqual(200, info.width);
assert.strictEqual(200, info.height);
//assert.strictEqual(1, info.channels);
assert.strictEqual(3, info.channels);
assert.strictEqual(1, info.channels);
fixtures.assertSimilar(fixtures.expected('bandbool_' + op + '_result.png'), data, done);
});
});
Expand Down
20 changes: 20 additions & 0 deletions test/unit/colourspace.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ describe('Colour space conversion', function() {
.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) {
it('From 1-bit TIFF to sRGB WebP [slow]', function(done) {
sharp(fixtures.inputTiff)
Expand Down Expand Up @@ -79,4 +93,10 @@ describe('Colour space conversion', function() {
});
});

it('Invalid input', function() {
assert.throws(function() {
sharp(fixtures.inputJpg)
.toColourspace(null);
});
});
});
Loading