overlayWith improvements: diff sizes/formats, gravity, buffer input

This commit is contained in:
Lovell Fuller
2016-02-29 14:30:29 +00:00
parent 55f204c6f9
commit d92ea31858
21 changed files with 407 additions and 242 deletions

View File

@@ -185,4 +185,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

View File

@@ -2,6 +2,8 @@
#define SRC_COMMON_H_
#include <string>
#include <tuple>
#include <vips/vips8>
using vips::VImage;
@@ -80,6 +82,13 @@ namespace sharp {
*/
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
#endif // SRC_COMMON_H_

View File

@@ -4,34 +4,49 @@
#include "operations.h"
using vips::VImage;
using vips::VError;
namespace sharp {
/*
Alpha composite src over dst
Assumes alpha channels are already premultiplied and will be unpremultiplied after
Alpha composite src over dst with given gravity.
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;
// 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 srcAlpha = src[src.bands() - 1] * (1.0 / 255.0);
// Split dst into non-alpha and alpha channels
VImage dstWithoutAlpha;
VImage dstAlpha;
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();
}
VImage dstWithoutAlpha = dst.extract_band(0, VImage::option()->set("n", dst.bands() - 1));
VImage dstAlpha = dst[dst.bands() - 1] * (1.0 / 255.0);
//
// Compute normalized output alpha channel:

View File

@@ -8,10 +8,10 @@ using vips::VImage;
namespace sharp {
/*
Composite images `src` and `dst` with premultiplied alpha channel and output
image with premultiplied alpha.
Alpha composite src over dst with given gravity.
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.

View File

@@ -1,10 +1,12 @@
#include <tuple>
#include <algorithm>
#include <utility>
#include <cmath>
#include <tuple>
#include <utility>
#include <vips/vips8>
#include <node.h>
#include <node_buffer.h>
#include <vips/vips8>
#include "nan.h"
@@ -62,133 +64,22 @@ using sharp::IsWebp;
using sharp::IsTiff;
using sharp::IsDz;
using sharp::FreeCallback;
using sharp::CalculateCrop;
using sharp::counterProcess;
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 {
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) {
if (baton->bufferInLength > 0) {
SaveToPersistent("bufferIn", bufferIn);
}
if (baton->overlayBufferInLength > 0) {
SaveToPersistent("overlayBufferIn", overlayBufferIn);
}
}
~PipelineWorker() {}
@@ -508,11 +399,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 shouldBlur = baton->blurSigma != 0.0;
bool shouldSharpen = baton->sharpenRadius != 0;
bool shouldThreshold = baton->threshold != 0;
bool hasOverlay = !baton->overlayPath.empty();
bool shouldPremultiplyAlpha = HasAlpha(image) &&
(shouldAffineTransform || shouldBlur || shouldSharpen || hasOverlay);
@@ -634,38 +533,41 @@ class PipelineWorker : public AsyncWorker {
// Composite with overlay, if present
if (hasOverlay) {
VImage overlayImage;
ImageType overlayImageType = DetermineImageType(baton->overlayPath.data());
if (overlayImageType != ImageType::UNKNOWN) {
overlayImage = VImage::new_from_file(
baton->overlayPath.data(),
VImage::option()->set("access", baton->accessMethod)
);
ImageType overlayImageType = ImageType::UNKNOWN;
if (baton->overlayBufferInLength > 0) {
// Overlay with image from buffer
overlayImageType = DetermineImageType(baton->overlayBufferIn, baton->overlayBufferInLength);
if (overlayImageType != ImageType::UNKNOWN) {
try {
overlayImage = VImage::new_from_buffer(baton->overlayBufferIn, baton->overlayBufferInLength,
nullptr, VImage::option()->set("access", baton->accessMethod));
} catch (...) {
(baton->err).append("Overlay buffer has corrupt header");
overlayImageType = ImageType::UNKNOWN;
}
} else {
(baton->err).append("Overlay buffer contains unsupported image format");
}
} else {
(baton->err).append("Overlay image is of an unsupported image format");
// 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();
}
if (image.format() != VIPS_FORMAT_UCHAR && image.format() != VIPS_FORMAT_FLOAT) {
(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
// Ensure overlay is premultiplied sRGB
overlayImage = overlayImage.colourspace(VIPS_INTERPRETATION_sRGB).premultiply();
image = Composite(overlayImage, image);
// Composite images with given gravity
image = Composite(overlayImage, image, baton->overlayGravity);
}
// Reverse premultiplication after all transformations:
@@ -708,6 +610,9 @@ class PipelineWorker : public AsyncWorker {
SetExifOrientation(image, baton->withMetadataOrientation);
}
// Number of channels used in output image
baton->channels = image.bands();
// Output
if (baton->fileOut == "") {
// Buffer output
@@ -728,6 +633,7 @@ class PipelineWorker : public AsyncWorker {
area->free_fn = nullptr;
vips_area_unref(area);
baton->formatOut = "jpeg";
baton->channels = std::min(baton->channels, 3);
} else if (baton->formatOut == "png" || (baton->formatOut == "input" && inputImageType == ImageType::PNG)) {
// Write PNG to buffer
VipsArea *area = VIPS_AREA(image.pngsave_buffer(VImage::option()
@@ -800,6 +706,7 @@ class PipelineWorker : public AsyncWorker {
->set("interlace", baton->progressive)
);
baton->formatOut = "jpeg";
baton->channels = std::min(baton->channels, 3);
} else if (baton->formatOut == "png" || isPng || (matchInput && inputImageType == ImageType::PNG)) {
// Write PNG to file
image.pngsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
@@ -824,6 +731,7 @@ class PipelineWorker : public AsyncWorker {
->set("compression", VIPS_FOREIGN_TIFF_COMPRESSION_JPEG)
);
baton->formatOut = "tiff";
baton->channels = std::min(baton->channels, 3);
} else if (baton->formatOut == "dz" || IsDz(baton->fileOut)) {
// Write DZ to file
image.dzsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
@@ -838,8 +746,6 @@ class PipelineWorker : public AsyncWorker {
return Error();
}
}
// Number of channels used in output image
baton->channels = image.bands();
} catch (VError const &err) {
(baton->err).append(err.what());
}
@@ -890,10 +796,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) {
GetFromPersistent("bufferIn");
}
if (baton->overlayBufferInLength > 0) {
GetFromPersistent("overlayBufferIn");
}
delete baton;
// Decrement processing task counter
@@ -944,46 +853,6 @@ class PipelineWorker : public AsyncWorker {
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
*/
@@ -1088,7 +957,14 @@ NAN_METHOD(pipeline) {
baton->background[i] = To<int32_t>(Get(background, i).ToLocalChecked()).FromJust();
}
// 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
baton->withoutEnlargement = attrAs<bool>(options, "withoutEnlargement");
baton->gravity = attrAs<int32_t>(options, "gravity");
@@ -1131,7 +1007,7 @@ NAN_METHOD(pipeline) {
// Join queue for worker thread
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
g_atomic_int_inc(&counterQueue);

View File

@@ -5,4 +5,125 @@
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;
std::string 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 gravity;
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;
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),
overlayBufferInLength(0),
overlayGravity(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;
}
};
#endif // SRC_PIPELINE_H_