diff --git a/Readme.md b/Readme.md index aee0467..dfd068d 100644 --- a/Readme.md +++ b/Readme.md @@ -64,6 +64,21 @@ ctx.drawImage(img, 50, 0, 50, 50); ctx.drawImage(img, 100, 0, 50, 50); ``` +### Image#dataMode + +node-canvas adds `Image#dataMode` support, which can be used to opt-in to mime data tracking of images (currently only JPEGs). + +When mime data is tracked, in PDF mode JPEGs can be embedded directly into the output, rather than being re-encoded into PNG. This can drastically reduce filesize, and speed up rendering. + +```javascript +var img = new Image; +img.dataMode = Image.MODE_IMAGE; // Only image data tracked +img.dataMode = Image.MODE_MIME; // Only mime data tracked +img.dataMode = Image.MODE_MIME | Image.MODE_IMAGE; // Both are tracked +``` + +If image data is not tracked, and the Image is drawn to an image rather than a PDF canvas, the output will be junk. Enabling mime data tracking has no benefits (only a slow down) unless you are generating a PDF. + ### Canvas#createPNGStream() To create a `PNGStream` simply call `canvas.createPNGStream()`, and the stream will start to emit _data_ events, finally emitting _end_ when finished. If an exception occurs the _error_ event is emitted. diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 97f9fcc..3952563 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -538,10 +538,6 @@ Context2d::DrawImage(const Arguments &args) { if (args.Length() < 3) return ThrowException(Exception::TypeError(String::New("invalid arguments"))); -#if CAIRO_VERSION_MINOR < 10 - return ThrowException(Exception::Error(String::New("drawImage() needs cairo >= 1.10.0"))); -#else - int sx = 0 , sy = 0 , sw = 0 @@ -611,14 +607,10 @@ Context2d::DrawImage(const Arguments &args) { // Start draw cairo_save(ctx); - // Source surface - // TODO: only works with cairo >= 1.10.0 - cairo_surface_t *src = cairo_surface_create_for_rectangle( - surface - , sx - , sy - , sw - , sh); + context->savePath(); + cairo_rectangle(ctx, dx, dy, dw, dh); + cairo_clip(ctx); + context->restorePath(); // Scale src if (dw != sw || dh != sh) { @@ -630,14 +622,11 @@ Context2d::DrawImage(const Arguments &args) { } // Paint - cairo_set_source_surface(ctx, src, dx, dy); + cairo_set_source_surface(ctx, surface, dx - sx, dy - sy); cairo_pattern_set_filter(cairo_get_source(ctx), context->state->patternQuality); cairo_paint_with_alpha(ctx, context->state->globalAlpha); cairo_restore(ctx); - cairo_surface_destroy(src); - -#endif return Undefined(); } diff --git a/src/Image.cc b/src/Image.cc index 894402a..a4aa048 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -12,11 +12,6 @@ #include #include -#ifdef HAVE_JPEG -#include -#include -#endif - #ifdef HAVE_GIF #include typedef struct { @@ -58,6 +53,11 @@ Image::Initialize(Handle target) { proto->SetAccessor(String::NewSymbol("height"), GetHeight); proto->SetAccessor(String::NewSymbol("onload"), GetOnload, SetOnload); proto->SetAccessor(String::NewSymbol("onerror"), GetOnerror, SetOnerror); +#if CAIRO_VERSION_MINOR >= 10 + proto->SetAccessor(String::NewSymbol("dataMode"), GetDataMode, SetDataMode); + constructor->Set(String::NewSymbol("MODE_IMAGE"), Number::New(1)); + constructor->Set(String::NewSymbol("MODE_MIME"), Number::New(2)); +#endif target->Set(String::NewSymbol("Image"), constructor->GetFunction()); } @@ -69,6 +69,7 @@ Handle Image::New(const Arguments &args) { HandleScope scope; Image *img = new Image; + img->data_mode = DATA_IMAGE; img->Wrap(args.This()); return args.This(); } @@ -84,6 +85,44 @@ Image::GetComplete(Local, const AccessorInfo &info) { return scope.Close(Boolean::New(Image::COMPLETE == img->state)); } +#if CAIRO_VERSION_MINOR >= 10 + +/* + * Get dataMode. + */ + +Handle +Image::GetDataMode(Local, const AccessorInfo &info) { + HandleScope scope; + Image *img = ObjectWrap::Unwrap(info.This()); + return scope.Close(Number::New(img->data_mode)); +} + +/* + * Set dataMode. + */ + +void +Image::SetDataMode(Local, Local val, const AccessorInfo &info) { + if (val->IsNumber()) { + Image *img = ObjectWrap::Unwrap(info.This()); + int mode = val->Uint32Value(); + switch (mode) { + case 1: + img->data_mode = DATA_IMAGE; + break; + case 2: + img->data_mode = DATA_MIME; + break; + case 3: + img->data_mode = DATA_IMAGE_AND_MIME; + break; + } + } +} + +#endif + /* * Get width. */ @@ -116,6 +155,30 @@ Image::GetSource(Local, const AccessorInfo &info) { return scope.Close(String::New(img->filename ? img->filename : "")); } +/* + * Clean up assets and variables. + */ + +void +Image::clearData() { + if (_surface) { + cairo_surface_destroy(_surface); + V8::AdjustAmountOfExternalAllocatedMemory(-_data_len); + _data_len = 0; + _surface = NULL; + } + + free(_data); + _data = NULL; + + width = height = 0; + + free(filename); + filename = NULL; + + state = DEFAULT; +} + /* * Set src path. */ @@ -126,6 +189,8 @@ Image::SetSource(Local, Local val, const AccessorInfo &info) { Image *img = ObjectWrap::Unwrap(info.This()); cairo_status_t status = CAIRO_STATUS_READ_ERROR; + img->clearData(); + // url string if (val->IsString()) { String::AsciiValue src(val); @@ -159,7 +224,23 @@ Image::loadFromBuffer(uint8_t *buf, unsigned len) { if (isGIF(buf)) return loadGIFFromBuffer(buf, len); #endif #ifdef HAVE_JPEG - if (isJPEG(buf)) return loadJPEGFromBuffer(buf, len); +#if CAIRO_VERSION_MINOR < 10 + if (isJPEG(buf)) return loadJPEGFromBuffer(buf, len); +#else + if (isJPEG(buf)) { + switch (data_mode) { + case DATA_IMAGE: + return loadJPEGFromBuffer(buf, len); + case DATA_MIME: + return decodeJPEGBufferIntoMimeSurface(buf, len); + case DATA_IMAGE_AND_MIME: + cairo_status_t status; + status = loadJPEGFromBuffer(buf, len); + if (status) return status; + return assignDataAsMime(buf, len, CAIRO_MIME_TYPE_JPEG); + } + } +#endif #endif return CAIRO_STATUS_READ_ERROR; } @@ -242,6 +323,7 @@ Image::SetOnerror(Local, Local val, const AccessorInfo &info) { Image::Image() { filename = NULL; _data = NULL; + _data_len = 0; _surface = NULL; width = height = 0; state = DEFAULT; @@ -252,13 +334,7 @@ Image::Image() { */ Image::~Image() { - if (_surface) { - V8::AdjustAmountOfExternalAllocatedMemory(-4 * width * height); - cairo_surface_destroy(_surface); - } - - free(_data); - free(filename); + clearData(); } /* @@ -285,8 +361,8 @@ Image::loaded() { width = cairo_image_surface_get_width(_surface); height = cairo_image_surface_get_height(_surface); - // TODO: adjust accordingly when re-assigned src - V8::AdjustAmountOfExternalAllocatedMemory(4 * width * height); + _data_len = height * cairo_image_surface_get_stride(_surface); + V8::AdjustAmountOfExternalAllocatedMemory(_data_len); if (!onload.IsEmpty()) { TryCatch try_catch; @@ -549,6 +625,7 @@ Image::loadGIFFromBuffer(uint8_t *buf, unsigned len) { } _data = data; + return CAIRO_STATUS_SUCCESS; } #endif /* HAVE_GIF */ @@ -602,44 +679,32 @@ static void jpeg_mem_src (j_decompress_ptr cinfo, void* buffer, long nbytes) { #endif /* - * Load jpeg from buffer. + * Takes an initialised jpeg_decompress_struct and decodes the + * data into _surface. */ cairo_status_t -Image::loadJPEGFromBuffer(uint8_t *buf, unsigned len) { - // TODO: remove this duplicate logic - // JPEG setup - struct jpeg_decompress_struct info; - struct jpeg_error_mgr err; - info.err = jpeg_std_error(&err); - jpeg_create_decompress(&info); - jpeg_mem_src(&info, buf, len); - jpeg_read_header(&info, 1); - jpeg_start_decompress(&info); - width = info.output_width; - height = info.output_height; - - // Data alloc +Image::decodeJPEGIntoSurface(jpeg_decompress_struct *info) { int stride = width * 4; - uint8_t *data = (uint8_t *) malloc(width * height * 4); + cairo_status_t status; + uint8_t *data = (uint8_t *) malloc(width * height * 4); if (!data) { - jpeg_finish_decompress(&info); - jpeg_destroy_decompress(&info); + jpeg_abort_decompress(info); + jpeg_destroy_decompress(info); return CAIRO_STATUS_NO_MEMORY; } - + uint8_t *src = (uint8_t *) malloc(width * 3); if (!src) { free(data); - jpeg_finish_decompress(&info); - jpeg_destroy_decompress(&info); + jpeg_abort_decompress(info); + jpeg_destroy_decompress(info); return CAIRO_STATUS_NO_MEMORY; } - // Copy RGB -> ARGB for (int y = 0; y < height; ++y) { - jpeg_read_scanlines(&info, &src, 1); + jpeg_read_scanlines(info, &src, 1); uint32_t *row = (uint32_t *)(data + stride * y); for (int x = 0; x < width; ++x) { int bx = 3 * x; @@ -651,7 +716,6 @@ Image::loadJPEGFromBuffer(uint8_t *buf, unsigned len) { } } - // New image surface _surface = cairo_image_surface_create_for_data( data , CAIRO_FORMAT_ARGB32 @@ -659,85 +723,62 @@ Image::loadJPEGFromBuffer(uint8_t *buf, unsigned len) { , height , cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width)); - // Cleanup - free(src); - jpeg_finish_decompress(&info); - jpeg_destroy_decompress(&info); - cairo_status_t status = cairo_surface_status(_surface); + jpeg_finish_decompress(info); + jpeg_destroy_decompress(info); + status = cairo_surface_status(_surface); if (status) { free(data); + free(src); return status; } + free(src); + _data = data; + return CAIRO_STATUS_SUCCESS; } +#if CAIRO_VERSION_MINOR >= 10 + /* - * Load JPEG, convert RGB to ARGB. + * Takes a jpeg data buffer and assigns it as mime data to a + * dummy surface */ cairo_status_t -Image::loadJPEG(FILE *stream) { +Image::decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len) { + // TODO: remove this duplicate logic // JPEG setup struct jpeg_decompress_struct info; struct jpeg_error_mgr err; info.err = jpeg_std_error(&err); jpeg_create_decompress(&info); - jpeg_stdio_src(&info, stream); + + jpeg_mem_src(&info, buf, len); + jpeg_read_header(&info, 1); jpeg_start_decompress(&info); width = info.output_width; height = info.output_height; // Data alloc - int stride = width * 4; - uint8_t *data = (uint8_t *) malloc(width * height * 4); - - if (!data) { - fclose(stream); - jpeg_finish_decompress(&info); - jpeg_destroy_decompress(&info); - return CAIRO_STATUS_NO_MEMORY; - } - - uint8_t *src = (uint8_t *) malloc(width * 3); - - if (!src) { - free(data); - fclose(stream); - jpeg_finish_decompress(&info); - jpeg_destroy_decompress(&info); - return CAIRO_STATUS_NO_MEMORY; - } - - // Copy RGB -> ARGB - for (int y = 0; y < height; ++y) { - jpeg_read_scanlines(&info, &src, 1); - uint32_t *row = (uint32_t *)(data + stride * y); - for (int x = 0; x < width; ++x) { - int bx = 3 * x; - uint32_t *pixel = row + x; - *pixel = 255 << 24 - | src[bx + 0] << 16 - | src[bx + 1] << 8 - | src[bx + 2]; - } - } + // 8 pixels per byte using Alpha Channel format to reduce memory requirement. + int buf_size = height * cairo_format_stride_for_width(CAIRO_FORMAT_A1, width); + uint8_t *data = (uint8_t *) malloc(buf_size); + if (!data) return CAIRO_STATUS_NO_MEMORY; // New image surface _surface = cairo_image_surface_create_for_data( data - , CAIRO_FORMAT_ARGB32 + , CAIRO_FORMAT_A1 , width , height - , cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width)); + , cairo_format_stride_for_width(CAIRO_FORMAT_A1, width)); // Cleanup - free(src); - fclose(stream); - jpeg_finish_decompress(&info); + jpeg_abort_decompress(&info); jpeg_destroy_decompress(&info); cairo_status_t status = cairo_surface_status(_surface); @@ -746,7 +787,130 @@ Image::loadJPEG(FILE *stream) { return status; } - return CAIRO_STATUS_SUCCESS; + _data = data; + + return assignDataAsMime(buf, len, CAIRO_MIME_TYPE_JPEG); +} + +/* + * Helper function for disposing of a mime data closure. + */ + +void +clearMimeData(void *closure) { + V8::AdjustAmountOfExternalAllocatedMemory(-((read_closure_t *)closure)->len); + free(((read_closure_t *) closure)->buf); + free(closure); +} + +/* + * Assign a given buffer as mime data against the surface. + * The provided buffer will be copied, and the copy will + * be automatically freed when the surface is destroyed. + */ + +cairo_status_t +Image::assignDataAsMime(uint8_t *data, int len, const char *mime_type) { + uint8_t *mime_data = (uint8_t *) malloc(len); + if (!mime_data) return CAIRO_STATUS_NO_MEMORY; + + read_closure_t *mime_closure = (read_closure_t *) malloc(sizeof(read_closure_t)); + if (!mime_closure) { + free(mime_data); + return CAIRO_STATUS_NO_MEMORY; + } + + memcpy(mime_data, data, len); + + mime_closure->buf = mime_data; + mime_closure->len = len; + + V8::AdjustAmountOfExternalAllocatedMemory(len); + + return cairo_surface_set_mime_data(_surface, mime_type, mime_data, len, clearMimeData, mime_closure); +} + +#endif + +/* + * Load jpeg from buffer. + */ + +cairo_status_t +Image::loadJPEGFromBuffer(uint8_t *buf, unsigned len) { + // TODO: remove this duplicate logic + // JPEG setup + struct jpeg_decompress_struct info; + struct jpeg_error_mgr err; + info.err = jpeg_std_error(&err); + jpeg_create_decompress(&info); + + jpeg_mem_src(&info, buf, len); + + jpeg_read_header(&info, 1); + jpeg_start_decompress(&info); + width = info.output_width; + height = info.output_height; + + return decodeJPEGIntoSurface(&info); +} + +/* + * Load JPEG, convert RGB to ARGB. + */ + +cairo_status_t +Image::loadJPEG(FILE *stream) { + cairo_status_t status; + + if (data_mode == DATA_IMAGE) { // Can lazily read in the JPEG. + // JPEG setup + struct jpeg_decompress_struct info; + struct jpeg_error_mgr err; + info.err = jpeg_std_error(&err); + jpeg_create_decompress(&info); + + jpeg_stdio_src(&info, stream); + + jpeg_read_header(&info, 1); + jpeg_start_decompress(&info); + width = info.output_width; + height = info.output_height; + + status = decodeJPEGIntoSurface(&info); + fclose(stream); + } else { // We'll need the actual source jpeg data, so read fully. +#if CAIRO_VERSION_MINOR >= 10 + uint8_t *buf; + unsigned len; + + fseek(stream, 0, SEEK_END); + len = ftell(stream); + fseek(stream, 0, SEEK_SET); + + buf = (uint8_t *) malloc(len); + if (!buf) return CAIRO_STATUS_NO_MEMORY; + + fread(buf, len, 1, stream); + fclose(stream); + + switch (data_mode) { + case DATA_IMAGE: // Can't be this, but compiler warning. + case DATA_IMAGE_AND_MIME: + status = loadJPEGFromBuffer(buf, len); + if (status) break; + status = assignDataAsMime(buf, len, CAIRO_MIME_TYPE_JPEG); + break; + case DATA_MIME: + status = decodeJPEGBufferIntoMimeSurface(buf, len); + break; + } + + free(buf); +#endif + } + + return status; } #endif /* HAVE_JPEG */ diff --git a/src/Image.h b/src/Image.h index da20a14..8a085c8 100644 --- a/src/Image.h +++ b/src/Image.h @@ -10,6 +10,11 @@ #include "Canvas.h" +#ifdef HAVE_JPEG +#include +#include +#endif + class Image: public node::ObjectWrap { public: char *filename; @@ -25,9 +30,11 @@ class Image: public node::ObjectWrap { static Handle GetComplete(Local prop, const AccessorInfo &info); static Handle GetWidth(Local prop, const AccessorInfo &info); static Handle GetHeight(Local prop, const AccessorInfo &info); + static Handle GetDataMode(Local prop, const AccessorInfo &info); static void SetSource(Local prop, Local val, const AccessorInfo &info); static void SetOnload(Local prop, Local val, const AccessorInfo &info); static void SetOnerror(Local prop, Local val, const AccessorInfo &info); + static void SetDataMode(Local prop, Local val, const AccessorInfo &info); inline cairo_surface_t *surface(){ return _surface; } inline uint8_t *data(){ return cairo_image_surface_get_data(_surface); } inline int stride(){ return cairo_image_surface_get_stride(_surface); } @@ -40,6 +47,7 @@ class Image: public node::ObjectWrap { cairo_status_t loadFromBuffer(uint8_t *buf, unsigned len); cairo_status_t loadPNGFromBuffer(uint8_t *buf); cairo_status_t loadPNG(); + void clearData(); #ifdef HAVE_GIF cairo_status_t loadGIFFromBuffer(uint8_t *buf, unsigned len); cairo_status_t loadGIF(FILE *stream); @@ -47,6 +55,11 @@ class Image: public node::ObjectWrap { #ifdef HAVE_JPEG cairo_status_t loadJPEGFromBuffer(uint8_t *buf, unsigned len); cairo_status_t loadJPEG(FILE *stream); + cairo_status_t decodeJPEGIntoSurface(jpeg_decompress_struct *info); +#if CAIRO_VERSION_MINOR >= 10 + cairo_status_t decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len); + cairo_status_t assignDataAsMime(uint8_t *data, int len, const char *mime_type); +#endif #endif void error(Local error); void loaded(); @@ -59,6 +72,12 @@ class Image: public node::ObjectWrap { , COMPLETE } state; + enum { + DATA_IMAGE = 1, + DATA_MIME, + DATA_IMAGE_AND_MIME + } data_mode; + typedef enum { UNKNOWN , GIF @@ -71,6 +90,7 @@ class Image: public node::ObjectWrap { private: cairo_surface_t *_surface; uint8_t *_data; + int _data_len; ~Image(); };