Major rewrite and therefore API change. Despite the name, image sharpening is now optional, plus there are other new options. Can now handle buffers in and out. Doubled performance in certain cases. Closes #3. Closes #4.

This commit is contained in:
Lovell Fuller
2014-01-19 21:38:37 +00:00
parent be8f35d830
commit d509458ba1
6 changed files with 454 additions and 219 deletions

View File

@@ -2,58 +2,93 @@
#include <node_buffer.h>
#include <math.h>
#include <string>
#include <string.h>
#include <vips/vips.h>
using namespace v8;
using namespace node;
struct ResizeBaton {
std::string src;
std::string dst;
struct resize_baton {
std::string file_in;
void* buffer_in;
size_t buffer_in_len;
std::string file_out;
void* buffer_out;
size_t buffer_out_len;
int cols;
int rows;
int width;
int height;
bool crop;
int embed;
bool sharpen;
bool progessive;
VipsAccess access_method;
std::string err;
Persistent<Function> callback;
ResizeBaton() : buffer_out_len(0) {}
resize_baton(): buffer_in_len(0), buffer_out_len(0) {}
};
bool EndsWith(std::string const &str, std::string const &end) {
typedef enum {
JPEG,
PNG
} ImageType;
unsigned char MARKER_JPEG[] = {0xff, 0xd8};
unsigned char MARKER_PNG[] = {0x89, 0x50};
bool ends_with(std::string const &str, std::string const &end) {
return str.length() >= end.length() && 0 == str.compare(str.length() - end.length(), end.length(), end);
}
bool IsJpeg(std::string const &str) {
return EndsWith(str, ".jpg") || EndsWith(str, ".jpeg");
bool is_jpeg(std::string const &str) {
return ends_with(str, ".jpg") || ends_with(str, ".jpeg");
}
bool IsPng(std::string const &str) {
return EndsWith(str, ".png");
bool is_png(std::string const &str) {
return ends_with(str, ".png");
}
void ResizeAsync(uv_work_t *work) {
ResizeBaton* baton = static_cast<ResizeBaton*>(work->data);
void resize_error(resize_baton *baton, VipsImage *unref) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
g_object_unref(unref);
vips_thread_shutdown();
return;
}
void resize_async(uv_work_t *work) {
resize_baton* baton = static_cast<resize_baton*>(work->data);
ImageType inputImageType = JPEG;
VipsImage *in = vips_image_new();
if (IsJpeg(baton->src)) {
vips_jpegload((baton->src).c_str(), &in, NULL);
} else if (IsPng(baton->src)) {
vips_pngload((baton->src).c_str(), &in, NULL);
if (baton->buffer_in_len > 1) {
if (memcmp(MARKER_JPEG, baton->buffer_in, 2) == 0) {
if (vips_jpegload_buffer(baton->buffer_in, baton->buffer_in_len, &in, "access", baton->access_method, NULL)) {
return resize_error(baton, in);
}
} else if(memcmp(MARKER_PNG, baton->buffer_in, 2) == 0) {
inputImageType = PNG;
if (vips_pngload_buffer(baton->buffer_in, baton->buffer_in_len, &in, "access", baton->access_method, NULL)) {
return resize_error(baton, in);
}
}
} else if (is_jpeg(baton->file_in)) {
if (vips_jpegload((baton->file_in).c_str(), &in, "access", baton->access_method, NULL)) {
return resize_error(baton, in);
}
} else if (is_png(baton->file_in)) {
inputImageType = PNG;
if (vips_pngload((baton->file_in).c_str(), &in, "access", baton->access_method, NULL)) {
return resize_error(baton, in);
}
} else {
(baton->err).append("Unsupported input file type");
return;
}
if (in == NULL) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
resize_error(baton, in);
(baton->err).append("Unsupported input " + baton->file_in);
return;
}
double xfactor = static_cast<double>(in->Xsize) / std::max(baton->cols, 1);
double yfactor = static_cast<double>(in->Ysize) / std::max(baton->rows, 1);
double xfactor = static_cast<double>(in->Xsize) / std::max(baton->width, 1);
double yfactor = static_cast<double>(in->Ysize) / std::max(baton->height, 1);
double factor = baton->crop ? std::min(xfactor, yfactor) : std::max(xfactor, yfactor);
factor = std::max(factor, 1.0);
int shrink = floor(factor);
@@ -61,7 +96,7 @@ void ResizeAsync(uv_work_t *work) {
// Try to use libjpeg shrink-on-load
int shrink_on_load = 1;
if (IsJpeg(baton->src)) {
if (inputImageType == JPEG) {
if (shrink >= 8) {
residual = residual * shrink / 8;
shrink_on_load = 8;
@@ -76,129 +111,117 @@ void ResizeAsync(uv_work_t *work) {
shrink = 1;
}
if (shrink_on_load > 1) {
if (vips_jpegload((baton->src).c_str(), &in, "shrink", shrink_on_load, NULL)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
g_object_unref(in);
return;
g_object_unref(in);
in = vips_image_new();
if (baton->buffer_in_len > 1) {
if (vips_jpegload_buffer(baton->buffer_in, baton->buffer_in_len, &in, "shrink", shrink_on_load, NULL)) {
return resize_error(baton, in);
}
} else {
if (vips_jpegload((baton->file_in).c_str(), &in, "shrink", shrink_on_load, NULL)) {
return resize_error(baton, in);
}
}
}
}
VipsImage* img = in;
VipsImage* t[4];
if (im_open_local_array(img, t, 4, "temp", "p")) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
g_object_unref(in);
return;
}
VipsImage *shrunk = vips_image_new();
if (shrink > 1) {
// Use vips_shrink with the integral reduction
if (vips_shrink(img, &t[0], shrink, shrink, NULL)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
g_object_unref(in);
return;
if (vips_shrink(in, &shrunk, shrink, shrink, NULL)) {
return resize_error(baton, in);
}
} else {
t[0] = img;
}
// Use vips_affine with the remaining float part using bilinear interpolation
if (vips_affine(t[0], &t[1], residual, 0, 0, residual, "interpolate", vips_interpolate_bilinear_static(), NULL)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
g_object_unref(in);
return;
}
img = t[1];
if (baton->crop) {
int width = std::min(img->Xsize, baton->cols);
int height = std::min(img->Ysize, baton->rows);
int left = (img->Xsize - width + 1) / 2;
int top = (img->Ysize - height + 1) / 2;
if (im_extract_area(img, t[2], left, top, width, height)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
g_object_unref(in);
return;
}
img = t[2];
} else {
int left = (baton->cols - img->Xsize) / 2;
int top = (baton->rows - img->Ysize) / 2;
if (im_embed(img, t[2], baton->embed, left, top, baton->cols, baton->rows)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
g_object_unref(in);
return;
}
img = t[2];
}
// Mild sharpen
INTMASK* sharpen = im_create_imaskv("sharpen", 3, 3,
-1, -1, -1,
-1, 32, -1,
-1, -1, -1);
sharpen->scale = 24;
if (im_conv(img, t[3], sharpen)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
g_object_unref(in);
return;
}
img = t[3];
if (baton->dst == "__jpeg") {
// Write JPEG to buffer
if (vips_jpegsave_buffer(img, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", 80, "optimize_coding", TRUE, NULL)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
}
} else if (baton->dst == "__png") {
// Write PNG to buffer
if (vips_pngsave_buffer(img, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "compression", 6, "interlace", FALSE, NULL)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
}
} else if (EndsWith(baton->dst, ".jpg") || EndsWith(baton->dst, ".jpeg")) {
// Write JPEG to file
if (vips_foreign_save(img, baton->dst.c_str(), "strip", TRUE, "Q", 80, "optimize_coding", TRUE, NULL)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
}
} else if (EndsWith(baton->dst, ".png")) {
// Write PNG to file
if (vips_foreign_save(img, baton->dst.c_str(), "strip", TRUE, "compression", 6, "interlace", FALSE, NULL)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
}
} else {
(baton->err).append("Unsupported output file type");
vips_copy(in, &shrunk, NULL);
}
g_object_unref(in);
// Use vips_affine with the remaining float part using bilinear interpolation
VipsImage *affined = vips_image_new();
if (vips_affine(shrunk, &affined, residual, 0, 0, residual, "interpolate", vips_interpolate_bilinear_static(), NULL)) {
return resize_error(baton, shrunk);
}
g_object_unref(shrunk);
VipsImage *canvased = vips_image_new();
if (baton->crop) {
int width = std::min(affined->Xsize, baton->width);
int height = std::min(affined->Ysize, baton->height);
int left = (affined->Xsize - width + 1) / 2;
int top = (affined->Ysize - height + 1) / 2;
if (vips_extract_area(affined, &canvased, left, top, width, height, NULL)) {
return resize_error(baton, affined);
}
} else {
int left = (baton->width - affined->Xsize) / 2;
int top = (baton->height - affined->Ysize) / 2;
if (vips_embed(affined, &canvased, baton->embed, left, top, baton->width, baton->height, NULL)) {
return resize_error(baton, affined);
}
}
g_object_unref(affined);
// Mild sharpen
VipsImage *sharpened = vips_image_new();
if (baton->sharpen) {
INTMASK* sharpen = im_create_imaskv("sharpen", 3, 3,
-1, -1, -1,
-1, 32, -1,
-1, -1, -1);
sharpen->scale = 24;
if (im_conv(canvased, sharpened, sharpen)) {
return resize_error(baton, canvased);
}
} else {
vips_copy(canvased, &sharpened, NULL);
}
g_object_unref(canvased);
if (baton->file_out == "__jpeg") {
// Write JPEG to buffer
if (vips_jpegsave_buffer(canvased, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", 80, "optimize_coding", TRUE, "interlace", baton->progessive, NULL)) {
return resize_error(baton, canvased);
}
} else if (baton->file_out == "__png") {
// Write PNG to buffer
if (vips_pngsave_buffer(canvased, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "compression", 6, "interlace", baton->progessive, NULL)) {
return resize_error(baton, canvased);
}
} else if (is_jpeg(baton->file_out)) {
// Write JPEG to file
if (vips_jpegsave(canvased, baton->file_out.c_str(), "strip", TRUE, "Q", 80, "optimize_coding", TRUE, "interlace", baton->progessive, NULL)) {
return resize_error(baton, canvased);
}
} else if (is_png(baton->file_out)) {
// Write PNG to file
if (vips_pngsave(canvased, baton->file_out.c_str(), "strip", TRUE, "compression", 6, "interlace", baton->progessive, NULL)) {
return resize_error(baton, canvased);
}
} else {
(baton->err).append("Unsupported output " + baton->file_out);
}
g_object_unref(canvased);
vips_thread_shutdown();
}
void ResizeAsyncAfter(uv_work_t *work, int status) {
void resize_async_after(uv_work_t *work, int status) {
HandleScope scope;
ResizeBaton *baton = static_cast<ResizeBaton*>(work->data);
resize_baton *baton = static_cast<resize_baton*>(work->data);
Local<Value> null = Local<Value>::New(Null());
Local<Value> argv[2] = {null, null};
Handle<Value> argv[2] = { Null(), Null() };
if (!baton->err.empty()) {
// Error
argv[0] = String::New(baton->err.data(), baton->err.size());
} else if (baton->buffer_out_len > 0) {
// Buffer
Buffer *buffer = Buffer::New((const char*)(baton->buffer_out), baton->buffer_out_len);
argv[1] = Local<Object>::New(buffer->handle_);
vips_free(baton->buffer_out);
Buffer *slowBuffer = Buffer::New(baton->buffer_out_len);
memcpy(Buffer::Data(slowBuffer), baton->buffer_out, baton->buffer_out_len);
Local<Object> globalObj = Context::GetCurrent()->Global();
Local<Function> bufferConstructor = Local<Function>::Cast(globalObj->Get(String::New("Buffer")));
Handle<Value> constructorArgs[3] = { slowBuffer->handle_, v8::Integer::New(baton->buffer_out_len), v8::Integer::New(0) };
argv[1] = bufferConstructor->NewInstance(3, constructorArgs);
g_free(baton->buffer_out);
}
baton->callback->Call(Context::GetCurrent()->Global(), 2, argv);
@@ -207,15 +230,20 @@ void ResizeAsyncAfter(uv_work_t *work, int status) {
delete work;
}
Handle<Value> Resize(const Arguments& args) {
Handle<Value> resize(const Arguments& args) {
HandleScope scope;
ResizeBaton *baton = new ResizeBaton;
baton->src = *String::Utf8Value(args[0]->ToString());
baton->dst = *String::Utf8Value(args[1]->ToString());
baton->cols = args[2]->Int32Value();
baton->rows = args[3]->Int32Value();
Local<String> canvas = args[4]->ToString();
resize_baton *baton = new resize_baton;
baton->file_in = *String::Utf8Value(args[0]->ToString());
if (args[1]->IsObject()) {
Local<Object> buffer = args[1]->ToObject();
baton->buffer_in = Buffer::Data(buffer);
baton->buffer_in_len = Buffer::Length(buffer);
}
baton->file_out = *String::Utf8Value(args[2]->ToString());
baton->width = args[3]->Int32Value();
baton->height = args[4]->Int32Value();
Local<String> canvas = args[5]->ToString();
if (canvas->Equals(String::NewSymbol("c"))) {
baton->crop = true;
} else if (canvas->Equals(String::NewSymbol("w"))) {
@@ -225,12 +253,15 @@ Handle<Value> Resize(const Arguments& args) {
baton->crop = false;
baton->embed = 0;
}
baton->callback = Persistent<Function>::New(Local<Function>::Cast(args[5]));
baton->sharpen = args[6]->BooleanValue();
baton->progessive = args[7]->BooleanValue();
baton->access_method = args[8]->BooleanValue() ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM;
baton->callback = Persistent<Function>::New(Local<Function>::Cast(args[9]));
uv_work_t *work = new uv_work_t;
work->data = baton;
uv_queue_work(uv_default_loop(), work, ResizeAsync, (uv_after_work_cb)ResizeAsyncAfter);
return Undefined();
uv_queue_work(uv_default_loop(), work, resize_async, (uv_after_work_cb)resize_async_after);
return scope.Close(Undefined());
}
static void at_exit(void* arg) {
@@ -242,7 +273,7 @@ extern "C" void init(Handle<Object> target) {
HandleScope scope;
vips_init("");
AtExit(at_exit);
NODE_SET_METHOD(target, "resize", Resize);
NODE_SET_METHOD(target, "resize", resize);
};
NODE_MODULE(sharp, init);