From 1a6dffe653d65871336b6e83d3c1dc1d34b0cd95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s-Combarro=20=27piranna?= Date: Wed, 26 Oct 2016 22:51:19 +0200 Subject: [PATCH 001/474] Backends support This is a stripped-down version of pull-request #571 focused only on the backends support, but adding also support for PDF and SVG backends and passing the tests. it also retain backwards compatibility with the old API, so this would be easy to merge since it's mostly to move around the PDF and SVG code and checks to independent classes. There's some code for `pdfStream` and similar methods that I think they should be moved too since they are mostly backend specific, and make more sense there. I didn't do that since it would break the API, but maybe it would be added some functions on the Canvas object that proxy the petitions to the backend to retain compatibility. I know it's a big pull-request, so I'm open for comments over it. --- .gitignore | 1 + binding.gyp | 25 +++-- examples/backends.js | 18 ++++ lib/canvas.js | 3 + src/Backends.cc | 18 ++++ src/Backends.h | 14 +++ src/Canvas.cc | 185 +++++++++++--------------------- src/Canvas.h | 37 +++---- src/CanvasRenderingContext2d.cc | 20 ++-- src/backend/Backend.cc | 89 +++++++++++++++ src/backend/Backend.h | 72 +++++++++++++ src/backend/ImageBackend.cc | 63 +++++++++++ src/backend/ImageBackend.h | 25 +++++ src/backend/PdfBackend.cc | 73 +++++++++++++ src/backend/PdfBackend.h | 25 +++++ src/backend/SvgBackend.cc | 76 +++++++++++++ src/backend/SvgBackend.h | 25 +++++ src/closure.cc | 30 ++++++ src/closure.h | 19 +--- src/init.cc | 9 +- src/toBuffer.cc | 33 ++++++ src/toBuffer.h | 4 + test/canvas.test.js | 10 +- 23 files changed, 688 insertions(+), 186 deletions(-) mode change 100755 => 100644 binding.gyp create mode 100644 examples/backends.js create mode 100644 src/Backends.cc create mode 100644 src/Backends.h mode change 100755 => 100644 src/CanvasRenderingContext2d.cc create mode 100644 src/backend/Backend.cc create mode 100644 src/backend/Backend.h create mode 100644 src/backend/ImageBackend.cc create mode 100644 src/backend/ImageBackend.h create mode 100644 src/backend/PdfBackend.cc create mode 100644 src/backend/PdfBackend.h create mode 100644 src/backend/SvgBackend.cc create mode 100644 src/backend/SvgBackend.h create mode 100644 src/closure.cc mode change 100755 => 100644 src/init.cc create mode 100644 src/toBuffer.cc create mode 100644 src/toBuffer.h diff --git a/.gitignore b/.gitignore index 130894492..04f1c001d 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ node_modules # Vim cruft *.swp *.un~ +npm-debug.log diff --git a/binding.gyp b/binding.gyp old mode 100755 new mode 100644 index 4f56e1a4c..d13923066 --- a/binding.gyp +++ b/binding.gyp @@ -2,11 +2,11 @@ 'conditions': [ ['OS=="win"', { 'variables': { - 'GTK_Root%': 'C:/GTK', # Set the location of GTK all-in-one bundle + 'GTK_Root%': 'C:/GTK', # Set the location of GTK all-in-one bundle 'with_jpeg%': 'false', 'with_gif%': 'false' } - }, { # 'OS!="win"' + }, { # 'OS!="win"' 'variables': { 'with_jpeg%': ' target) { + Nan::HandleScope scope; + + Local obj = Nan::New(); + ImageBackend::Initialize(obj); + PdfBackend::Initialize(obj); + SvgBackend::Initialize(obj); + + target->Set(Nan::New("Backends").ToLocalChecked(), obj); +} diff --git a/src/Backends.h b/src/Backends.h new file mode 100644 index 000000000..1519883d1 --- /dev/null +++ b/src/Backends.h @@ -0,0 +1,14 @@ +#ifndef __NODE_BACKENDS_H__ +#define __NODE_BACKENDS_H__ + +#include + +#include "backend/Backend.h" + + +class Backends : public Nan::ObjectWrap { + public: + static void Initialize(v8::Handle target); +}; + +#endif diff --git a/src/Canvas.cc b/src/Canvas.cc index c7415e246..1b6423507 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -18,11 +18,16 @@ #include "CanvasRenderingContext2d.h" #include "closure.h" #include "register_font.h" +#include "toBuffer.h" #ifdef HAVE_JPEG #include "JPEGStream.h" #endif +#include "backend/ImageBackend.h" +#include "backend/PdfBackend.h" +#include "backend/SvgBackend.h" + #define GENERIC_FACE_ERROR \ "The second argument to registerFont is required, and should be an object " \ "with at least a family (string) and optionally weight (string/number) " \ @@ -80,17 +85,35 @@ NAN_METHOD(Canvas::New) { return Nan::ThrowTypeError("Class constructors cannot be invoked without 'new'"); } - int width = 0, height = 0; - canvas_type_t type = CANVAS_TYPE_IMAGE; - if (info[0]->IsNumber()) width = info[0]->Uint32Value(); - if (info[1]->IsNumber()) height = info[1]->Uint32Value(); - if (info[2]->IsString()) type = !strcmp("pdf", *String::Utf8Value(info[2])) - ? CANVAS_TYPE_PDF - : !strcmp("svg", *String::Utf8Value(info[2])) - ? CANVAS_TYPE_SVG - : CANVAS_TYPE_IMAGE; - Canvas *canvas = new Canvas(width, height, type); + Backend* backend = NULL; + if (info[0]->IsNumber()) { + int width = info[0]->Uint32Value(), height = 0; + + if (info[1]->IsNumber()) height = info[1]->Uint32Value(); + + if (info[2]->IsString()) { + if (0 == strcmp("pdf", *String::Utf8Value(info[2]))) + backend = new PdfBackend(width, height); + else if (0 == strcmp("svg", *String::Utf8Value(info[2]))) + backend = new SvgBackend(width, height); + else + backend = new ImageBackend(width, height); + } + else + backend = new ImageBackend(width, height); + } + else if (info[0]->IsObject()) { + backend = Nan::ObjectWrap::Unwrap(info[0]->ToObject()); + } + else { + backend = new ImageBackend(0, 0); + } + + Canvas* canvas = new Canvas(backend); canvas->Wrap(info.This()); + + backend->setCanvas(canvas); + info.GetReturnValue().Set(info.This()); } @@ -100,7 +123,7 @@ NAN_METHOD(Canvas::New) { NAN_GETTER(Canvas::GetType) { Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(canvas->isPDF() ? "pdf" : canvas->isSVG() ? "svg" : "image").ToLocalChecked()); + info.GetReturnValue().Set(Nan::New(canvas->backend()->getName()).ToLocalChecked()); } /* @@ -117,7 +140,7 @@ NAN_GETTER(Canvas::GetStride) { NAN_GETTER(Canvas::GetWidth) { Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(canvas->width)); + info.GetReturnValue().Set(Nan::New(canvas->getWidth())); } /* @@ -127,7 +150,7 @@ NAN_GETTER(Canvas::GetWidth) { NAN_SETTER(Canvas::SetWidth) { if (value->IsNumber()) { Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - canvas->width = value->Uint32Value(); + canvas->backend()->setWidth(value->Uint32Value()); canvas->resurface(info.This()); } } @@ -138,7 +161,7 @@ NAN_SETTER(Canvas::SetWidth) { NAN_GETTER(Canvas::GetHeight) { Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(canvas->height)); + info.GetReturnValue().Set(Nan::New(canvas->getHeight())); } /* @@ -148,39 +171,11 @@ NAN_GETTER(Canvas::GetHeight) { NAN_SETTER(Canvas::SetHeight) { if (value->IsNumber()) { Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - canvas->height = value->Uint32Value(); + canvas->backend()->setHeight(value->Uint32Value()); canvas->resurface(info.This()); } } -/* - * Canvas::ToBuffer callback. - */ - -static cairo_status_t -toBuffer(void *c, const uint8_t *data, unsigned len) { - closure_t *closure = (closure_t *) c; - - if (closure->len + len > closure->max_len) { - uint8_t *data; - unsigned max = closure->max_len; - - do { - max *= 2; - } while (closure->len + len > max); - - data = (uint8_t *) realloc(closure->data, max); - if (!data) return CAIRO_STATUS_NO_MEMORY; - closure->data = data; - closure->max_len = max; - } - - memcpy(closure->data + closure->len, data, len); - closure->len += len; - - return CAIRO_STATUS_SUCCESS; -} - /* * EIO toBuffer callback. */ @@ -259,9 +254,10 @@ NAN_METHOD(Canvas::ToBuffer) { Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); // TODO: async / move this out - if (canvas->isPDF() || canvas->isSVG()) { + const string name = canvas->backend()->getName(); + if (name == "pdf" || name == "svg") { cairo_surface_finish(canvas->surface()); - closure_t *closure = (closure_t *) canvas->closure(); + closure_t *closure = (closure_t *) canvas->backend()->closure(); Local buf = Nan::CopyBuffer((char*) closure->data, closure->len).ToLocalChecked(); info.GetReturnValue().Set(buf); @@ -515,14 +511,14 @@ NAN_METHOD(Canvas::StreamPDFSync) { Canvas *canvas = Nan::ObjectWrap::Unwrap(info.Holder()); - if (!canvas->isPDF()) + if (canvas->backend()->getName() != "pdf") return Nan::ThrowTypeError("wrong canvas type"); cairo_surface_finish(canvas->surface()); closure_t closure; - closure.data = static_cast(canvas->closure())->data; - closure.len = static_cast(canvas->closure())->len; + closure.data = static_cast(canvas->backend()->closure())->data; + closure.len = static_cast(canvas->backend()->closure())->len; closure.fn = info[0].As(); Nan::TryCatch try_catch; @@ -650,30 +646,9 @@ NAN_METHOD(Canvas::RegisterFont) { * Initialize cairo surface. */ -Canvas::Canvas(int w, int h, canvas_type_t t): Nan::ObjectWrap() { - type = t; - width = w; - height = h; - _surface = NULL; - _closure = NULL; - - if (CANVAS_TYPE_PDF == t) { - _closure = malloc(sizeof(closure_t)); - assert(_closure); - cairo_status_t status = closure_init((closure_t *) _closure, this, 0, PNG_NO_FILTERS); - assert(status == CAIRO_STATUS_SUCCESS); - _surface = cairo_pdf_surface_create_for_stream(toBuffer, _closure, w, h); - } else if (CANVAS_TYPE_SVG == t) { - _closure = malloc(sizeof(closure_t)); - assert(_closure); - cairo_status_t status = closure_init((closure_t *) _closure, this, 0, PNG_NO_FILTERS); - assert(status == CAIRO_STATUS_SUCCESS); - _surface = cairo_svg_surface_create_for_stream(toBuffer, _closure, w, h); - } else { - _surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h); - assert(_surface); - Nan::AdjustExternalMemory(nBytes()); - } +Canvas::Canvas(Backend* backend) : ObjectWrap() { + _backend = backend; + this->backend()->createSurface(); } /* @@ -681,20 +656,9 @@ Canvas::Canvas(int w, int h, canvas_type_t t): Nan::ObjectWrap() { */ Canvas::~Canvas() { - switch (type) { - case CANVAS_TYPE_PDF: - case CANVAS_TYPE_SVG: - cairo_surface_finish(_surface); - closure_destroy((closure_t *) _closure); - free(_closure); - cairo_surface_destroy(_surface); - break; - case CANVAS_TYPE_IMAGE: - int oldNBytes = nBytes(); - cairo_surface_destroy(_surface); - Nan::AdjustExternalMemory(-oldNBytes); - break; - } + if (_backend != NULL) { + delete _backend; + } } std::vector @@ -812,44 +776,17 @@ void Canvas::resurface(Local canvas) { Nan::HandleScope scope; Local context; - switch (type) { - case CANVAS_TYPE_PDF: - cairo_pdf_surface_set_size(_surface, width, height); - break; - case CANVAS_TYPE_SVG: - // Re-surface - cairo_surface_finish(_surface); - closure_destroy((closure_t *) _closure); - cairo_surface_destroy(_surface); - closure_init((closure_t *) _closure, this, 0, PNG_NO_FILTERS); - _surface = cairo_svg_surface_create_for_stream(toBuffer, _closure, width, height); - - // Reset context - context = canvas->Get(Nan::New("context").ToLocalChecked()); - if (!context->IsUndefined()) { - Context2d *context2d = Nan::ObjectWrap::Unwrap(context->ToObject()); - cairo_t *prev = context2d->context(); - context2d->setContext(cairo_create(surface())); - cairo_destroy(prev); - } - break; - case CANVAS_TYPE_IMAGE: - // Re-surface - size_t oldNBytes = nBytes(); - cairo_surface_destroy(_surface); - _surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); - Nan::AdjustExternalMemory(nBytes() - oldNBytes); - - // Reset context - context = canvas->Get(Nan::New("context").ToLocalChecked()); - if (!context->IsUndefined()) { - Context2d *context2d = Nan::ObjectWrap::Unwrap(context->ToObject()); - cairo_t *prev = context2d->context(); - context2d->setContext(cairo_create(surface())); - cairo_destroy(prev); - } - break; - } + + backend()->recreateSurface(); + + // Reset context + context = canvas->Get(Nan::New("context").ToLocalChecked()); + if (!context->IsUndefined()) { + Context2d *context2d = ObjectWrap::Unwrap(context->ToObject()); + cairo_t *prev = context2d->context(); + context2d->setContext(cairo_create(surface())); + cairo_destroy(prev); + } } /* diff --git a/src/Canvas.h b/src/Canvas.h index 9411a1863..5b12dc5b6 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -17,6 +17,8 @@ #include #include +#include "backend/Backend.h" + using namespace node; using namespace v8; @@ -30,16 +32,6 @@ using namespace v8; #define CANVAS_MAX_STATES 64 #endif -/* - * Canvas types. - */ - -typedef enum { - CANVAS_TYPE_IMAGE, - CANVAS_TYPE_PDF, - CANVAS_TYPE_SVG -} canvas_type_t; - /* * FontFace describes a font file in terms of one PangoFontDescription that * will resolve to it and one that the user describes it as (like @font-face) @@ -56,9 +48,6 @@ class FontFace { class Canvas: public Nan::ObjectWrap { public: - int width; - int height; - canvas_type_t type; static Nan::Persistent constructor; static void Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target); static NAN_METHOD(New); @@ -91,20 +80,22 @@ class Canvas: public Nan::ObjectWrap { static PangoStyle GetStyleFromCSSString(const char *style); static PangoFontDescription *ResolveFontDescription(const PangoFontDescription *desc); - inline bool isPDF(){ return CANVAS_TYPE_PDF == type; } - inline bool isSVG(){ return CANVAS_TYPE_SVG == type; } - inline cairo_surface_t *surface(){ return _surface; } - inline void *closure(){ return _closure; } - inline uint8_t *data(){ return cairo_image_surface_get_data(_surface); } - inline int stride(){ return cairo_image_surface_get_stride(_surface); } - inline int nBytes(){ return height * stride(); } - Canvas(int width, int height, canvas_type_t type); + inline Backend* backend() { return _backend; } + inline cairo_surface_t* surface(){ return backend()->getSurface(); } + + inline uint8_t *data(){ return cairo_image_surface_get_data(surface()); } + inline int stride(){ return cairo_image_surface_get_stride(surface()); } + inline int nBytes(){ return backend()->getWidth() * stride(); } + + inline int getWidth() { return backend()->getWidth(); } + inline int getHeight() { return backend()->getHeight(); } + + Canvas(Backend* backend); void resurface(Local canvas); private: ~Canvas(); - cairo_surface_t *_surface; - void *_closure; + Backend* _backend; static std::vector _font_face_list; }; diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc old mode 100755 new mode 100644 index c570541ed..aee9aaf26 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -528,7 +528,7 @@ NAN_METHOD(Context2d::New) { NAN_METHOD(Context2d::AddPage) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - if (!context->canvas()->isPDF()) { + if (context->canvas()->backend()->getName() != "pdf") { return Nan::ThrowError("only PDF canvases support .nextPage()"); } cairo_show_page(context->context()); @@ -573,8 +573,8 @@ NAN_METHOD(Context2d::PutImageData) { case 3: // Need to wrap std::min calls using parens to prevent macro expansion on // windows. See http://stackoverflow.com/questions/5004858/stdmin-gives-error - cols = (std::min)(imageData->width(), context->canvas()->width - dx); - rows = (std::min)(imageData->height(), context->canvas()->height - dy); + cols = (std::min)(imageData->width(), context->canvas()->getWidth() - dx); + rows = (std::min)(imageData->height(), context->canvas()->getHeight() - dy); break; // imageData, dx, dy, sx, sy, sw, sh case 7: @@ -600,8 +600,8 @@ NAN_METHOD(Context2d::PutImageData) { // clamp width at canvas size // Need to wrap std::min calls using parens to prevent macro expansion on // windows. See http://stackoverflow.com/questions/5004858/stdmin-gives-error - cols = (std::min)(sw, context->canvas()->width - dx); - rows = (std::min)(sh, context->canvas()->height - dy); + cols = (std::min)(sw, context->canvas()->getWidth() - dx); + rows = (std::min)(sh, context->canvas()->getHeight() - dy); break; default: return Nan::ThrowError("invalid arguments"); @@ -686,8 +686,10 @@ NAN_METHOD(Context2d::GetImageData) { sh = -sh; } - if (sx + sw > canvas->width) sw = canvas->width - sx; - if (sy + sh > canvas->height) sh = canvas->height - sy; + int width = canvas->getWidth(); + int height = canvas->getHeight(); + if (sx + sw > width) sw = width - sx; + if (sy + sh > height) sh = height - sy; // WebKit/moz functionality. node-canvas used to return in either case. if (sw <= 0) sw = 1; @@ -801,8 +803,8 @@ NAN_METHOD(Context2d::DrawImage) { // Canvas } else if (Nan::New(Canvas::constructor)->HasInstance(obj)) { Canvas *canvas = Nan::ObjectWrap::Unwrap(obj); - sw = canvas->width; - sh = canvas->height; + sw = canvas->getWidth(); + sh = canvas->getHeight(); surface = canvas->surface(); // Invalid diff --git a/src/backend/Backend.cc b/src/backend/Backend.cc new file mode 100644 index 000000000..a82774025 --- /dev/null +++ b/src/backend/Backend.cc @@ -0,0 +1,89 @@ +#include "Backend.h" + + +Backend::Backend(string name, int width, int height) + : name(name) + , width(width) + , height(height) + , surface(NULL) + , canvas(NULL) + , _closure(NULL) +{} + +Backend::~Backend() +{ + this->destroySurface(); +} + + +void Backend::setCanvas(Canvas* canvas) +{ + this->canvas = canvas; +} + + +cairo_surface_t* Backend::recreateSurface() +{ + this->destroySurface(); + + return this->createSurface(); +} + +cairo_surface_t* Backend::getSurface() +{ + return surface; +} + +void Backend::destroySurface() +{ + if(this->surface) + { + cairo_surface_destroy(this->surface); + this->surface = NULL; + } +} + + +string Backend::getName() +{ + return name; +} + +int Backend::getWidth() +{ + return this->width; +} +void Backend::setWidth(int width) +{ + this->width = width; + this->recreateSurface(); +} + +int Backend::getHeight() +{ + return this->height; +} +void Backend::setHeight(int height) +{ + this->height = height; + this->recreateSurface(); +} + + +BackendOperationNotAvailable::BackendOperationNotAvailable(Backend* backend, + string operation_name) + : backend(backend) + , operation_name(operation_name) +{}; + +BackendOperationNotAvailable::~BackendOperationNotAvailable() throw() {}; + +const char* BackendOperationNotAvailable::what() const throw() +{ + std::ostringstream o; + + o << "operation " << this->operation_name; + o << " not supported by backend " + backend->getName(); + + return o.str().c_str(); +}; diff --git a/src/backend/Backend.h b/src/backend/Backend.h new file mode 100644 index 000000000..5877c34e0 --- /dev/null +++ b/src/backend/Backend.h @@ -0,0 +1,72 @@ +#ifndef __BACKEND_H__ +#define __BACKEND_H__ + +#include +#include +#include +#include + +#include + +#if HAVE_PANGO + #include +#else + #include +#endif + +class Canvas; + +using namespace std; + +class Backend : public Nan::ObjectWrap +{ + private: + const string name; + + protected: + int width; + int height; + cairo_surface_t* surface; + Canvas* canvas; + + Backend(string name, int width, int height); + + public: + virtual ~Backend(); + + // TODO Used only by SVG and PDF, move there + void* _closure; + inline void* closure(){ return _closure; } + + void setCanvas(Canvas* canvas); + + virtual cairo_surface_t* createSurface() = 0; + virtual cairo_surface_t* recreateSurface(); + + cairo_surface_t* getSurface(); + void destroySurface(); + + string getName(); + + int getWidth(); + virtual void setWidth(int width); + + int getHeight(); + virtual void setHeight(int height); +}; + + +class BackendOperationNotAvailable: public exception +{ + private: + Backend* backend; + string operation_name; + + public: + BackendOperationNotAvailable(Backend* backend, string operation_name); + ~BackendOperationNotAvailable() throw(); + + const char* what() const throw(); +}; + +#endif diff --git a/src/backend/ImageBackend.cc b/src/backend/ImageBackend.cc new file mode 100644 index 000000000..7cdcff2a8 --- /dev/null +++ b/src/backend/ImageBackend.cc @@ -0,0 +1,63 @@ +#include "ImageBackend.h" + +using namespace v8; + +ImageBackend::ImageBackend(int width, int height) + : Backend("image", width, height) +{ + createSurface(); +} + +ImageBackend::~ImageBackend() +{ + destroySurface(); + + Nan::AdjustExternalMemory(-4 * width * height); +} + +cairo_surface_t* ImageBackend::createSurface() +{ + this->surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + assert(this->surface); + Nan::AdjustExternalMemory(4 * width * height); + + return this->surface; +} + +cairo_surface_t* ImageBackend::recreateSurface() +{ + // Re-surface + int old_width = cairo_image_surface_get_width(this->surface); + int old_height = cairo_image_surface_get_height(this->surface); + this->destroySurface(); + Nan::AdjustExternalMemory(-4 * old_width * old_height); + + return createSurface(); +} + + +Nan::Persistent ImageBackend::constructor; + +void ImageBackend::Initialize(Handle target) +{ + Nan::HandleScope scope; + + Local ctor = Nan::New(ImageBackend::New); + ImageBackend::constructor.Reset(ctor); + ctor->InstanceTemplate()->SetInternalFieldCount(1); + ctor->SetClassName(Nan::New("ImageBackend").ToLocalChecked()); + target->Set(Nan::New("ImageBackend").ToLocalChecked(), ctor->GetFunction()); +} + +NAN_METHOD(ImageBackend::New) +{ + int width = 0; + int height = 0; + if (info[0]->IsNumber()) width = info[0]->Uint32Value(); + if (info[1]->IsNumber()) height = info[1]->Uint32Value(); + + ImageBackend* backend = new ImageBackend(width, height); + + backend->Wrap(info.This()); + info.GetReturnValue().Set(info.This()); +} diff --git a/src/backend/ImageBackend.h b/src/backend/ImageBackend.h new file mode 100644 index 000000000..fe5482aa2 --- /dev/null +++ b/src/backend/ImageBackend.h @@ -0,0 +1,25 @@ +#ifndef __IMAGE_BACKEND_H__ +#define __IMAGE_BACKEND_H__ + +#include + +#include "Backend.h" + +using namespace std; + +class ImageBackend : public Backend +{ + private: + cairo_surface_t* createSurface(); + cairo_surface_t* recreateSurface(); + + public: + ImageBackend(int width, int height); + ~ImageBackend(); + + static Nan::Persistent constructor; + static void Initialize(v8::Handle target); + static NAN_METHOD(New); +}; + +#endif diff --git a/src/backend/PdfBackend.cc b/src/backend/PdfBackend.cc new file mode 100644 index 000000000..a187d90a0 --- /dev/null +++ b/src/backend/PdfBackend.cc @@ -0,0 +1,73 @@ +#include "PdfBackend.h" + +#include +#include + +#include "../Canvas.h" +#include "../closure.h" +#include "../toBuffer.h" + + +using namespace v8; + +PdfBackend::PdfBackend(int width, int height) + : Backend("pdf", width, height) +{ + createSurface(); +} + +PdfBackend::~PdfBackend() +{ + cairo_surface_finish(this->surface); + closure_destroy((closure_t*)_closure); + free(_closure); + + destroySurface(); +} + + +cairo_surface_t* PdfBackend::createSurface() +{ + _closure = malloc(sizeof(closure_t)); + assert(_closure); + cairo_status_t status = closure_init((closure_t*)_closure, this->canvas, 0, PNG_NO_FILTERS); + assert(status == CAIRO_STATUS_SUCCESS); + + this->surface = cairo_pdf_surface_create_for_stream(toBuffer, _closure, width, height); + + return this->surface; +} + +cairo_surface_t* PdfBackend::recreateSurface() +{ + cairo_pdf_surface_set_size(this->surface, width, height); + + return this->surface; +} + + +Nan::Persistent PdfBackend::constructor; + +void PdfBackend::Initialize(Handle target) +{ + Nan::HandleScope scope; + + Local ctor = Nan::New(PdfBackend::New); + PdfBackend::constructor.Reset(ctor); + ctor->InstanceTemplate()->SetInternalFieldCount(1); + ctor->SetClassName(Nan::New("PdfBackend").ToLocalChecked()); + target->Set(Nan::New("PdfBackend").ToLocalChecked(), ctor->GetFunction()); +} + +NAN_METHOD(PdfBackend::New) +{ + int width = 0; + int height = 0; + if (info[0]->IsNumber()) width = info[0]->Uint32Value(); + if (info[1]->IsNumber()) height = info[1]->Uint32Value(); + + PdfBackend* backend = new PdfBackend(width, height); + + backend->Wrap(info.This()); + info.GetReturnValue().Set(info.This()); +} diff --git a/src/backend/PdfBackend.h b/src/backend/PdfBackend.h new file mode 100644 index 000000000..9287c0fd5 --- /dev/null +++ b/src/backend/PdfBackend.h @@ -0,0 +1,25 @@ +#ifndef __PDF_BACKEND_H__ +#define __PDF_BACKEND_H__ + +#include + +#include "Backend.h" + +using namespace std; + +class PdfBackend : public Backend +{ + private: + cairo_surface_t* createSurface(); + cairo_surface_t* recreateSurface(); + + public: + PdfBackend(int width, int height); + ~PdfBackend(); + + static Nan::Persistent constructor; + static void Initialize(v8::Handle target); + static NAN_METHOD(New); +}; + +#endif diff --git a/src/backend/SvgBackend.cc b/src/backend/SvgBackend.cc new file mode 100644 index 000000000..ffd4ce13f --- /dev/null +++ b/src/backend/SvgBackend.cc @@ -0,0 +1,76 @@ +#include "SvgBackend.h" + +#include +#include + +#include "../Canvas.h" +#include "../closure.h" +#include "../toBuffer.h" + + +using namespace v8; + +SvgBackend::SvgBackend(int width, int height) + : Backend("svg", width, height) +{ + _closure = malloc(sizeof(closure_t)); + assert(_closure); + + createSurface(); +} + +SvgBackend::~SvgBackend() +{ + cairo_surface_finish(this->surface); + closure_destroy((closure_t*)_closure); + free(_closure); + + destroySurface(); +} + + +cairo_surface_t* SvgBackend::createSurface() +{ + cairo_status_t status = closure_init((closure_t*)_closure, this->canvas, 0, PNG_NO_FILTERS); + assert(status == CAIRO_STATUS_SUCCESS); + + this->surface = cairo_svg_surface_create_for_stream(toBuffer, _closure, width, height); + + return this->surface; +} + +cairo_surface_t* SvgBackend::recreateSurface() +{ + cairo_surface_finish(this->surface); + closure_destroy((closure_t*)_closure); + cairo_surface_destroy(this->surface); + + return createSurface(); + } + + +Nan::Persistent SvgBackend::constructor; + +void SvgBackend::Initialize(Handle target) +{ + Nan::HandleScope scope; + + Local ctor = Nan::New(SvgBackend::New); + SvgBackend::constructor.Reset(ctor); + ctor->InstanceTemplate()->SetInternalFieldCount(1); + ctor->SetClassName(Nan::New("SvgBackend").ToLocalChecked()); + target->Set(Nan::New("SvgBackend").ToLocalChecked(), ctor->GetFunction()); +} + +NAN_METHOD(SvgBackend::New) +{ + int width = 0; + int height = 0; + if (info[0]->IsNumber()) width = info[0]->Uint32Value(); + if (info[1]->IsNumber()) height = info[1]->Uint32Value(); + + SvgBackend* backend = new SvgBackend(width, height); + + backend->Wrap(info.This()); + info.GetReturnValue().Set(info.This()); +} diff --git a/src/backend/SvgBackend.h b/src/backend/SvgBackend.h new file mode 100644 index 000000000..fda10d66e --- /dev/null +++ b/src/backend/SvgBackend.h @@ -0,0 +1,25 @@ +#ifndef __SVG_BACKEND_H__ +#define __SVG_BACKEND_H__ + +#include + +#include "Backend.h" + +using namespace std; + +class SvgBackend : public Backend +{ + private: + cairo_surface_t* createSurface(); + cairo_surface_t* recreateSurface(); + + public: + SvgBackend(int width, int height); + ~SvgBackend(); + + static Nan::Persistent constructor; + static void Initialize(v8::Handle target); + static NAN_METHOD(New); +}; + +#endif diff --git a/src/closure.cc b/src/closure.cc new file mode 100644 index 000000000..940210028 --- /dev/null +++ b/src/closure.cc @@ -0,0 +1,30 @@ +#include "closure.h" + + +/* + * Initialize the given closure. + */ + +cairo_status_t +closure_init(closure_t *closure, Canvas *canvas, unsigned int compression_level, unsigned int filter) { + closure->len = 0; + closure->canvas = canvas; + closure->data = (uint8_t *) malloc(closure->max_len = PAGE_SIZE); + if (!closure->data) return CAIRO_STATUS_NO_MEMORY; + closure->compression_level = compression_level; + closure->filter = filter; + return CAIRO_STATUS_SUCCESS; +} + +/* + * Free the given closure's data, + * and hint V8 at the memory dealloc. + */ + +void +closure_destroy(closure_t *closure) { + if (closure->len) { + free(closure->data); + Nan::AdjustExternalMemory(-((intptr_t) closure->max_len)); + } +} diff --git a/src/closure.h b/src/closure.h index 1fb6bced2..e15a36730 100644 --- a/src/closure.h +++ b/src/closure.h @@ -18,6 +18,8 @@ #include +#include "Canvas.h" + /* * PNG stream closure. */ @@ -39,15 +41,7 @@ typedef struct { */ cairo_status_t -closure_init(closure_t *closure, Canvas *canvas, unsigned int compression_level, unsigned int filter) { - closure->len = 0; - closure->canvas = canvas; - closure->data = (uint8_t *) malloc(closure->max_len = PAGE_SIZE); - if (!closure->data) return CAIRO_STATUS_NO_MEMORY; - closure->compression_level = compression_level; - closure->filter = filter; - return CAIRO_STATUS_SUCCESS; -} +closure_init(closure_t *closure, Canvas *canvas, unsigned int compression_level, unsigned int filter); /* * Free the given closure's data, @@ -55,11 +49,6 @@ closure_init(closure_t *closure, Canvas *canvas, unsigned int compression_level, */ void -closure_destroy(closure_t *closure) { - if (closure->len) { - free(closure->data); - Nan::AdjustExternalMemory(-((intptr_t) closure->max_len)); - } -} +closure_destroy(closure_t *closure); #endif /* __NODE_CLOSURE_H__ */ diff --git a/src/init.cc b/src/init.cc old mode 100755 new mode 100644 index dc2fa5963..29158ab3b --- a/src/init.cc +++ b/src/init.cc @@ -8,12 +8,15 @@ #include #include #include + +#include "Backends.h" #include "Canvas.h" -#include "Image.h" -#include "ImageData.h" #include "CanvasGradient.h" #include "CanvasPattern.h" #include "CanvasRenderingContext2d.h" +#include "Image.h" +#include "ImageData.h" + #include #include FT_FREETYPE_H @@ -73,4 +76,4 @@ NAN_MODULE_INIT(init) { target->Set(Nan::New("freetypeVersion").ToLocalChecked(), Nan::New(freetype_version).ToLocalChecked()); } -NODE_MODULE(canvas,init); +NODE_MODULE(canvas, init); diff --git a/src/toBuffer.cc b/src/toBuffer.cc new file mode 100644 index 000000000..2171ed105 --- /dev/null +++ b/src/toBuffer.cc @@ -0,0 +1,33 @@ +#include + +#include "closure.h" +#include "toBuffer.h" + + +/* + * Canvas::ToBuffer callback. + */ + +cairo_status_t +toBuffer(void *c, const uint8_t *data, unsigned len) { + closure_t *closure = (closure_t *) c; + + if (closure->len + len > closure->max_len) { + uint8_t *data; + unsigned max = closure->max_len; + + do { + max *= 2; + } while (closure->len + len > max); + + data = (uint8_t *) realloc(closure->data, max); + if (!data) return CAIRO_STATUS_NO_MEMORY; + closure->data = data; + closure->max_len = max; + } + + memcpy(closure->data + closure->len, data, len); + closure->len += len; + + return CAIRO_STATUS_SUCCESS; +} diff --git a/src/toBuffer.h b/src/toBuffer.h new file mode 100644 index 000000000..4faede02f --- /dev/null +++ b/src/toBuffer.h @@ -0,0 +1,4 @@ +#include + + +cairo_status_t toBuffer(void* c, const uint8_t* data, unsigned len); diff --git a/test/canvas.test.js b/test/canvas.test.js index b6c678df8..383a17dd2 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -215,13 +215,13 @@ describe('Canvas', function () { it('Canvas#type', function () { var canvas = new Canvas(10, 10); - assert('image' == canvas.type); + assert.equal(canvas.type, 'image'); var canvas = new Canvas(10, 10, 'pdf'); - assert('pdf' == canvas.type); + assert.equal(canvas.type, 'pdf'); var canvas = new Canvas(10, 10, 'svg'); - assert('svg' == canvas.type); + assert.equal(canvas.type, 'svg'); var canvas = new Canvas(10, 10, 'hey'); - assert('image' == canvas.type); + assert.equal(canvas.type, 'image'); }); it('Canvas#getContext("2d")', function () { @@ -907,7 +907,7 @@ describe('Canvas', function () { stream.on('data', function (chunk) { if (firstChunk) { firstChunk = false; - assert.equal('PDF', chunk.slice(1, 4).toString()); + assert.equal(chunk.slice(1, 4).toString(), 'PDF'); } }); stream.on('end', function () { From 8b5dc056b3045ffe6665c38c732dc7b37ce7c454 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 19 Mar 2016 15:10:37 -0700 Subject: [PATCH 002/474] Implement Readable instead of Stream. --- lib/jpegstream.js | 45 ++++++++++++++++++++++------------ lib/pdfstream.js | 37 ++++++++++++++++++---------- lib/pngstream.js | 60 +++++++++++++++++++++++++++++++++------------ src/Canvas.cc | 2 +- test/canvas.test.js | 6 ++++- 5 files changed, 104 insertions(+), 46 deletions(-) diff --git a/lib/jpegstream.js b/lib/jpegstream.js index 24ca6f396..6c8f0e8cb 100644 --- a/lib/jpegstream.js +++ b/lib/jpegstream.js @@ -10,7 +10,8 @@ * Module dependencies. */ -var Stream = require('stream').Stream; +var Readable = require('stream').Readable; +var util = require('util'); /** * Initialize a `JPEGStream` with the given `canvas`. @@ -30,33 +31,47 @@ var Stream = require('stream').Stream; */ var JPEGStream = module.exports = function JPEGStream(canvas, options, sync) { - var self = this - , method = sync + if (!(this instanceof JPEGStream)) { + throw new TypeError("Class constructors cannot be invoked without 'new'"); + } + + Readable.call(this); + + var self = this; + var method = sync ? 'streamJPEGSync' : 'streamJPEG'; this.options = options; this.sync = sync; this.canvas = canvas; - this.readable = true; + // TODO: implement async if ('streamJPEG' == method) method = 'streamJPEGSync'; + this.method = method; +}; + +util.inherits(JPEGStream, Readable); + +function noop() {} + +JPEGStream.prototype._read = function _read() { + // For now we're not controlling the c++ code's data emission, so we only + // call canvas.streamJPEGSync once and let it emit data at will. + this._read = noop; + var self = this; + var method = this.method; + var bufsize = this.options.bufsize; + var quality = this.options.quality; + var progressive = this.options.progressive; process.nextTick(function(){ - canvas[method](options.bufsize, options.quality, options.progressive, function(err, chunk){ + self.canvas[method](bufsize, quality, progressive, function(err, chunk){ if (err) { self.emit('error', err); - self.readable = false; } else if (chunk) { - self.emit('data', chunk); + self.push(chunk); } else { - self.emit('end'); - self.readable = false; + self.push(null); } }); }); }; - -/** - * Inherit from `EventEmitter`. - */ - -JPEGStream.prototype.__proto__ = Stream.prototype; diff --git a/lib/pdfstream.js b/lib/pdfstream.js index 92560ccbc..5f96ea702 100644 --- a/lib/pdfstream.js +++ b/lib/pdfstream.js @@ -8,7 +8,8 @@ * Module dependencies. */ -var Stream = require('stream').Stream; +var Readable = require('stream').Readable; +var util = require('util'); /** * Initialize a `PDFStream` with the given `canvas`. @@ -28,32 +29,42 @@ var Stream = require('stream').Stream; */ var PDFStream = module.exports = function PDFStream(canvas, sync) { + if (!(this instanceof PDFStream)) { + throw new TypeError("Class constructors cannot be invoked without 'new'"); + } + + Readable.call(this); + var self = this , method = sync ? 'streamPDFSync' : 'streamPDF'; this.sync = sync; this.canvas = canvas; - this.readable = true; + // TODO: implement async if ('streamPDF' == method) method = 'streamPDFSync'; + this.method = method; +}; + +util.inherits(PDFStream, Readable); + +function noop() {} + +PDFStream.prototype._read = function _read() { + // For now we're not controlling the c++ code's data emission, so we only + // call canvas.streamPDFSync once and let it emit data at will. + this._read = noop; + var self = this; process.nextTick(function(){ - canvas[method](function(err, chunk, len){ + self.canvas[self.method](function(err, chunk, len){ if (err) { self.emit('error', err); - self.readable = false; } else if (len) { - self.emit('data', chunk, len); + self.push(chunk); } else { - self.emit('end'); - self.readable = false; + self.push(null); } }); }); }; - -/** - * Inherit from `EventEmitter`. - */ - -PDFStream.prototype.__proto__ = Stream.prototype; diff --git a/lib/pngstream.js b/lib/pngstream.js index 8a538d03a..b0a68f04f 100644 --- a/lib/pngstream.js +++ b/lib/pngstream.js @@ -10,7 +10,8 @@ * Module dependencies. */ -var Stream = require('stream').Stream; +var Readable = require('stream').Readable; +var util = require('util'); /** * Initialize a `PNGStream` with the given `canvas`. @@ -30,32 +31,59 @@ var Stream = require('stream').Stream; */ var PNGStream = module.exports = function PNGStream(canvas, sync) { - var self = this - , method = sync + if (!(this instanceof PNGStream)) { + throw new TypeError("Class constructors cannot be invoked without 'new'"); + } + + Readable.call(this); + + var self = this; + var method = sync + ? 'streamPNGSync' + : 'streamPNG'; + this.sync = sync; + this.canvas = canvas; + + // TODO: implement async + if ('streamPNG' === method) method = 'streamPNGSync'; + this.method = method; +}; + +util.inherits(PNGStream, Readable); + +var PNGStream = module.exports = function PNGStream(canvas, sync) { + Readable.call(this); + + var self = this; + var method = sync ? 'streamPNGSync' : 'streamPNG'; this.sync = sync; this.canvas = canvas; - this.readable = true; + // TODO: implement async - if ('streamPNG' == method) method = 'streamPNGSync'; + if ('streamPNG' === method) method = 'streamPNGSync'; + this.method = method; +}; + +util.inherits(PNGStream, Readable); + +function noop() {} + +PNGStream.prototype._read = function _read() { + // For now we're not controlling the c++ code's data emission, so we only + // call canvas.streamPNGSync once and let it emit data at will. + this._read = noop; + var self = this; process.nextTick(function(){ - canvas[method](function(err, chunk, len){ + self.canvas[self.method](function(err, chunk, len){ if (err) { self.emit('error', err); - self.readable = false; } else if (len) { - self.emit('data', chunk, len); + self.push(chunk); } else { - self.emit('end'); - self.readable = false; + self.push(null); } }); }); }; - -/** - * Inherit from `EventEmitter`. - */ - -PNGStream.prototype.__proto__ = Stream.prototype; diff --git a/src/Canvas.cc b/src/Canvas.cc index 89c1e5950..716add382 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -460,7 +460,7 @@ NAN_METHOD(Canvas::StreamPNGSync) { Nan::Null() , Nan::Null() , Nan::New(0) }; - Nan::MakeCallback(Nan::GetCurrentContext()->Global(), (v8::Local)closure.fn, 1, argv); + Nan::MakeCallback(Nan::GetCurrentContext()->Global(), (v8::Local)closure.fn, 3, argv); } return; } diff --git a/test/canvas.test.js b/test/canvas.test.js index 72bf6be90..b6247cef1 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -6,7 +6,8 @@ var Canvas = require('../') , assert = require('assert') , parseFont = Canvas.Context2d.parseFont , fs = require('fs') - , os = require('os'); + , os = require('os') + , Readable = require('stream').Readable; console.log(); console.log(' canvas: %s', Canvas.version); @@ -878,6 +879,7 @@ describe('Canvas', function () { it('Canvas#createSyncPNGStream()', function (done) { var canvas = new Canvas(20, 20); var stream = canvas.createSyncPNGStream(); + assert(stream instanceof Readable); var firstChunk = true; stream.on('data', function(chunk){ if (firstChunk) { @@ -896,6 +898,7 @@ describe('Canvas', function () { it('Canvas#createSyncPDFStream()', function (done) { var canvas = new Canvas(20, 20, 'pdf'); var stream = canvas.createSyncPDFStream(); + assert(stream instanceof Readable); var firstChunk = true; stream.on('data', function (chunk) { if (firstChunk) { @@ -914,6 +917,7 @@ describe('Canvas', function () { it('Canvas#jpegStream()', function (done) { var canvas = new Canvas(640, 480); var stream = canvas.jpegStream(); + assert(stream instanceof Readable); var firstChunk = true; var bytes = 0; stream.on('data', function(chunk){ From 42e9a7412dbc96544f28027a2ba9827dfcde97e8 Mon Sep 17 00:00:00 2001 From: Tim Knip Date: Tue, 8 Nov 2016 20:18:25 +0100 Subject: [PATCH 003/474] windows jpeg support --- binding.gyp | 24 ++++++++++++++++++++++-- src/Image.cc | 14 ++++++++++++-- test/image.test.js | 27 +++++++++++++++++++++++++++ util/win_jpeg_lookup.js | 21 +++++++++++++++++++++ 4 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 util/win_jpeg_lookup.js diff --git a/binding.gyp b/binding.gyp index 4f56e1a4c..d184f2f9a 100755 --- a/binding.gyp +++ b/binding.gyp @@ -4,7 +4,18 @@ 'variables': { 'GTK_Root%': 'C:/GTK', # Set the location of GTK all-in-one bundle 'with_jpeg%': 'false', - 'with_gif%': 'false' + 'with_gif%': 'false', + 'variables': { # Nest jpeg_root to evaluate it before with_jpeg + 'jpeg_root%': ' Date: Mon, 2 Jan 2017 15:25:49 -0800 Subject: [PATCH 004/474] Fix isnan() and isinf() on Clang. --- src/CanvasRenderingContext2d.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index f3f32d50d..047efd83c 100755 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -5,7 +5,7 @@ // Copyright (c) 2010 LearnBoost // -#include +#include #include #include #include From 7279d2b20766554f660b431ce595342adcefec8b Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Wed, 22 Feb 2017 23:41:23 +0700 Subject: [PATCH 005/474] Add test for dataURL with callback always returning image data async --- test/canvas.test.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/canvas.test.js b/test/canvas.test.js index 72bf6be90..006e77616 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -519,6 +519,14 @@ describe('Canvas', function () { }); }); + it('toDataURL(function (err, str) {...}) is async even with no canvas data', function (done) { + new Canvas().toDataURL(function(err, str){ + assert.ifError(err); + assert.ok('data:,' === str); + done(); + }); + }); + it('toDataURL(0.5, function (err, str) {...}) works and defaults to PNG', function (done) { new Canvas(200,200).toDataURL(0.5, function(err, str){ assert.ifError(err); From f6d8214017508ea6eadb1fcd7491caaf99c3366b Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Wed, 22 Feb 2017 23:45:14 +0700 Subject: [PATCH 006/474] Return the no pixel string async if a callback was passed in --- lib/canvas.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/canvas.js b/lib/canvas.js index 8272c9b86..6a7de5c82 100644 --- a/lib/canvas.js +++ b/lib/canvas.js @@ -231,11 +231,6 @@ Canvas.prototype.toDataURL = function(a1, a2, a3){ // ['image/jpeg', qual, fn] -> ['image/jpeg', {quality: qual}, fn] // ['image/jpeg', undefined, fn] -> ['image/jpeg', null, fn] - if (this.width === 0 || this.height === 0) { - // Per spec, if the bitmap has no pixels, return this string: - return "data:,"; - } - var type = 'image/png'; var opts = {}; var fn; @@ -264,6 +259,11 @@ Canvas.prototype.toDataURL = function(a1, a2, a3){ } } + if (this.width === 0 || this.height === 0) { + // Per spec, if the bitmap has no pixels, return this string: + return fn ? fn(null, "data:,") : "data:,"; + } + if ('image/png' === type) { if (fn) { this.toBuffer(function(err, buf){ From 3bd0dffea82ecb3bc79fc936cb4c08ec991c0e1a Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Thu, 23 Feb 2017 00:18:51 +0700 Subject: [PATCH 007/474] Make sure callback isn't executed synchronously --- lib/canvas.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/canvas.js b/lib/canvas.js index 6a7de5c82..11aab6f18 100644 --- a/lib/canvas.js +++ b/lib/canvas.js @@ -261,7 +261,13 @@ Canvas.prototype.toDataURL = function(a1, a2, a3){ if (this.width === 0 || this.height === 0) { // Per spec, if the bitmap has no pixels, return this string: - return fn ? fn(null, "data:,") : "data:,"; + if (fn) { + setTimeout(function() { + fn(null, "data:,"); + }); + return; + } + return "data:,"; } if ('image/png' === type) { From 818a26a43a8a5b1867e20d584b0a4423f9f6868b Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Thu, 23 Feb 2017 00:30:00 +0700 Subject: [PATCH 008/474] Return no canvas data string synchronously for backwards compatibility --- lib/canvas.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/canvas.js b/lib/canvas.js index 11aab6f18..efb873dc4 100644 --- a/lib/canvas.js +++ b/lib/canvas.js @@ -261,13 +261,13 @@ Canvas.prototype.toDataURL = function(a1, a2, a3){ if (this.width === 0 || this.height === 0) { // Per spec, if the bitmap has no pixels, return this string: + var str = "data:,"; if (fn) { setTimeout(function() { - fn(null, "data:,"); + fn(null, str); }); - return; } - return "data:,"; + return str; } if ('image/png' === type) { From 5702c4745a72956a34d635344937384a1b3bcd2e Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Tue, 21 Feb 2017 10:04:48 -0800 Subject: [PATCH 009/474] src(build): port has_lib.js to javascript Ref #754 (fix build on BSD) Ref #813 (static build) --- History.md | 5 +++ binding.gyp | 4 +- package.json | 2 +- util/has_lib.js | 111 ++++++++++++++++++++++++++++++++++++++++++++++++ util/has_lib.sh | 64 ---------------------------- 5 files changed, 119 insertions(+), 67 deletions(-) create mode 100644 util/has_lib.js delete mode 100755 util/has_lib.sh diff --git a/History.md b/History.md index 70c707d72..3e2060ac7 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,8 @@ +Unreleased / patch +================== + + * Port has_lib.sh to javascript (#872) + 1.6.0 / 2016-10-16 ================== diff --git a/binding.gyp b/binding.gyp index 4f56e1a4c..8b2a2f4d3 100755 --- a/binding.gyp +++ b/binding.gyp @@ -8,8 +8,8 @@ } }, { # 'OS!="win"' 'variables': { - 'with_jpeg%': '/dev/null | grep -E "' + libName + '"').length) { + return true + } + } catch (err) { + // noop -- proceed to other search methods + } + } + + // Try checking common library locations + return SYSTEM_PATHS.some(function (systemPath) { + try { + var dirListing = fs.readdirSync(systemPath) + return dirListing.some(function (file) { + return libNameRegex.test(file) + }) + } catch (err) { + return false + } + }) +} + +/** + * Checks for ldconfig on the path and /sbin + * @return Boolean exists + */ +function hasLdconfig () { + try { + // Add /sbin to path as ldconfig is located there on some systems -- e.g. + // Debian (and it can still be used by unprivileged users): + childProcess.execSync('export PATH="$PATH:/sbin"') + process.env.PATH = '...' + // execSync throws on nonzero exit + childProcess.execSync('hash ldconfig 2>/dev/null') + return true + } catch (err) { + return false + } +} + +/** + * Checks for freetype2 with --cflags-only-I + * @return Boolean exists + */ +function hasFreetype () { + try { + if (childProcess.execSync('pkg-config cairo --cflags-only-I 2>/dev/null | grep freetype2').length) { + return true + } + } catch (err) { + // noop + } + return false +} + +/** + * Checks for lib using pkg-config. + * @param String library name + * @return Boolean exists + */ +function hasPkgconfigLib (lib) { + try { + // execSync throws on nonzero exit + childProcess.execSync('pkg-config --exists "' + lib + '" 2>/dev/null') + return true + } catch (err) { + return false + } +} + +function main (query) { + switch (query) { + case 'gif': + case 'jpeg': + case 'cairo': + return hasSystemLib(query) + case 'pango': + return hasPkgconfigLib(query) + case 'freetype': + return hasFreetype() + default: + throw new Error('Unknown library: ' + query) + } +} + +process.stdout.write(main(query).toString()) diff --git a/util/has_lib.sh b/util/has_lib.sh deleted file mode 100755 index 75e216c8c..000000000 --- a/util/has_lib.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/bin/sh - -has_ldconfig() { - hash ldconfig 2>/dev/null -} - -has_system_lib() { - regex="lib$1.+(so|dylib)" - - # Add /sbin to path as ldconfig is located there on some systems - e.g. Debian - # (and it still can be used by unprivileged users): - PATH="$PATH:/sbin" - export PATH - - # Try using ldconfig on Linux systems - if has_ldconfig; then - for _ in $(ldconfig -p 2>/dev/null | grep -E "$regex"); do - return 0 - done - fi - - # Try just checking common library locations - for dir in /lib /usr/lib /usr/local/lib /opt/local/lib /usr/lib/x86_64-linux-gnu /usr/lib/i386-linux-gnu; do - test -d "$dir" && echo "$dir"/* | grep -E "$regex" && return 0 - done - - return 1 -} - -has_freetype() { - pkg-config cairo --cflags-only-I | grep freetype2 -} - -has_pkgconfig_lib() { - pkg-config --exists "$1" -} - -case "$1" in - gif) - has_system_lib "gif" > /dev/null - result=$? - ;; - jpeg) - has_system_lib "jpeg" > /dev/null - result=$? - ;; - pango) - has_pkgconfig_lib "pango" > /dev/null - result=$? - ;; - freetype) - has_freetype > /dev/null - result=$? - ;; - *) - >&2 echo "Unknown library: $1" - exit 1 -esac - -if test $result -eq 0; then - echo "true" -else - echo "false" -fi From a2d11873ca0892c737b800fd729168d1b186f344 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s-Combarro=20=27piranna?= Date: Mon, 27 Feb 2017 22:56:18 +0100 Subject: [PATCH 010/474] Added missing `backends` namespace --- src/init.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/init.cc b/src/init.cc index 29158ab3b..9cf3ef370 100644 --- a/src/init.cc +++ b/src/init.cc @@ -26,6 +26,7 @@ #endif NAN_MODULE_INIT(init) { + Backends::Initialize(target); Canvas::Initialize(target); Image::Initialize(target); ImageData::Initialize(target); From 3d545056e2bb61b25f2d8bb1445c2e424a615163 Mon Sep 17 00:00:00 2001 From: Ya'ar Hever Date: Thu, 16 Mar 2017 23:19:51 +0100 Subject: [PATCH 011/474] Parse font using parse-css-font and units-css --- lib/context2d.js | 69 ++++++++++++++++++++------------------------ package.json | 4 ++- test/public/tests.js | 12 ++++++++ 3 files changed, 46 insertions(+), 39 deletions(-) diff --git a/lib/context2d.js b/lib/context2d.js index 3ecb37922..e95421e61 100644 --- a/lib/context2d.js +++ b/lib/context2d.js @@ -16,6 +16,10 @@ var canvas = require('./bindings') , CanvasPattern = canvas.CanvasPattern , ImageData = canvas.ImageData; +var parseCssFont = require('parse-css-font'); + +var unitsCss = require('units-css'); + /** * Export `Context2d` as the module. */ @@ -34,26 +38,6 @@ var cache = {}; var baselines = ['alphabetic', 'top', 'bottom', 'middle', 'ideographic', 'hanging']; -/** - * Font RegExp helpers. - */ - -var weights = 'normal|bold|bolder|lighter|[1-9]00' - , styles = 'normal|italic|oblique' - , units = 'px|pt|pc|in|cm|mm|%' - , string = '\'([^\']+)\'|"([^"]+)"|[\\w-]+'; - -/** - * Font parser RegExp; - */ - -var fontre = new RegExp('^ *' - + '(?:(' + weights + ') *)?' - + '(?:(' + styles + ') *)?' - + '([\\d\\.]+)(' + units + ') *' - + '((?:' + string + ')( *, *(?:' + string + '))*)' - ); - /** * Parse font `str`. * @@ -62,42 +46,51 @@ var fontre = new RegExp('^ *' * @api private */ -var parseFont = exports.parseFont = function(str){ - var font = {} - , captures = fontre.exec(str); +var parseFont = exports.parseFont = function(str) { + var parsedFont; - // Invalid - if (!captures) return; + // Try to parse the font string using parse-css-font. + // It will throw an exception if it fails. + try { + parsedFont = parseCssFont(str); + } + catch (e) { + // Invalid + return; + } // Cached if (cache[str]) return cache[str]; - // Populate font object - font.weight = captures[1] || 'normal'; - font.style = captures[2] || 'normal'; - font.size = parseFloat(captures[3]); - font.unit = captures[4]; - font.family = captures[5].replace(/["']/g, '').split(',').map(function (family) { - return family.trim(); - }).join(','); + // Parse size into value and unit using units-css + var size = unitsCss.parse(parsedFont.size); // TODO: dpi // TODO: remaining unit conversion - switch (font.unit) { + switch (size.unit) { case 'pt': - font.size /= .75; + size.value /= .75; break; case 'in': - font.size *= 96; + size.value *= 96; break; case 'mm': - font.size *= 96.0 / 25.4; + size.value *= 96.0 / 25.4; break; case 'cm': - font.size *= 96.0 / 2.54; + size.value *= 96.0 / 2.54; break; } + // Populate font object + var font = { + weight: parsedFont.weight, + style: parsedFont.style, + size: size.value, + unit: size.unit, + family: parsedFont.family.join(',') + }; + return cache[str] = font; }; diff --git a/package.json b/package.json index a946bf81b..f87c02633 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,9 @@ "test-server": "node test/server.js" }, "dependencies": { - "nan": "^2.4.0" + "nan": "^2.4.0", + "parse-css-font": "^2.0.2", + "units-css": "^0.4.0" }, "devDependencies": { "express": "^4.14.0", diff --git a/test/public/tests.js b/test/public/tests.js index 4f24d7ab1..68170c3fe 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -999,6 +999,18 @@ tests['font family invalid'] = function (ctx) { ctx.fillText('14px Invalid, Impact', 100, 100) } +tests['font style variant weight size family'] = function (ctx) { + ctx.strokeStyle = '#666' + ctx.strokeRect(0, 0, 200, 200) + ctx.lineTo(0, 100) + ctx.lineTo(200, 100) + ctx.stroke() + + ctx.font = 'normal normal normal 16px Impact' + ctx.textAlign = 'center' + ctx.fillText('normal normal normal 16px', 100, 100) +} + tests['globalCompositeOperation source-over'] = function (ctx) { ctx.fillStyle = 'blue' ctx.fillRect(0, 0, 100, 100) From 653f0d9803e6af541ea26ea202fac8fe26c983d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Wed, 3 May 2017 23:44:00 +0200 Subject: [PATCH 012/474] Drop support for Node.js 0.x --- .travis.yml | 4 ---- Readme.md | 2 +- package.json | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3a25554e9..ae4e7e377 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,6 @@ language: node_js node_js: - '6' - '4' - - '0.12' - - '0.10' addons: apt: sources: @@ -16,6 +14,4 @@ addons: - g++-4.9 env: - CXX=g++-4.9 -before_install: - - npm explore npm -g -- npm install node-gyp@latest sudo: false diff --git a/Readme.md b/Readme.md index 1e679c6dd..ed71402f8 100644 --- a/Readme.md +++ b/Readme.md @@ -29,7 +29,7 @@ $ npm install canvas Unless previously installed you'll _need_ __Cairo__ and __Pango__. For system-specific installation view the [Wiki](https://github.com/Automattic/node-canvas/wiki/_pages). -Currently the minimum version of node required is __0.10.0__ +Currently the minimum version of node required is __4.0.0__ You can quickly install the dependencies by using the command for your OS: diff --git a/package.json b/package.json index f87c02633..5ac28bc1a 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "standard": "^8.5.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=4" }, "main": "./lib/canvas.js", "license": "MIT" From f8f03d9d39176065c30f75fa959cb8be20f84337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Wed, 3 May 2017 23:44:25 +0200 Subject: [PATCH 013/474] Run Travis on Node.js 7 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index ae4e7e377..eb0fcc120 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: node_js node_js: + - '7' - '6' - '4' addons: From 6b72722ca65013e4303769e5551aa877fd5d92b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Thu, 4 May 2017 00:06:21 +0200 Subject: [PATCH 014/474] 2.0.0-alpha.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5ac28bc1a..efe6416cd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "1.6.0", + "version": "2.0.0-alpha.1", "author": "TJ Holowaychuk ", "contributors": [ "Nathan Rajlich ", From 8d0ecb042fb5bfda38a23c0721ad5edef84347ab Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 4 May 2017 09:57:47 -0700 Subject: [PATCH 015/474] Fix regression in toBuffer("raw") nBytes is height * stride, not width. Introduced by 1a6dffe --- src/Canvas.h | 2 +- test/canvas.test.js | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Canvas.h b/src/Canvas.h index 5b12dc5b6..4258e7de1 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -85,7 +85,7 @@ class Canvas: public Nan::ObjectWrap { inline uint8_t *data(){ return cairo_image_surface_get_data(surface()); } inline int stride(){ return cairo_image_surface_get_stride(surface()); } - inline int nBytes(){ return backend()->getWidth() * stride(); } + inline int nBytes(){ return getHeight() * stride(); } inline int getWidth() { return backend()->getWidth(); } inline int getHeight() { return backend()->getHeight(); } diff --git a/test/canvas.test.js b/test/canvas.test.js index c4e66d299..172e3360d 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -386,10 +386,10 @@ describe('Canvas', function () { }); describe('#toBuffer("raw")', function() { - var canvas = new Canvas(10, 10) + var canvas = new Canvas(11, 10) , ctx = canvas.getContext('2d'); - ctx.clearRect(0, 0, 10, 10); + ctx.clearRect(0, 0, 11, 10); ctx.fillStyle = 'rgba(200, 200, 200, 0.505)'; ctx.fillRect(0, 0, 5, 5); @@ -404,16 +404,16 @@ describe('Canvas', function () { ctx.fillRect(5, 5, 4, 5); /** Output: - * *****RRRRR - * *****RRRRR - * *****RRRRR - * *****RRRRR - * *****RRRRR - * GGGGGBBBB- - * GGGGGBBBB- - * GGGGGBBBB- - * GGGGGBBBB- - * GGGGGBBBB- + * *****RRRRR- + * *****RRRRR- + * *****RRRRR- + * *****RRRRR- + * *****RRRRR- + * GGGGGBBBB-- + * GGGGGBBBB-- + * GGGGGBBBB-- + * GGGGGBBBB-- + * GGGGGBBBB-- */ var buf = canvas.toBuffer('raw'); From f9708bce207d27b5c9b8a6748ad6ca15821a8e0e Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 4 May 2017 09:58:20 -0700 Subject: [PATCH 016/474] Allow alpha/beta versions in test suite --- test/canvas.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/canvas.test.js b/test/canvas.test.js index 172e3360d..764caf9da 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -19,7 +19,7 @@ describe('Canvas', function () { }); it('.version', function () { - assert.ok(/^\d+\.\d+\.\d+$/.test(Canvas.version)); + assert.ok(/^\d+\.\d+\.\d+(-(alpha|beta)\.\d+)?$/.test(Canvas.version)); }); it('.cairoVersion', function () { From dc8964abbcd255b7fcaa3b9bbf72c42ef953b4cc Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 4 May 2017 10:09:17 -0700 Subject: [PATCH 017/474] src: fix shadowing warnings Renames a few variables and parameters to avoid shadowing names. --- src/CanvasRenderingContext2d.cc | 4 ++-- src/backend/Backend.cc | 12 ++++++------ src/toBuffer.cc | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 9e8a506be..377955a0d 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -745,8 +745,8 @@ NAN_METHOD(Context2d::GetImageData) { Local shHandle = Nan::New(sh); Local argv[argc] = { clampedArray, swHandle, shHandle }; - Local constructor = Nan::GetFunction(Nan::New(ImageData::constructor)).ToLocalChecked(); - Local instance = Nan::NewInstance(constructor, argc, argv).ToLocalChecked(); + Local ctor = Nan::GetFunction(Nan::New(ImageData::constructor)).ToLocalChecked(); + Local instance = Nan::NewInstance(ctor, argc, argv).ToLocalChecked(); info.GetReturnValue().Set(instance); } diff --git a/src/backend/Backend.cc b/src/backend/Backend.cc index a82774025..e2a2e9a5a 100644 --- a/src/backend/Backend.cc +++ b/src/backend/Backend.cc @@ -16,9 +16,9 @@ Backend::~Backend() } -void Backend::setCanvas(Canvas* canvas) +void Backend::setCanvas(Canvas* _canvas) { - this->canvas = canvas; + this->canvas = _canvas; } @@ -53,9 +53,9 @@ int Backend::getWidth() { return this->width; } -void Backend::setWidth(int width) +void Backend::setWidth(int width_) { - this->width = width; + this->width = width_; this->recreateSurface(); } @@ -63,9 +63,9 @@ int Backend::getHeight() { return this->height; } -void Backend::setHeight(int height) +void Backend::setHeight(int height_) { - this->height = height; + this->height = height_; this->recreateSurface(); } diff --git a/src/toBuffer.cc b/src/toBuffer.cc index 2171ed105..10e0a56cd 100644 --- a/src/toBuffer.cc +++ b/src/toBuffer.cc @@ -9,7 +9,7 @@ */ cairo_status_t -toBuffer(void *c, const uint8_t *data, unsigned len) { +toBuffer(void *c, const uint8_t *odata, unsigned len) { closure_t *closure = (closure_t *) c; if (closure->len + len > closure->max_len) { @@ -26,7 +26,7 @@ toBuffer(void *c, const uint8_t *data, unsigned len) { closure->max_len = max; } - memcpy(closure->data + closure->len, data, len); + memcpy(closure->data + closure->len, odata, len); closure->len += len; return CAIRO_STATUS_SUCCESS; From 198a61df1bd090a79cb63d6be2f0b1d27c13cf8e Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 4 May 2017 10:14:57 -0700 Subject: [PATCH 018/474] Fix v8 memory dealloc hint -(unsigned x) is (usually) 2^32 - x. Need to cast to signed first. --- src/Image.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Image.cc b/src/Image.cc index cb05e3d84..33c436877 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -810,7 +810,7 @@ Image::decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len) { void clearMimeData(void *closure) { - Nan::AdjustExternalMemory(-((read_closure_t *)closure)->len); + Nan::AdjustExternalMemory(-static_cast(((read_closure_t *)closure)->len)); free(((read_closure_t *) closure)->buf); free(closure); } From 82aedcdfad0a83a4cf681a7b34dc258138578a0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Sat, 6 May 2017 16:25:06 +0200 Subject: [PATCH 019/474] 2.0.0-alpha.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index efe6416cd..e55be7287 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.0.0-alpha.1", + "version": "2.0.0-alpha.2", "author": "TJ Holowaychuk ", "contributors": [ "Nathan Rajlich ", From 2bd2f7c81003b82207120a95d368629707f31e4a Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 4 May 2017 10:22:33 -0700 Subject: [PATCH 020/474] Remove nextTicks since they were only needed by node 0.10 See discussion in #740 --- lib/jpegstream.js | 18 ++++++++---------- lib/pdfstream.js | 18 ++++++++---------- lib/pngstream.js | 18 ++++++++---------- 3 files changed, 24 insertions(+), 30 deletions(-) diff --git a/lib/jpegstream.js b/lib/jpegstream.js index 6c8f0e8cb..5705a9aa3 100644 --- a/lib/jpegstream.js +++ b/lib/jpegstream.js @@ -63,15 +63,13 @@ JPEGStream.prototype._read = function _read() { var bufsize = this.options.bufsize; var quality = this.options.quality; var progressive = this.options.progressive; - process.nextTick(function(){ - self.canvas[method](bufsize, quality, progressive, function(err, chunk){ - if (err) { - self.emit('error', err); - } else if (chunk) { - self.push(chunk); - } else { - self.push(null); - } - }); + self.canvas[method](bufsize, quality, progressive, function(err, chunk){ + if (err) { + self.emit('error', err); + } else if (chunk) { + self.push(chunk); + } else { + self.push(null); + } }); }; diff --git a/lib/pdfstream.js b/lib/pdfstream.js index 5f96ea702..084ad3a22 100644 --- a/lib/pdfstream.js +++ b/lib/pdfstream.js @@ -56,15 +56,13 @@ PDFStream.prototype._read = function _read() { // call canvas.streamPDFSync once and let it emit data at will. this._read = noop; var self = this; - process.nextTick(function(){ - self.canvas[self.method](function(err, chunk, len){ - if (err) { - self.emit('error', err); - } else if (len) { - self.push(chunk); - } else { - self.push(null); - } - }); + self.canvas[self.method](function(err, chunk, len){ + if (err) { + self.emit('error', err); + } else if (len) { + self.push(chunk); + } else { + self.push(null); + } }); }; diff --git a/lib/pngstream.js b/lib/pngstream.js index b0a68f04f..94e5cdeef 100644 --- a/lib/pngstream.js +++ b/lib/pngstream.js @@ -75,15 +75,13 @@ PNGStream.prototype._read = function _read() { // call canvas.streamPNGSync once and let it emit data at will. this._read = noop; var self = this; - process.nextTick(function(){ - self.canvas[self.method](function(err, chunk, len){ - if (err) { - self.emit('error', err); - } else if (len) { - self.push(chunk); - } else { - self.push(null); - } - }); + self.canvas[self.method](function(err, chunk, len){ + if (err) { + self.emit('error', err); + } else if (len) { + self.push(chunk); + } else { + self.push(null); + } }); }; From fec05445ea08af4849232062c4984327ed04728c Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 4 May 2017 10:27:09 -0700 Subject: [PATCH 021/474] Remove preprocessor directives for node <4 --- src/Canvas.cc | 31 ------------------------------- src/Canvas.h | 11 ----------- src/CanvasRenderingContext2d.cc | 8 -------- src/ImageData.cc | 18 ------------------ 4 files changed, 68 deletions(-) diff --git a/src/Canvas.cc b/src/Canvas.cc index e7445bf14..6f887e8af 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -180,47 +180,25 @@ NAN_SETTER(Canvas::SetHeight) { * EIO toBuffer callback. */ -#if NODE_VERSION_AT_LEAST(0, 6, 0) void Canvas::ToBufferAsync(uv_work_t *req) { -#elif NODE_VERSION_AT_LEAST(0, 5, 4) -void -Canvas::EIO_ToBuffer(eio_req *req) { -#else -int -Canvas::EIO_ToBuffer(eio_req *req) { -#endif closure_t *closure = (closure_t *) req->data; closure->status = canvas_write_to_png_stream( closure->canvas->surface() , toBuffer , closure); - -#if !NODE_VERSION_AT_LEAST(0, 5, 4) - return 0; -#endif } /* * EIO after toBuffer callback. */ -#if NODE_VERSION_AT_LEAST(0, 6, 0) void Canvas::ToBufferAsyncAfter(uv_work_t *req) { -#else -int -Canvas::EIO_AfterToBuffer(eio_req *req) { -#endif - Nan::HandleScope scope; closure_t *closure = (closure_t *) req->data; -#if NODE_VERSION_AT_LEAST(0, 6, 0) delete req; -#else - ev_unref(EV_DEFAULT_UC); -#endif if (closure->status) { Local argv[1] = { Canvas::Error(closure->status) }; @@ -236,10 +214,6 @@ Canvas::EIO_AfterToBuffer(eio_req *req) { delete closure->pfn; closure_destroy(closure); free(closure); - -#if !NODE_VERSION_AT_LEAST(0, 6, 0) - return 0; -#endif } /* @@ -328,14 +302,9 @@ NAN_METHOD(Canvas::ToBuffer) { canvas->Ref(); closure->pfn = new Nan::Callback(info[0].As()); -#if NODE_VERSION_AT_LEAST(0, 6, 0) uv_work_t* req = new uv_work_t; req->data = closure; uv_queue_work(uv_default_loop(), req, ToBufferAsync, (uv_after_work_cb)ToBufferAsyncAfter); -#else - eio_custom(EIO_ToBuffer, EIO_PRI_DEFAULT, EIO_AfterToBuffer, closure); - ev_ref(EV_DEFAULT_UC); -#endif return; // Sync diff --git a/src/Canvas.h b/src/Canvas.h index 4258e7de1..14f399059 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -63,19 +63,8 @@ class Canvas: public Nan::ObjectWrap { static NAN_METHOD(StreamJPEGSync); static NAN_METHOD(RegisterFont); static Local Error(cairo_status_t status); -#if NODE_VERSION_AT_LEAST(0, 6, 0) static void ToBufferAsync(uv_work_t *req); static void ToBufferAsyncAfter(uv_work_t *req); -#else - static -#if NODE_VERSION_AT_LEAST(0, 5, 4) - void -#else - int -#endif - EIO_ToBuffer(eio_req *req); - static int EIO_AfterToBuffer(eio_req *req); -#endif static PangoWeight GetWeightFromCSSString(const char *weight); static PangoStyle GetStyleFromCSSString(const char *style); static PangoFontDescription *ResolveFontDescription(const PangoFontDescription *desc); diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 377955a0d..f13b6d81e 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -697,16 +697,8 @@ NAN_METHOD(Context2d::GetImageData) { uint8_t *src = canvas->data(); -#if NODE_MAJOR_VERSION == 0 && NODE_MINOR_VERSION <= 10 - Local global = Context::GetCurrent()->Global(); - - Local sizeHandle = Nan::New(size); - Local caargv[] = { sizeHandle }; - Local clampedArray = global->Get(Nan::New("Uint8ClampedArray").ToLocalChecked()).As()->NewInstance(1, caargv); -#else Local buffer = ArrayBuffer::New(Isolate::GetCurrent(), size); Local clampedArray = Uint8ClampedArray::New(buffer, 0, size); -#endif Nan::TypedArrayContents typedArrayContents(clampedArray); uint8_t* dst = *typedArrayContents; diff --git a/src/ImageData.cc b/src/ImageData.cc index 73730da3c..709dae182 100644 --- a/src/ImageData.cc +++ b/src/ImageData.cc @@ -39,13 +39,7 @@ NAN_METHOD(ImageData::New) { return Nan::ThrowTypeError("Class constructors cannot be invoked without 'new'"); } -#if NODE_MAJOR_VERSION == 0 && NODE_MINOR_VERSION <= 10 - Local clampedArray; - Local global = Context::GetCurrent()->Global(); -#else Local clampedArray; -#endif - uint32_t width; uint32_t height; int length; @@ -63,23 +57,11 @@ NAN_METHOD(ImageData::New) { } length = width * height * 4; -#if NODE_MAJOR_VERSION == 0 && NODE_MINOR_VERSION <= 10 - Local sizeHandle = Nan::New(length); - Local caargv[] = { sizeHandle }; - clampedArray = global->Get(Nan::New("Uint8ClampedArray").ToLocalChecked()).As()->NewInstance(1, caargv); -#else clampedArray = Uint8ClampedArray::New(ArrayBuffer::New(Isolate::GetCurrent(), length), 0, length); -#endif -#if NODE_MAJOR_VERSION == 0 && NODE_MINOR_VERSION <= 10 - } else if (info[0]->ToObject()->GetIndexedPropertiesExternalArrayDataType() == kExternalPixelArray && info[1]->IsUint32()) { - clampedArray = info[0]->ToObject(); - length = clampedArray->GetIndexedPropertiesExternalArrayDataLength(); -#else } else if (info[0]->IsUint8ClampedArray() && info[1]->IsUint32()) { clampedArray = info[0].As(); length = clampedArray->Length(); -#endif if (length == 0) { Nan::ThrowRangeError("The input data has a zero byte length."); return; From e5c83083852b532c288bae4fb5d99f36c16ff156 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Mon, 19 Jun 2017 17:49:53 -0700 Subject: [PATCH 022/474] fix merge error in pngstream.js See https://github.com/Automattic/node-canvas/pull/740#commitcomment-22033227 --- lib/pngstream.js | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/lib/pngstream.js b/lib/pngstream.js index 94e5cdeef..3a50dbaf0 100644 --- a/lib/pngstream.js +++ b/lib/pngstream.js @@ -51,23 +51,6 @@ var PNGStream = module.exports = function PNGStream(canvas, sync) { util.inherits(PNGStream, Readable); -var PNGStream = module.exports = function PNGStream(canvas, sync) { - Readable.call(this); - - var self = this; - var method = sync - ? 'streamPNGSync' - : 'streamPNG'; - this.sync = sync; - this.canvas = canvas; - - // TODO: implement async - if ('streamPNG' === method) method = 'streamPNGSync'; - this.method = method; -}; - -util.inherits(PNGStream, Readable); - function noop() {} PNGStream.prototype._read = function _read() { From 6a29a230d44c066864c8b151587f9794b0453e89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Sat, 24 Jun 2017 20:56:53 +0200 Subject: [PATCH 023/474] Add browser compatible API --- Readme.md | 40 +++-- browser.js | 35 ++++ index.js | 29 +++- lib/context2d.js | 80 +-------- lib/parse-font.js | 63 ++++++++ package.json | 3 +- test/canvas.test.js | 190 ++++++++++------------ test/image.test.js | 357 +++++++++++++++++------------------------ test/imageData.test.js | 90 +++++------ 9 files changed, 435 insertions(+), 452 deletions(-) create mode 100644 browser.js create mode 100644 lib/parse-font.js diff --git a/Readme.md b/Readme.md index ed71402f8..075b1d46f 100644 --- a/Readme.md +++ b/Readme.md @@ -50,23 +50,29 @@ Windows | [Instructions on our wiki](https://github.com/Automattic/node-canvas/w ## Example ```javascript -var Canvas = require('canvas') - , Image = Canvas.Image - , canvas = new Canvas(200, 200) - , ctx = canvas.getContext('2d'); - -ctx.font = '30px Impact'; -ctx.rotate(.1); -ctx.fillText("Awesome!", 50, 100); - -var te = ctx.measureText('Awesome!'); -ctx.strokeStyle = 'rgba(0,0,0,0.5)'; -ctx.beginPath(); -ctx.lineTo(50, 102); -ctx.lineTo(50 + te.width, 102); -ctx.stroke(); - -console.log(''); +const { createCanvas, loadImage } = require('canvas') +const canvas = createCanvas(200, 200) +const ctx = canvas.getContext('2d') + +// Write "Awesome!" +ctx.font = '30px Impact' +ctx.rotate(0.1) +ctx.fillText('Awesome!', 50, 100) + +// Draw line under text +var text = ctx.measureText('Awesome!') +ctx.strokeStyle = 'rgba(0,0,0,0.5)' +ctx.beginPath() +ctx.lineTo(50, 102) +ctx.lineTo(50 + text.width, 102) +ctx.stroke() + +// Draw cat with lime helmet +loadImage('examples/images/lime-cat.jpg').then((image) => { + ctx.drawImage(image, 50, 0, 70, 70) + + console.log('') +}) ``` ## Non-Standard API diff --git a/browser.js b/browser.js new file mode 100644 index 000000000..c0a7b9fa8 --- /dev/null +++ b/browser.js @@ -0,0 +1,35 @@ +/* globals document, ImageData */ + +const parseFont = require('./lib/parse-font') + +exports.parseFont = parseFont + +exports.createCanvas = function (width, height) { + return Object.assign(document.createElement('canvas'), { width, height }) +} + +exports.createImageData = function (array, width, height) { + // Browser implementation of ImageData looks at the number of arguments passed + switch (arguments.length) { + case 0: return new ImageData() + case 1: return new ImageData(array) + case 2: return new ImageData(array, width) + default: return new ImageData(array, width, height) + } +} + +exports.loadImage = function (src) { + return new Promise((resolve, reject) => { + const image = document.createElement('img') + + function cleanup () { + image.onload = null + image.onerror = null + } + + image.onload = () => { cleanup(); resolve(image) } + image.onerror = () => { cleanup(); reject(new Error(`Failed to load the image "${src}"`)) } + + image.src = src + }) +} diff --git a/index.js b/index.js index a006b6107..18fb76452 100644 --- a/index.js +++ b/index.js @@ -1 +1,28 @@ -module.exports = require('./lib/canvas'); \ No newline at end of file +const Canvas = require('./lib/canvas') +const parseFont = require('./lib/parse-font') + +exports.parseFont = parseFont + +exports.createCanvas = function (width, height, type) { + return new Canvas(width, height, type) +} + +exports.createImageData = function (array, width, height) { + return new Canvas.ImageData(array, width, height) +} + +exports.loadImage = function (src) { + return new Promise((resolve, reject) => { + const image = new Canvas.Image() + + function cleanup () { + image.onload = null + image.onerror = null + } + + image.onload = () => { cleanup(); resolve(image) } + image.onerror = (err) => { cleanup(); reject(err) } + + image.src = src + }) +} diff --git a/lib/context2d.js b/lib/context2d.js index e95421e61..8ca28e762 100644 --- a/lib/context2d.js +++ b/lib/context2d.js @@ -11,89 +11,24 @@ */ var canvas = require('./bindings') + , parseFont = require('./parse-font') , Context2d = canvas.CanvasRenderingContext2d , CanvasGradient = canvas.CanvasGradient , CanvasPattern = canvas.CanvasPattern , ImageData = canvas.ImageData; -var parseCssFont = require('parse-css-font'); - -var unitsCss = require('units-css'); - /** * Export `Context2d` as the module. */ var Context2d = exports = module.exports = Context2d; -/** - * Cache color string RGBA values. - */ - -var cache = {}; - /** * Text baselines. */ var baselines = ['alphabetic', 'top', 'bottom', 'middle', 'ideographic', 'hanging']; -/** - * Parse font `str`. - * - * @param {String} str - * @return {Object} - * @api private - */ - -var parseFont = exports.parseFont = function(str) { - var parsedFont; - - // Try to parse the font string using parse-css-font. - // It will throw an exception if it fails. - try { - parsedFont = parseCssFont(str); - } - catch (e) { - // Invalid - return; - } - - // Cached - if (cache[str]) return cache[str]; - - // Parse size into value and unit using units-css - var size = unitsCss.parse(parsedFont.size); - - // TODO: dpi - // TODO: remaining unit conversion - switch (size.unit) { - case 'pt': - size.value /= .75; - break; - case 'in': - size.value *= 96; - break; - case 'mm': - size.value *= 96.0 / 25.4; - break; - case 'cm': - size.value *= 96.0 / 2.54; - break; - } - - // Populate font object - var font = { - weight: parsedFont.weight, - style: parsedFont.style, - size: size.value, - unit: size.unit, - family: parsedFont.family.join(',') - }; - - return cache[str] = font; -}; - /** * Enable or disable image smoothing. * @@ -334,10 +269,11 @@ Context2d.prototype.__defineGetter__('textAlign', function(){ * @api public */ -Context2d.prototype.createImageData = function(width, height){ - if ('ImageData' == width.constructor.name) { - height = width.height; - width = width.width; +Context2d.prototype.createImageData = function (width, height) { + if (typeof width === 'object') { + height = width.height + width = width.width } - return new ImageData(new Uint8ClampedArray(width * height * 4), width, height); -}; + + return new ImageData(width, height) +} diff --git a/lib/parse-font.js b/lib/parse-font.js new file mode 100644 index 000000000..948af1ae4 --- /dev/null +++ b/lib/parse-font.js @@ -0,0 +1,63 @@ +const parseCssFont = require('parse-css-font') +const unitsCss = require('units-css') + +/** + * Cache color string RGBA values. + */ + +const cache = {} + +/** + * Parse font `str`. + * + * @param {String} str + * @return {Object} + * @api private + */ + +module.exports = function (str) { + let parsedFont + + // Try to parse the font string using parse-css-font. + // It will throw an exception if it fails. + try { + parsedFont = parseCssFont(str) + } catch (_) { + // Invalid + return undefined + } + + // Cached + if (cache[str]) return cache[str] + + // Parse size into value and unit using units-css + var size = unitsCss.parse(parsedFont.size) + + // TODO: dpi + // TODO: remaining unit conversion + switch (size.unit) { + case 'pt': + size.value /= 0.75 + break + case 'in': + size.value *= 96 + break + case 'mm': + size.value *= 96.0 / 25.4 + break + case 'cm': + size.value *= 96.0 / 2.54 + break + } + + // Populate font object + var font = { + weight: parsedFont.weight, + style: parsedFont.style, + size: size.value, + unit: size.unit, + family: parsedFont.family.join(',') + } + + return (cache[str] = font) +} diff --git a/package.json b/package.json index e55be7287..1649157f0 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "description": "Canvas graphics API backed by Cairo", "version": "2.0.0-alpha.2", "author": "TJ Holowaychuk ", + "browser": "browser.js", "contributors": [ "Nathan Rajlich ", "Rod Vagg ", @@ -29,6 +30,7 @@ "test-server": "node test/server.js" }, "dependencies": { + "assert-rejects": "^0.1.1", "nan": "^2.4.0", "parse-css-font": "^2.0.2", "units-css": "^0.4.0" @@ -41,6 +43,5 @@ "engines": { "node": ">=4" }, - "main": "./lib/canvas.js", "license": "MIT" } diff --git a/test/canvas.test.js b/test/canvas.test.js index 764caf9da..337a1c0b8 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -1,31 +1,18 @@ +/* eslint-env mocha */ + /** * Module dependencies. */ -var Canvas = require('../') - , assert = require('assert') - , parseFont = Canvas.Context2d.parseFont - , fs = require('fs') - , os = require('os') - , Readable = require('stream').Readable; +const createCanvas = require('../').createCanvas +const loadImage = require('../').loadImage +const parseFont = require('../').parseFont -console.log(); -console.log(' canvas: %s', Canvas.version); -console.log(' cairo: %s', Canvas.cairoVersion); +const assert = require('assert') +const os = require('os') +const Readable = require('stream').Readable describe('Canvas', function () { - it('should require new', function () { - assert.throws(function () { Canvas(); }, TypeError); - }); - - it('.version', function () { - assert.ok(/^\d+\.\d+\.\d+(-(alpha|beta)\.\d+)?$/.test(Canvas.version)); - }); - - it('.cairoVersion', function () { - assert.ok(/^\d+\.\d+\.\d+$/.test(Canvas.cairoVersion)); - }); - it('.parseFont()', function () { var tests = [ '20px Arial' @@ -85,7 +72,7 @@ describe('Canvas', function () { }); it('color serialization', function () { - var canvas = new Canvas(200, 200) + var canvas = createCanvas(200, 200) , ctx = canvas.getContext('2d'); ['fillStyle', 'strokeStyle', 'shadowColor'].forEach(function(prop){ @@ -113,7 +100,7 @@ describe('Canvas', function () { }); it('color parser', function () { - var canvas = new Canvas(200, 200) + var canvas = createCanvas(200, 200) , ctx = canvas.getContext('2d'); ctx.fillStyle = '#ffccaa'; @@ -215,18 +202,18 @@ describe('Canvas', function () { }); it('Canvas#type', function () { - var canvas = new Canvas(10, 10); + var canvas = createCanvas(10, 10); assert.equal(canvas.type, 'image'); - var canvas = new Canvas(10, 10, 'pdf'); + var canvas = createCanvas(10, 10, 'pdf'); assert.equal(canvas.type, 'pdf'); - var canvas = new Canvas(10, 10, 'svg'); + var canvas = createCanvas(10, 10, 'svg'); assert.equal(canvas.type, 'svg'); - var canvas = new Canvas(10, 10, 'hey'); + var canvas = createCanvas(10, 10, 'hey'); assert.equal(canvas.type, 'image'); }); it('Canvas#getContext("2d")', function () { - var canvas = new Canvas(200, 300) + var canvas = createCanvas(200, 300) , ctx = canvas.getContext('2d'); assert.ok('object' == typeof ctx); assert.equal(canvas, ctx.canvas, 'context.canvas is not canvas'); @@ -234,11 +221,11 @@ describe('Canvas', function () { }); it('Canvas#{width,height}=', function () { - var canvas = new Canvas(100, 200); + var canvas = createCanvas(100, 200); assert.equal(100, canvas.width); assert.equal(200, canvas.height); - canvas = new Canvas; + canvas = createCanvas(); assert.equal(0, canvas.width); assert.equal(0, canvas.height); @@ -249,17 +236,17 @@ describe('Canvas', function () { }); it('Canvas#stride', function() { - var canvas = new Canvas(24, 10); + var canvas = createCanvas(24, 10); assert.ok(canvas.stride >= 24, 'canvas.stride is too short'); assert.ok(canvas.stride < 1024, 'canvas.stride seems too long'); }); it('Canvas#getContext("invalid")', function () { - assert.equal(null, new Canvas(200, 300).getContext('invalid')); + assert.equal(null, createCanvas(200, 300).getContext('invalid')); }); it('Context2d#patternQuality', function () { - var canvas = new Canvas(200, 200) + var canvas = createCanvas(200, 200) , ctx = canvas.getContext('2d'); assert.equal('good', ctx.patternQuality); @@ -270,7 +257,7 @@ describe('Canvas', function () { }); it('Context2d#font=', function () { - var canvas = new Canvas(200, 200) + var canvas = createCanvas(200, 200) , ctx = canvas.getContext('2d'); assert.equal('10px sans-serif', ctx.font); @@ -279,7 +266,7 @@ describe('Canvas', function () { }); it('Context2d#lineWidth=', function () { - var canvas = new Canvas(200, 200) + var canvas = createCanvas(200, 200) , ctx = canvas.getContext('2d'); ctx.lineWidth = 10.0; @@ -295,7 +282,7 @@ describe('Canvas', function () { }); it('Context2d#antiAlias=', function () { - var canvas = new Canvas(200, 200) + var canvas = createCanvas(200, 200) , ctx = canvas.getContext('2d'); assert.equal('default', ctx.antialias); @@ -312,7 +299,7 @@ describe('Canvas', function () { }); it('Context2d#lineCap=', function () { - var canvas = new Canvas(200, 200) + var canvas = createCanvas(200, 200) , ctx = canvas.getContext('2d'); assert.equal('butt', ctx.lineCap); @@ -321,7 +308,7 @@ describe('Canvas', function () { }); it('Context2d#lineJoin=', function () { - var canvas = new Canvas(200, 200) + var canvas = createCanvas(200, 200) , ctx = canvas.getContext('2d'); assert.equal('miter', ctx.lineJoin); @@ -330,7 +317,7 @@ describe('Canvas', function () { }); it('Context2d#globalAlpha=', function () { - var canvas = new Canvas(200, 200) + var canvas = createCanvas(200, 200) , ctx = canvas.getContext('2d'); assert.equal(1, ctx.globalAlpha); @@ -339,7 +326,7 @@ describe('Canvas', function () { }); it('Context2d#isPointInPath()', function () { - var canvas = new Canvas(200, 200) + var canvas = createCanvas(200, 200) , ctx = canvas.getContext('2d'); ctx.rect(5,5,100,100); @@ -358,7 +345,7 @@ describe('Canvas', function () { }); it('Context2d#textAlign', function () { - var canvas = new Canvas(200,200) + var canvas = createCanvas(200,200) , ctx = canvas.getContext('2d'); assert.equal('start', ctx.textAlign); @@ -373,12 +360,12 @@ describe('Canvas', function () { }); it('Canvas#toBuffer()', function () { - var buf = new Canvas(200,200).toBuffer(); + var buf = createCanvas(200,200).toBuffer(); assert.equal('PNG', buf.slice(1,4).toString()); }); it('Canvas#toBuffer() async', function (done) { - new Canvas(200, 200).toBuffer(function(err, buf){ + createCanvas(200, 200).toBuffer(function(err, buf){ assert.ok(!err); assert.equal('PNG', buf.slice(1,4).toString()); done(); @@ -386,7 +373,7 @@ describe('Canvas', function () { }); describe('#toBuffer("raw")', function() { - var canvas = new Canvas(11, 10) + var canvas = createCanvas(11, 10) , ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, 11, 10); @@ -470,7 +457,7 @@ describe('Canvas', function () { }); describe('#toDataURL()', function () { - var canvas = new Canvas(200, 200) + var canvas = createCanvas(200, 200) , ctx = canvas.getContext('2d'); ctx.fillRect(0,0,100,100); @@ -513,7 +500,7 @@ describe('Canvas', function () { }); it('toDataURL(function (err, str) {...}) works and defaults to PNG', function (done) { - new Canvas(200,200).toDataURL(function(err, str){ + createCanvas(200,200).toDataURL(function(err, str){ assert.ifError(err); assert.ok(0 === str.indexOf('data:image/png;base64,')); done(); @@ -521,7 +508,7 @@ describe('Canvas', function () { }); it('toDataURL(function (err, str) {...}) is async even with no canvas data', function (done) { - new Canvas().toDataURL(function(err, str){ + createCanvas().toDataURL(function(err, str){ assert.ifError(err); assert.ok('data:,' === str); done(); @@ -529,7 +516,7 @@ describe('Canvas', function () { }); it('toDataURL(0.5, function (err, str) {...}) works and defaults to PNG', function (done) { - new Canvas(200,200).toDataURL(0.5, function(err, str){ + createCanvas(200,200).toDataURL(0.5, function(err, str){ assert.ifError(err); assert.ok(0 === str.indexOf('data:image/png;base64,')); done(); @@ -537,7 +524,7 @@ describe('Canvas', function () { }); it('toDataURL(undefined, function (err, str) {...}) works and defaults to PNG', function (done) { - new Canvas(200,200).toDataURL(undefined, function(err, str){ + createCanvas(200,200).toDataURL(undefined, function(err, str){ assert.ifError(err); assert.ok(0 === str.indexOf('data:image/png;base64,')); done(); @@ -545,7 +532,7 @@ describe('Canvas', function () { }); it('toDataURL("image/png", function (err, str) {...}) works', function (done) { - new Canvas(200,200).toDataURL('image/png', function(err, str){ + createCanvas(200,200).toDataURL('image/png', function(err, str){ assert.ifError(err); assert.ok(0 === str.indexOf('data:image/png;base64,')); done(); @@ -553,7 +540,7 @@ describe('Canvas', function () { }); it('toDataURL("image/png", 0.5, function (err, str) {...}) works', function (done) { - new Canvas(200,200).toDataURL('image/png', 0.5, function(err, str){ + createCanvas(200,200).toDataURL('image/png', 0.5, function(err, str){ assert.ifError(err); assert.ok(0 === str.indexOf('data:image/png;base64,')); done(); @@ -576,7 +563,7 @@ describe('Canvas', function () { }); it('toDataURL("image/jpeg", function (err, str) {...}) works', function (done) { - new Canvas(200,200).toDataURL('image/jpeg', function(err, str){ + createCanvas(200,200).toDataURL('image/jpeg', function(err, str){ assert.ifError(err); assert.ok(0 === str.indexOf('data:image/jpeg;base64,')); done(); @@ -584,7 +571,7 @@ describe('Canvas', function () { }); it('toDataURL("iMAge/JPEG", function (err, str) {...}) works', function (done) { - new Canvas(200,200).toDataURL('iMAge/JPEG', function(err, str){ + createCanvas(200,200).toDataURL('iMAge/JPEG', function(err, str){ assert.ifError(err); assert.ok(0 === str.indexOf('data:image/jpeg;base64,')); done(); @@ -592,7 +579,7 @@ describe('Canvas', function () { }); it('toDataURL("image/jpeg", undefined, function (err, str) {...}) works', function (done) { - new Canvas(200,200).toDataURL('image/jpeg', undefined, function(err, str){ + createCanvas(200,200).toDataURL('image/jpeg', undefined, function(err, str){ assert.ifError(err); assert.ok(0 === str.indexOf('data:image/jpeg;base64,')); done(); @@ -600,7 +587,7 @@ describe('Canvas', function () { }); it('toDataURL("image/jpeg", 0.5, function (err, str) {...}) works', function (done) { - new Canvas(200,200).toDataURL('image/jpeg', 0.5, function(err, str){ + createCanvas(200,200).toDataURL('image/jpeg', 0.5, function(err, str){ assert.ifError(err); assert.ok(0 === str.indexOf('data:image/jpeg;base64,')); done(); @@ -608,7 +595,7 @@ describe('Canvas', function () { }); it('toDataURL("image/jpeg", opts, function (err, str) {...}) works', function (done) { - new Canvas(200,200).toDataURL('image/jpeg', {quality: 100}, function(err, str){ + createCanvas(200,200).toDataURL('image/jpeg', {quality: 100}, function(err, str){ assert.ifError(err); assert.ok(0 === str.indexOf('data:image/jpeg;base64,')); done(); @@ -617,7 +604,7 @@ describe('Canvas', function () { }); it('Context2d#createImageData(width, height)', function () { - var canvas = new Canvas(20, 20) + var canvas = createCanvas(20, 20) , ctx = canvas.getContext('2d'); var imageData = ctx.createImageData(2,6); @@ -632,7 +619,7 @@ describe('Canvas', function () { }); it('Context2d#measureText().width', function () { - var canvas = new Canvas(20, 20) + var canvas = createCanvas(20, 20) , ctx = canvas.getContext('2d'); assert.ok(ctx.measureText('foo').width); @@ -641,7 +628,7 @@ describe('Canvas', function () { }); it('Context2d#createImageData(ImageData)', function () { - var canvas = new Canvas(20, 20) + var canvas = createCanvas(20, 20) , ctx = canvas.getContext('2d'); var imageData = ctx.createImageData(ctx.createImageData(2, 6)); @@ -651,7 +638,7 @@ describe('Canvas', function () { }); it('Context2d#getImageData()', function () { - var canvas = new Canvas(3, 6) + var canvas = createCanvas(3, 6) , ctx = canvas.getContext('2d'); ctx.fillStyle = '#f00'; @@ -711,7 +698,7 @@ describe('Canvas', function () { }); it('Context2d#createPattern(Canvas)', function () { - var pattern = new Canvas(2,2) + var pattern = createCanvas(2,2) , checkers = pattern.getContext('2d'); // white @@ -752,7 +739,7 @@ describe('Canvas', function () { assert.equal(0, imageData.data[14]); assert.equal(255, imageData.data[15]); - var canvas = new Canvas(20, 20) + var canvas = createCanvas(20, 20) , ctx = canvas.getContext('2d') , pattern = ctx.createPattern(pattern); @@ -783,41 +770,40 @@ describe('Canvas', function () { }); it('Context2d#createPattern(Image)', function () { - var img = new Canvas.Image(); - img.src = __dirname + '/fixtures/checkers.png'; - - var canvas = new Canvas(20, 20) - , ctx = canvas.getContext('2d') - , pattern = ctx.createPattern(img); - - ctx.fillStyle = pattern; - ctx.fillRect(0,0,20,20); - - var imageData = ctx.getImageData(0,0,20,20); - assert.equal(20, imageData.width); - assert.equal(20, imageData.height); - assert.equal(1600, imageData.data.length); - - var i=0, b = true; - while(i { + var canvas = createCanvas(20, 20) + , ctx = canvas.getContext('2d') + , pattern = ctx.createPattern(img); + + ctx.fillStyle = pattern; + ctx.fillRect(0,0,20,20); + + var imageData = ctx.getImageData(0,0,20,20); + assert.equal(20, imageData.width); + assert.equal(20, imageData.height); + assert.equal(1600, imageData.data.length); + + var i=0, b = true; + while (i { + assert.strictEqual(img.onerror, null) + assert.strictEqual(img.onload, null) - it('Image#{width,height}', function () { - var img = new Image - , onloadCalled = 0; + assert.strictEqual(img.src, jpg_face) + assert.strictEqual(img.width, 485) + assert.strictEqual(img.height, 401) + assert.strictEqual(img.complete, true) + }) + }) + + it('loads PNG image', function () { + return loadImage(png_clock).then((img) => { + assert.strictEqual(img.onerror, null) + assert.strictEqual(img.onload, null) + + assert.strictEqual(img.src, png_clock) + assert.strictEqual(img.width, 320) + assert.strictEqual(img.height, 320) + assert.strictEqual(img.complete, true) + }) + }) + + it('calls Image#onload multiple times', function () { + return loadImage(png_clock).then((img) => { + let onloadCalled = 0 + + img.onload = () => { onloadCalled += 1 } + + img.src = png_checkers + assert.strictEqual(img.src, png_checkers) + assert.strictEqual(img.complete, true) + assert.strictEqual(img.width, 2) + assert.strictEqual(img.height, 2) + + img.src = png_clock + assert.strictEqual(img.src, png_clock) + assert.strictEqual(true, img.complete) + assert.strictEqual(320, img.width) + assert.strictEqual(320, img.height) + + assert.strictEqual(onloadCalled, 2) + + onloadCalled = 0 + img.onload = () => { onloadCalled += 1 } + + img.src = png_clock + assert.strictEqual(onloadCalled, 1) + }) + }) + + it('handles errors', function () { + return assertRejects(loadImage(`${png_clock}fail`), Error) + }) + + it('calls Image#onerror multiple times', function () { + return loadImage(png_clock).then((img) => { + let onloadCalled = 0 + let onerrorCalled = 0 - assert.strictEqual(0, img.width); - assert.strictEqual(0, img.height); + img.onload = () => { onloadCalled += 1 } + img.onerror = () => { onerrorCalled += 1 } - img.onload = function () { - onloadCalled += 1; - assert.strictEqual(320, img.width); - assert.strictEqual(320, img.height); - }; + img.src = `${png_clock}s1` + assert.strictEqual(img.src, `${png_clock}s1`) - img.src = png_clock; - assert.strictEqual(1, onloadCalled); - assert.strictEqual(320, img.width); - assert.strictEqual(320, img.height); - }); + img.src = `${png_clock}s2` + assert.strictEqual(img.src, `${png_clock}s2`) + + assert.strictEqual(onerrorCalled, 2) + + onerrorCalled = 0 + img.onerror = () => { onerrorCalled += 1 } + + img.src = `${png_clock}s3` + assert.strictEqual(img.src, `${png_clock}s3`) + + assert.strictEqual(onerrorCalled, 1) + assert.strictEqual(onloadCalled, 0) + }) + }) + + it('Image#{width,height}', function () { + return loadImage(png_clock).then((img) => { + img.src = '' + assert.strictEqual(img.width, 0) + assert.strictEqual(img.height, 0) + + img.src = png_clock + assert.strictEqual(img.width, 320) + assert.strictEqual(img.height, 320) + }) + }) it('Image#src set empty buffer', function () { - var image = new Canvas.Image(); - image.src = new Buffer(0); - image.src = new Buffer(''); - }); + return loadImage(png_clock).then((img) => { + let onerrorCalled = 0 + + img.onerror = () => { onerrorCalled += 1 } + + img.src = new Buffer(0) + assert.strictEqual(img.width, 0) + assert.strictEqual(img.height, 0) + + assert.strictEqual(onerrorCalled, 1) + }) + }) it('should unbind Image#onload', function() { - var img = new Image - , onloadCalled = 0; + return loadImage(png_clock).then((img) => { + let onloadCalled = 0 + + img.onload = () => { onloadCalled += 1 } - img.onload = function() { - onloadCalled += 1; - }; + img.src = png_checkers + assert.strictEqual(img.src, png_checkers) + assert.strictEqual(img.complete, true) + assert.strictEqual(img.width, 2) + assert.strictEqual(img.height, 2) - img.src = png_checkers; - assert.equal(img.src, png_checkers); - assert.strictEqual(true, img.complete); - assert.strictEqual(2, img.width); - assert.strictEqual(2, img.height); + assert.strictEqual(onloadCalled, 1) - assert.equal(onloadCalled, 1); + onloadCalled = 0 + img.onload = null - onloadCalled = 0; - img.onload = null; - img.src = png_clock; - assert.equal(img.src, png_clock); - assert.strictEqual(true, img.complete); - assert.strictEqual(320, img.width); - assert.strictEqual(320, img.height); + img.src = png_clock + assert.strictEqual(img.src, png_clock) + assert.strictEqual(img.complete, true) + assert.strictEqual(img.width, 320) + assert.strictEqual(img.height, 320) - assert.equal(onloadCalled, 0); - }); + assert.strictEqual(onloadCalled, 0) + }) + }) it('should unbind Image#onerror', function() { - var img = new Image - , onerrorCalled = 0; + return loadImage(png_clock).then((img) => { + let onloadCalled = 0 + let onerrorCalled = 0 + + img.onload = () => { onloadCalled += 1 } + img.onerror = () => { onerrorCalled += 1 } + img.src = `${png_clock}s1` + assert.strictEqual(img.src, `${png_clock}s1`) - img.onload = function() { - assert.fail('called onload'); - }; + img.src = `${png_clock}s2` + assert.strictEqual(img.src, `${png_clock}s2`) - img.onerror = function() { - onerrorCalled += 1; - }; + assert.strictEqual(onerrorCalled, 2) - img.src = png_clock + 's1'; - assert.equal(img.src, png_clock + 's1'); + onerrorCalled = 0 + img.onerror = null - assert.equal(onerrorCalled, 1); + img.src = `${png_clock}s3` + assert.strictEqual(img.src, `${png_clock}s3`) - onerrorCalled = 0; - img.onerror = null; - img.src = png_clock + 's3'; - assert.equal(img.src, png_clock + 's3'); - assert.equal(onerrorCalled, 0); - }); -}); + assert.strictEqual(onloadCalled, 0) + assert.strictEqual(onerrorCalled, 0) + }) + }) +}) diff --git a/test/imageData.test.js b/test/imageData.test.js index fe2f46a44..3e9314967 100644 --- a/test/imageData.test.js +++ b/test/imageData.test.js @@ -1,62 +1,48 @@ -'use strict'; +/* eslint-env mocha */ -var Canvas = require('../') - , ImageData = Canvas.ImageData - , assert = require('assert'); +const createImageData = require('../').createImageData -describe('ImageData', function () { - it('should require new', function () { - assert.throws(function () { ImageData(); }, TypeError); - }); +const assert = require('assert') +describe('ImageData', function () { it('should throw with invalid numeric arguments', function () { - assert.throws(function () { - new ImageData(0, 0); - }, /width is zero/); - assert.throws(function () { - new ImageData(1, 0); - }, /height is zero/); - assert.throws(function () { - new ImageData(0); - }, TypeError); - }); + assert.throws(() => { createImageData(0, 0) }, /width is zero/) + assert.throws(() => { createImageData(1, 0) }, /height is zero/) + assert.throws(() => { createImageData(0) }, TypeError) + }) it('should construct with width and height', function () { - var imagedata = new ImageData(2, 3); - assert.strictEqual(imagedata.width, 2); - assert.strictEqual(imagedata.height, 3); - assert(imagedata.data instanceof Uint8ClampedArray); - assert.strictEqual(imagedata.data.length, 24); - }); + const imageData = createImageData(2, 3) + + assert.strictEqual(imageData.width, 2) + assert.strictEqual(imageData.height, 3) + + assert.ok(imageData.data instanceof Uint8ClampedArray) + assert.strictEqual(imageData.data.length, 24) + }) it('should throw with invalid typed array', function () { - assert.throws(function () { - new ImageData(new Uint8ClampedArray(0), 0); - }, /input data has a zero byte length/); - assert.throws(function () { - new ImageData(new Uint8ClampedArray(3), 0); - }, /input data byte length is not a multiple of 4/); - assert.throws(function () { - new ImageData(new Uint8ClampedArray(16), 3); - }, RangeError); - assert.throws(function () { - new ImageData(new Uint8ClampedArray(12), 3, 5); - }, RangeError); - }); + assert.throws(() => { createImageData(new Uint8ClampedArray(0), 0) }, /input data has a zero byte length/) + assert.throws(() => { createImageData(new Uint8ClampedArray(3), 0) }, /input data byte length is not a multiple of 4/) + assert.throws(() => { createImageData(new Uint8ClampedArray(16), 3) }, RangeError) + assert.throws(() => { createImageData(new Uint8ClampedArray(12), 3, 5) }, RangeError) + }) it('should construct with typed array', function () { - var data = new Uint8ClampedArray(2 * 3 * 4); - var imagedata = new ImageData(data, 2); - assert.strictEqual(imagedata.width, 2); - assert.strictEqual(imagedata.height, 3); - assert(imagedata.data instanceof Uint8ClampedArray); - assert.strictEqual(imagedata.data.length, 24); - - data = new Uint8ClampedArray(3 * 4 * 4); - imagedata = new ImageData(data, 3, 4); - assert.strictEqual(imagedata.width, 3); - assert.strictEqual(imagedata.height, 4); - assert(imagedata.data instanceof Uint8ClampedArray); - assert.strictEqual(imagedata.data.length, 48); - }); -}); + let data, imageData + + data = new Uint8ClampedArray(2 * 3 * 4) + imageData = createImageData(data, 2) + assert.strictEqual(imageData.width, 2) + assert.strictEqual(imageData.height, 3) + assert.ok(imageData.data instanceof Uint8ClampedArray) + assert.strictEqual(imageData.data.length, 24) + + data = new Uint8ClampedArray(3 * 4 * 4) + imageData = createImageData(data, 3, 4) + assert.strictEqual(imageData.width, 3) + assert.strictEqual(imageData.height, 4) + assert.ok(imageData.data instanceof Uint8ClampedArray) + assert.strictEqual(imageData.data.length, 48) + }) +}) From 09cb28c1595d27680b0a9a5d7a4c2e0d30ee7082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Sat, 24 Jun 2017 21:05:29 +0200 Subject: [PATCH 024/474] Add use strict --- lib/parse-font.js | 2 ++ test/canvas.test.js | 2 ++ test/image.test.js | 2 ++ test/imageData.test.js | 2 ++ 4 files changed, 8 insertions(+) diff --git a/lib/parse-font.js b/lib/parse-font.js index 948af1ae4..b42bca471 100644 --- a/lib/parse-font.js +++ b/lib/parse-font.js @@ -1,3 +1,5 @@ +'use strict' + const parseCssFont = require('parse-css-font') const unitsCss = require('units-css') diff --git a/test/canvas.test.js b/test/canvas.test.js index 337a1c0b8..8678bf4d5 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -1,5 +1,7 @@ /* eslint-env mocha */ +'use strict' + /** * Module dependencies. */ diff --git a/test/image.test.js b/test/image.test.js index e2ac830b8..ae0fd5eac 100644 --- a/test/image.test.js +++ b/test/image.test.js @@ -1,5 +1,7 @@ /* eslint-env mocha */ +'use strict' + /** * Module dependencies. */ diff --git a/test/imageData.test.js b/test/imageData.test.js index 3e9314967..b78fadcf1 100644 --- a/test/imageData.test.js +++ b/test/imageData.test.js @@ -1,5 +1,7 @@ /* eslint-env mocha */ +'use strict' + const createImageData = require('../').createImageData const assert = require('assert') From a5d1e352b343919f13dcb1970fcf65f11dbe4363 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Sat, 24 Jun 2017 21:26:18 +0200 Subject: [PATCH 025/474] Move assert-rejects to devDependencies --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1649157f0..dc620ac24 100644 --- a/package.json +++ b/package.json @@ -30,12 +30,12 @@ "test-server": "node test/server.js" }, "dependencies": { - "assert-rejects": "^0.1.1", "nan": "^2.4.0", "parse-css-font": "^2.0.2", "units-css": "^0.4.0" }, "devDependencies": { + "assert-rejects": "^0.1.1", "express": "^4.14.0", "mocha": "^3.1.2", "standard": "^8.5.0" From 5889663f55c6721665bfb28e55431d5ed733d713 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Mon, 19 Jun 2017 20:07:21 -0700 Subject: [PATCH 026/474] fix memory leak in Canvas constructor Don't recreate the cairo surface when the canvas is constructed. Fixes #922 --- src/Canvas.cc | 1 - src/backend/ImageBackend.cc | 4 ++-- src/backend/PdfBackend.cc | 4 ++-- src/backend/SvgBackend.cc | 1 - 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Canvas.cc b/src/Canvas.cc index 6f887e8af..0a5752f41 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -617,7 +617,6 @@ NAN_METHOD(Canvas::RegisterFont) { Canvas::Canvas(Backend* backend) : ObjectWrap() { _backend = backend; - this->backend()->createSurface(); } /* diff --git a/src/backend/ImageBackend.cc b/src/backend/ImageBackend.cc index 7cdcff2a8..5c64e5708 100644 --- a/src/backend/ImageBackend.cc +++ b/src/backend/ImageBackend.cc @@ -17,9 +17,10 @@ ImageBackend::~ImageBackend() cairo_surface_t* ImageBackend::createSurface() { + assert(!this->surface); this->surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); assert(this->surface); - Nan::AdjustExternalMemory(4 * width * height); + Nan::AdjustExternalMemory(4 * width * height); return this->surface; } @@ -35,7 +36,6 @@ cairo_surface_t* ImageBackend::recreateSurface() return createSurface(); } - Nan::Persistent ImageBackend::constructor; void ImageBackend::Initialize(Handle target) diff --git a/src/backend/PdfBackend.cc b/src/backend/PdfBackend.cc index a187d90a0..a976b6f86 100644 --- a/src/backend/PdfBackend.cc +++ b/src/backend/PdfBackend.cc @@ -13,6 +13,8 @@ using namespace v8; PdfBackend::PdfBackend(int width, int height) : Backend("pdf", width, height) { + _closure = malloc(sizeof(closure_t)); + assert(_closure); createSurface(); } @@ -28,8 +30,6 @@ PdfBackend::~PdfBackend() cairo_surface_t* PdfBackend::createSurface() { - _closure = malloc(sizeof(closure_t)); - assert(_closure); cairo_status_t status = closure_init((closure_t*)_closure, this->canvas, 0, PNG_NO_FILTERS); assert(status == CAIRO_STATUS_SUCCESS); diff --git a/src/backend/SvgBackend.cc b/src/backend/SvgBackend.cc index ffd4ce13f..71249a16b 100644 --- a/src/backend/SvgBackend.cc +++ b/src/backend/SvgBackend.cc @@ -15,7 +15,6 @@ SvgBackend::SvgBackend(int width, int height) { _closure = malloc(sizeof(closure_t)); assert(_closure); - createSurface(); } From 490ef47432af83582cf5e491a8ca8797d07c93b0 Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Fri, 30 Jun 2017 11:18:48 +0200 Subject: [PATCH 027/474] Use .node extension for requiring native module According to the [Node.js documentation][1] the file extension CAN be omitted but omitting it has the disadvantage that webpack can't match these modules. So if you try to package a JavaScript application which use node-canvas then you can't simply use webpack plugins like [node-native-loader][2]. With this little change webpack and the node-native-loader plugin can be used to easily create an application package with the canvas.node lib copied beside the packed JavaScript and everything works fine. [1]: https://nodejs.org/api/addons.html#addons_loading_addons_using_require [2]: https://www.npmjs.com/package/node-native-loader --- lib/bindings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/bindings.js b/lib/bindings.js index c5c95b522..c0afc9841 100644 --- a/lib/bindings.js +++ b/lib/bindings.js @@ -1,3 +1,3 @@ 'use strict'; -module.exports = require('../build/Release/canvas'); +module.exports = require('../build/Release/canvas.node'); From b8b8f2af6108842ac2298de83d4c992217ca4d43 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 1 Jul 2017 17:51:44 -0700 Subject: [PATCH 028/474] Support A1, A8, RGB30, RGB16_565, RGB24 pixelFormats; alpha ctx option --- History.md | 2 + Readme.md | 67 +++++ lib/canvas.js | 9 +- lib/context2d.js | 5 +- src/Canvas.cc | 3 + src/CanvasRenderingContext2d.cc | 288 +++++++++++++++++---- src/CanvasRenderingContext2d.h | 1 + src/ImageData.cc | 29 ++- src/ImageData.h | 1 - src/PNG.h | 32 ++- src/backend/Backend.cc | 4 +- src/backend/Backend.h | 3 + src/backend/ImageBackend.cc | 49 +++- src/backend/ImageBackend.h | 7 + test/canvas.test.js | 436 ++++++++++++++++++++++++++------ test/imageData.test.js | 8 +- test/public/app.js | 7 +- test/server.js | 9 +- 18 files changed, 782 insertions(+), 178 deletions(-) diff --git a/History.md b/History.md index 3e2060ac7..805cc220e 100644 --- a/History.md +++ b/History.md @@ -2,6 +2,8 @@ Unreleased / patch ================== * Port has_lib.sh to javascript (#872) + * Support canvas.getContext("2d", {alpha: boolean}) and + canvas.getContext("2d", {pixelFormat: "..."}) 1.6.0 / 2016-10-16 ================== diff --git a/Readme.md b/Readme.md index 075b1d46f..1a4c6b810 100644 --- a/Readme.md +++ b/Readme.md @@ -312,6 +312,73 @@ var canvas = new Canvas(200, 500, 'svg'); fs.writeFile('out.svg', canvas.toBuffer()); ``` +## Image pixel formats (experimental) + +node-canvas has experimental support for additional pixel formats, roughly +following the [Canvas color space proposal](https://github.com/WICG/canvas-color-space/blob/master/CanvasColorSpaceProposal.md). + +```js +var canvas = new Canvas(200, 200); +var ctx = canvas.getContext('2d', {pixelFormat: 'A8'}); +``` + +By default, canvases are created in the `RGBA32` format, which corresponds to +the native HTML Canvas behavior. Each pixel is 32 bits. The JavaScript APIs +that involve pixel data (`getImageData`, `putImageData`) store the colors in +the order {red, green, blue, alpha} without alpha pre-multiplication. (The C++ +API stores the colors in the order {alpha, red, green, blue} in native-[endian](https://en.wikipedia.org/wiki/Endianness) +ordering, with alpha pre-multiplication.) + +These additional pixel formats have experimental support: + +* `RGB24` Like `RGBA32`, but the 8 alpha bits are always opaque. This format is + always used if the `alpha` context attribute is set to false (i.e. + `canvas.getContext('2d', {alpha: false})`). This format can be faster than + `RGBA32` because transparency does not need to be calculated. +* `A8` Each pixel is 8 bits. This format can either be used for creating + grayscale images (treating each byte as an alpha value), or for creating + indexed PNGs (treating each byte as a palette index). +* `RGB16_565` Each pixel is 16 bits, with red in the upper 5 bits, green in the + middle 6 bits, and blue in the lower 5 bits, in native platform endianness. + Some hardware devices and frame buffers use this format. Note that PNG does + not support this format; when creating a PNG, the image will be converted to + 24-bit RGB. This format is thus suboptimal for generating PNGs. +* `A1` Each pixel is 1 bit, and pixels are packed together into 32-bit + quantities. The ordering of the bits matches the endianness of the + platform: on a little-endian machine, the first pixel is the least- + significant bit. This format can be used for creating single-color images. + *Support for this format is incomplete, see note below.* +* `RGB30` Each pixel is 30 bits, with red in the upper 10, green + in the middle 10, and blue in the lower 10. (Requires Cairo 1.12 or later.) + *Support for this format is incomplete, see note below.* + +Notes and caveats: + +* Using a non-default format can affect the behavior of APIs that involve pixel + data: + + * `context2d.createImageData` The size of the array returned depends on the + number of bit per pixel for the underlying image data format, per the above + descriptions. + * `context2d.getImageData` The format of the array returned depends on the + underlying image mode, per the above descriptions. Be aware of platform + endianness, which can be determined using node.js's [`os.endianness()`](https://nodejs.org/api/os.html#os_os_endianness) + function. + * `context2d.putImageData` As above. + +* `A1` and `RGB30` do not yet support `getImageData` or `putImageData`. Have a + use case and/or opinion on working with these formats? Open an issue and let + us know! + +* `A1`, `A8`, `RGB30` and `RGB16_565` with shadow blurs may crash or not render + properly. + +* The `ImageData(width, height)` and `ImageData(Uint8ClampedArray, width)` + constructors assume 4 bytes per pixel. To create an `ImageData` instance with + a different number of bytes per pixel, use + `new ImageData(new Uint8ClampedArray(size), width, height)` or + `new ImageData(new Uint16ClampedArray(size), width, height)`. + ## Benchmarks Although node-canvas is extremely new, and we have not even begun optimization yet it is already quite fast. For benchmarks vs other node canvas implementations view this [gist](https://gist.github.com/664922), or update the submodules and run `$ make benchmark` yourself. diff --git a/lib/canvas.js b/lib/canvas.js index fc77ff52e..3b4c6bc57 100644 --- a/lib/canvas.js +++ b/lib/canvas.js @@ -112,14 +112,15 @@ Canvas.prototype.inspect = function(){ /** * Get a context object. * - * @param {String} contextId + * @param {String} contextType must be "2d" + * @param {Object {alpha: boolean, pixelFormat: PIXEL_FORMAT} } contextAttributes Optional * @return {Context2d} * @api public */ -Canvas.prototype.getContext = function(contextId){ - if ('2d' == contextId) { - var ctx = this._context2d || (this._context2d = new Context2d(this)); +Canvas.prototype.getContext = function (contextType, contextAttributes) { + if ('2d' == contextType) { + var ctx = this._context2d || (this._context2d = new Context2d(this, contextAttributes)); this.context = ctx; ctx.canvas = this; return ctx; diff --git a/lib/context2d.js b/lib/context2d.js index 8ca28e762..35b3e8db3 100644 --- a/lib/context2d.js +++ b/lib/context2d.js @@ -274,6 +274,7 @@ Context2d.prototype.createImageData = function (width, height) { height = width.height width = width.width } - - return new ImageData(width, height) + var Bpp = this.canvas.stride / this.canvas.width; + var nBytes = Bpp * width * height + return new ImageData(new Uint8ClampedArray(nBytes), width, height) } diff --git a/src/Canvas.cc b/src/Canvas.cc index 0a5752f41..37e4bb10d 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -103,6 +103,7 @@ NAN_METHOD(Canvas::New) { backend = new ImageBackend(width, height); } else if (info[0]->IsObject()) { + // TODO need to check if this is actually an instance of a Backend to avoid a fault backend = Nan::ObjectWrap::Unwrap(info[0]->ToObject()); } else { @@ -304,6 +305,8 @@ NAN_METHOD(Canvas::ToBuffer) { uv_work_t* req = new uv_work_t; req->data = closure; + // Make sure the surface exists since we won't have an isolate context in the async block: + canvas->surface(); uv_queue_work(uv_default_loop(), req, ToBufferAsync, (uv_after_work_cb)ToBufferAsyncAfter); return; diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index f13b6d81e..41db59fc6 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -10,6 +10,7 @@ #include #include #include +#include #include "Canvas.h" #include "Point.h" @@ -18,6 +19,7 @@ #include "CanvasRenderingContext2d.h" #include "CanvasGradient.h" #include "CanvasPattern.h" +#include "backend/ImageBackend.h" // Windows doesn't support the C99 names for these #ifdef _MSC_VER @@ -123,6 +125,7 @@ Context2d::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { Nan::SetPrototypeMethod(ctor, "_setStrokePattern", SetStrokePattern); Nan::SetPrototypeMethod(ctor, "_setTextBaseline", SetTextBaseline); Nan::SetPrototypeMethod(ctor, "_setTextAlignment", SetTextAlignment); + Nan::SetAccessor(proto, Nan::New("pixelFormat").ToLocalChecked(), GetFormat); Nan::SetAccessor(proto, Nan::New("patternQuality").ToLocalChecked(), GetPatternQuality, SetPatternQuality); Nan::SetAccessor(proto, Nan::New("globalCompositeOperation").ToLocalChecked(), GetGlobalCompositeOperation, SetGlobalCompositeOperation); Nan::SetAccessor(proto, Nan::New("globalAlpha").ToLocalChecked(), GetGlobalAlpha, SetGlobalAlpha); @@ -501,11 +504,61 @@ NAN_METHOD(Context2d::New) { if (!Nan::New(Canvas::constructor)->HasInstance(obj)) return Nan::ThrowTypeError("Canvas expected"); Canvas *canvas = Nan::ObjectWrap::Unwrap(obj); + + bool isImageBackend = canvas->backend()->getName() == "image"; + if (isImageBackend) { + cairo_format_t format = ImageBackend::DEFAULT_FORMAT; + if (info[1]->IsObject()) { + Local ctxAttributes = info[1]->ToObject(); + + Local pixelFormat = ctxAttributes->Get(Nan::New("pixelFormat").ToLocalChecked()); + if (pixelFormat->IsString()) { + String::Utf8Value utf8PixelFormat(pixelFormat); + if (!strcmp(*utf8PixelFormat, "RGBA32")) format = CAIRO_FORMAT_ARGB32; + else if (!strcmp(*utf8PixelFormat, "RGB24")) format = CAIRO_FORMAT_RGB24; + else if (!strcmp(*utf8PixelFormat, "A8")) format = CAIRO_FORMAT_A8; + else if (!strcmp(*utf8PixelFormat, "RGB16_565")) format = CAIRO_FORMAT_RGB16_565; + else if (!strcmp(*utf8PixelFormat, "A1")) format = CAIRO_FORMAT_A1; +#ifdef CAIRO_FORMAT_RGB30 + else if (!strcmp(utf8PixelFormat, "RGB30")) format = CAIRO_FORMAT_RGB30; +#endif + } + + // alpha: false forces use of RGB24 + Local alpha = ctxAttributes->Get(Nan::New("alpha").ToLocalChecked()); + if (alpha->IsBoolean() && !alpha->BooleanValue()) { + format = CAIRO_FORMAT_RGB24; + } + } + static_cast(canvas->backend())->setFormat(format); + } + Context2d *context = new Context2d(canvas); context->Wrap(info.This()); info.GetReturnValue().Set(info.This()); } +/* +* Get format (string). +*/ + +NAN_GETTER(Context2d::GetFormat) { + Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); + std::string pixelFormatString; + switch (context->canvas()->backend()->getFormat()) { + case CAIRO_FORMAT_ARGB32: pixelFormatString = "RGBA32"; break; + case CAIRO_FORMAT_RGB24: pixelFormatString = "RGB24"; break; + case CAIRO_FORMAT_A8: pixelFormatString = "A8"; break; + case CAIRO_FORMAT_A1: pixelFormatString = "A1"; break; + case CAIRO_FORMAT_RGB16_565: pixelFormatString = "RGB16_565"; break; +#ifdef CAIRO_FORMAT_RGB30 + case CAIRO_FORMAT_RGB30: pixelFormatString = "RGB30"; break; +#endif + default: return info.GetReturnValue().SetNull(); + } + info.GetReturnValue().Set(Nan::New(pixelFormatString).ToLocalChecked()); +} + /* * Create a new page. */ @@ -540,8 +593,9 @@ NAN_METHOD(Context2d::PutImageData) { uint8_t *src = imageData->data(); uint8_t *dst = context->canvas()->data(); - int srcStride = imageData->stride() - , dstStride = context->canvas()->stride(); + int dstStride = context->canvas()->stride(); + int Bpp = dstStride / context->canvas()->getWidth(); + int srcStride = Bpp * imageData->width(); int sx = 0 , sy = 0 @@ -593,41 +647,111 @@ NAN_METHOD(Context2d::PutImageData) { if (cols <= 0 || rows <= 0) return; - src += sy * srcStride + sx * 4; - dst += dstStride * dy + 4 * dx; - for (int y = 0; y < rows; ++y) { - uint8_t *dstRow = dst; - uint8_t *srcRow = src; - for (int x = 0; x < cols; ++x) { - // rgba - uint8_t r = *srcRow++; - uint8_t g = *srcRow++; - uint8_t b = *srcRow++; - uint8_t a = *srcRow++; - - // argb - // performance optimization: fully transparent/opaque pixels can be - // processed more efficiently. - if (a == 0) { - *dstRow++ = 0; - *dstRow++ = 0; - *dstRow++ = 0; - *dstRow++ = 0; - } else if (a == 255) { + switch (context->canvas()->backend()->getFormat()) { + case CAIRO_FORMAT_ARGB32: { + src += sy * srcStride + sx * 4; + dst += dstStride * dy + 4 * dx; + for (int y = 0; y < rows; ++y) { + uint8_t *dstRow = dst; + uint8_t *srcRow = src; + for (int x = 0; x < cols; ++x) { + // rgba + uint8_t r = *srcRow++; + uint8_t g = *srcRow++; + uint8_t b = *srcRow++; + uint8_t a = *srcRow++; + + // argb + // performance optimization: fully transparent/opaque pixels can be + // processed more efficiently. + if (a == 0) { + *dstRow++ = 0; + *dstRow++ = 0; + *dstRow++ = 0; + *dstRow++ = 0; + } else if (a == 255) { + *dstRow++ = b; + *dstRow++ = g; + *dstRow++ = r; + *dstRow++ = a; + } else { + float alpha = (float)a / 255; + *dstRow++ = b * alpha; + *dstRow++ = g * alpha; + *dstRow++ = r * alpha; + *dstRow++ = a; + } + } + dst += dstStride; + src += srcStride; + } + break; + } + case CAIRO_FORMAT_RGB24: { + src += sy * srcStride + sx * 4; + dst += dstStride * dy + 4 * dx; + for (int y = 0; y < rows; ++y) { + uint8_t *dstRow = dst; + uint8_t *srcRow = src; + for (int x = 0; x < cols; ++x) { + // rgba + uint8_t r = *srcRow++; + uint8_t g = *srcRow++; + uint8_t b = *srcRow++; + srcRow++; + + // argb *dstRow++ = b; *dstRow++ = g; *dstRow++ = r; - *dstRow++ = a; - } else { - float alpha = (float)a / 255; - *dstRow++ = b * alpha; - *dstRow++ = g * alpha; - *dstRow++ = r * alpha; - *dstRow++ = a; + *dstRow++ = 255; } + dst += dstStride; + src += srcStride; } - dst += dstStride; - src += srcStride; + break; + } + case CAIRO_FORMAT_A8: { + src += sy * srcStride + sx; + dst += dstStride * dy + dx; + if (srcStride == dstStride && cols == dstStride) { + // fast path: strides are the same and doing a full-width put + memcpy(dst, src, cols * rows); + } else { + for (int y = 0; y < rows; ++y) { + memcpy(dst, src, cols); + dst += dstStride; + src += srcStride; + } + } + break; + } + case CAIRO_FORMAT_A1: { + // TODO Should this be totally packed, or maintain a stride divisible by 4? + Nan::ThrowError("putImageData for CANVAS_FORMAT_A1 is not yet implemented"); + break; + } + case CAIRO_FORMAT_RGB16_565: { + src += sy * srcStride + sx * 2; + dst += dstStride * dy + 2 * dx; + for (int y = 0; y < rows; ++y) { + memcpy(dst, src, cols * 2); + dst += dstStride; + src += srcStride; + } + break; + } +#ifdef CAIRO_FORMAT_RGB30 + case CAIRO_FORMAT_RGB30: { + // TODO + Nan::ThrowError("putImageData for CANVAS_FORMAT_RGB30 is not yet implemented"); + break; + } +#endif + default: { + Nan::ThrowError("Invalid pixel format"); + break; + } } cairo_surface_mark_dirty_rectangle( @@ -690,10 +814,10 @@ NAN_METHOD(Context2d::GetImageData) { sy = 0; } - int size = sw * sh * 4; - int srcStride = canvas->stride(); - int dstStride = sw * 4; + int bpp = srcStride / canvas->getWidth(); + int size = sw * sh * bpp; + int dstStride = sw * bpp; uint8_t *src = canvas->data(); @@ -703,33 +827,93 @@ NAN_METHOD(Context2d::GetImageData) { Nan::TypedArrayContents typedArrayContents(clampedArray); uint8_t* dst = *typedArrayContents; - // Normalize data (argb -> rgba) - for (int y = 0; y < sh; ++y) { + switch (canvas->backend()->getFormat()) { + case CAIRO_FORMAT_ARGB32: { + // Rearrange alpha (argb -> rgba), undo alpha pre-multiplication, + // and store in big-endian format + for (int y = 0; y < sh; ++y) { + uint32_t *row = (uint32_t *)(src + srcStride * (y + sy)); + for (int x = 0; x < sw; ++x) { + int bx = x * 4; + uint32_t *pixel = row + x + sx; + uint8_t a = *pixel >> 24; + uint8_t r = *pixel >> 16; + uint8_t g = *pixel >> 8; + uint8_t b = *pixel; + dst[bx + 3] = a; + + // Performance optimization: fully transparent/opaque pixels can be + // processed more efficiently. + if (a == 0 || a == 255) { + dst[bx + 0] = r; + dst[bx + 1] = g; + dst[bx + 2] = b; + } else { + // Undo alpha pre-multiplication + float alphaR = (float)255 / a; + dst[bx + 0] = (int)((float)r * alphaR); + dst[bx + 1] = (int)((float)g * alphaR); + dst[bx + 2] = (int)((float)b * alphaR); + } + + } + dst += dstStride; + } + break; + } + case CAIRO_FORMAT_RGB24: { + // Rearrange alpha (argb -> rgba) and store in big-endian format + for (int y = 0; y < sh; ++y) { uint32_t *row = (uint32_t *)(src + srcStride * (y + sy)); for (int x = 0; x < sw; ++x) { int bx = x * 4; uint32_t *pixel = row + x + sx; - uint8_t a = *pixel >> 24; uint8_t r = *pixel >> 16; uint8_t g = *pixel >> 8; uint8_t b = *pixel; - dst[bx + 3] = a; - - // Performance optimization: fully transparent/opaque pixels can be - // processed more efficiently. - if (a == 0 || a == 255) { - dst[bx + 0] = r; - dst[bx + 1] = g; - dst[bx + 2] = b; - } else { - float alpha = (float)a / 255; - dst[bx + 0] = (int)((float)r / alpha); - dst[bx + 1] = (int)((float)g / alpha); - dst[bx + 2] = (int)((float)b / alpha); - } + dst[bx + 0] = r; + dst[bx + 1] = g; + dst[bx + 2] = b; + dst[bx + 3] = 255; } dst += dstStride; + } + break; + } + case CAIRO_FORMAT_A8: { + for (int y = 0; y < sh; ++y) { + uint8_t *row = (uint8_t *)(src + srcStride * (y + sy)); + memcpy(dst, row + sx, dstStride); + dst += dstStride; + } + break; + } + case CAIRO_FORMAT_A1: { + // TODO Should this be totally packed, or maintain a stride divisible by 4? + Nan::ThrowError("getImageData for CANVAS_FORMAT_A1 is not yet implemented"); + break; + } + case CAIRO_FORMAT_RGB16_565: { + for (int y = 0; y < sh; ++y) { + uint16_t *row = (uint16_t *)(src + srcStride * (y + sy)); + memcpy(dst, row + sx, dstStride); + dst += dstStride; + } + break; + } +#ifdef CAIRO_FORMAT_RGB30 + case CAIRO_FORMAT_RGB30: { + // TODO + Nan::ThrowError("getImageData for CANVAS_FORMAT_RGB30 is not yet implemented"); + break; + } +#endif + default: { + // Unlikely + Nan::ThrowError("Invalid pixel format"); + break; + } } const int argc = 3; diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index fccb5d184..a43881441 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -98,6 +98,7 @@ class Context2d: public Nan::ObjectWrap { static NAN_METHOD(Arc); static NAN_METHOD(ArcTo); static NAN_METHOD(GetImageData); + static NAN_GETTER(GetFormat); static NAN_GETTER(GetPatternQuality); static NAN_GETTER(GetGlobalCompositeOperation); static NAN_GETTER(GetGlobalAlpha); diff --git a/src/ImageData.cc b/src/ImageData.cc index 709dae182..ca4dfdd58 100644 --- a/src/ImageData.cc +++ b/src/ImageData.cc @@ -55,36 +55,37 @@ NAN_METHOD(ImageData::New) { Nan::ThrowRangeError("The source height is zero."); return; } - length = width * height * 4; + length = width * height * 4; // ImageData(w, h) constructor assumes 4 BPP; documented. clampedArray = Uint8ClampedArray::New(ArrayBuffer::New(Isolate::GetCurrent(), length), 0, length); } else if (info[0]->IsUint8ClampedArray() && info[1]->IsUint32()) { clampedArray = info[0].As(); + length = clampedArray->Length(); if (length == 0) { Nan::ThrowRangeError("The input data has a zero byte length."); return; } - if (length % 4 != 0) { - Nan::ThrowRangeError("The input data byte length is not a multiple of 4."); - return; - } + + // Don't assert that the ImageData length is a multiple of four because some + // data formats are not 4 BPP. + width = info[1]->Uint32Value(); - int size = length / 4; if (width == 0) { Nan::ThrowRangeError("The source width is zero."); return; } - if (size % width != 0) { - Nan::ThrowRangeError("The input data byte length is not a multiple of (4 * width)."); - return; - } - height = size / width; - if (info[2]->IsUint32() && info[2]->Uint32Value() != height) { - Nan::ThrowRangeError("The input data byte length is not equal to (4 * width * height)."); - return; + + // Don't assert that the byte length is a multiple of 4 * width, ditto. + + if (info[2]->IsUint32()) { // Explicit height given + height = info[2]->Uint32Value(); + } else { // Calculate height assuming 4 BPP + int size = length / 4; + height = size / width; } + } else { Nan::ThrowTypeError("Expected (Uint8ClampedArray, width[, height]) or (width, height)"); return; diff --git a/src/ImageData.h b/src/ImageData.h index 074d1ff8a..8007f4d31 100644 --- a/src/ImageData.h +++ b/src/ImageData.h @@ -23,7 +23,6 @@ class ImageData: public Nan::ObjectWrap { inline int width() { return _width; } inline int height() { return _height; } inline uint8_t *data() { return _data; } - inline int stride() { return _width * 4; } ImageData(uint8_t *data, int width, int height) : _width(width), _height(height), _data(data) {} private: diff --git a/src/PNG.h b/src/PNG.h index 4cdb78919..558e4c177 100644 --- a/src/PNG.h +++ b/src/PNG.h @@ -63,6 +63,28 @@ static void canvas_unpremultiply_data(png_structp png, png_row_infop row_info, p } } +/* Converts RGB16_565 format data to RGBA32 */ +static void canvas_convert_565_to_888(png_structp png, png_row_infop row_info, png_bytep data) { + // Loop in reverse to unpack in-place. + for (ptrdiff_t col = row_info->width - 1; col >= 0; col--) { + uint8_t* src = &data[col * sizeof(uint16_t)]; + uint8_t* dst = &data[col * 3]; + uint16_t pixel; + + memcpy(&pixel, src, sizeof(uint16_t)); + + // Convert and rescale to the full 0-255 range + // See http://stackoverflow.com/a/29326693 + const uint8_t red5 = (pixel & 0xF800) >> 11; + const uint8_t green6 = (pixel & 0x7E0) >> 5; + const uint8_t blue5 = (pixel & 0x001F); + + dst[0] = ((red5 * 255 + 15) / 31); + dst[1] = ((green6 * 255 + 31) / 63); + dst[2] = ((blue5 * 255 + 15) / 31); + } +} + struct canvas_png_write_closure_t { cairo_write_func_t write_func; void *closure; @@ -99,8 +121,9 @@ static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr writ return status; } + int stride = cairo_image_surface_get_stride(surface); for (i = 0; i < height; i++) { - rows[i] = (png_byte *) data + i * cairo_image_surface_get_stride(surface); + rows[i] = (png_byte *) data + i * stride; } #ifdef PNG_USER_MEM_SUPPORTED @@ -162,8 +185,11 @@ static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr writ png_set_packswap(png); #endif break; - case CAIRO_FORMAT_INVALID: case CAIRO_FORMAT_RGB16_565: + bpc = 8; // 565 gets upconverted to 888 + png_color_type = PNG_COLOR_TYPE_RGB; + break; + case CAIRO_FORMAT_INVALID: default: status = CAIRO_STATUS_INVALID_FORMAT; png_destroy_write_struct(&png, &info); @@ -184,6 +210,8 @@ static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr writ png_write_info(png, info); if (png_color_type == PNG_COLOR_TYPE_RGB_ALPHA) { png_set_write_user_transform_fn(png, canvas_unpremultiply_data); + } else if (cairo_image_surface_get_format(surface) == CAIRO_FORMAT_RGB16_565) { + png_set_write_user_transform_fn(png, canvas_convert_565_to_888); } else if (png_color_type == PNG_COLOR_TYPE_RGB) { png_set_write_user_transform_fn(png, canvas_convert_data_to_bytes); png_set_filler(png, 0, PNG_FILLER_AFTER); diff --git a/src/backend/Backend.cc b/src/backend/Backend.cc index e2a2e9a5a..b3e3dac45 100644 --- a/src/backend/Backend.cc +++ b/src/backend/Backend.cc @@ -29,8 +29,8 @@ cairo_surface_t* Backend::recreateSurface() return this->createSurface(); } -cairo_surface_t* Backend::getSurface() -{ +cairo_surface_t* Backend::getSurface() { + if (!surface) createSurface(); return surface; } diff --git a/src/backend/Backend.h b/src/backend/Backend.h index 5877c34e0..a20215355 100644 --- a/src/backend/Backend.h +++ b/src/backend/Backend.h @@ -53,6 +53,9 @@ class Backend : public Nan::ObjectWrap int getHeight(); virtual void setHeight(int height); + + // Overridden by ImageBackend. SVG and PDF thus always return INVALID. + virtual cairo_format_t getFormat() { return CAIRO_FORMAT_INVALID; } }; diff --git a/src/backend/ImageBackend.cc b/src/backend/ImageBackend.cc index 5c64e5708..2aa91a108 100644 --- a/src/backend/ImageBackend.cc +++ b/src/backend/ImageBackend.cc @@ -4,23 +4,42 @@ using namespace v8; ImageBackend::ImageBackend(int width, int height) : Backend("image", width, height) -{ - createSurface(); -} + {} ImageBackend::~ImageBackend() { destroySurface(); - Nan::AdjustExternalMemory(-4 * width * height); + Nan::AdjustExternalMemory(-1 * approxBytesPerPixel() * width * height); +} + +// This returns an approximate value only, suitable for Nan::AdjustExternalMemory. +// The formats that don't map to intrinsic types (RGB30, A1) round up. +uint32_t ImageBackend::approxBytesPerPixel() { + switch (format) { + case CAIRO_FORMAT_ARGB32: + case CAIRO_FORMAT_RGB24: + return 4; +#ifdef CAIRO_FORMAT_RGB30 + case CAIRO_FORMAT_RGB30: + return 3; +#endif + case CAIRO_FORMAT_RGB16_565: + return 2; + case CAIRO_FORMAT_A8: + case CAIRO_FORMAT_A1: + return 1; + default: + return 0; + } } cairo_surface_t* ImageBackend::createSurface() { assert(!this->surface); - this->surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + this->surface = cairo_image_surface_create(this->format, width, height); assert(this->surface); - Nan::AdjustExternalMemory(4 * width * height); + Nan::AdjustExternalMemory(approxBytesPerPixel() * width * height); return this->surface; } @@ -28,14 +47,24 @@ cairo_surface_t* ImageBackend::createSurface() cairo_surface_t* ImageBackend::recreateSurface() { // Re-surface - int old_width = cairo_image_surface_get_width(this->surface); - int old_height = cairo_image_surface_get_height(this->surface); - this->destroySurface(); - Nan::AdjustExternalMemory(-4 * old_width * old_height); + if (this->surface) { + int old_width = cairo_image_surface_get_width(this->surface); + int old_height = cairo_image_surface_get_height(this->surface); + this->destroySurface(); + Nan::AdjustExternalMemory(-1 * approxBytesPerPixel() * old_width * old_height); + } return createSurface(); } +cairo_format_t ImageBackend::getFormat() { + return format; +} + +void ImageBackend::setFormat(cairo_format_t _format) { + this->format = _format; +} + Nan::Persistent ImageBackend::constructor; void ImageBackend::Initialize(Handle target) diff --git a/src/backend/ImageBackend.h b/src/backend/ImageBackend.h index fe5482aa2..2e5a82a7a 100644 --- a/src/backend/ImageBackend.h +++ b/src/backend/ImageBackend.h @@ -12,14 +12,21 @@ class ImageBackend : public Backend private: cairo_surface_t* createSurface(); cairo_surface_t* recreateSurface(); + cairo_format_t format = DEFAULT_FORMAT; public: ImageBackend(int width, int height); ~ImageBackend(); + cairo_format_t getFormat(); + void setFormat(cairo_format_t format); + + uint32_t approxBytesPerPixel(); + static Nan::Persistent constructor; static void Initialize(v8::Handle target); static NAN_METHOD(New); + const static cairo_format_t DEFAULT_FORMAT = CAIRO_FORMAT_ARGB32; }; #endif diff --git a/test/canvas.test.js b/test/canvas.test.js index 8678bf4d5..ec96f4ebf 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -222,6 +222,54 @@ describe('Canvas', function () { assert.equal(ctx, canvas.context, 'canvas.context is not context'); }); + it('Canvas#getContext("2d", {pixelFormat: string})', function () { + var canvas, context; + + // default: + canvas = createCanvas(10, 10); + context = canvas.getContext("2d", {pixelFormat: "RGBA32"}); + assert.equal(context.pixelFormat, "RGBA32"); + + canvas = createCanvas(10, 10); + context = canvas.getContext("2d", {pixelFormat: "RGBA32"}); + assert.equal(context.pixelFormat, "RGBA32"); + + canvas = createCanvas(10, 10); + context = canvas.getContext("2d", {pixelFormat: "RGB24"}); + assert.equal(context.pixelFormat, "RGB24"); + + canvas = createCanvas(10, 10); + context = canvas.getContext("2d", {pixelFormat: "A8"}); + assert.equal(context.pixelFormat, "A8"); + + canvas = createCanvas(10, 10); + context = canvas.getContext("2d", {pixelFormat: "A1"}); + assert.equal(context.pixelFormat, "A1"); + + canvas = createCanvas(10, 10); + context = canvas.getContext("2d", {pixelFormat: "RGB16_565"}); + assert.equal(context.pixelFormat, "RGB16_565"); + + // Not tested: RGB30 + }); + + it('Canvas#getContext("2d", {alpha: boolean})', function () { + var canvas, context; + + canvas = createCanvas(10, 10); + context = canvas.getContext("2d", {alpha: true}); + assert.equal(context.pixelFormat, "RGBA32"); + + canvas = createCanvas(10, 10); + context = canvas.getContext("2d", {alpha: false}); + assert.equal(context.pixelFormat, "RGB24"); + + // alpha takes priority: + canvas = createCanvas(10, 10); + context = canvas.getContext("2d", {pixelFormat: "RGBA32", alpha: false}); + assert.equal(context.pixelFormat, "RGB24"); + }); + it('Canvas#{width,height}=', function () { var canvas = createCanvas(100, 200); assert.equal(100, canvas.width); @@ -241,6 +289,8 @@ describe('Canvas', function () { var canvas = createCanvas(24, 10); assert.ok(canvas.stride >= 24, 'canvas.stride is too short'); assert.ok(canvas.stride < 1024, 'canvas.stride seems too long'); + + // TODO test stride on other formats }); it('Canvas#getContext("invalid")', function () { @@ -605,19 +655,77 @@ describe('Canvas', function () { }); }); - it('Context2d#createImageData(width, height)', function () { - var canvas = createCanvas(20, 20) - , ctx = canvas.getContext('2d'); + describe('Context2d#createImageData(width, height)', function () { + it("works", function () { + var canvas = createCanvas(20, 20) + , ctx = canvas.getContext('2d'); - var imageData = ctx.createImageData(2,6); - assert.equal(2, imageData.width); - assert.equal(6, imageData.height); - assert.equal(2 * 6 * 4, imageData.data.length); + var imageData = ctx.createImageData(2,6); + assert.equal(2, imageData.width); + assert.equal(6, imageData.height); + assert.equal(2 * 6 * 4, imageData.data.length); - assert.equal(0, imageData.data[0]); - assert.equal(0, imageData.data[1]); - assert.equal(0, imageData.data[2]); - assert.equal(0, imageData.data[3]); + assert.equal(0, imageData.data[0]); + assert.equal(0, imageData.data[1]); + assert.equal(0, imageData.data[2]); + assert.equal(0, imageData.data[3]); + }); + + it("works, A8 format", function () { + var canvas = createCanvas(20, 20) + , ctx = canvas.getContext('2d', {pixelFormat: "A8"}); + + var imageData = ctx.createImageData(2,6); + assert.equal(2, imageData.width); + assert.equal(6, imageData.height); + assert.equal(2 * 6 * 1, imageData.data.length); + + assert.equal(0, imageData.data[0]); + assert.equal(0, imageData.data[1]); + assert.equal(0, imageData.data[2]); + assert.equal(0, imageData.data[3]); + }); + + it("works, A1 format", function () { + var canvas = createCanvas(20, 20) + , ctx = canvas.getContext('2d', {pixelFormat: "A1"}); + + var imageData = ctx.createImageData(2,6); + assert.equal(2, imageData.width); + assert.equal(6, imageData.height); + assert.equal(Math.ceil(2 * 6 / 8), imageData.data.length); + + assert.equal(0, imageData.data[0]); + assert.equal(0, imageData.data[1]); + }); + + it("works, RGB24 format", function () { + var canvas = createCanvas(20, 20) + , ctx = canvas.getContext('2d', {pixelFormat: "RGB24"}); + + var imageData = ctx.createImageData(2,6); + assert.equal(2, imageData.width); + assert.equal(6, imageData.height); + assert.equal(2 * 6 * 4, imageData.data.length); + + assert.equal(0, imageData.data[0]); + assert.equal(0, imageData.data[1]); + assert.equal(0, imageData.data[2]); + assert.equal(0, imageData.data[3]); + }); + + it("works, RGB16_565 format", function () { + var canvas = createCanvas(20, 20) + , ctx = canvas.getContext('2d', {pixelFormat: "RGB16_565"}); + + var imageData = ctx.createImageData(2,6); + assert.equal(2, imageData.width); + assert.equal(6, imageData.height); + assert.equal(2 * 6 * 2, imageData.data.length); + + assert.equal(0, imageData.data[0]); + assert.equal(0, imageData.data[1]); + }); }); it('Context2d#measureText().width', function () { @@ -639,64 +747,189 @@ describe('Canvas', function () { assert.equal(2 * 6 * 4, imageData.data.length); }); - it('Context2d#getImageData()', function () { - var canvas = createCanvas(3, 6) - , ctx = canvas.getContext('2d'); + describe('Context2d#getImageData()', function () { + function createTestCanvas(useAlpha, attributes) { + var canvas = createCanvas(3, 6); + var ctx = canvas.getContext('2d', attributes); - ctx.fillStyle = '#f00'; - ctx.fillRect(0,0,1,6); + ctx.fillStyle = useAlpha ? 'rgba(255,0,0,0.25)' : '#f00'; + ctx.fillRect(0,0,1,6); - ctx.fillStyle = '#0f0'; - ctx.fillRect(1,0,1,6); + ctx.fillStyle = useAlpha ? 'rgba(0,255,0,0.5)' : '#0f0'; + ctx.fillRect(1,0,1,6); - ctx.fillStyle = '#00f'; - ctx.fillRect(2,0,1,6); + ctx.fillStyle = useAlpha ? 'rgba(0,0,255,0.75)' : '#00f'; + ctx.fillRect(2,0,1,6); - // Full width - var imageData = ctx.getImageData(0,0,3,6); - assert.equal(3, imageData.width); - assert.equal(6, imageData.height); - assert.equal(3 * 6 * 4, imageData.data.length); + return ctx; + } - assert.equal(255, imageData.data[0]); - assert.equal(0, imageData.data[1]); - assert.equal(0, imageData.data[2]); - assert.equal(255, imageData.data[3]); + it("works, full width, RGBA32", function () { + var ctx = createTestCanvas(); + var imageData = ctx.getImageData(0,0,3,6); - assert.equal(0, imageData.data[4]); - assert.equal(255, imageData.data[5]); - assert.equal(0, imageData.data[6]); - assert.equal(255, imageData.data[7]); + assert.equal(3, imageData.width); + assert.equal(6, imageData.height); + assert.equal(3 * 6 * 4, imageData.data.length); - assert.equal(0, imageData.data[8]); - assert.equal(0, imageData.data[9]); - assert.equal(255, imageData.data[10]); - assert.equal(255, imageData.data[11]); + assert.equal(255, imageData.data[0]); + assert.equal(0, imageData.data[1]); + assert.equal(0, imageData.data[2]); + assert.equal(255, imageData.data[3]); - // Slice - var imageData = ctx.getImageData(0,0,2,1); - assert.equal(2, imageData.width); - assert.equal(1, imageData.height); - assert.equal(8, imageData.data.length); + assert.equal(0, imageData.data[4]); + assert.equal(255, imageData.data[5]); + assert.equal(0, imageData.data[6]); + assert.equal(255, imageData.data[7]); - assert.equal(255, imageData.data[0]); - assert.equal(0, imageData.data[1]); - assert.equal(0, imageData.data[2]); - assert.equal(255, imageData.data[3]); + assert.equal(0, imageData.data[8]); + assert.equal(0, imageData.data[9]); + assert.equal(255, imageData.data[10]); + assert.equal(255, imageData.data[11]); + }); - assert.equal(0, imageData.data[4]); - assert.equal(255, imageData.data[5]); - assert.equal(0, imageData.data[6]); - assert.equal(255, imageData.data[7]); + it("works, full width, RGB24", function () { + var ctx = createTestCanvas(false, {pixelFormat: "RGB24"}); + var imageData = ctx.getImageData(0,0,3,6); + assert.equal(3, imageData.width); + assert.equal(6, imageData.height); + assert.equal(3 * 6 * 4, imageData.data.length); + + assert.equal(255, imageData.data[0]); + assert.equal(0, imageData.data[1]); + assert.equal(0, imageData.data[2]); + assert.equal(255, imageData.data[3]); + + assert.equal(0, imageData.data[4]); + assert.equal(255, imageData.data[5]); + assert.equal(0, imageData.data[6]); + assert.equal(255, imageData.data[7]); + + assert.equal(0, imageData.data[8]); + assert.equal(0, imageData.data[9]); + assert.equal(255, imageData.data[10]); + assert.equal(255, imageData.data[11]); + }); + + it("works, full width, RGB16_565", function () { + var ctx = createTestCanvas(false, {pixelFormat: "RGB16_565"}); + var imageData = ctx.getImageData(0,0,3,6); + assert.equal(3, imageData.width); + assert.equal(6, imageData.height); + assert.equal(3 * 6 * 2, imageData.data.length); + + // TODO should be a Uint16Array already? + var uint16data = new Uint16Array(imageData.data.buffer, imageData.data.byteOffset, 18); - // Assignment - var data = ctx.getImageData(0,0,5,5).data; - data[0] = 50; - assert.equal(50, data[0]); - data[0] = 280; - assert.equal(255, data[0]); - data[0] = -4444; - assert.equal(0, data[0]); + assert.equal((255 & 0b11111) << 11, uint16data[0]); + assert.equal((255 & 0b111111) << 5, uint16data[1]); + assert.equal((255 & 0b11111), uint16data[2]); + + assert.equal((255 & 0b11111) << 11, uint16data[3]); + assert.equal((255 & 0b111111) << 5, uint16data[4]); + assert.equal((255 & 0b11111), uint16data[5]); + }); + + it("works, full width, A8", function () { + var ctx = createTestCanvas(true, {pixelFormat: "A8"}); + var imageData = ctx.getImageData(0,0,3,6); + assert.equal(3, imageData.width); + assert.equal(6, imageData.height); + assert.equal(3 * 6, imageData.data.length); + + assert.equal(63, imageData.data[0]); + assert.equal(127, imageData.data[1]); + assert.equal(191, imageData.data[2]); + + assert.equal(63, imageData.data[3]); + assert.equal(127, imageData.data[4]); + assert.equal(191, imageData.data[5]); + }); + + it("works, full width, A1"); + + it("works, full width, RGB30"); + + it("works, slice, RGBA32", function () { + var ctx = createTestCanvas(); + var imageData = ctx.getImageData(0,0,2,1); + assert.equal(2, imageData.width); + assert.equal(1, imageData.height); + assert.equal(8, imageData.data.length); + + assert.equal(255, imageData.data[0]); + assert.equal(0, imageData.data[1]); + assert.equal(0, imageData.data[2]); + assert.equal(255, imageData.data[3]); + + assert.equal(0, imageData.data[4]); + assert.equal(255, imageData.data[5]); + assert.equal(0, imageData.data[6]); + assert.equal(255, imageData.data[7]); + }); + + it("works, slice, RGB24", function () { + var ctx = createTestCanvas(false, {pixelFormat: "RGB24"}); + var imageData = ctx.getImageData(0,0,2,1); + assert.equal(2, imageData.width); + assert.equal(1, imageData.height); + assert.equal(8, imageData.data.length); + + assert.equal(255, imageData.data[0]); + assert.equal(0, imageData.data[1]); + assert.equal(0, imageData.data[2]); + assert.equal(255, imageData.data[3]); + + assert.equal(0, imageData.data[4]); + assert.equal(255, imageData.data[5]); + assert.equal(0, imageData.data[6]); + assert.equal(255, imageData.data[7]); + }); + + it("works, slice, RGB16_565", function () { + var ctx = createTestCanvas(false, {pixelFormat: "RGB16_565"}); + var imageData = ctx.getImageData(0,0,2,1); + assert.equal(2, imageData.width); + assert.equal(1, imageData.height); + assert.equal(2 * 1 * 2, imageData.data.length); + + // TODO should be a Uint16Array already? + var uint16data = new Uint16Array(imageData.data.buffer, imageData.data.byteOffset, 2); + + assert.equal((255 & 0b11111) << 11, uint16data[0]); + assert.equal((255 & 0b111111) << 5, uint16data[1]); + }); + + it("works, slice, A8", function () { + var ctx = createTestCanvas(true, {pixelFormat: "A8"}); + var imageData = ctx.getImageData(0,0,2,1); + assert.equal(2, imageData.width); + assert.equal(1, imageData.height); + assert.equal(2 * 1, imageData.data.length); + + assert.equal(63, imageData.data[0]); + assert.equal(127, imageData.data[1]); + }); + + it("works, slice, A1"); + + it("works, slice, RGB30"); + + it("works, assignment", function () { + var ctx = createTestCanvas(); + var data = ctx.getImageData(0,0,5,5).data; + data[0] = 50; + assert.equal(50, data[0]); + data[0] = 280; + assert.equal(255, data[0]); + data[0] = -4444; + assert.equal(0, data[0]); + }); + + it("throws if indexes are invalid", function () { + var ctx = createTestCanvas(); + assert.throws(function () { ctx.getImageData(0, 0, 0, 0); }, /IndexSizeError/); + }); }); it('Context2d#createPattern(Canvas)', function () { @@ -834,42 +1067,77 @@ describe('Canvas', function () { assert.equal(255, imageData.data[i+3]); }); - it('Context2d#getImageData()', function () { - var canvas = createCanvas(1, 1) - , ctx = canvas.getContext('2d'); + describe('Context2d#putImageData()', function () { + it('throws for invalid arguments', function () { + var canvas = createCanvas(2, 1); + var ctx = canvas.getContext('2d'); + assert.throws(function () { ctx.putImageData({}, 0, 0); }, TypeError); + assert.throws(function () { ctx.putImageData(undefined, 0, 0); }, TypeError); + }); - assert.throws(function () { ctx.getImageData(0, 0, 0, 0); }, /IndexSizeError/); + it('works, RGBA32', function () { + var canvas = createCanvas(2, 1); + var ctx = canvas.getContext('2d'); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 1, 1); - ctx.fillStyle = '#f00'; - ctx.fillRect(0, 0, 1, 1); + // Copy left pixel to the right pixel + ctx.putImageData(ctx.getImageData(0, 0, 1, 1), 1, 0); - var pixel = ctx.getImageData(0, 0, 1, 1); + var pixel = ctx.getImageData(1, 0, 1, 1); - assert.equal(pixel.data[0], 255); - assert.equal(pixel.data[1], 0); - assert.equal(pixel.data[2], 0); - assert.equal(pixel.data[3], 255); - }); + assert.equal(pixel.data[0], 255); + assert.equal(pixel.data[1], 0); + assert.equal(pixel.data[2], 0); + assert.equal(pixel.data[3], 255); + }); - it('Context2d#putImageData()', function () { - var canvas = createCanvas(2, 1) - , ctx = canvas.getContext('2d'); + it('works, RGB24/alpha:false', function () { + var canvas = createCanvas(2, 1); + var ctx = canvas.getContext('2d', {pixelFormat: 'RGB24'}); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 1, 1); - assert.throws(function () { ctx.putImageData({}, 0, 0); }, TypeError); - assert.throws(function () { ctx.putImageData(undefined, 0, 0); }, TypeError); + // Copy left pixel to the right pixel + ctx.putImageData(ctx.getImageData(0, 0, 1, 1), 1, 0); + + var pixel = ctx.getImageData(1, 0, 1, 1); + + assert.equal(pixel.data[0], 255); + assert.equal(pixel.data[1], 0); + assert.equal(pixel.data[2], 0); + assert.equal(pixel.data[3], 255); + }); - ctx.fillStyle = '#f00'; - ctx.fillRect(0, 0, 1, 1); + it('works, A8', function () { + var canvas = createCanvas(2, 1); + var ctx = canvas.getContext('2d', {pixelFormat: 'A8'}); - // Copy left pixel to the right pixel - ctx.putImageData(ctx.getImageData(0, 0, 1, 1), 1, 0); + var imgData = ctx.getImageData(0, 0, 2, 1); + imgData.data[0] = 4; + imgData.data[1] = 21; + ctx.putImageData(imgData, 0, 0); - var pixel = ctx.getImageData(1, 0, 1, 1); + var pixel = ctx.getImageData(0, 0, 2, 1); - assert.equal(pixel.data[0], 255); - assert.equal(pixel.data[1], 0); - assert.equal(pixel.data[2], 0); - assert.equal(pixel.data[3], 255); + assert.equal(pixel.data[0], 4); + assert.equal(pixel.data[1], 21); + }); + + xit('works, RGB16_565', function () { + var canvas = createCanvas(2, 1); + var ctx = canvas.getContext('2d', {pixelFormat: 'RGB16_565'}); + + var imgData = ctx.getImageData(0, 0, 2, 1); + imgData.data[0] = 65535; // 2**16 - 1 + imgData.data[1] = 65500; + ctx.putImageData(imgData, 0, 0); + + var pixel = ctx.getImageData(0, 0, 2, 1); + + assert.equal(pixel.data[0], 65535); + assert.equal(pixel.data[1], 65500); + }); }); it('Canvas#createSyncPNGStream()', function (done) { diff --git a/test/imageData.test.js b/test/imageData.test.js index b78fadcf1..df44042f6 100644 --- a/test/imageData.test.js +++ b/test/imageData.test.js @@ -25,10 +25,10 @@ describe('ImageData', function () { it('should throw with invalid typed array', function () { assert.throws(() => { createImageData(new Uint8ClampedArray(0), 0) }, /input data has a zero byte length/) - assert.throws(() => { createImageData(new Uint8ClampedArray(3), 0) }, /input data byte length is not a multiple of 4/) - assert.throws(() => { createImageData(new Uint8ClampedArray(16), 3) }, RangeError) - assert.throws(() => { createImageData(new Uint8ClampedArray(12), 3, 5) }, RangeError) - }) + assert.throws(() => { createImageData(new Uint8ClampedArray(3), 0) }, /source width is zero/) + // Note: Some errors thrown by browsers are not thrown by node-canvas + // because our ImageData can support different BPPs. + }); it('should construct with typed array', function () { let data, imageData diff --git a/test/public/app.js b/test/public/app.js index ff53f4a5c..e706f2c72 100644 --- a/test/public/app.js +++ b/test/public/app.js @@ -21,7 +21,12 @@ function pdfLink (name) { function localRendering (name) { var canvas = create('canvas', { width: 200, height: 200, title: name }) - window.tests[name](canvas.getContext('2d'), function () {}) + var ctx = canvas.getContext('2d', {alpha: true}) + var initialFillStyle = ctx.fillStyle + ctx.fillStyle = 'white' + ctx.fillRect(0, 0, 200, 200) + ctx.fillStyle = initialFillStyle + window.tests[name](ctx, function () {}) return canvas } diff --git a/test/server.js b/test/server.js index 04207becb..e1378c9bf 100644 --- a/test/server.js +++ b/test/server.js @@ -12,10 +12,15 @@ function renderTest (canvas, name, cb) { throw new Error('Unknown test: ' + name) } + var ctx = canvas.getContext('2d', {pixelFormat: 'RGBA32'}) + var initialFillStyle = ctx.fillStyle + ctx.fillStyle = 'white' + ctx.fillRect(0, 0, 200, 200) + ctx.fillStyle = initialFillStyle if (tests[name].length === 2) { - tests[name](canvas.getContext('2d'), cb) + tests[name](ctx, cb) } else { - tests[name](canvas.getContext('2d')) + tests[name](ctx) cb(null) } } From 9991c133aabb96010442df0c69b3d08cd05303ae Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 1 Jul 2017 18:53:12 -0700 Subject: [PATCH 029/474] Use Uint16Array for RGB16_565 format --- Readme.md | 1 + lib/context2d.js | 8 +++++++- src/CanvasRenderingContext2d.cc | 12 ++++++++--- src/ImageData.cc | 36 ++++++++++++++++++++++++++------- test/canvas.test.js | 27 ++++++++++--------------- test/imageData.test.js | 26 +++++++++++++++++++----- test/public/tests.js | 22 ++++++++++++-------- 7 files changed, 92 insertions(+), 40 deletions(-) diff --git a/Readme.md b/Readme.md index 1a4c6b810..5731e4026 100644 --- a/Readme.md +++ b/Readme.md @@ -343,6 +343,7 @@ These additional pixel formats have experimental support: Some hardware devices and frame buffers use this format. Note that PNG does not support this format; when creating a PNG, the image will be converted to 24-bit RGB. This format is thus suboptimal for generating PNGs. + `ImageData` instances for this mode use a `Uint16Array` instead of a `Uint8ClampedArray`. * `A1` Each pixel is 1 bit, and pixels are packed together into 32-bit quantities. The ordering of the bits matches the endianness of the platform: on a little-endian machine, the first pixel is the least- diff --git a/lib/context2d.js b/lib/context2d.js index 35b3e8db3..8c9ac74e7 100644 --- a/lib/context2d.js +++ b/lib/context2d.js @@ -276,5 +276,11 @@ Context2d.prototype.createImageData = function (width, height) { } var Bpp = this.canvas.stride / this.canvas.width; var nBytes = Bpp * width * height - return new ImageData(new Uint8ClampedArray(nBytes), width, height) + var arr; + if (this.pixelFormat === "RGB16_565") { + arr = new Uint16Array(nBytes / 2); + } else { + arr = new Uint8ClampedArray(nBytes); + } + return new ImageData(arr, width, height); } diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 41db59fc6..e00a11206 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -822,9 +822,15 @@ NAN_METHOD(Context2d::GetImageData) { uint8_t *src = canvas->data(); Local buffer = ArrayBuffer::New(Isolate::GetCurrent(), size); - Local clampedArray = Uint8ClampedArray::New(buffer, 0, size); + Local dataArray; - Nan::TypedArrayContents typedArrayContents(clampedArray); + if (canvas->backend()->getFormat() == CAIRO_FORMAT_RGB16_565) { + dataArray = Uint16Array::New(buffer, 0, size); + } else { + dataArray = Uint8ClampedArray::New(buffer, 0, size); + } + + Nan::TypedArrayContents typedArrayContents(dataArray); uint8_t* dst = *typedArrayContents; switch (canvas->backend()->getFormat()) { @@ -919,7 +925,7 @@ NAN_METHOD(Context2d::GetImageData) { const int argc = 3; Local swHandle = Nan::New(sw); Local shHandle = Nan::New(sh); - Local argv[argc] = { clampedArray, swHandle, shHandle }; + Local argv[argc] = { dataArray, swHandle, shHandle }; Local ctor = Nan::GetFunction(Nan::New(ImageData::constructor)).ToLocalChecked(); Local instance = Nan::NewInstance(ctor, argc, argv).ToLocalChecked(); diff --git a/src/ImageData.cc b/src/ImageData.cc index ca4dfdd58..1d47a01ed 100644 --- a/src/ImageData.cc +++ b/src/ImageData.cc @@ -39,7 +39,7 @@ NAN_METHOD(ImageData::New) { return Nan::ThrowTypeError("Class constructors cannot be invoked without 'new'"); } - Local clampedArray; + Local dataArray; uint32_t width; uint32_t height; int length; @@ -57,12 +57,12 @@ NAN_METHOD(ImageData::New) { } length = width * height * 4; // ImageData(w, h) constructor assumes 4 BPP; documented. - clampedArray = Uint8ClampedArray::New(ArrayBuffer::New(Isolate::GetCurrent(), length), 0, length); + dataArray = Uint8ClampedArray::New(ArrayBuffer::New(Isolate::GetCurrent(), length), 0, length); } else if (info[0]->IsUint8ClampedArray() && info[1]->IsUint32()) { - clampedArray = info[0].As(); + dataArray = info[0].As(); - length = clampedArray->Length(); + length = dataArray->Length(); if (length == 0) { Nan::ThrowRangeError("The input data has a zero byte length."); return; @@ -86,16 +86,38 @@ NAN_METHOD(ImageData::New) { height = size / width; } + } else if (info[0]->IsUint16Array() && info[1]->IsUint32()) { // Intended for RGB16_565 format + dataArray = info[0].As(); + + length = dataArray->Length(); + if (length == 0) { + Nan::ThrowRangeError("The input data has a zero byte length."); + return; + } + + width = info[1]->Uint32Value(); + if (width == 0) { + Nan::ThrowRangeError("The source width is zero."); + return; + } + + if (info[2]->IsUint32()) { // Explicit height given + height = info[2]->Uint32Value(); + } else { // Calculate height assuming 2 BPP + int size = length / 2; + height = size / width; + } + } else { - Nan::ThrowTypeError("Expected (Uint8ClampedArray, width[, height]) or (width, height)"); + Nan::ThrowTypeError("Expected (Uint8ClampedArray, width[, height]), (Uint16Array, width[, height]) or (width, height)"); return; } - Nan::TypedArrayContents dataPtr(clampedArray); + Nan::TypedArrayContents dataPtr(dataArray); ImageData *imageData = new ImageData(reinterpret_cast(*dataPtr), width, height); imageData->Wrap(info.This()); - info.This()->Set(Nan::New("data").ToLocalChecked(), clampedArray); + info.This()->Set(Nan::New("data").ToLocalChecked(), dataArray); info.GetReturnValue().Set(info.This()); } diff --git a/test/canvas.test.js b/test/canvas.test.js index ec96f4ebf..a307f51af 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -719,9 +719,10 @@ describe('Canvas', function () { , ctx = canvas.getContext('2d', {pixelFormat: "RGB16_565"}); var imageData = ctx.createImageData(2,6); + assert(imageData.data instanceof Uint16Array); assert.equal(2, imageData.width); assert.equal(6, imageData.height); - assert.equal(2 * 6 * 2, imageData.data.length); + assert.equal(2 * 6, imageData.data.length); assert.equal(0, imageData.data[0]); assert.equal(0, imageData.data[1]); @@ -818,16 +819,13 @@ describe('Canvas', function () { assert.equal(6, imageData.height); assert.equal(3 * 6 * 2, imageData.data.length); - // TODO should be a Uint16Array already? - var uint16data = new Uint16Array(imageData.data.buffer, imageData.data.byteOffset, 18); + assert.equal((255 & 0b11111) << 11, imageData.data[0]); + assert.equal((255 & 0b111111) << 5, imageData.data[1]); + assert.equal((255 & 0b11111), imageData.data[2]); - assert.equal((255 & 0b11111) << 11, uint16data[0]); - assert.equal((255 & 0b111111) << 5, uint16data[1]); - assert.equal((255 & 0b11111), uint16data[2]); - - assert.equal((255 & 0b11111) << 11, uint16data[3]); - assert.equal((255 & 0b111111) << 5, uint16data[4]); - assert.equal((255 & 0b11111), uint16data[5]); + assert.equal((255 & 0b11111) << 11, imageData.data[3]); + assert.equal((255 & 0b111111) << 5, imageData.data[4]); + assert.equal((255 & 0b11111), imageData.data[5]); }); it("works, full width, A8", function () { @@ -893,11 +891,8 @@ describe('Canvas', function () { assert.equal(1, imageData.height); assert.equal(2 * 1 * 2, imageData.data.length); - // TODO should be a Uint16Array already? - var uint16data = new Uint16Array(imageData.data.buffer, imageData.data.byteOffset, 2); - - assert.equal((255 & 0b11111) << 11, uint16data[0]); - assert.equal((255 & 0b111111) << 5, uint16data[1]); + assert.equal((255 & 0b11111) << 11, imageData.data[0]); + assert.equal((255 & 0b111111) << 5, imageData.data[1]); }); it("works, slice, A8", function () { @@ -1124,7 +1119,7 @@ describe('Canvas', function () { assert.equal(pixel.data[1], 21); }); - xit('works, RGB16_565', function () { + it('works, RGB16_565', function () { var canvas = createCanvas(2, 1); var ctx = canvas.getContext('2d', {pixelFormat: 'RGB16_565'}); diff --git a/test/imageData.test.js b/test/imageData.test.js index df44042f6..30e60bbcd 100644 --- a/test/imageData.test.js +++ b/test/imageData.test.js @@ -30,21 +30,37 @@ describe('ImageData', function () { // because our ImageData can support different BPPs. }); - it('should construct with typed array', function () { + it('should construct with Uint8ClampedArray', function () { let data, imageData data = new Uint8ClampedArray(2 * 3 * 4) imageData = createImageData(data, 2) assert.strictEqual(imageData.width, 2) assert.strictEqual(imageData.height, 3) - assert.ok(imageData.data instanceof Uint8ClampedArray) + assert(imageData.data instanceof Uint8ClampedArray) assert.strictEqual(imageData.data.length, 24) data = new Uint8ClampedArray(3 * 4 * 4) imageData = createImageData(data, 3, 4) assert.strictEqual(imageData.width, 3) assert.strictEqual(imageData.height, 4) - assert.ok(imageData.data instanceof Uint8ClampedArray) + assert(imageData.data instanceof Uint8ClampedArray) assert.strictEqual(imageData.data.length, 48) - }) -}) + }); + + it('should construct with Uint16Array', function () { + let data = new Uint16Array(2 * 3 * 2) + let imagedata = createImageData(data, 2) + assert.strictEqual(imagedata.width, 2) + assert.strictEqual(imagedata.height, 3) + assert(imagedata.data instanceof Uint16Array) + assert.strictEqual(imagedata.data.length, 12) + + data = new Uint16Array(3 * 4 * 2) + imagedata = createImageData(data, 3, 4) + assert.strictEqual(imagedata.width, 3) + assert.strictEqual(imagedata.height, 4) + assert(imagedata.data instanceof Uint16Array) + assert.strictEqual(imagedata.data.length, 24) + }); +}); diff --git a/test/public/tests.js b/test/public/tests.js index 68170c3fe..58f656b3c 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -1912,8 +1912,10 @@ tests['putImageData() png data'] = function (ctx, done) { ctx.drawImage(img, 0, 0, 200, 200) var imageData = ctx.getImageData(0, 0, 50, 50) var data = imageData.data - for (var i = 0, len = data.length; i < len; i += 4) { - data[i + 3] = 80 + if (data instanceof Uint8ClampedArray) { + for (var i = 0, len = data.length; i < len; i += 4) { + data[i + 3] = 80 + } } ctx.putImageData(imageData, 50, 50) done(null) @@ -1933,8 +1935,10 @@ tests['putImageData() png data 2'] = function (ctx, done) { ctx.drawImage(img, 0, 0, 200, 200) var imageData = ctx.getImageData(0, 0, 50, 50) var data = imageData.data - for (var i = 0, len = data.length; i < len; i += 4) { - data[i + 3] = 80 + if (data instanceof Uint8ClampedArray) { + for (var i = 0, len = data.length; i < len; i += 4) { + data[i + 3] = 80 + } } ctx.putImageData(imageData, 50, 50, 10, 10, 20, 20) done(null) @@ -1954,10 +1958,12 @@ tests['putImageData() png data 3'] = function (ctx, done) { ctx.drawImage(img, 0, 0, 200, 200) var imageData = ctx.getImageData(0, 0, 50, 50) var data = imageData.data - for (var i = 0, len = data.length; i < len; i += 4) { - data[i + 0] = data[i + 0] * 0.2 - data[i + 1] = data[i + 1] * 0.2 - data[i + 2] = data[i + 2] * 0.2 + if (data instanceof Uint8ClampedArray) { + for (var i = 0, len = data.length; i < len; i += 4) { + data[i + 0] = data[i + 0] * 0.2 + data[i + 1] = data[i + 1] * 0.2 + data[i + 2] = data[i + 2] * 0.2 + } } ctx.putImageData(imageData, 50, 50) done(null) From a72c3d32b930266d4d1ebcdcf12305eb63e7807d Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sun, 2 Jul 2017 14:50:31 -0700 Subject: [PATCH 030/474] Indexed PNG encoding --- History.md | 1 + Readme.md | 24 ++++++++++++++--- examples/indexed-png-alpha.js | 34 ++++++++++++++++++++++++ examples/indexed-png-image-data.js | 39 +++++++++++++++++++++++++++ lib/canvas.js | 18 ++++++++++--- lib/pngstream.js | 10 +++++-- src/Canvas.cc | 42 +++++++++++++++++++++++++++--- src/PNG.h | 42 ++++++++++++++++++++++++++---- src/closure.h | 3 +++ 9 files changed, 195 insertions(+), 18 deletions(-) create mode 100644 examples/indexed-png-alpha.js create mode 100644 examples/indexed-png-image-data.js diff --git a/History.md b/History.md index 805cc220e..1dcf3a0a0 100644 --- a/History.md +++ b/History.md @@ -4,6 +4,7 @@ Unreleased / patch * Port has_lib.sh to javascript (#872) * Support canvas.getContext("2d", {alpha: boolean}) and canvas.getContext("2d", {pixelFormat: "..."}) + * Support indexed PNG encoding. 1.6.0 / 2016-10-16 ================== diff --git a/Readme.md b/Readme.md index 5731e4026..8a91014ef 100644 --- a/Readme.md +++ b/Readme.md @@ -117,7 +117,7 @@ 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#pngStream() +### Canvas#pngStream(options) To create a `PNGStream` simply call `canvas.pngStream()`, and the stream will start to emit _data_ events, finally emitting _end_ when finished. If an exception occurs the _error_ event is emitted. @@ -137,6 +137,22 @@ stream.on('end', function(){ Currently _only_ sync streaming is supported, however we plan on supporting async streaming as well (of course :) ). Until then the `Canvas#toBuffer(callback)` alternative is async utilizing `eio_custom()`. +To encode indexed PNGs from canvases with `pixelFormat: 'A8'` or `'A1'`, provide an options object: + +```js +var palette = new Uint8ClampedArray([ + //r g b a + 0, 50, 50, 255, // index 1 + 10, 90, 90, 255, // index 2 + 127, 127, 255, 255 + // ... +]); +canvas.pngStream({ + palette: palette, + backgroundIndex: 0 // optional, defaults to 0 +}) +``` + ### Canvas#jpegStream() and Canvas#syncJPEGStream() You can likewise create a `JPEGStream` by calling `canvas.jpegStream()` with @@ -337,7 +353,9 @@ These additional pixel formats have experimental support: `RGBA32` because transparency does not need to be calculated. * `A8` Each pixel is 8 bits. This format can either be used for creating grayscale images (treating each byte as an alpha value), or for creating - indexed PNGs (treating each byte as a palette index). + indexed PNGs (treating each byte as a palette index) (see [the example using + alpha values with `fillStyle`](examples/indexed-png-alpha.js) and [the + example using `imageData`](examples/indexed-png-image-data.js)). * `RGB16_565` Each pixel is 16 bits, with red in the upper 5 bits, green in the middle 6 bits, and blue in the lower 5 bits, in native platform endianness. Some hardware devices and frame buffers use this format. Note that PNG does @@ -369,7 +387,7 @@ Notes and caveats: * `A1` and `RGB30` do not yet support `getImageData` or `putImageData`. Have a use case and/or opinion on working with these formats? Open an issue and let - us know! + us know! (See #935.) * `A1`, `A8`, `RGB30` and `RGB16_565` with shadow blurs may crash or not render properly. diff --git a/examples/indexed-png-alpha.js b/examples/indexed-png-alpha.js new file mode 100644 index 000000000..86ff31010 --- /dev/null +++ b/examples/indexed-png-alpha.js @@ -0,0 +1,34 @@ +var Canvas = require('..') +var fs = require('fs') +var path = require('path') +var canvas = new Canvas(200, 200) +var ctx = canvas.getContext('2d', {pixelFormat: 'A8'}) + +// Matches the "fillStyle" browser test, made by using alpha fillStyle value +var palette = new Uint8ClampedArray(37 * 4) +var i, j +var k = 0 +// First value is opaque white: +palette[k++] = 255 +palette[k++] = 255 +palette[k++] = 255 +palette[k++] = 255 +for (i = 0; i < 6; i++) { + for (j = 0; j < 6; j++) { + palette[k++] = Math.floor(255 - 42.5 * i) + palette[k++] = Math.floor(255 - 42.5 * j) + palette[k++] = 0 + palette[k++] = 255 + } +} +for (i = 0; i < 6; i++) { + for (j = 0; j < 6; j++) { + var index = i * 6 + j + 1.5 // 0.5 to bias rounding + var fraction = index / 255 + ctx.fillStyle = 'rgba(0,0,0,' + fraction + ')' + ctx.fillRect(j * 25, i * 25, 25, 25) + } +} + +canvas.createPNGStream({palette: palette}) + .pipe(fs.createWriteStream(path.join(__dirname, 'indexed2.png'))) diff --git a/examples/indexed-png-image-data.js b/examples/indexed-png-image-data.js new file mode 100644 index 000000000..d675f1b1b --- /dev/null +++ b/examples/indexed-png-image-data.js @@ -0,0 +1,39 @@ +var Canvas = require('..') +var fs = require('fs') +var path = require('path') +var canvas = new Canvas(200, 200) +var ctx = canvas.getContext('2d', {pixelFormat: 'A8'}) + +// Matches the "fillStyle" browser test, made by manipulating imageData +var palette = new Uint8ClampedArray(37 * 4) +var k = 0 +var i, j +// First value is opaque white: +palette[k++] = 255 +palette[k++] = 255 +palette[k++] = 255 +palette[k++] = 255 +for (i = 0; i < 6; i++) { + for (j = 0; j < 6; j++) { + palette[k++] = Math.floor(255 - 42.5 * i) + palette[k++] = Math.floor(255 - 42.5 * j) + palette[k++] = 0 + palette[k++] = 255 + } +} +var idata = ctx.getImageData(0, 0, 200, 200) +for (i = 0; i < 6; i++) { + for (j = 0; j < 6; j++) { + var index = j * 6 + i + // fill rect: + for (var xr = j * 25; xr < j * 25 + 25; xr++) { + for (var yr = i * 25; yr < i * 25 + 25; yr++) { + idata.data[xr * 200 + yr] = index + 1 + } + } + } +} +ctx.putImageData(idata, 0, 0) + +canvas.createPNGStream({palette: palette}) + .pipe(fs.createWriteStream(path.join(__dirname, 'indexed.png'))) diff --git a/lib/canvas.js b/lib/canvas.js index 3b4c6bc57..30a0c59e6 100644 --- a/lib/canvas.js +++ b/lib/canvas.js @@ -130,25 +130,35 @@ Canvas.prototype.getContext = function (contextType, contextAttributes) { /** * Create a `PNGStream` for `this` canvas. * + * @param {Object} options + * @param {Uint8ClampedArray} options.palette Provide for indexed PNG encoding. + * entries should be R-G-B-A values. + * @param {Number} options.backgroundIndex Optional index of background color + * for indexed PNGs. Defaults to 0. * @return {PNGStream} * @api public */ Canvas.prototype.pngStream = -Canvas.prototype.createPNGStream = function(){ - return new PNGStream(this); +Canvas.prototype.createPNGStream = function(options){ + return new PNGStream(this, false, options); }; /** * Create a synchronous `PNGStream` for `this` canvas. * + * @param {Object} options + * @param {Uint8ClampedArray} options.palette Provide for indexed PNG encoding. + * entries should be R-G-B-A values. + * @param {Number} options.backgroundIndex Optional index of background color + * for indexed PNGs. Defaults to 0. * @return {PNGStream} * @api public */ Canvas.prototype.syncPNGStream = -Canvas.prototype.createSyncPNGStream = function(){ - return new PNGStream(this, true); +Canvas.prototype.createSyncPNGStream = function(options){ + return new PNGStream(this, true, options); }; /** diff --git a/lib/pngstream.js b/lib/pngstream.js index 3a50dbaf0..fb1469185 100644 --- a/lib/pngstream.js +++ b/lib/pngstream.js @@ -27,10 +27,15 @@ var util = require('util'); * * @param {Canvas} canvas * @param {Boolean} sync + * @param {Object} options + * @param {Uint8ClampedArray} options.palette Provide for indexed PNG encoding. + * entries should be R-G-B-A values. + * @param {Number} options.backgroundIndex Optional index of background color + * for indexed PNGs. Defaults to 0. * @api public */ -var PNGStream = module.exports = function PNGStream(canvas, sync) { +var PNGStream = module.exports = function PNGStream(canvas, sync, options) { if (!(this instanceof PNGStream)) { throw new TypeError("Class constructors cannot be invoked without 'new'"); } @@ -43,6 +48,7 @@ var PNGStream = module.exports = function PNGStream(canvas, sync) { : 'streamPNG'; this.sync = sync; this.canvas = canvas; + this.options = options || {}; // TODO: implement async if ('streamPNG' === method) method = 'streamPNGSync'; @@ -66,5 +72,5 @@ PNGStream.prototype._read = function _read() { } else { self.push(null); } - }); + }, self.options); }; diff --git a/src/Canvas.cc b/src/Canvas.cc index 37e4bb10d..61cf2134f 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -359,6 +359,10 @@ streamPNG(void *c, const uint8_t *data, unsigned len) { /* * Stream PNG data synchronously. + * TODO the compression level and filter args don't seem to be documented. + * Maybe move them to named properties in the options object? + * StreamPngSync(this, options: {palette?: Uint8ClampedArray}) + * StreamPngSync(this, compression_level?: uint32, filter?: uint32) */ NAN_METHOD(Canvas::StreamPNGSync) { @@ -368,6 +372,11 @@ NAN_METHOD(Canvas::StreamPNGSync) { if (!info[0]->IsFunction()) return Nan::ThrowTypeError("callback function required"); + Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); + uint8_t* paletteColors = NULL; + size_t nPaletteColors = 0; + uint8_t backgroundIndex = 0; + if (info.Length() > 1 && !(info[1]->IsUndefined() && info[2]->IsUndefined())) { if (!info[1]->IsUndefined()) { bool good = true; @@ -384,9 +393,32 @@ NAN_METHOD(Canvas::StreamPNGSync) { compression_level = tmp; } } - } else { - good = false; - } + } else if (info[1]->IsObject()) { + // If canvas is A8 or A1 and options obj has Uint8ClampedArray palette, + // encode as indexed PNG. + cairo_format_t format = canvas->backend()->getFormat(); + if (format == CAIRO_FORMAT_A8 || format == CAIRO_FORMAT_A1) { + Local attrs = info[1]->ToObject(); + Local palette = attrs->Get(Nan::New("palette").ToLocalChecked()); + if (palette->IsUint8ClampedArray()) { + Local palette_ta = palette.As(); + nPaletteColors = palette_ta->Length(); + if (nPaletteColors % 4 != 0) { + Nan::ThrowError("Palette length must be a multiple of 4."); + } + nPaletteColors /= 4; + Nan::TypedArrayContents _paletteColors(palette_ta); + paletteColors = *_paletteColors; + // Optional background color index: + Local backgroundIndexVal = attrs->Get(Nan::New("backgroundIndex").ToLocalChecked()); + if (backgroundIndexVal->IsUint32()) { + backgroundIndex = static_cast(backgroundIndexVal->Uint32Value()); + } + } + } + } else { + good = false; + } if (good) { if (compression_level > 9) { @@ -407,11 +439,13 @@ NAN_METHOD(Canvas::StreamPNGSync) { } - Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); closure_t closure; closure.fn = Local::Cast(info[0]); closure.compression_level = compression_level; closure.filter = filter; + closure.palette = paletteColors; + closure.nPaletteColors = nPaletteColors; + closure.backgroundIndex = backgroundIndex; Nan::TryCatch try_catch; diff --git a/src/PNG.h b/src/PNG.h index 558e4c177..92b321d28 100644 --- a/src/PNG.h +++ b/src/PNG.h @@ -156,10 +156,13 @@ static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr writ #endif png_set_write_fn(png, closure, write_func, canvas_png_flush); + // FIXME why is this not typed properly? png_set_compression_level(png, ((closure_t *) ((canvas_png_write_closure_t *) closure)->closure)->compression_level); png_set_filter(png, 0, ((closure_t *) ((canvas_png_write_closure_t *) closure)->closure)->filter); - switch (cairo_image_surface_get_format(surface)) { + cairo_format_t format = cairo_image_surface_get_format(surface); + + switch (format) { case CAIRO_FORMAT_ARGB32: bpc = 8; png_color_type = PNG_COLOR_TYPE_RGB_ALPHA; @@ -197,11 +200,40 @@ static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr writ return status; } + if ((format == CAIRO_FORMAT_A8 || format == CAIRO_FORMAT_A1) && + ((closure_t *) ((canvas_png_write_closure_t *) closure)->closure)->palette != NULL) { + png_color_type = PNG_COLOR_TYPE_PALETTE; + } + png_set_IHDR(png, info, width, height, bpc, png_color_type, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); - white.gray = (1 << bpc) - 1; - white.red = white.blue = white.green = white.gray; - png_set_bKGD(png, info, &white); + if (png_color_type == PNG_COLOR_TYPE_PALETTE) { + size_t nColors = ((closure_t *) ((canvas_png_write_closure_t *) closure)->closure)->nPaletteColors; + uint8_t* colors = ((closure_t *) ((canvas_png_write_closure_t *) closure)->closure)->palette; + uint8_t backgroundIndex = ((closure_t *) ((canvas_png_write_closure_t *) closure)->closure)->backgroundIndex; + png_colorp pngPalette = (png_colorp)png_malloc(png, nColors * sizeof(png_colorp)); + png_bytep transparency = (png_bytep)png_malloc(png, nColors * sizeof(png_bytep)); + for (i = 0; i < nColors; i++) { + pngPalette[i].red = colors[4 * i]; + pngPalette[i].green = colors[4 * i + 1]; + pngPalette[i].blue = colors[4 * i + 2]; + transparency[i] = colors[4 * i + 3]; + } + png_set_PLTE(png, info, pngPalette, nColors); + png_set_tRNS(png, info, transparency, nColors, NULL); + png_set_packing(png); // pack pixels + // have libpng free palette and trans: + png_data_freer(png, info, PNG_DESTROY_WILL_FREE_DATA, PNG_FREE_PLTE | PNG_FREE_TRNS); + png_color_16 bkg; + bkg.index = backgroundIndex; + png_set_bKGD(png, info, &bkg); + } + + if (png_color_type != PNG_COLOR_TYPE_PALETTE) { + white.gray = (1 << bpc) - 1; + white.red = white.blue = white.green = white.gray; + png_set_bKGD(png, info, &white); + } /* We have to call png_write_info() before setting up the write * transformation, since it stores data internally in 'png' @@ -210,7 +242,7 @@ static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr writ png_write_info(png, info); if (png_color_type == PNG_COLOR_TYPE_RGB_ALPHA) { png_set_write_user_transform_fn(png, canvas_unpremultiply_data); - } else if (cairo_image_surface_get_format(surface) == CAIRO_FORMAT_RGB16_565) { + } else if (format == CAIRO_FORMAT_RGB16_565) { png_set_write_user_transform_fn(png, canvas_convert_565_to_888); } else if (png_color_type == PNG_COLOR_TYPE_RGB) { png_set_write_user_transform_fn(png, canvas_convert_data_to_bytes); diff --git a/src/closure.h b/src/closure.h index e15a36730..76cd2fe15 100644 --- a/src/closure.h +++ b/src/closure.h @@ -34,6 +34,9 @@ typedef struct { cairo_status_t status; uint32_t compression_level; uint32_t filter; + uint8_t *palette; + size_t nPaletteColors; + uint8_t backgroundIndex; } closure_t; /* From 8c41bb111d4e6a0aa6713e82f7f7ae22b21ef680 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sun, 2 Jul 2017 14:59:56 -0700 Subject: [PATCH 031/474] Type closure_t/png_closure_t Can't tell if there was a reason this wasn't done previously, so putting it in a separate commit. --- src/PNG.h | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/PNG.h b/src/PNG.h index 92b321d28..944795f24 100644 --- a/src/PNG.h +++ b/src/PNG.h @@ -87,10 +87,10 @@ static void canvas_convert_565_to_888(png_structp png, png_row_infop row_info, p struct canvas_png_write_closure_t { cairo_write_func_t write_func; - void *closure; + closure_t *closure; }; -static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr write_func, void *closure) { +static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr write_func, canvas_png_write_closure_t *closure) { unsigned int i; cairo_status_t status = CAIRO_STATUS_SUCCESS; uint8_t *data; @@ -156,9 +156,8 @@ static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr writ #endif png_set_write_fn(png, closure, write_func, canvas_png_flush); - // FIXME why is this not typed properly? - png_set_compression_level(png, ((closure_t *) ((canvas_png_write_closure_t *) closure)->closure)->compression_level); - png_set_filter(png, 0, ((closure_t *) ((canvas_png_write_closure_t *) closure)->closure)->filter); + png_set_compression_level(png, closure->closure->compression_level); + png_set_filter(png, 0, closure->closure->filter); cairo_format_t format = cairo_image_surface_get_format(surface); @@ -201,16 +200,16 @@ static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr writ } if ((format == CAIRO_FORMAT_A8 || format == CAIRO_FORMAT_A1) && - ((closure_t *) ((canvas_png_write_closure_t *) closure)->closure)->palette != NULL) { + closure->closure->palette != NULL) { png_color_type = PNG_COLOR_TYPE_PALETTE; } png_set_IHDR(png, info, width, height, bpc, png_color_type, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); if (png_color_type == PNG_COLOR_TYPE_PALETTE) { - size_t nColors = ((closure_t *) ((canvas_png_write_closure_t *) closure)->closure)->nPaletteColors; - uint8_t* colors = ((closure_t *) ((canvas_png_write_closure_t *) closure)->closure)->palette; - uint8_t backgroundIndex = ((closure_t *) ((canvas_png_write_closure_t *) closure)->closure)->backgroundIndex; + size_t nColors = closure->closure->nPaletteColors; + uint8_t* colors = closure->closure->palette; + uint8_t backgroundIndex = closure->closure->backgroundIndex; png_colorp pngPalette = (png_colorp)png_malloc(png, nColors * sizeof(png_colorp)); png_bytep transparency = (png_bytep)png_malloc(png, nColors * sizeof(png_bytep)); for (i = 0; i < nColors; i++) { @@ -272,7 +271,7 @@ static void canvas_stream_write_func(png_structp png, png_bytep data, png_size_t } } -static cairo_status_t canvas_write_to_png_stream(cairo_surface_t *surface, cairo_write_func_t write_func, void *closure) { +static cairo_status_t canvas_write_to_png_stream(cairo_surface_t *surface, cairo_write_func_t write_func, closure_t *closure) { struct canvas_png_write_closure_t png_closure; if (cairo_surface_status(surface)) { From 4625efae9661ba0a42fee994cbebc1abe0d2102e Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sun, 23 Jul 2017 21:30:40 -0700 Subject: [PATCH 032/474] Faster, more compliant parseFont --- lib/parse-font.js | 105 ++++++++++++++++++++++++++++++-------------- package.json | 4 +- test/canvas.test.js | 20 ++++++--- 3 files changed, 87 insertions(+), 42 deletions(-) diff --git a/lib/parse-font.js b/lib/parse-font.js index b42bca471..cf91b6e61 100644 --- a/lib/parse-font.js +++ b/lib/parse-font.js @@ -1,64 +1,101 @@ 'use strict' -const parseCssFont = require('parse-css-font') -const unitsCss = require('units-css') +/** + * Font RegExp helpers. + */ + +const weights = 'bold|bolder|lighter|[1-9]00' + , styles = 'italic|oblique' + , variants = 'small-caps' + , stretches = 'ultra-condensed|extra-condensed|condensed|semi-condensed|semi-expanded|expanded|extra-expanded|ultra-expanded' + , units = 'px|pt|pc|in|cm|mm|%|em|ex|ch|rem|q' + , string = '\'([^\']+)\'|"([^"]+)"|[\\w-]+' + +// [ [ <‘font-style’> || || <‘font-weight’> || <‘font-stretch’> ]? +// <‘font-size’> [ / <‘line-height’> ]? <‘font-family’> ] +// https://drafts.csswg.org/css-fonts-3/#font-prop +const weightRe = new RegExp(`(${weights}) +`, 'i') +const styleRe = new RegExp(`(${styles}) +`, 'i') +const variantRe = new RegExp(`(${variants}) +`, 'i') +const stretchRe = new RegExp(`(${stretches}) +`, 'i') +const sizeFamilyRe = new RegExp( + '([\\d\\.]+)(' + units + ') *' + + '((?:' + string + ')( *, *(?:' + string + '))*)') /** - * Cache color string RGBA values. + * Cache font parsing. */ const cache = {} +const defaultHeight = 16 // pt, common browser default + /** * Parse font `str`. * * @param {String} str - * @return {Object} + * @return {Object} Parsed font. `size` is in device units. `unit` is the unit + * appearing in the input string. * @api private */ module.exports = function (str) { - let parsedFont - - // Try to parse the font string using parse-css-font. - // It will throw an exception if it fails. - try { - parsedFont = parseCssFont(str) - } catch (_) { - // Invalid - return undefined - } - // Cached if (cache[str]) return cache[str] - // Parse size into value and unit using units-css - var size = unitsCss.parse(parsedFont.size) + // Try for required properties first. + const sizeFamily = sizeFamilyRe.exec(str) + if (!sizeFamily) return // invalid + + // Default values and required properties + const font = { + weight: 'normal', + style: 'normal', + stretch: 'normal', + variant: 'normal', + size: parseFloat(sizeFamily[1]), + unit: sizeFamily[2], + family: sizeFamily[3].replace(/["']/g, '').replace(/ *, */g, ',') + } - // TODO: dpi - // TODO: remaining unit conversion - switch (size.unit) { + // Optional, unordered properties. + let weight, style, variant, stretch + // Stop search at `sizeFamily.index` + let substr = str.substring(0, sizeFamily.index) + if ((weight = weightRe.exec(substr))) font.weight = weight[1] + if ((style = styleRe.exec(substr))) font.style = style[1] + if ((variant = variantRe.exec(substr))) font.variant = variant[1] + if ((stretch = stretchRe.exec(substr))) font.stretch = stretch[1] + + // Convert to device units. (`font.unit` is the original unit) + // TODO: ch, ex + switch (font.unit) { case 'pt': - size.value /= 0.75 + font.size /= 0.75 + break + case 'pc': + font.size *= 16 break case 'in': - size.value *= 96 + font.size *= 96 + break + case 'cm': + font.size *= 96.0 / 2.54 break case 'mm': - size.value *= 96.0 / 25.4 + font.size *= 96.0 / 25.4 break - case 'cm': - size.value *= 96.0 / 2.54 + case '%': + // TODO disabled because existing unit tests assume 100 + // font.size *= defaultHeight / 100 / 0.75 + break + case 'em': + case 'rem': + font.size *= defaultHeight / 0.75 + break + case 'q': + font.size *= 96 / 25.4 / 4 break - } - - // Populate font object - var font = { - weight: parsedFont.weight, - style: parsedFont.style, - size: size.value, - unit: size.unit, - family: parsedFont.family.join(',') } return (cache[str] = font) diff --git a/package.json b/package.json index dc620ac24..ade5f0d3c 100644 --- a/package.json +++ b/package.json @@ -30,9 +30,7 @@ "test-server": "node test/server.js" }, "dependencies": { - "nan": "^2.4.0", - "parse-css-font": "^2.0.2", - "units-css": "^0.4.0" + "nan": "^2.4.0" }, "devDependencies": { "assert-rejects": "^0.1.1", diff --git a/test/canvas.test.js b/test/canvas.test.js index a307f51af..d0e7efd17 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -24,7 +24,7 @@ describe('Canvas', function () { , '20.5pt Arial' , { size: 27.333333333333332, unit: 'pt', family: 'Arial' } , '20% Arial' - , { size: 20, unit: '%', family: 'Arial' } + , { size: 20, unit: '%', family: 'Arial' } // TODO I think this is a bad assertion - ZB 23-Jul-2017 , '20mm Arial' , { size: 75.59055118110237, unit: 'mm', family: 'Arial' } , '20px serif' @@ -59,17 +59,27 @@ describe('Canvas', function () { , { size: 20, unit: 'px', weight: 'bolder', family: 'Arial' } , 'lighter 20px Arial' , { size: 20, unit: 'px', weight: 'lighter', family: 'Arial' } + , 'normal normal normal 16px Impact' + , { size: 16, unit: 'px', weight: 'normal', family: 'Impact', style: 'normal', variant: 'normal' } + , 'italic small-caps bolder 16px cursive' + , { size: 16, unit: 'px', style: 'italic', variant: 'small-caps', weight: 'bolder', family: 'cursive' } + , '20px "new century schoolbook", serif' + , { size: 20, unit: 'px', family: 'new century schoolbook,serif' } + , '20px "Arial bold 300"' // synthetic case with weight keyword inside family + , { size: 20, unit: 'px', family: 'Arial bold 300', variant: 'normal' } ]; for (var i = 0, len = tests.length; i < len; ++i) { var str = tests[i++] - , obj = tests[i] + , expected = tests[i] , actual = parseFont(str); - if (!obj.style) obj.style = 'normal'; - if (!obj.weight) obj.weight = 'normal'; + if (!expected.style) expected.style = 'normal'; + if (!expected.weight) expected.weight = 'normal'; + if (!expected.stretch) expected.stretch = 'normal'; + if (!expected.variant) expected.variant = 'normal'; - assert.deepEqual(obj, actual); + assert.deepEqual(actual, expected, 'Failed to parse: ' + str); } }); From 4d4a7260f180d9ab7aac66e0bdadbfb999a13717 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Tue, 25 Jul 2017 12:51:13 -0700 Subject: [PATCH 033/474] Add support for ellipse method --- src/CanvasRenderingContext2d.cc | 58 +++++++++++++++++++++++++++++++++ src/CanvasRenderingContext2d.h | 1 + test/public/tests.js | 48 +++++++++++++++++++++++++++ test/server.js | 4 +-- 4 files changed, 109 insertions(+), 2 deletions(-) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index e00a11206..02919714b 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -116,6 +116,7 @@ Context2d::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { Nan::SetPrototypeMethod(ctor, "closePath", ClosePath); Nan::SetPrototypeMethod(ctor, "arc", Arc); Nan::SetPrototypeMethod(ctor, "arcTo", ArcTo); + Nan::SetPrototypeMethod(ctor, "ellipse", Ellipse); Nan::SetPrototypeMethod(ctor, "setLineDash", SetLineDash); Nan::SetPrototypeMethod(ctor, "getLineDash", GetLineDash); Nan::SetPrototypeMethod(ctor, "_setFont", SetFont); @@ -2390,3 +2391,60 @@ NAN_METHOD(Context2d::ArcTo) { , ea); } } + +/* + * Adds an ellipse to the path which is centered at (x, y) position with the + * radii radiusX and radiusY starting at startAngle and ending at endAngle + * going in the given direction by anticlockwise (defaulting to clockwise). + */ + +NAN_METHOD(Context2d::Ellipse) { + if (!info[0]->IsNumber() + || !info[1]->IsNumber() + || !info[2]->IsNumber() + || !info[3]->IsNumber() + || !info[4]->IsNumber() + || !info[5]->IsNumber() + || !info[6]->IsNumber()) return; + + double radiusX = info[2]->NumberValue(); + double radiusY = info[3]->NumberValue(); + + if (radiusX == 0 || radiusY == 0) return; + + double x = info[0]->NumberValue(); + double y = info[1]->NumberValue(); + double rotation = info[4]->NumberValue(); + double startAngle = info[5]->NumberValue(); + double endAngle = info[6]->NumberValue(); + bool anticlockwise = info[7]->BooleanValue(); + + Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); + cairo_t *ctx = context->context(); + + // See https://www.cairographics.org/cookbook/ellipses/ + double xRatio = radiusX / radiusY; + + cairo_matrix_t save_matrix; + cairo_get_matrix(ctx, &save_matrix); + cairo_translate(ctx, x, y); + cairo_rotate(ctx, rotation); + cairo_scale(ctx, xRatio, 1.0); + cairo_translate(ctx, -x, -y); + if (anticlockwise && M_PI * 2 != info[4]->NumberValue()) { + cairo_arc_negative(ctx, + x, + y, + radiusY, + startAngle, + endAngle); + } else { + cairo_arc(ctx, + x, + y, + radiusY, + startAngle, + endAngle); + } + cairo_set_matrix(ctx, &save_matrix); +} diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index a43881441..af373bfd3 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -97,6 +97,7 @@ class Context2d: public Nan::ObjectWrap { static NAN_METHOD(Rect); static NAN_METHOD(Arc); static NAN_METHOD(ArcTo); + static NAN_METHOD(Ellipse); static NAN_METHOD(GetImageData); static NAN_GETTER(GetFormat); static NAN_GETTER(GetPatternQuality); diff --git a/test/public/tests.js b/test/public/tests.js index 58f656b3c..48e16684d 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -162,6 +162,54 @@ tests['arcTo()'] = function (ctx) { ctx.fillText('node', 120, 155) } +tests['ellipse() 1'] = function (ctx) { + var n = 8 + for (var i = 0; i < n; i++) { + ctx.beginPath() + var a = i * 2 * Math.PI / n + var x = 100 + 50 * Math.cos(a) + var y = 100 + 50 * Math.sin(a) + ctx.ellipse(x, y, 10, 15, a, 0, 2 * Math.PI) + ctx.stroke() + } +} + +tests['ellipse() 2'] = function (ctx) { + var n = 8 + for (var i = 0; i < n; i++) { + ctx.beginPath() + var a = i * 2 * Math.PI / n + var x = 100 + 50 * Math.cos(a) + var y = 100 + 50 * Math.sin(a) + ctx.ellipse(x, y, 10, 15, a, 0, a) + ctx.stroke() + } +} + +tests['ellipse() 3'] = function (ctx) { + var n = 8 + for (var i = 0; i < n; i++) { + ctx.beginPath() + var a = i * 2 * Math.PI / n + var x = 100 + 50 * Math.cos(a) + var y = 100 + 50 * Math.sin(a) + ctx.ellipse(x, y, 10, 15, a, 0, a, true) + ctx.stroke() + } +} + +tests['ellipse() 4'] = function (ctx) { + var n = 8 + for (var i = 0; i < n; i++) { + ctx.beginPath() + var a = i * 2 * Math.PI / n + var x = 100 + 50 * Math.cos(a) + var y = 100 + 50 * Math.sin(a) + ctx.ellipse(x, y, 10, 15, a, a, 0, true) + ctx.stroke() + } +} + tests['bezierCurveTo()'] = function (ctx) { ctx.beginPath() ctx.moveTo(75, 40) diff --git a/test/server.js b/test/server.js index e1378c9bf..d373ea9ca 100644 --- a/test/server.js +++ b/test/server.js @@ -33,7 +33,7 @@ app.get('/', function (req, res) { }) app.get('/render', function (req, res, next) { - var canvas = new Canvas(200, 200) + var canvas = Canvas.createCanvas(200, 200) renderTest(canvas, req.query.name, function (err) { if (err) return next(err) @@ -44,7 +44,7 @@ app.get('/render', function (req, res, next) { }) app.get('/pdf', function (req, res, next) { - var canvas = new Canvas(200, 200, 'pdf') + var canvas = Canvas.createCanvas(200, 200, 'pdf') renderTest(canvas, req.query.name, function (err) { if (err) return next(err) From 1baf00ee61d795933490b21ff4d9c21c6ed4ac92 Mon Sep 17 00:00:00 2001 From: Foy Savas Date: Sun, 11 Sep 2016 22:36:54 -0400 Subject: [PATCH 034/474] Render SVG img elements when librsvg is available In the browser, can render SVG elements. This commit enables node-canvas to do the same, if librsvg is installed. Note that librsvg remains an optional dependency. Use cases for embedded SVG images include the rendering of d3 charts, which often blend SVG and canvas content. Big thanks to petarov and scott113341 for much of the original code. --- binding.gyp | 23 +++++++- src/Image.cc | 131 ++++++++++++++++++++++++++++++++++++++++- src/Image.h | 10 ++++ test/fixtures/tree.svg | 9 +++ test/public/tests.js | 12 ++++ util/has_lib.js | 2 + 6 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 test/fixtures/tree.svg diff --git a/binding.gyp b/binding.gyp index 8a0d84e49..23e2316da 100644 --- a/binding.gyp +++ b/binding.gyp @@ -5,6 +5,7 @@ 'GTK_Root%': 'C:/GTK', # Set the location of GTK all-in-one bundle 'with_jpeg%': 'false', 'with_gif%': 'false', + 'with_rsvg%': 'false', 'variables': { # Nest jpeg_root to evaluate it before with_jpeg 'jpeg_root%': ' tag, isSVG returns false + uint8_t head[1000] = {0}; + unsigned head_len = (len < 1000 ? len : 1000); + memcpy(head, buf, head_len * sizeof(uint8_t)); + if (isSVG(head, head_len)) return loadSVGFromBuffer(buf, len); #endif return CAIRO_STATUS_READ_ERROR; } @@ -420,6 +428,24 @@ Image::loadSurface() { if (isJPEG(buf)) return loadJPEG(stream); #endif +// svg +#ifdef HAVE_RSVG + // confirm svg using first 1000 chars + // if a very long comment precedes the root tag, isSVG returns false + uint8_t head[1000] = {0}; + fseek(stream, 0 , SEEK_END); + long len = ftell(stream); + unsigned head_len = (len < 1000 ? len : 1000); + unsigned head_size = head_len * sizeof(uint8_t); + rewind(stream); + if (head_size != fread(&head, 1, head_size, stream)) { + fclose(stream); + return CAIRO_STATUS_READ_ERROR; + } + fseek(stream, 0, SEEK_SET); + if (isSVG(head, head_len)) return loadSVG(stream); +#endif + fclose(stream); return CAIRO_STATUS_READ_ERROR; } @@ -941,8 +967,89 @@ Image::loadJPEG(FILE *stream) { #endif /* HAVE_JPEG */ +#ifdef HAVE_RSVG + +/* + * Load SVG from buffer + */ + +cairo_status_t +Image::loadSVGFromBuffer(uint8_t *buf, unsigned len) { + cairo_status_t status; + RsvgHandle *rsvg; + GError *gerr = NULL; + + if (NULL == (rsvg = rsvg_handle_new_from_data(buf, len, &gerr))) { + return CAIRO_STATUS_READ_ERROR; + } + + RsvgDimensionData *dims = new RsvgDimensionData(); + rsvg_handle_get_dimensions(rsvg, dims); + + _surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, dims->width, dims->height); + + status = cairo_surface_status(_surface); + if (status != CAIRO_STATUS_SUCCESS) { + g_object_unref(rsvg); + return status; + } + + cairo_t *cr = cairo_create(_surface); + status = cairo_status(cr); + if (status != CAIRO_STATUS_SUCCESS) { + g_object_unref(rsvg); + return status; + } + + gboolean render_ok = !rsvg_handle_render_cairo(rsvg, cr); + if (render_ok) { + g_object_unref(rsvg); + cairo_destroy(cr); + return CAIRO_STATUS_READ_ERROR; // or WRITE? + } + + g_object_unref(rsvg); + cairo_destroy(cr); + + return CAIRO_STATUS_SUCCESS; +} + +/* + * Load SVG + */ + +cairo_status_t +Image::loadSVG(FILE *stream) { + struct stat s; + int fd = fileno(stream); + + // stat + if (fstat(fd, &s) < 0) { + fclose(stream); + return CAIRO_STATUS_READ_ERROR; + } + + uint8_t *buf = (uint8_t *) malloc(s.st_size); + + if (!buf) { + fclose(stream); + return CAIRO_STATUS_NO_MEMORY; + } + + size_t read = fread(buf, s.st_size, 1, stream); + fclose(stream); + + cairo_status_t result = CAIRO_STATUS_READ_ERROR; + if (1 == read) result = loadSVGFromBuffer(buf, s.st_size); + free(buf); + + return result; +} + +#endif /* HAVE_RSVG */ + /* - * Return UNKNOWN, JPEG, or PNG based on the filename. + * Return UNKNOWN, SVG, GIF, JPEG, or PNG based on the filename. */ Image::type @@ -953,6 +1060,7 @@ Image::extension(const char *filename) { if (len >= 4 && 0 == strcmp(".gif", filename - 4)) return Image::GIF; if (len >= 4 && 0 == strcmp(".jpg", filename - 4)) return Image::JPEG; if (len >= 4 && 0 == strcmp(".png", filename - 4)) return Image::PNG; + if (len >= 4 && 0 == strcmp(".svg", filename - 4)) return Image::SVG; return Image::UNKNOWN; } @@ -982,3 +1090,24 @@ int Image::isPNG(uint8_t *data) { return 'P' == data[1] && 'N' == data[2] && 'G' == data[3]; } + +/* + * Skip " +#endif + class Image: public Nan::ObjectWrap { @@ -53,6 +57,7 @@ class Image: public Nan::ObjectWrap { static int isPNG(uint8_t *data); static int isJPEG(uint8_t *data); static int isGIF(uint8_t *data); + static int isSVG(uint8_t *data, unsigned len); static cairo_status_t readPNG(void *closure, unsigned char *data, unsigned len); inline int isComplete(){ return COMPLETE == state; } cairo_status_t loadSurface(); @@ -60,6 +65,10 @@ class Image: public Nan::ObjectWrap { cairo_status_t loadPNGFromBuffer(uint8_t *buf); cairo_status_t loadPNG(); void clearData(); +#ifdef HAVE_RSVG + cairo_status_t loadSVGFromBuffer(uint8_t *buf, unsigned len); + cairo_status_t loadSVG(FILE *stream); +#endif #ifdef HAVE_GIF cairo_status_t loadGIFFromBuffer(uint8_t *buf, unsigned len); cairo_status_t loadGIF(FILE *stream); @@ -94,6 +103,7 @@ class Image: public Nan::ObjectWrap { , GIF , JPEG , PNG + , SVG } type; static type extension(const char *filename); diff --git a/test/fixtures/tree.svg b/test/fixtures/tree.svg new file mode 100644 index 000000000..b9adb0802 --- /dev/null +++ b/test/fixtures/tree.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/test/public/tests.js b/test/public/tests.js index 58f656b3c..cd735cf71 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -1658,6 +1658,18 @@ tests['drawImage(img) jpeg'] = function (ctx, done) { img.src = imageSrc('face.jpeg') } +tests['drawImage(img) svg'] = function (ctx, done) { + var img = new Image() + img.onload = function () { + ctx.drawImage(img, 0, 0, 100, 100) + done(null) + } + img.onerror = function () { + done(new Error('Failed to load image')) + } + img.src = imageSrc('tree.svg') +} + tests['drawImage(img,x,y)'] = function (ctx, done) { var img = new Image() img.onload = function () { diff --git a/util/has_lib.js b/util/has_lib.js index 9df6b9d0b..00e0ebfa8 100644 --- a/util/has_lib.js +++ b/util/has_lib.js @@ -103,6 +103,8 @@ function main (query) { return hasPkgconfigLib(query) case 'freetype': return hasFreetype() + case 'rsvg': + return hasPkgconfigLib('librsvg-2.0') default: throw new Error('Unknown library: ' + query) } From 4895b40d3510d6e08373f61e5276387a438ce849 Mon Sep 17 00:00:00 2001 From: Foy Savas Date: Mon, 12 Sep 2016 00:18:50 -0400 Subject: [PATCH 035/474] support librsvg <= 2.36.1 --- src/Image.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Image.h b/src/Image.h index 9354b4bd0..ecef7599e 100644 --- a/src/Image.h +++ b/src/Image.h @@ -27,6 +27,10 @@ #ifdef HAVE_RSVG #include + // librsvg <= 2.36.1, identified by undefined macro, needs an extra include + #ifndef LIBRSVG_CHECK_VERSION + #include + #endif #endif From 72e226525f8f776ba56a491e33157fdcb5fd8d3d Mon Sep 17 00:00:00 2001 From: Foy Savas Date: Thu, 15 Sep 2016 00:03:42 -0400 Subject: [PATCH 036/474] validate SVG img using input buffer directly --- src/Image.cc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Image.cc b/src/Image.cc index 89d2ec70e..a1db4d3fb 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -221,10 +221,8 @@ Image::loadFromBuffer(uint8_t *buf, unsigned len) { #ifdef HAVE_RSVG // confirm svg using first 1000 chars // if a very long comment precedes the root tag, isSVG returns false - uint8_t head[1000] = {0}; unsigned head_len = (len < 1000 ? len : 1000); - memcpy(head, buf, head_len * sizeof(uint8_t)); - if (isSVG(head, head_len)) return loadSVGFromBuffer(buf, len); + if (isSVG(buf, head_len)) return loadSVGFromBuffer(buf, len); #endif return CAIRO_STATUS_READ_ERROR; } From 67576823ecc71001b54a6b14f98e482d03dfdae8 Mon Sep 17 00:00:00 2001 From: Foy Savas Date: Tue, 29 Nov 2016 15:08:49 -0500 Subject: [PATCH 037/474] add readme text about SVG image support --- Readme.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 8a91014ef..ac83074b9 100644 --- a/Readme.md +++ b/Readme.md @@ -317,7 +317,7 @@ ctx.fillText('Hello World 3', 50, 80); ctx.addPage(); ``` -## SVG support +## SVG Support Just like PDF support, make sure to install cairo with `--enable-svg=yes`. You also need to tell node-canvas that it is working on SVG upon its initialization: @@ -328,6 +328,16 @@ var canvas = new Canvas(200, 500, 'svg'); fs.writeFile('out.svg', canvas.toBuffer()); ``` +## SVG Image Support + +If librsvg is on your system when node-canvas is installed, node-canvas can render SVG images within your canvas context. Note that this currently works by simply rasterizing the SVG image using librsvg. + +```js +var img = new Image; +img.src = './example.svg'; +ctx.drawImage(img, 0, 0, 100, 100); +``` + ## Image pixel formats (experimental) node-canvas has experimental support for additional pixel formats, roughly From c927c809200e699d41269fff2d2bdfe8cc1d70a1 Mon Sep 17 00:00:00 2001 From: cho45 Date: Mon, 31 Jul 2017 13:23:28 +0900 Subject: [PATCH 038/474] Fix: textBaseline consider current scale. --- src/CanvasRenderingContext2d.cc | 9 ++++++--- test/public/tests.js | 23 +++++++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index e00a11206..e39008c2e 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -1901,10 +1901,13 @@ void Context2d::setTextPath(const char *str, double x, double y) { PangoRectangle ink_rect, logical_rect; PangoFontMetrics *metrics = NULL; + cairo_matrix_t matrix; pango_layout_set_text(_layout, str, -1); pango_cairo_update_layout(_context, _layout); + cairo_get_matrix(_context, &matrix); + switch (state->textAlignment) { // center case 0: @@ -1921,15 +1924,15 @@ Context2d::setTextPath(const char *str, double x, double y) { switch (state->textBaseline) { case TEXT_BASELINE_ALPHABETIC: metrics = PANGO_LAYOUT_GET_METRICS(_layout); - y -= pango_font_metrics_get_ascent(metrics) / PANGO_SCALE; + y -= (pango_font_metrics_get_ascent(metrics) / PANGO_SCALE) * matrix.yy; break; case TEXT_BASELINE_MIDDLE: metrics = PANGO_LAYOUT_GET_METRICS(_layout); - y -= (pango_font_metrics_get_ascent(metrics) + pango_font_metrics_get_descent(metrics))/(2.0 * PANGO_SCALE); + y -= ((pango_font_metrics_get_ascent(metrics) + pango_font_metrics_get_descent(metrics))/(2.0 * PANGO_SCALE)) * matrix.yy; break; case TEXT_BASELINE_BOTTOM: metrics = PANGO_LAYOUT_GET_METRICS(_layout); - y -= (pango_font_metrics_get_ascent(metrics) + pango_font_metrics_get_descent(metrics)) / PANGO_SCALE; + y -= ((pango_font_metrics_get_ascent(metrics) + pango_font_metrics_get_descent(metrics)) / PANGO_SCALE) * matrix.yy; break; } diff --git a/test/public/tests.js b/test/public/tests.js index 58f656b3c..1fd5cc4d7 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -2058,3 +2058,26 @@ tests['fillStyle=\'hsla(...)\''] = function (ctx) { } } } + +tests['textBaseline and scale'] = function (ctx) { + ctx.strokeStyle = '#666' + ctx.strokeRect(0, 0, 200, 200) + ctx.lineTo(0, 50) + ctx.lineTo(200, 50) + ctx.stroke() + ctx.beginPath() + ctx.lineTo(0, 150) + ctx.lineTo(200, 150) + ctx.stroke() + + ctx.font = 'normal 20px Arial' + ctx.textBaseline = 'bottom' + ctx.textAlign = 'center' + ctx.fillText('bottom', 100, 50) + + ctx.scale(0.1, 0.1) + ctx.font = 'normal 200px Arial' + ctx.textBaseline = 'bottom' + ctx.textAlign = 'center' + ctx.fillText('bottom', 1000, 1500) +} From 84e6e996baaaec94b4928b004e9f993a07c24cf0 Mon Sep 17 00:00:00 2001 From: Sascha Gehlich Date: Tue, 15 Aug 2017 12:23:16 +0200 Subject: [PATCH 039/474] Add width/height setter --- src/Image.cc | 24 ++++++++++++++++++++++-- src/Image.h | 2 ++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Image.cc b/src/Image.cc index a1db4d3fb..0b155112e 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -47,8 +47,8 @@ Image::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { Local proto = ctor->PrototypeTemplate(); Nan::SetAccessor(proto, Nan::New("source").ToLocalChecked(), GetSource, SetSource); Nan::SetAccessor(proto, Nan::New("complete").ToLocalChecked(), GetComplete); - Nan::SetAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth); - Nan::SetAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight); + Nan::SetAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, SetWidth); + Nan::SetAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, SetHeight); Nan::SetAccessor(proto, Nan::New("onload").ToLocalChecked(), GetOnload, SetOnload); Nan::SetAccessor(proto, Nan::New("onerror").ToLocalChecked(), GetOnerror, SetOnerror); #if CAIRO_VERSION_MINOR >= 10 @@ -116,6 +116,16 @@ NAN_GETTER(Image::GetWidth) { Image *img = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(img->width)); } +/* + * Set width. + */ + +NAN_SETTER(Image::SetWidth) { + if (value->IsNumber()) { + Image *img = Nan::ObjectWrap::Unwrap(info.This()); + img->width = value->Uint32Value(); + } +} /* * Get height. */ @@ -124,6 +134,16 @@ NAN_GETTER(Image::GetHeight) { Image *img = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(img->height)); } +/* + * Set height. + */ + +NAN_SETTER(Image::SetHeight) { + if (value->IsNumber()) { + Image *img = Nan::ObjectWrap::Unwrap(info.This()); + img->height = value->Uint32Value(); + } +} /* * Get src path. diff --git a/src/Image.h b/src/Image.h index ecef7599e..a41452b93 100644 --- a/src/Image.h +++ b/src/Image.h @@ -55,6 +55,8 @@ class Image: public Nan::ObjectWrap { static NAN_SETTER(SetOnload); static NAN_SETTER(SetOnerror); static NAN_SETTER(SetDataMode); + static NAN_SETTER(SetWidth); + static NAN_SETTER(SetHeight); 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); } From a5915f8c17f9819dde94a84da72d723417b7a1ea Mon Sep 17 00:00:00 2001 From: Sascha Gehlich Date: Tue, 15 Aug 2017 12:43:53 +0200 Subject: [PATCH 040/474] Add `naturalWidth` and `naturalHeight` properties to Image --- src/Image.cc | 24 ++++++++++++++++++++++++ src/Image.h | 3 +++ 2 files changed, 27 insertions(+) diff --git a/src/Image.cc b/src/Image.cc index 0b155112e..81cf88d84 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -49,6 +49,8 @@ Image::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { Nan::SetAccessor(proto, Nan::New("complete").ToLocalChecked(), GetComplete); Nan::SetAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, SetWidth); Nan::SetAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, SetHeight); + Nan::SetAccessor(proto, Nan::New("naturalWidth").ToLocalChecked(), GetNaturalWidth); + Nan::SetAccessor(proto, Nan::New("naturalHeight").ToLocalChecked(), GetNaturalHeight); Nan::SetAccessor(proto, Nan::New("onload").ToLocalChecked(), GetOnload, SetOnload); Nan::SetAccessor(proto, Nan::New("onerror").ToLocalChecked(), GetOnerror, SetOnerror); #if CAIRO_VERSION_MINOR >= 10 @@ -108,6 +110,15 @@ NAN_SETTER(Image::SetDataMode) { #endif +/* + * Get natural width + */ + +NAN_GETTER(Image::GetNaturalWidth) { + Image *img = Nan::ObjectWrap::Unwrap(info.This()); + info.GetReturnValue().Set(Nan::New(img->naturalWidth)); +} + /* * Get width. */ @@ -116,6 +127,7 @@ NAN_GETTER(Image::GetWidth) { Image *img = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(img->width)); } + /* * Set width. */ @@ -126,6 +138,16 @@ NAN_SETTER(Image::SetWidth) { img->width = value->Uint32Value(); } } + +/* + * Get natural height + */ + +NAN_GETTER(Image::GetNaturalHeight) { + Image *img = Nan::ObjectWrap::Unwrap(info.This()); + info.GetReturnValue().Set(Nan::New(img->naturalHeight)); +} + /* * Get height. */ @@ -174,6 +196,7 @@ Image::clearData() { filename = NULL; width = height = 0; + naturalWidth = naturalHeight = 0; state = DEFAULT; } @@ -344,6 +367,7 @@ Image::Image() { _data_len = 0; _surface = NULL; width = height = 0; + naturalWidth = naturalHeight = 0; state = DEFAULT; onload = NULL; onerror = NULL; diff --git a/src/Image.h b/src/Image.h index a41452b93..04213c1af 100644 --- a/src/Image.h +++ b/src/Image.h @@ -39,6 +39,7 @@ class Image: public Nan::ObjectWrap { public: char *filename; int width, height; + int naturalWidth, naturalHeight; Nan::Callback *onload; Nan::Callback *onerror; static Nan::Persistent constructor; @@ -50,6 +51,8 @@ class Image: public Nan::ObjectWrap { static NAN_GETTER(GetComplete); static NAN_GETTER(GetWidth); static NAN_GETTER(GetHeight); + static NAN_GETTER(GetNaturalWidth); + static NAN_GETTER(GetNaturalHeight); static NAN_GETTER(GetDataMode); static NAN_SETTER(SetSource); static NAN_SETTER(SetOnload); From 7a86a2b15d7021ed29964e164c79710d1c658a84 Mon Sep 17 00:00:00 2001 From: Sascha Gehlich Date: Tue, 15 Aug 2017 12:44:03 +0200 Subject: [PATCH 041/474] Use `naturalWidth/Height` over `width` for now --- src/Image.cc | 70 ++++++++++++++++++++++++++-------------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/src/Image.cc b/src/Image.cc index 81cf88d84..270db9b1c 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -413,9 +413,9 @@ Image::loaded() { Nan::HandleScope scope; state = COMPLETE; - width = cairo_image_surface_get_width(_surface); - height = cairo_image_surface_get_height(_surface); - _data_len = height * cairo_image_surface_get_stride(_surface); + width = naturalWidth = cairo_image_surface_get_width(_surface); + height = naturalHeight = cairo_image_surface_get_height(_surface); + _data_len = naturalHeight * cairo_image_surface_get_stride(_surface); Nan::AdjustExternalMemory(_data_len); if (onload != NULL) { @@ -592,10 +592,10 @@ Image::loadGIFFromBuffer(uint8_t *buf, unsigned len) { return CAIRO_STATUS_READ_ERROR; } - width = gif->SWidth; - height = gif->SHeight; + width = naturalWidth = gif->SWidth; + height = naturalHeight = gif->SHeight; - uint8_t *data = (uint8_t *) malloc(width * height * 4); + uint8_t *data = (uint8_t *) malloc(naturalWidth * naturalHeight * 4); if (!data) { GIF_CLOSE_FILE(gif); return CAIRO_STATUS_NO_MEMORY; @@ -617,9 +617,9 @@ Image::loadGIFFromBuffer(uint8_t *buf, unsigned len) { uint32_t *dst_data = (uint32_t*) data; if (!gif->Image.Interlace) { - if (width == img->Width && height == img->Height) { - for (int y = 0; y < height; ++y) { - for (int x = 0; x < width; ++x) { + if (naturalWidth == img->Width && naturalHeight == img->Height) { + for (int y = 0; y < naturalHeight; ++y) { + for (int x = 0; x < naturalWidth; ++x) { *dst_data = ((*src_data == alphaColor) ? 0 : 255) << 24 | colormap->Colors[*src_data].Red << 16 | colormap->Colors[*src_data].Green << 8 @@ -634,8 +634,8 @@ Image::loadGIFFromBuffer(uint8_t *buf, unsigned len) { int bottom = img->Top + img->Height; int right = img->Left + img->Width; - for (int y = 0; y < height; ++y) { - for (int x = 0; x < width; ++x) { + for (int y = 0; y < naturalHeight; ++y) { + for (int x = 0; x < naturalWidth; ++x) { if (y < img->Top || y >= bottom || x < img->Left || x >= right) { *dst_data = ((bgColor == alphaColor) ? 0 : 255) << 24 | colormap->Colors[bgColor].Red << 16 @@ -664,9 +664,9 @@ Image::loadGIFFromBuffer(uint8_t *buf, unsigned len) { uint32_t *dst_ptr; for(int z = 0; z < 4; z++) { - for(int y = ioffs[z]; y < height; y += ijumps[z]) { - dst_ptr = dst_data + width * y; - for(int x = 0; x < width; ++x) { + for(int y = ioffs[z]; y < naturalHeight; y += ijumps[z]) { + dst_ptr = dst_data + naturalWidth * y; + for(int x = 0; x < naturalWidth; ++x) { *dst_ptr = ((*src_ptr == alphaColor) ? 0 : 255) << 24 | (colormap->Colors[*src_ptr].Red) << 16 | (colormap->Colors[*src_ptr].Green) << 8 @@ -685,9 +685,9 @@ Image::loadGIFFromBuffer(uint8_t *buf, unsigned len) { _surface = cairo_image_surface_create_for_data( data , CAIRO_FORMAT_ARGB32 - , width - , height - , cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width)); + , naturalWidth + , naturalHeight + , cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, naturalWidth)); cairo_status_t status = cairo_surface_status(_surface); @@ -757,17 +757,17 @@ static void jpeg_mem_src (j_decompress_ptr cinfo, void* buffer, long nbytes) { cairo_status_t Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args) { - int stride = width * 4; + int stride = naturalWidth * 4; cairo_status_t status; - uint8_t *data = (uint8_t *) malloc(width * height * 4); + uint8_t *data = (uint8_t *) malloc(naturalWidth * naturalHeight * 4); if (!data) { jpeg_abort_decompress(args); jpeg_destroy_decompress(args); return CAIRO_STATUS_NO_MEMORY; } - uint8_t *src = (uint8_t *) malloc(width * args->output_components); + uint8_t *src = (uint8_t *) malloc(naturalWidth * args->output_components); if (!src) { free(data); jpeg_abort_decompress(args); @@ -775,10 +775,10 @@ Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args) { return CAIRO_STATUS_NO_MEMORY; } - for (int y = 0; y < height; ++y) { + for (int y = 0; y < naturalHeight; ++y) { jpeg_read_scanlines(args, &src, 1); uint32_t *row = (uint32_t *)(data + stride * y); - for (int x = 0; x < width; ++x) { + for (int x = 0; x < naturalWidth; ++x) { if (args->jpeg_color_space == 1) { uint32_t *pixel = row + x; *pixel = 255 << 24 @@ -799,9 +799,9 @@ Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args) { _surface = cairo_image_surface_create_for_data( data , CAIRO_FORMAT_ARGB32 - , width - , height - , cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width)); + , naturalWidth + , naturalHeight + , cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, naturalWidth)); jpeg_finish_decompress(args); jpeg_destroy_decompress(args); @@ -840,12 +840,12 @@ Image::decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len) { jpeg_read_header(&args, 1); jpeg_start_decompress(&args); - width = args.output_width; - height = args.output_height; + width = naturalWidth = args.output_width; + height = naturalHeight = args.output_height; // Data alloc // 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); + int buf_size = naturalHeight * cairo_format_stride_for_width(CAIRO_FORMAT_A1, naturalWidth); uint8_t *data = (uint8_t *) malloc(buf_size); if (!data) return CAIRO_STATUS_NO_MEMORY; @@ -853,9 +853,9 @@ Image::decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len) { _surface = cairo_image_surface_create_for_data( data , CAIRO_FORMAT_A1 - , width - , height - , cairo_format_stride_for_width(CAIRO_FORMAT_A1, width)); + , naturalWidth + , naturalHeight + , cairo_format_stride_for_width(CAIRO_FORMAT_A1, naturalWidth)); // Cleanup jpeg_abort_decompress(&args); @@ -934,8 +934,8 @@ Image::loadJPEGFromBuffer(uint8_t *buf, unsigned len) { jpeg_read_header(&args, 1); jpeg_start_decompress(&args); - width = args.output_width; - height = args.output_height; + width = naturalWidth = args.output_width; + height = naturalHeight = args.output_height; return decodeJPEGIntoSurface(&args); } @@ -963,8 +963,8 @@ Image::loadJPEG(FILE *stream) { jpeg_read_header(&args, 1); jpeg_start_decompress(&args); - width = args.output_width; - height = args.output_height; + width = naturalWidth = args.output_width; + height = naturalHeight = args.output_height; status = decodeJPEGIntoSurface(&args); fclose(stream); From ee3871095a3096ddf81d68951839c57b2f8d6bd3 Mon Sep 17 00:00:00 2001 From: Sascha Gehlich Date: Tue, 15 Aug 2017 12:46:37 +0200 Subject: [PATCH 042/474] Keep RsvgHandle --- src/Image.cc | 15 +++++++-------- src/Image.h | 3 +++ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Image.cc b/src/Image.cc index 270db9b1c..67de14250 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -1018,39 +1018,38 @@ Image::loadJPEG(FILE *stream) { cairo_status_t Image::loadSVGFromBuffer(uint8_t *buf, unsigned len) { cairo_status_t status; - RsvgHandle *rsvg; GError *gerr = NULL; - if (NULL == (rsvg = rsvg_handle_new_from_data(buf, len, &gerr))) { + if (NULL == (_rsvg = rsvg_handle_new_from_data(buf, len, &gerr))) { return CAIRO_STATUS_READ_ERROR; } RsvgDimensionData *dims = new RsvgDimensionData(); - rsvg_handle_get_dimensions(rsvg, dims); + rsvg_handle_get_dimensions(_rsvg, dims); _surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, dims->width, dims->height); status = cairo_surface_status(_surface); if (status != CAIRO_STATUS_SUCCESS) { - g_object_unref(rsvg); + g_object_unref(_rsvg); return status; } cairo_t *cr = cairo_create(_surface); status = cairo_status(cr); if (status != CAIRO_STATUS_SUCCESS) { - g_object_unref(rsvg); + g_object_unref(_rsvg); return status; } - gboolean render_ok = !rsvg_handle_render_cairo(rsvg, cr); + gboolean render_ok = !rsvg_handle_render_cairo(_rsvg, cr); if (render_ok) { - g_object_unref(rsvg); + g_object_unref(_rsvg); cairo_destroy(cr); return CAIRO_STATUS_READ_ERROR; // or WRITE? } - g_object_unref(rsvg); + g_object_unref(_rsvg); cairo_destroy(cr); return CAIRO_STATUS_SUCCESS; diff --git a/src/Image.h b/src/Image.h index 04213c1af..a4e9702df 100644 --- a/src/Image.h +++ b/src/Image.h @@ -121,6 +121,9 @@ class Image: public Nan::ObjectWrap { cairo_surface_t *_surface; uint8_t *_data; int _data_len; +#ifdef HAVE_RSVG + RsvgHandle *_rsvg; +#endif ~Image(); }; From c7448d4200786e7e099bce15f2a2baebfc6c1ab2 Mon Sep 17 00:00:00 2001 From: Sascha Gehlich Date: Tue, 15 Aug 2017 12:49:56 +0200 Subject: [PATCH 043/474] Move rSVG rendering to separate method --- src/Image.cc | 14 +++++++++++++- src/Image.h | 1 + 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Image.cc b/src/Image.cc index 67de14250..304c98de7 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -1035,6 +1035,18 @@ Image::loadSVGFromBuffer(uint8_t *buf, unsigned len) { return status; } + renderSVGToSurface(); + + return CAIRO_STATUS_SUCCESS; +} + +/** + * Renders the Rsvg handle to this image's surface + */ +cairo_status_t +Image::renderSVGToSurface() { + cairo_status_t status; + cairo_t *cr = cairo_create(_surface); status = cairo_status(cr); if (status != CAIRO_STATUS_SUCCESS) { @@ -1052,7 +1064,7 @@ Image::loadSVGFromBuffer(uint8_t *buf, unsigned len) { g_object_unref(_rsvg); cairo_destroy(cr); - return CAIRO_STATUS_SUCCESS; + return status; } /* diff --git a/src/Image.h b/src/Image.h index a4e9702df..f2da2da3d 100644 --- a/src/Image.h +++ b/src/Image.h @@ -77,6 +77,7 @@ class Image: public Nan::ObjectWrap { #ifdef HAVE_RSVG cairo_status_t loadSVGFromBuffer(uint8_t *buf, unsigned len); cairo_status_t loadSVG(FILE *stream); + cairo_status_t renderSVGToSurface(); #endif #ifdef HAVE_GIF cairo_status_t loadGIFFromBuffer(uint8_t *buf, unsigned len); From ef93b8cff152b1d6fd0fb5de1f595cf4193812b2 Mon Sep 17 00:00:00 2001 From: Sascha Gehlich Date: Tue, 15 Aug 2017 12:52:41 +0200 Subject: [PATCH 044/474] =?UTF-8?q?Don=E2=80=99t=20inline=20Image#surface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Image.cc | 9 ++++++++- src/Image.h | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Image.cc b/src/Image.cc index 304c98de7..370ee8d90 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -436,6 +436,13 @@ Image::error(Local err) { } } +/* + * Returns this image's surface. + */ +cairo_surface_t *surface() { + return _surface; +} + /* * Load cairo surface from the image src. * @@ -1040,7 +1047,7 @@ Image::loadSVGFromBuffer(uint8_t *buf, unsigned len) { return CAIRO_STATUS_SUCCESS; } -/** +/* * Renders the Rsvg handle to this image's surface */ cairo_status_t diff --git a/src/Image.h b/src/Image.h index f2da2da3d..d8956a196 100644 --- a/src/Image.h +++ b/src/Image.h @@ -60,7 +60,7 @@ class Image: public Nan::ObjectWrap { static NAN_SETTER(SetDataMode); static NAN_SETTER(SetWidth); static NAN_SETTER(SetHeight); - inline cairo_surface_t *surface(){ return _surface; } + static cairo_surface_t *surface(); inline uint8_t *data(){ return cairo_image_surface_get_data(_surface); } inline int stride(){ return cairo_image_surface_get_stride(_surface); } static int isPNG(uint8_t *data); From 79bf2328dc882e62c2a2c262cde424311692a8a1 Mon Sep 17 00:00:00 2001 From: Sascha Gehlich Date: Tue, 15 Aug 2017 13:24:56 +0200 Subject: [PATCH 045/474] Re-rasterize SVG when getting surface (and dimensions changed) --- examples/image-src-svg.js | 18 +++++++++++++ examples/images/small-svg.svg | 6 +++++ src/Image.cc | 49 +++++++++++++++++++++++++++++------ src/Image.h | 5 +++- 4 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 examples/image-src-svg.js create mode 100755 examples/images/small-svg.svg diff --git a/examples/image-src-svg.js b/examples/image-src-svg.js new file mode 100644 index 000000000..6a6567651 --- /dev/null +++ b/examples/image-src-svg.js @@ -0,0 +1,18 @@ +var fs = require('fs') +var path = require('path') +var Canvas = require('..') + +var canvas = Canvas.createCanvas(500, 500) +var ctx = canvas.getContext('2d') +ctx.fillStyle = 'white' +ctx.fillRect(0, 0, 500, 500) + +Canvas.loadImage(path.join(__dirname, 'images', 'small-svg.svg')) + .then(image => { + image.width *= 1.5 + image.height *= 1.5 + ctx.drawImage(image, canvas.width / 2 - image.width / 2, canvas.height / 2 - image.height / 2) + + canvas.createPNGStream().pipe(fs.createWriteStream(path.join(__dirname, 'image-src-svg.png'))) + }) + .catch(e => console.error(e)) diff --git a/examples/images/small-svg.svg b/examples/images/small-svg.svg new file mode 100755 index 000000000..abf449bce --- /dev/null +++ b/examples/images/small-svg.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/Image.cc b/src/Image.cc index 370ee8d90..79fba0912 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -371,6 +371,10 @@ Image::Image() { state = DEFAULT; onload = NULL; onerror = NULL; +#ifdef HAVE_RSVG + _is_svg = false; + _svg_last_width = _svg_last_height = 0; +#endif } /* @@ -439,7 +443,22 @@ Image::error(Local err) { /* * Returns this image's surface. */ -cairo_surface_t *surface() { +cairo_surface_t *Image::surface() { +#ifdef HAVE_RSVG + if (_is_svg && (_svg_last_width != width || _svg_last_height != height)) { + if (_surface != NULL) { + cairo_surface_destroy(_surface); + _surface = NULL; + } + + cairo_status_t status = renderSVGToSurface(); + if (status != CAIRO_STATUS_SUCCESS) { + g_object_unref(_rsvg); + error(Canvas::Error(status)); + return NULL; + } + } +#endif return _surface; } @@ -1034,16 +1053,15 @@ Image::loadSVGFromBuffer(uint8_t *buf, unsigned len) { RsvgDimensionData *dims = new RsvgDimensionData(); rsvg_handle_get_dimensions(_rsvg, dims); - _surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, dims->width, dims->height); + width = naturalWidth = dims->width; + height = naturalHeight = dims->height; - status = cairo_surface_status(_surface); + status = renderSVGToSurface(); if (status != CAIRO_STATUS_SUCCESS) { g_object_unref(_rsvg); return status; } - renderSVGToSurface(); - return CAIRO_STATUS_SUCCESS; } @@ -1054,23 +1072,36 @@ cairo_status_t Image::renderSVGToSurface() { cairo_status_t status; + _surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + + status = cairo_surface_status(_surface); + if (status != CAIRO_STATUS_SUCCESS) { + g_object_unref(_rsvg); + return status; + } + cairo_t *cr = cairo_create(_surface); + cairo_scale(cr, + (double)width / (double)naturalWidth, + (double)height / (double)naturalHeight); status = cairo_status(cr); if (status != CAIRO_STATUS_SUCCESS) { g_object_unref(_rsvg); return status; } - gboolean render_ok = !rsvg_handle_render_cairo(_rsvg, cr); - if (render_ok) { + gboolean render_ok = rsvg_handle_render_cairo(_rsvg, cr); + if (!render_ok) { g_object_unref(_rsvg); cairo_destroy(cr); return CAIRO_STATUS_READ_ERROR; // or WRITE? } - g_object_unref(_rsvg); cairo_destroy(cr); + _svg_last_width = width; + _svg_last_height = height; + return status; } @@ -1080,6 +1111,8 @@ Image::renderSVGToSurface() { cairo_status_t Image::loadSVG(FILE *stream) { + _is_svg = true; + struct stat s; int fd = fileno(stream); diff --git a/src/Image.h b/src/Image.h index d8956a196..054295cc7 100644 --- a/src/Image.h +++ b/src/Image.h @@ -60,7 +60,6 @@ class Image: public Nan::ObjectWrap { static NAN_SETTER(SetDataMode); static NAN_SETTER(SetWidth); static NAN_SETTER(SetHeight); - static cairo_surface_t *surface(); inline uint8_t *data(){ return cairo_image_surface_get_data(_surface); } inline int stride(){ return cairo_image_surface_get_stride(_surface); } static int isPNG(uint8_t *data); @@ -69,6 +68,7 @@ class Image: public Nan::ObjectWrap { static int isSVG(uint8_t *data, unsigned len); static cairo_status_t readPNG(void *closure, unsigned char *data, unsigned len); inline int isComplete(){ return COMPLETE == state; } + cairo_surface_t *surface(); cairo_status_t loadSurface(); cairo_status_t loadFromBuffer(uint8_t *buf, unsigned len); cairo_status_t loadPNGFromBuffer(uint8_t *buf); @@ -124,6 +124,9 @@ class Image: public Nan::ObjectWrap { int _data_len; #ifdef HAVE_RSVG RsvgHandle *_rsvg; + bool _is_svg; + int _svg_last_width; + int _svg_last_height; #endif ~Image(); }; From b6e62b959b97b9154a3f49b2230195a3c3d194d1 Mon Sep 17 00:00:00 2001 From: Sascha Gehlich Date: Tue, 15 Aug 2017 13:33:37 +0200 Subject: [PATCH 046/474] Dispose RsvgHandle --- src/Image.cc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Image.cc b/src/Image.cc index 79fba0912..959f220dd 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -195,6 +195,12 @@ Image::clearData() { free(filename); filename = NULL; +#ifdef HAVE_RSVG + if (_rsvg) { + g_object_unref(_rsvg); + } +#endif + width = height = 0; naturalWidth = naturalHeight = 0; state = DEFAULT; From f6fe4abe3757fadfce82dbc99c6f96e185b654e9 Mon Sep 17 00:00:00 2001 From: Sascha Gehlich Date: Tue, 15 Aug 2017 14:48:14 +0200 Subject: [PATCH 047/474] Fix segfault --- src/Image.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Image.cc b/src/Image.cc index 959f220dd..5ac9dae12 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -196,8 +196,9 @@ Image::clearData() { filename = NULL; #ifdef HAVE_RSVG - if (_rsvg) { + if (_rsvg != NULL) { g_object_unref(_rsvg); + _rsvg = NULL; } #endif @@ -378,6 +379,7 @@ Image::Image() { onload = NULL; onerror = NULL; #ifdef HAVE_RSVG + _rsvg = NULL; _is_svg = false; _svg_last_width = _svg_last_height = 0; #endif From 77749e6e82110e883cfb37ec030a5ea5db907dc9 Mon Sep 17 00:00:00 2001 From: Sascha Gehlich Date: Fri, 25 Aug 2017 10:12:56 +0200 Subject: [PATCH 048/474] :bug: Fix SVG recognition when loading from buffer --- src/Image.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Image.cc b/src/Image.cc index 5ac9dae12..73bfcc77e 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -1051,6 +1051,8 @@ Image::loadJPEG(FILE *stream) { cairo_status_t Image::loadSVGFromBuffer(uint8_t *buf, unsigned len) { + _is_svg = true; + cairo_status_t status; GError *gerr = NULL; From 175b40d564e8c99459521929f76a74aab3ddf3af Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sat, 26 Aug 2017 20:37:25 -0400 Subject: [PATCH 049/474] support maxWidth arg to fill/strokeText --- examples/font.js | 2 +- src/CanvasRenderingContext2d.cc | 33 +++++++++++++++++++++++++++++++++ test/public/tests.js | 22 ++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/examples/font.js b/examples/font.js index 2cf485639..3a69624d7 100644 --- a/examples/font.js +++ b/examples/font.js @@ -23,7 +23,7 @@ ctx.font = 'normal normal 50px Helvetica' ctx.fillText('Quo Vaids?', 0, 70) ctx.font = 'bold 50px pfennigFont' -ctx.fillText('Quo Vaids?', 0, 140) +ctx.fillText('Quo Vaids?', 0, 140, 100) ctx.font = 'italic 50px pfennigFont' ctx.fillText('Quo Vaids?', 0, 210) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index e21a26faa..9fbefb954 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -1844,6 +1844,23 @@ NAN_METHOD(Context2d::Stroke) { context->stroke(true); } +/* + * Helper for fillText/strokeText + */ + +double +get_text_scale(Context2d *context, char *str, double maxWidth) { + PangoLayout *layout = context->layout(); + PangoRectangle ink_rect, logical_rect; + pango_layout_get_pixel_extents(layout, &ink_rect, &logical_rect); + + if (logical_rect.width > maxWidth) { + return maxWidth / logical_rect.width; + } else { + return 1.0; + } +} + /* * Fill text at (x, y). */ @@ -1855,9 +1872,15 @@ NAN_METHOD(Context2d::FillText) { String::Utf8Value str(info[0]->ToString()); double x = info[1]->NumberValue(); double y = info[2]->NumberValue(); + double scaled_by = 1; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); + if (info[3]->IsNumber()) { + scaled_by = get_text_scale(context, *str, info[3]->NumberValue()); + cairo_scale(context->context(), scaled_by, 1); + } + context->savePath(); if (context->state->textDrawingMode == TEXT_DRAW_GLYPHS) { context->fill(); @@ -1867,6 +1890,8 @@ NAN_METHOD(Context2d::FillText) { context->fill(); } context->restorePath(); + + cairo_scale(context->context(), 1 / scaled_by, 1); } /* @@ -1880,9 +1905,15 @@ NAN_METHOD(Context2d::StrokeText) { String::Utf8Value str(info[0]->ToString()); double x = info[1]->NumberValue(); double y = info[2]->NumberValue(); + double scaled_by = 1; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); + if (info[3]->IsNumber()) { + scaled_by = get_text_scale(context, *str, info[3]->NumberValue()); + cairo_scale(context->context(), scaled_by, 1); + } + context->savePath(); if (context->state->textDrawingMode == TEXT_DRAW_GLYPHS) { context->stroke(); @@ -1892,6 +1923,8 @@ NAN_METHOD(Context2d::StrokeText) { context->stroke(); } context->restorePath(); + + cairo_scale(context->context(), 1 / scaled_by, 1); } /* diff --git a/test/public/tests.js b/test/public/tests.js index 2dc7de003..e5f107dad 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -745,6 +745,17 @@ tests['fillText() transformations'] = function (ctx) { ctx.fillText('bar', 50, 100) } +tests['fillText() maxWidth argument'] = function (ctx) { + ctx.font = 'Helvetica, sans' + ctx.fillText('Drawing text can be fun!', 0, 20) + + for (var i = 1; i < 6; i++) { + ctx.fillText('Drawing text can be fun!', 0, 20 * (7 - i), i * 20) + } + + ctx.fillText('Drawing text can be fun!', 0, 20 * 7) +} + tests['strokeText()'] = function (ctx) { ctx.strokeStyle = '#666' ctx.strokeRect(0, 0, 200, 200) @@ -762,6 +773,17 @@ tests['strokeText()'] = function (ctx) { ctx.strokeText('bar', 100, 100) } +tests['strokeText() maxWidth argument'] = function (ctx) { + ctx.font = 'Helvetica, sans' + ctx.strokeText('Drawing text can be fun!', 0, 20) + + for (var i = 1; i < 6; i++) { + ctx.strokeText('Drawing text can be fun!', 0, 20 * (7 - i), i * 20) + } + + ctx.strokeText('Drawing text can be fun!', 0, 20 * 7) +} + tests['textAlign right'] = function (ctx) { ctx.strokeStyle = '#666' ctx.strokeRect(0, 0, 200, 200) From c47aa3def92f26f6be699be024f474fee2a6cf21 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sun, 27 Aug 2017 14:04:20 -0700 Subject: [PATCH 050/474] Export more functions and properties See https://github.com/Automattic/node-canvas/pull/929#issuecomment-314305548 PR: https://github.com/Automattic/node-canvas/pull/943 --- .github/ISSUE_TEMPLATE.md | 2 +- Readme.md | 20 ++++--- benchmarks/run.js | 6 +- examples/backends.js | 2 +- examples/clock.js | 2 +- examples/crop.js | 5 +- examples/fill-evenodd.js | 2 +- examples/font.js | 2 +- examples/globalAlpha.js | 2 +- examples/gradients.js | 2 +- examples/grayscale-image.js | 2 +- examples/image-src.js | 2 +- examples/indexed-png-alpha.js | 2 +- examples/indexed-png-image-data.js | 2 +- examples/kraken.js | 2 +- examples/live-clock.js | 2 +- examples/multi-page-pdf.js | 2 +- examples/pango-glyphs.js | 2 +- examples/pdf-images.js | 2 +- examples/ray.js | 2 +- examples/resize.js | 2 +- examples/small-pdf.js | 2 +- examples/small-svg.js | 2 +- examples/spark.js | 2 +- examples/state.js | 2 +- examples/text.js | 2 +- examples/voronoi.js | 2 +- index.js | 64 +++++++++++++++++--- lib/canvas.js | 94 +++--------------------------- lib/context2d.js | 18 ++---- lib/image.js | 4 +- 31 files changed, 113 insertions(+), 146 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 1034f6461..83f9fa52f 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -15,7 +15,7 @@ Still having problems, found a bug or want a feature? Fill out the form below. ```js var Canvas = require('canvas'); -var canvas = new Canvas(200, 200); +var canvas = Canvas.createCanvas(200, 200); var ctx = canvas.getContext('2d'); // etc. ``` diff --git a/Readme.md b/Readme.md index ac83074b9..c0285d048 100644 --- a/Readme.md +++ b/Readme.md @@ -84,6 +84,7 @@ loadImage('examples/images/lime-cat.jpg').then((image) => { node-canvas adds `Image#src=Buffer` support, allowing you to read images from disc, redis, etc and apply them via `ctx.drawImage()`. Below we draw scaled down squid png by reading it from the disk with node's I/O. ```javascript +const { Image } = require('canvas'); fs.readFile(__dirname + '/images/squid.png', function(err, squid){ if (err) throw err; img = new Image; @@ -95,6 +96,7 @@ fs.readFile(__dirname + '/images/squid.png', function(err, squid){ Below is an example of a canvas drawing it-self as the source several time: ```javascript +const { Image } = require('canvas'); var img = new Image; img.src = canvas.toBuffer(); ctx.drawImage(img, 0, 0, 50, 50); @@ -109,7 +111,8 @@ node-canvas adds `Image#dataMode` support, which can be used to opt-in to mime d 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; +const { Image } = require('canvas'); +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 @@ -214,18 +217,19 @@ canvas.toDataURL('image/jpeg', {opts...}, function(err, jpeg){ }); // see Canvas canvas.toDataURL('image/jpeg', quality, function(err, jpeg){ }); // spec-following; quality from 0 to 1 ``` -### Canvas.registerFont for bundled fonts +### `registerFont` for bundled fonts It can be useful to use a custom font file if you are distributing code that uses node-canvas and a specific font. Or perhaps you are using it to do automated tests and you want the renderings to be the same across operating systems regardless of what fonts are installed. -To do that, you should use `Canvas.registerFont`. +To do that, you should use `registerFont()`. **You need to call it before the Canvas is created** ```javascript -Canvas.registerFont('comicsans.ttf', {family: 'Comic Sans'}); +const { registerFont, createCanvas } = require('canvas'); +registerFont('comicsans.ttf', {family: 'Comic Sans'}); -var canvas = new Canvas(500, 500), +var canvas = createCanvas(500, 500), ctx = canvas.getContext('2d'); ctx.font = '12px "Comic Sans"'; @@ -297,7 +301,7 @@ ctx.antialias = 'none'; a PDF on initialization, using the "pdf" string: ```js -var canvas = new Canvas(200, 500, 'pdf'); +var canvas = createCanvas(200, 500, 'pdf'); ``` An additional method `.addPage()` is then available to create @@ -323,7 +327,7 @@ ctx.addPage(); You also need to tell node-canvas that it is working on SVG upon its initialization: ```js -var canvas = new Canvas(200, 500, 'svg'); +var canvas = createCanvas(200, 500, 'svg'); // Use the normal primitives. fs.writeFile('out.svg', canvas.toBuffer()); ``` @@ -344,7 +348,7 @@ node-canvas has experimental support for additional pixel formats, roughly following the [Canvas color space proposal](https://github.com/WICG/canvas-color-space/blob/master/CanvasColorSpaceProposal.md). ```js -var canvas = new Canvas(200, 200); +var canvas = createCanvas(200, 200); var ctx = canvas.getContext('2d', {pixelFormat: 'A8'}); ``` diff --git a/benchmarks/run.js b/benchmarks/run.js index 6a3f06154..531055524 100644 --- a/benchmarks/run.js +++ b/benchmarks/run.js @@ -4,9 +4,9 @@ * milliseconds to complete. */ -var Canvas = require('../') -var canvas = new Canvas(200, 200) -var largeCanvas = new Canvas(1000, 1000) +var createCanvas = require('../').createCanvas +var canvas = createCanvas(200, 200) +var largeCanvas = createCanvas(1000, 1000) var ctx = canvas.getContext('2d') var initialTimes = 10 diff --git a/examples/backends.js b/examples/backends.js index 668efbe25..eacc39431 100644 --- a/examples/backends.js +++ b/examples/backends.js @@ -5,7 +5,7 @@ var Canvas = require('..') var imagebackend = new Canvas.backends.ImageBackend(800, 600) -var canvas = new Canvas(imagebackend) +var canvas = new Canvas.Canvas(imagebackend) var ctx = canvas.getContext('2d') console.log('Width: ' + canvas.width + ', Height: ' + canvas.height) diff --git a/examples/clock.js b/examples/clock.js index 3d847a710..abe3915f2 100644 --- a/examples/clock.js +++ b/examples/clock.js @@ -104,7 +104,7 @@ function clock (ctx) { module.exports = clock if (require.main === module) { - var canvas = new Canvas(320, 320) + var canvas = Canvas.createCanvas(320, 320) var ctx = canvas.getContext('2d') clock(ctx) diff --git a/examples/crop.js b/examples/crop.js index 86fe1c7ba..5d14354a8 100644 --- a/examples/crop.js +++ b/examples/crop.js @@ -1,8 +1,7 @@ var fs = require('fs') var path = require('path') -var Canvas = require('..') +var {createCanvas, Image} = require('..') -var Image = Canvas.Image var img = new Image() img.onerror = function (err) { @@ -12,7 +11,7 @@ img.onerror = function (err) { img.onload = function () { var w = img.width / 2 var h = img.height / 2 - var canvas = new Canvas(w, h) + var canvas = createCanvas(w, h) var ctx = canvas.getContext('2d') ctx.drawImage(img, 0, 0, w, h, 0, 0, w, h) diff --git a/examples/fill-evenodd.js b/examples/fill-evenodd.js index f71f3aef0..12e0d4ea8 100644 --- a/examples/fill-evenodd.js +++ b/examples/fill-evenodd.js @@ -2,7 +2,7 @@ var fs = require('fs') var path = require('path') var Canvas = require('..') -var canvas = new Canvas(100, 100) +var canvas = Canvas.createCanvas(100, 100) var ctx = canvas.getContext('2d') ctx.fillStyle = '#f00' diff --git a/examples/font.js b/examples/font.js index 3a69624d7..8c4bdfb21 100644 --- a/examples/font.js +++ b/examples/font.js @@ -15,7 +15,7 @@ Canvas.registerFont(fontFile('PfennigBold.ttf'), {family: 'pfennigFont', weight: Canvas.registerFont(fontFile('PfennigItalic.ttf'), {family: 'pfennigFont', style: 'italic'}) Canvas.registerFont(fontFile('PfennigBoldItalic.ttf'), {family: 'pfennigFont', weight: 'bold', style: 'italic'}) -var canvas = new Canvas(320, 320) +var canvas = Canvas.createCanvas(320, 320) var ctx = canvas.getContext('2d') ctx.font = 'normal normal 50px Helvetica' diff --git a/examples/globalAlpha.js b/examples/globalAlpha.js index 2af849ebf..523af3b3d 100644 --- a/examples/globalAlpha.js +++ b/examples/globalAlpha.js @@ -2,7 +2,7 @@ var fs = require('fs') var path = require('path') var Canvas = require('..') -var canvas = new Canvas(150, 150) +var canvas = Canvas.createCanvas(150, 150) var ctx = canvas.getContext('2d') ctx.fillStyle = '#FD0' diff --git a/examples/gradients.js b/examples/gradients.js index 3a6ff8116..5cdfddfb2 100644 --- a/examples/gradients.js +++ b/examples/gradients.js @@ -2,7 +2,7 @@ var fs = require('fs') var path = require('path') var Canvas = require('..') -var canvas = new Canvas(320, 320) +var canvas = Canvas.createCanvas(320, 320) var ctx = canvas.getContext('2d') // Create gradients diff --git a/examples/grayscale-image.js b/examples/grayscale-image.js index 1ee12b153..ef6bac719 100644 --- a/examples/grayscale-image.js +++ b/examples/grayscale-image.js @@ -3,7 +3,7 @@ var path = require('path') var Canvas = require('..') var Image = Canvas.Image -var canvas = new Canvas(288, 288) +var canvas = Canvas.createCanvas(288, 288) var ctx = canvas.getContext('2d') var img = new Image() diff --git a/examples/image-src.js b/examples/image-src.js index 51ed4b351..8d10aec58 100644 --- a/examples/image-src.js +++ b/examples/image-src.js @@ -3,7 +3,7 @@ var path = require(path) var Canvas = require('..') var Image = Canvas.Image -var canvas = new Canvas(200, 200) +var canvas = Canvas.createCanvas(200, 200) var ctx = canvas.getContext('2d') ctx.fillRect(0, 0, 150, 150) diff --git a/examples/indexed-png-alpha.js b/examples/indexed-png-alpha.js index 86ff31010..63b062d81 100644 --- a/examples/indexed-png-alpha.js +++ b/examples/indexed-png-alpha.js @@ -1,7 +1,7 @@ var Canvas = require('..') var fs = require('fs') var path = require('path') -var canvas = new Canvas(200, 200) +var canvas = Canvas.createCanvas(200, 200) var ctx = canvas.getContext('2d', {pixelFormat: 'A8'}) // Matches the "fillStyle" browser test, made by using alpha fillStyle value diff --git a/examples/indexed-png-image-data.js b/examples/indexed-png-image-data.js index d675f1b1b..3481ac99d 100644 --- a/examples/indexed-png-image-data.js +++ b/examples/indexed-png-image-data.js @@ -1,7 +1,7 @@ var Canvas = require('..') var fs = require('fs') var path = require('path') -var canvas = new Canvas(200, 200) +var canvas = Canvas.createCanvas(200, 200) var ctx = canvas.getContext('2d', {pixelFormat: 'A8'}) // Matches the "fillStyle" browser test, made by manipulating imageData diff --git a/examples/kraken.js b/examples/kraken.js index 21d278c57..7dca25b8c 100644 --- a/examples/kraken.js +++ b/examples/kraken.js @@ -3,7 +3,7 @@ var path = require('path') var Canvas = require('..') var Image = Canvas.Image -var canvas = new Canvas(400, 267) +var canvas = Canvas.createCanvas(400, 267) var ctx = canvas.getContext('2d') var img = new Image() diff --git a/examples/live-clock.js b/examples/live-clock.js index 08bf25526..9d3a643f3 100644 --- a/examples/live-clock.js +++ b/examples/live-clock.js @@ -3,7 +3,7 @@ var Canvas = require('..') var clock = require('./clock') -var canvas = new Canvas(320, 320) +var canvas = Canvas.createCanvas(320, 320) var ctx = canvas.getContext('2d') http.createServer(function (req, res) { diff --git a/examples/multi-page-pdf.js b/examples/multi-page-pdf.js index cd27f2f1d..5470843f7 100644 --- a/examples/multi-page-pdf.js +++ b/examples/multi-page-pdf.js @@ -1,7 +1,7 @@ var fs = require('fs') var Canvas = require('..') -var canvas = new Canvas(500, 500, 'pdf') +var canvas = Canvas.createCanvas(500, 500, 'pdf') var ctx = canvas.getContext('2d') var x, y diff --git a/examples/pango-glyphs.js b/examples/pango-glyphs.js index 695fe1731..4047715ff 100644 --- a/examples/pango-glyphs.js +++ b/examples/pango-glyphs.js @@ -2,7 +2,7 @@ var fs = require('fs') var path = require('path') var Canvas = require('..') -var canvas = new Canvas(400, 100) +var canvas = Canvas.createCanvas(400, 100) var ctx = canvas.getContext('2d') ctx.globalAlpha = 1 diff --git a/examples/pdf-images.js b/examples/pdf-images.js index 69e59e3e4..d19a7e9c7 100644 --- a/examples/pdf-images.js +++ b/examples/pdf-images.js @@ -2,7 +2,7 @@ var fs = require('fs') var Canvas = require('..') var Image = Canvas.Image -var canvas = new Canvas(500, 500, 'pdf') +var canvas = Canvas.createCanvas(500, 500, 'pdf') var ctx = canvas.getContext('2d') var x, y diff --git a/examples/ray.js b/examples/ray.js index a191034f2..93fd80184 100644 --- a/examples/ray.js +++ b/examples/ray.js @@ -2,7 +2,7 @@ var fs = require('fs') var path = require('path') var Canvas = require('..') -var canvas = new Canvas(243 * 4, 243) +var canvas = Canvas.createCanvas(243 * 4, 243) var ctx = canvas.getContext('2d') function render (level) { diff --git a/examples/resize.js b/examples/resize.js index 9703b3240..151630ed5 100644 --- a/examples/resize.js +++ b/examples/resize.js @@ -14,7 +14,7 @@ img.onerror = function (err) { img.onload = function () { var width = 100 var height = 100 - var canvas = new Canvas(width, height) + var canvas = Canvas.createCanvas(width, height) var ctx = canvas.getContext('2d') var out = fs.createWriteStream(path.join(__dirname, 'resize.png')) diff --git a/examples/small-pdf.js b/examples/small-pdf.js index 94d25d7bd..f7a405309 100644 --- a/examples/small-pdf.js +++ b/examples/small-pdf.js @@ -1,7 +1,7 @@ var fs = require('fs') var Canvas = require('..') -var canvas = new Canvas(400, 200, 'pdf') +var canvas = Canvas.createCanvas(400, 200, 'pdf') var ctx = canvas.getContext('2d') var y = 80 diff --git a/examples/small-svg.js b/examples/small-svg.js index 992830c31..099ff6925 100644 --- a/examples/small-svg.js +++ b/examples/small-svg.js @@ -1,7 +1,7 @@ var fs = require('fs') var Canvas = require('..') -var canvas = new Canvas(400, 200, 'svg') +var canvas = Canvas.createCanvas(400, 200, 'svg') var ctx = canvas.getContext('2d') var y = 80 diff --git a/examples/spark.js b/examples/spark.js index c6aafa960..9e99c9fb6 100644 --- a/examples/spark.js +++ b/examples/spark.js @@ -2,7 +2,7 @@ var fs = require('fs') var path = require('path') var Canvas = require('..') -var canvas = new Canvas(40, 15) +var canvas = Canvas.createCanvas(40, 15) var ctx = canvas.getContext('2d') function spark (ctx, data) { diff --git a/examples/state.js b/examples/state.js index a8518382e..c3302f4cf 100644 --- a/examples/state.js +++ b/examples/state.js @@ -2,7 +2,7 @@ var fs = require('fs') var path = require('path') var Canvas = require('..') -var canvas = new Canvas(150, 150) +var canvas = Canvas.createCanvas(150, 150) var ctx = canvas.getContext('2d') ctx.fillRect(0, 0, 150, 150) // Draw a rectangle with default settings diff --git a/examples/text.js b/examples/text.js index 112741db4..39ddc544a 100644 --- a/examples/text.js +++ b/examples/text.js @@ -2,7 +2,7 @@ var fs = require('fs') var path = require('path') var Canvas = require('..') -var canvas = new Canvas(200, 200) +var canvas = Canvas.createCanvas(200, 200) var ctx = canvas.getContext('2d') ctx.globalAlpha = 0.2 diff --git a/examples/voronoi.js b/examples/voronoi.js index 567d02b8a..963c9922a 100644 --- a/examples/voronoi.js +++ b/examples/voronoi.js @@ -1,7 +1,7 @@ var http = require('http') var Canvas = require('..') -var canvas = new Canvas(1920, 1200) +var canvas = Canvas.createCanvas(1920, 1200) var ctx = canvas.getContext('2d') var voronoiFactory = require('./rhill-voronoi-core-min') diff --git a/index.js b/index.js index 18fb76452..b67669016 100644 --- a/index.js +++ b/index.js @@ -1,19 +1,25 @@ const Canvas = require('./lib/canvas') +const Image = require('./lib/image') +const CanvasRenderingContext2D = require('./lib/context2d') const parseFont = require('./lib/parse-font') +const packageJson = require('./package.json') +const bindings = require('./lib/bindings') +const fs = require('fs') +const PNGStream = require('./lib/pngstream') +const PDFStream = require('./lib/pdfstream') +const JPEGStream = require('./lib/jpegstream') -exports.parseFont = parseFont - -exports.createCanvas = function (width, height, type) { +function createCanvas(width, height, type) { return new Canvas(width, height, type) } -exports.createImageData = function (array, width, height) { - return new Canvas.ImageData(array, width, height) +function createImageData(array, width, height) { + return new bindings.ImageData(array, width, height) } -exports.loadImage = function (src) { +function loadImage(src) { return new Promise((resolve, reject) => { - const image = new Canvas.Image() + const image = new Image() function cleanup () { image.onload = null @@ -26,3 +32,47 @@ exports.loadImage = function (src) { image.src = src }) } + +/** + * Resolve paths for registerFont. Must be called *before* creating a Canvas + * instance. + * @param src {string} Path to font file. + * @param fontFace {{family: string, weight?: string, style?: string}} Object + * specifying font information. `weight` and `style` default to `"normal"`. + */ +function registerFont(src, fontFace){ + return bindings._registerFont(fs.realpathSync(src), fontFace) +} + +module.exports = { + Canvas, + Context2d: CanvasRenderingContext2D, // Legacy/compat export + CanvasRenderingContext2D, + CanvasPattern: bindings.CanvasPattern, + Image, + ImageData: bindings.ImageData, + PNGStream, + PDFStream, + JPEGStream, + + registerFont, + parseFont, + + createCanvas, + createImageData, + loadImage, + + backends: bindings.Backends, + + /** Library version. */ + version: packageJson.version, + /** Cairo version. */ + cairoVersion: bindings.cairoVersion, + /** jpeglib version. */ + jpegVersion: bindings.jpegVersion, + /** gif_lib version. */ + gifVersion: bindings.gifVersion ? + bindings.gifVersion.replace(/[^.\d]/g, '') : undefined, + /** freetype version. */ + freetypeVersion: bindings.freetypeVersion, +} diff --git a/lib/canvas.js b/lib/canvas.js index 30a0c59e6..4cc567afe 100644 --- a/lib/canvas.js +++ b/lib/canvas.js @@ -10,93 +10,13 @@ * Module dependencies. */ -var canvas = require('./bindings') - , Backends = canvas.Backends - , Canvas = canvas.Canvas - , Image = canvas.Image - , cairoVersion = canvas.cairoVersion - , Context2d = require('./context2d') - , PNGStream = require('./pngstream') - , PDFStream = require('./pdfstream') - , JPEGStream = require('./jpegstream') - , fs = require('fs') - , packageJson = require("../package.json") - , FORMATS = ['image/png', 'image/jpeg']; - -/** - * Export `Canvas` as the module. - */ - -var Canvas = exports = module.exports = Canvas; - -exports.backends = Backends; - -/** - * Library version. - */ - -exports.version = packageJson.version; - -/** - * Cairo version. - */ - -exports.cairoVersion = cairoVersion; - -/** - * jpeglib version. - */ - -if (canvas.jpegVersion) { - exports.jpegVersion = canvas.jpegVersion; -} - -/** - * gif_lib version. - */ - -if (canvas.gifVersion) { - exports.gifVersion = canvas.gifVersion.replace(/[^.\d]/g, ''); -} - -/** - * freetype version. - */ - -if (canvas.freetypeVersion) { - exports.freetypeVersion = canvas.freetypeVersion; -} - -/** - * Expose constructors. - */ - -exports.Context2d = Context2d; -exports.PNGStream = PNGStream; -exports.PDFStream = PDFStream; -exports.JPEGStream = JPEGStream; -exports.Image = Image; -exports.ImageData = canvas.ImageData; - -/** - * Resolve paths for registerFont - */ - -Canvas.registerFont = function(src, fontFace){ - return Canvas._registerFont(fs.realpathSync(src), fontFace); -}; - -/** - * Context2d implementation. - */ - -require('./context2d'); - -/** - * Image implementation. - */ - -require('./image'); +const bindings = require('./bindings') +const Canvas = module.exports = bindings.Canvas +const Context2d = require('./context2d') +const PNGStream = require('./pngstream') +const PDFStream = require('./pdfstream') +const JPEGStream = require('./jpegstream') +const FORMATS = ['image/png', 'image/jpeg'] /** * Inspect canvas. diff --git a/lib/context2d.js b/lib/context2d.js index 8c9ac74e7..f26e06c2a 100644 --- a/lib/context2d.js +++ b/lib/context2d.js @@ -10,18 +10,12 @@ * Module dependencies. */ -var canvas = require('./bindings') - , parseFont = require('./parse-font') - , Context2d = canvas.CanvasRenderingContext2d - , CanvasGradient = canvas.CanvasGradient - , CanvasPattern = canvas.CanvasPattern - , ImageData = canvas.ImageData; - -/** - * Export `Context2d` as the module. - */ - -var Context2d = exports = module.exports = Context2d; +const bindings = require('./bindings') +const parseFont = require('./parse-font') +const Context2d = module.exports = bindings.CanvasRenderingContext2d +const CanvasGradient = bindings.CanvasGradient +const CanvasPattern = bindings.CanvasPattern +const ImageData = bindings.ImageData /** * Text baselines. diff --git a/lib/image.js b/lib/image.js index 932ad9012..33a796b82 100644 --- a/lib/image.js +++ b/lib/image.js @@ -10,8 +10,8 @@ * Module dependencies. */ -var Canvas = require('./bindings') - , Image = Canvas.Image; +const bindings = require('./bindings') +const Image = module.exports = bindings.Image /** * Src setter. From ef28969b2b82ef438c9d267f79925141472a6a38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Mon, 28 Aug 2017 11:29:15 +0200 Subject: [PATCH 051/474] 2.0.0-alpha.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ade5f0d3c..52387b6cf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.0.0-alpha.2", + "version": "2.0.0-alpha.3", "author": "TJ Holowaychuk ", "browser": "browser.js", "contributors": [ From 6bc467d95c93279ee00eb14ccb22a018dfd85719 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Tue, 29 Aug 2017 10:21:34 -0700 Subject: [PATCH 052/474] Build with node.js v8 in CI --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index eb0fcc120..747f4c97b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: node_js node_js: - - '7' + - '8' - '6' - '4' addons: From a27bcbc8ac1184904c885ab62073b36b927be3a4 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Tue, 29 Aug 2017 10:17:55 -0700 Subject: [PATCH 053/474] Fix registerFont Fixes #974 --- index.js | 4 +++- test/canvas.test.js | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index b67669016..738d8cb4e 100644 --- a/index.js +++ b/index.js @@ -41,7 +41,9 @@ function loadImage(src) { * specifying font information. `weight` and `style` default to `"normal"`. */ function registerFont(src, fontFace){ - return bindings._registerFont(fs.realpathSync(src), fontFace) + // TODO this doesn't need to be on Canvas; it should just be a static method + // of `bindings`. + return Canvas._registerFont(fs.realpathSync(src), fontFace) } module.exports = { diff --git a/test/canvas.test.js b/test/canvas.test.js index d0e7efd17..daf4f2fa0 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -9,6 +9,7 @@ const createCanvas = require('../').createCanvas const loadImage = require('../').loadImage const parseFont = require('../').parseFont +const registerFont = require('../').registerFont const assert = require('assert') const os = require('os') @@ -83,6 +84,12 @@ describe('Canvas', function () { } }); + it('registerFont', function () { + // Minimal test to make sure nothing is thrown + registerFont('./examples/pfennigFont/Pfennig.ttf', {family: 'Pfennig'}) + registerFont('./examples/pfennigFont/PfennigBold.ttf', {family: 'Pfennig', weight: 'bold'}) + }); + it('color serialization', function () { var canvas = createCanvas(200, 200) , ctx = canvas.getContext('2d'); From 0bf10cb1c1dd044c99fe0b3f334b85c801af1e0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Tue, 29 Aug 2017 21:36:17 +0200 Subject: [PATCH 054/474] Lint browser.js and index.js with standard --- index.js | 13 ++++++------- package.json | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index 738d8cb4e..00eb3979a 100644 --- a/index.js +++ b/index.js @@ -9,15 +9,15 @@ const PNGStream = require('./lib/pngstream') const PDFStream = require('./lib/pdfstream') const JPEGStream = require('./lib/jpegstream') -function createCanvas(width, height, type) { +function createCanvas (width, height, type) { return new Canvas(width, height, type) } -function createImageData(array, width, height) { +function createImageData (array, width, height) { return new bindings.ImageData(array, width, height) } -function loadImage(src) { +function loadImage (src) { return new Promise((resolve, reject) => { const image = new Image() @@ -40,7 +40,7 @@ function loadImage(src) { * @param fontFace {{family: string, weight?: string, style?: string}} Object * specifying font information. `weight` and `style` default to `"normal"`. */ -function registerFont(src, fontFace){ +function registerFont (src, fontFace) { // TODO this doesn't need to be on Canvas; it should just be a static method // of `bindings`. return Canvas._registerFont(fs.realpathSync(src), fontFace) @@ -73,8 +73,7 @@ module.exports = { /** jpeglib version. */ jpegVersion: bindings.jpegVersion, /** gif_lib version. */ - gifVersion: bindings.gifVersion ? - bindings.gifVersion.replace(/[^.\d]/g, '') : undefined, + gifVersion: bindings.gifVersion ? bindings.gifVersion.replace(/[^.\d]/g, '') : undefined, /** freetype version. */ - freetypeVersion: bindings.freetypeVersion, + freetypeVersion: bindings.freetypeVersion } diff --git a/package.json b/package.json index 52387b6cf..fbb8610b8 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "prebenchmark": "node-gyp build", "benchmark": "node benchmarks/run.js", "pretest": "node-gyp build", - "test": "standard examples/*.js test/server.js test/public/*.js benchmark/run.js util/has_lib.js && mocha test/*.test.js", + "test": "standard examples/*.js test/server.js test/public/*.js benchmark/run.js util/has_lib.js browser.js index.js && mocha test/*.test.js", "pretest-server": "node-gyp build", "test-server": "node test/server.js" }, From 3370e6471276fc0236a8dea50ecea7ec42d65654 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Tue, 29 Aug 2017 21:36:34 +0200 Subject: [PATCH 055/474] 2.0.0-alpha.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fbb8610b8..b06ea5ace 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.0.0-alpha.3", + "version": "2.0.0-alpha.4", "author": "TJ Holowaychuk ", "browser": "browser.js", "contributors": [ From bc5305967ed1425b6e8924ceb655e7f04faca497 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Wed, 30 Aug 2017 16:23:20 -0700 Subject: [PATCH 056/474] Remove sync streams --- Readme.md | 2 -- lib/canvas.js | 42 ------------------------------------------ lib/jpegstream.js | 13 ++----------- lib/pdfstream.js | 14 ++------------ lib/pngstream.js | 13 ++----------- test/canvas.test.js | 8 ++++---- 6 files changed, 10 insertions(+), 82 deletions(-) diff --git a/Readme.md b/Readme.md index c0285d048..66a649316 100644 --- a/Readme.md +++ b/Readme.md @@ -138,8 +138,6 @@ stream.on('end', function(){ }); ``` -Currently _only_ sync streaming is supported, however we plan on supporting async streaming as well (of course :) ). Until then the `Canvas#toBuffer(callback)` alternative is async utilizing `eio_custom()`. - To encode indexed PNGs from canvases with `pixelFormat: 'A8'` or `'A1'`, provide an options object: ```js diff --git a/lib/canvas.js b/lib/canvas.js index 4cc567afe..918a33f40 100644 --- a/lib/canvas.js +++ b/lib/canvas.js @@ -64,23 +64,6 @@ Canvas.prototype.createPNGStream = function(options){ return new PNGStream(this, false, options); }; -/** - * Create a synchronous `PNGStream` for `this` canvas. - * - * @param {Object} options - * @param {Uint8ClampedArray} options.palette Provide for indexed PNG encoding. - * entries should be R-G-B-A values. - * @param {Number} options.backgroundIndex Optional index of background color - * for indexed PNGs. Defaults to 0. - * @return {PNGStream} - * @api public - */ - -Canvas.prototype.syncPNGStream = -Canvas.prototype.createSyncPNGStream = function(options){ - return new PNGStream(this, true, options); -}; - /** * Create a `PDFStream` for `this` canvas. * @@ -93,18 +76,6 @@ Canvas.prototype.createPDFStream = function(){ return new PDFStream(this); }; -/** - * Create a synchronous `PDFStream` for `this` canvas. - * - * @return {PDFStream} - * @api public - */ - -Canvas.prototype.syncPDFStream = -Canvas.prototype.createSyncPDFStream = function(){ - return new PDFStream(this, true); -}; - /** * Create a `JPEGStream` for `this` canvas. * @@ -115,19 +86,6 @@ Canvas.prototype.createSyncPDFStream = function(){ Canvas.prototype.jpegStream = Canvas.prototype.createJPEGStream = function(options){ - return this.createSyncJPEGStream(options); -}; - -/** - * Create a synchronous `JPEGStream` for `this` canvas. - * - * @param {Object} options - * @return {JPEGStream} - * @api public - */ - -Canvas.prototype.syncJPEGStream = -Canvas.prototype.createSyncJPEGStream = function(options){ options = options || {}; // Don't allow the buffer size to exceed the size of the canvas (#674) var maxBufSize = this.width * this.height * 4; diff --git a/lib/jpegstream.js b/lib/jpegstream.js index 5705a9aa3..efae2f776 100644 --- a/lib/jpegstream.js +++ b/lib/jpegstream.js @@ -26,11 +26,10 @@ var util = require('util'); * stream.pipe(out); * * @param {Canvas} canvas - * @param {Boolean} sync * @api public */ -var JPEGStream = module.exports = function JPEGStream(canvas, options, sync) { +var JPEGStream = module.exports = function JPEGStream(canvas, options) { if (!(this instanceof JPEGStream)) { throw new TypeError("Class constructors cannot be invoked without 'new'"); } @@ -38,16 +37,8 @@ var JPEGStream = module.exports = function JPEGStream(canvas, options, sync) { Readable.call(this); var self = this; - var method = sync - ? 'streamJPEGSync' - : 'streamJPEG'; this.options = options; - this.sync = sync; this.canvas = canvas; - - // TODO: implement async - if ('streamJPEG' == method) method = 'streamJPEGSync'; - this.method = method; }; util.inherits(JPEGStream, Readable); @@ -63,7 +54,7 @@ JPEGStream.prototype._read = function _read() { var bufsize = this.options.bufsize; var quality = this.options.quality; var progressive = this.options.progressive; - self.canvas[method](bufsize, quality, progressive, function(err, chunk){ + self.canvas.streamJPEGSync(bufsize, quality, progressive, function(err, chunk){ if (err) { self.emit('error', err); } else if (chunk) { diff --git a/lib/pdfstream.js b/lib/pdfstream.js index 084ad3a22..837a034c3 100644 --- a/lib/pdfstream.js +++ b/lib/pdfstream.js @@ -24,27 +24,17 @@ var util = require('util'); * stream.pipe(out); * * @param {Canvas} canvas - * @param {Boolean} sync * @api public */ -var PDFStream = module.exports = function PDFStream(canvas, sync) { +var PDFStream = module.exports = function PDFStream(canvas) { if (!(this instanceof PDFStream)) { throw new TypeError("Class constructors cannot be invoked without 'new'"); } Readable.call(this); - var self = this - , method = sync - ? 'streamPDFSync' - : 'streamPDF'; - this.sync = sync; this.canvas = canvas; - - // TODO: implement async - if ('streamPDF' == method) method = 'streamPDFSync'; - this.method = method; }; util.inherits(PDFStream, Readable); @@ -56,7 +46,7 @@ PDFStream.prototype._read = function _read() { // call canvas.streamPDFSync once and let it emit data at will. this._read = noop; var self = this; - self.canvas[self.method](function(err, chunk, len){ + self.canvas.streamPDFSync(function(err, chunk, len){ if (err) { self.emit('error', err); } else if (len) { diff --git a/lib/pngstream.js b/lib/pngstream.js index fb1469185..a774d97ac 100644 --- a/lib/pngstream.js +++ b/lib/pngstream.js @@ -26,7 +26,6 @@ var util = require('util'); * stream.pipe(out); * * @param {Canvas} canvas - * @param {Boolean} sync * @param {Object} options * @param {Uint8ClampedArray} options.palette Provide for indexed PNG encoding. * entries should be R-G-B-A values. @@ -35,7 +34,7 @@ var util = require('util'); * @api public */ -var PNGStream = module.exports = function PNGStream(canvas, sync, options) { +var PNGStream = module.exports = function PNGStream(canvas, options) { if (!(this instanceof PNGStream)) { throw new TypeError("Class constructors cannot be invoked without 'new'"); } @@ -43,16 +42,8 @@ var PNGStream = module.exports = function PNGStream(canvas, sync, options) { Readable.call(this); var self = this; - var method = sync - ? 'streamPNGSync' - : 'streamPNG'; - this.sync = sync; this.canvas = canvas; this.options = options || {}; - - // TODO: implement async - if ('streamPNG' === method) method = 'streamPNGSync'; - this.method = method; }; util.inherits(PNGStream, Readable); @@ -64,7 +55,7 @@ PNGStream.prototype._read = function _read() { // call canvas.streamPNGSync once and let it emit data at will. this._read = noop; var self = this; - self.canvas[self.method](function(err, chunk, len){ + self.canvas.streamPNGSync(function(err, chunk, len){ if (err) { self.emit('error', err); } else if (len) { diff --git a/test/canvas.test.js b/test/canvas.test.js index daf4f2fa0..78c59e60e 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -1152,9 +1152,9 @@ describe('Canvas', function () { }); }); - it('Canvas#createSyncPNGStream()', function (done) { + it('Canvas#createPNGStream()', function (done) { var canvas = createCanvas(20, 20); - var stream = canvas.createSyncPNGStream(); + var stream = canvas.createPNGStream(); assert(stream instanceof Readable); var firstChunk = true; stream.on('data', function(chunk){ @@ -1171,9 +1171,9 @@ describe('Canvas', function () { }); }); - it('Canvas#createSyncPDFStream()', function (done) { + it('Canvas#createPDFStream()', function (done) { var canvas = createCanvas(20, 20, 'pdf'); - var stream = canvas.createSyncPDFStream(); + var stream = canvas.createPDFStream(); assert(stream instanceof Readable); var firstChunk = true; stream.on('data', function (chunk) { From 715847893d24eedbe5427b829ee394b8b20f843e Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Fri, 1 Sep 2017 13:55:39 -0400 Subject: [PATCH 057/474] macOS font selection improvement (see #977) Works around https://bugzilla.gnome.org/show_bug.cgi?id=762873 for when `ctx.font` is set to something that resolves to more than one registered font with identical SFNT names --- src/Canvas.cc | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Canvas.cc b/src/Canvas.cc index 61cf2134f..cebbaf2ee 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -742,6 +742,7 @@ Canvas::ResolveFontDescription(const PangoFontDescription *desc) { // if someone registered two different fonts under the same family name. // https://drafts.csswg.org/css-fonts-3/#font-style-matching char **families = g_strsplit(pango_font_description_get_family(desc), ",", -1); + GHashTable *seen_families = g_hash_table_new(g_str_hash, g_str_equal); GString *resolved_families = g_string_new(""); for (int i = 0; families[i]; ++i) { @@ -750,8 +751,14 @@ Canvas::ResolveFontDescription(const PangoFontDescription *desc) { for (; it != _font_face_list.end(); ++it) { if (g_ascii_strcasecmp(families[i], pango_font_description_get_family(it->user_desc)) == 0) { - if (renamed_families->len) g_string_append(renamed_families, ","); - g_string_append(renamed_families, pango_font_description_get_family(it->sys_desc)); + char *name = g_strdup(pango_font_description_get_family(it->sys_desc)); + + // Avoid sending duplicate SFNT font names due to a bug in Pango for macOS: + // https://bugzilla.gnome.org/show_bug.cgi?id=762873 + if (g_hash_table_add(seen_families, name)) { + if (renamed_families->len) g_string_append(renamed_families, ","); + g_string_append(renamed_families, name); + } if (i == 0 && (best.user_desc == NULL || pango_font_description_better_match(desc, best.user_desc, it->user_desc))) { best = *it; @@ -769,6 +776,7 @@ Canvas::ResolveFontDescription(const PangoFontDescription *desc) { g_strfreev(families); g_string_free(resolved_families, false); + g_hash_table_destroy(seen_families); return ret; } From 2102e257866993494bdeecd1ad7895f7cb33ae73 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Wed, 30 Aug 2017 17:48:07 -0700 Subject: [PATCH 058/474] Fix putImageData(data, neg, neg) Fixes #962 --- src/CanvasRenderingContext2d.cc | 23 +++++++++++------------ test/canvas.test.js | 18 ++++++++++++++++++ test/public/tests.js | 11 +++++++++++ 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 9fbefb954..ef1e5fc7e 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -610,10 +610,8 @@ NAN_METHOD(Context2d::PutImageData) { switch (info.Length()) { // imageData, dx, dy case 3: - // Need to wrap std::min calls using parens to prevent macro expansion on - // windows. See http://stackoverflow.com/questions/5004858/stdmin-gives-error - cols = (std::min)(imageData->width(), context->canvas()->getWidth() - dx); - rows = (std::min)(imageData->height(), context->canvas()->getHeight() - dy); + sw = imageData->width(); + sh = imageData->height(); break; // imageData, dx, dy, sx, sy, sw, sh case 7: @@ -633,19 +631,20 @@ NAN_METHOD(Context2d::PutImageData) { // start destination at source offset dx += sx; dy += sy; - // chop off outlying source data - if (dx < 0) sw += dx, sx -= dx, dx = 0; - if (dy < 0) sh += dy, sy -= dy, dy = 0; - // clamp width at canvas size - // Need to wrap std::min calls using parens to prevent macro expansion on - // windows. See http://stackoverflow.com/questions/5004858/stdmin-gives-error - cols = (std::min)(sw, context->canvas()->getWidth() - dx); - rows = (std::min)(sh, context->canvas()->getHeight() - dy); break; default: return Nan::ThrowError("invalid arguments"); } + // chop off outlying source data + if (dx < 0) sw += dx, sx -= dx, dx = 0; + if (dy < 0) sh += dy, sy -= dy, dy = 0; + // clamp width at canvas size + // Need to wrap std::min calls using parens to prevent macro expansion on + // windows. See http://stackoverflow.com/questions/5004858/stdmin-gives-error + cols = (std::min)(sw, context->canvas()->getWidth() - dx); + rows = (std::min)(sh, context->canvas()->getHeight() - dy); + if (cols <= 0 || rows <= 0) return; switch (context->canvas()->backend()->getFormat()) { diff --git a/test/canvas.test.js b/test/canvas.test.js index daf4f2fa0..87d77b7a6 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -7,6 +7,7 @@ */ const createCanvas = require('../').createCanvas +const createImageData = require('../').createImageData const loadImage = require('../').loadImage const parseFont = require('../').parseFont const registerFont = require('../').registerFont @@ -1087,6 +1088,23 @@ describe('Canvas', function () { assert.throws(function () { ctx.putImageData(undefined, 0, 0); }, TypeError); }); + it('works for negative source values', function () { + var canvas = createCanvas(2, 2); + var ctx = canvas.getContext('2d'); + var srcImageData = createImageData(new Uint8ClampedArray([ + 1,2,3,255, 5,6,7,255, + 0,1,2,255, 4,5,6,255 + ]), 2); + + ctx.putImageData(srcImageData, -1, -1); + + var resImageData = ctx.getImageData(0, 0, 2, 2); + assert.deepEqual(resImageData.data, new Uint8ClampedArray([ + 4,5,6,255, 0,0,0,0, + 0,0,0,0, 0,0,0,0 + ])); + }); + it('works, RGBA32', function () { var canvas = createCanvas(2, 1); var ctx = canvas.getContext('2d'); diff --git a/test/public/tests.js b/test/public/tests.js index e5f107dad..1203644b9 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -1842,6 +1842,17 @@ tests['putImageData()'] = function (ctx) { ctx.putImageData(data, 10, 10) } +tests['putImageData() 1'] = function (ctx) { + for (var i = 0; i < 6; i++) { + for (var j = 0; j < 6; j++) { + ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' + ctx.fillRect(j * 25, i * 25, 25, 25) + } + } + var data = ctx.getImageData(0, 0, 50, 50) + ctx.putImageData(data, -10, -10) +} + tests['putImageData() 2'] = function (ctx) { for (var i = 0; i < 6; i++) { for (var j = 0; j < 6; j++) { From ec4871f3963f47a821e5321886d045d8d3d72c25 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Fri, 1 Sep 2017 21:05:13 -0700 Subject: [PATCH 059/474] Fix text baselines and measureText * Fixes text baseline adjustment (fillText, strokeText, measureText) when rotation transform is active. * Fixes measureText results. Some where negative when they should have been positive and weren't adjusting for the baseline properly. Fixes #983 --- src/CanvasRenderingContext2d.cc | 67 +++++++++++------------- test/canvas.test.js | 36 ++++++++++--- test/public/tests.js | 92 +++++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 43 deletions(-) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 9fbefb954..c5b6fa5d1 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -1927,6 +1927,25 @@ NAN_METHOD(Context2d::StrokeText) { cairo_scale(context->context(), 1 / scaled_by, 1); } +/* + * Gets the baseline adjustment in device pixels, taking into account the + * transformation matrix. TODO This does not handle skew (which cannot easily + * be extracted from the matrix separately from rotation). + */ +inline double getBaselineAdjustment(PangoFontMetrics* metrics, cairo_matrix_t matrix, short baseline) { + double yScale = sqrt(matrix.yx * matrix.yx + matrix.yy * matrix.yy); + switch (baseline) { + case TEXT_BASELINE_ALPHABETIC: + return (pango_font_metrics_get_ascent(metrics) / PANGO_SCALE) * yScale; + case TEXT_BASELINE_MIDDLE: + return ((pango_font_metrics_get_ascent(metrics) + pango_font_metrics_get_descent(metrics)) / (2.0 * PANGO_SCALE)) * yScale; + case TEXT_BASELINE_BOTTOM: + return ((pango_font_metrics_get_ascent(metrics) + pango_font_metrics_get_descent(metrics)) / PANGO_SCALE) * yScale; + default: + return 0; + } +} + /* * Set text path for the given string at (x, y). */ @@ -1934,7 +1953,6 @@ NAN_METHOD(Context2d::StrokeText) { void Context2d::setTextPath(const char *str, double x, double y) { PangoRectangle ink_rect, logical_rect; - PangoFontMetrics *metrics = NULL; cairo_matrix_t matrix; pango_layout_set_text(_layout, str, -1); @@ -1955,22 +1973,9 @@ Context2d::setTextPath(const char *str, double x, double y) { break; } - switch (state->textBaseline) { - case TEXT_BASELINE_ALPHABETIC: - metrics = PANGO_LAYOUT_GET_METRICS(_layout); - y -= (pango_font_metrics_get_ascent(metrics) / PANGO_SCALE) * matrix.yy; - break; - case TEXT_BASELINE_MIDDLE: - metrics = PANGO_LAYOUT_GET_METRICS(_layout); - y -= ((pango_font_metrics_get_ascent(metrics) + pango_font_metrics_get_descent(metrics))/(2.0 * PANGO_SCALE)) * matrix.yy; - break; - case TEXT_BASELINE_BOTTOM: - metrics = PANGO_LAYOUT_GET_METRICS(_layout); - y -= ((pango_font_metrics_get_ascent(metrics) + pango_font_metrics_get_descent(metrics)) / PANGO_SCALE) * matrix.yy; - break; - } - - if (metrics) pango_font_metrics_unref(metrics); + PangoFontMetrics *metrics = PANGO_LAYOUT_GET_METRICS(_layout); + y -= getBaselineAdjustment(metrics, matrix, state->textBaseline); + pango_font_metrics_unref(metrics); cairo_move_to(_context, x, y); if (state->textDrawingMode == TEXT_DRAW_PATHS) { @@ -2091,20 +2096,9 @@ NAN_METHOD(Context2d::MeasureText) { x_offset = 0.0; } - double y_offset; - switch (context->state->textBaseline) { - case TEXT_BASELINE_ALPHABETIC: - y_offset = -pango_font_metrics_get_ascent(metrics) / PANGO_SCALE; - break; - case TEXT_BASELINE_MIDDLE: - y_offset = -(pango_font_metrics_get_ascent(metrics) + pango_font_metrics_get_descent(metrics))/(2.0 * PANGO_SCALE); - break; - case TEXT_BASELINE_BOTTOM: - y_offset = -(pango_font_metrics_get_ascent(metrics) + pango_font_metrics_get_descent(metrics)) / PANGO_SCALE; - break; - default: - y_offset = 0.0; - } + cairo_matrix_t matrix; + cairo_get_matrix(ctx, &matrix); + double y_offset = getBaselineAdjustment(metrics, matrix, context->state->textBaseline); obj->Set(Nan::New("width").ToLocalChecked(), Nan::New(logical_rect.width)); @@ -2113,16 +2107,15 @@ NAN_METHOD(Context2d::MeasureText) { obj->Set(Nan::New("actualBoundingBoxRight").ToLocalChecked(), Nan::New(x_offset + PANGO_RBEARING(logical_rect))); obj->Set(Nan::New("actualBoundingBoxAscent").ToLocalChecked(), - Nan::New(-(y_offset+ink_rect.y))); + Nan::New(y_offset + PANGO_ASCENT(ink_rect))); obj->Set(Nan::New("actualBoundingBoxDescent").ToLocalChecked(), - Nan::New((PANGO_DESCENT(ink_rect) + y_offset))); + Nan::New(PANGO_DESCENT(ink_rect) - y_offset)); obj->Set(Nan::New("emHeightAscent").ToLocalChecked(), - Nan::New(PANGO_ASCENT(logical_rect) - y_offset)); + Nan::New(-(PANGO_ASCENT(logical_rect) - y_offset))); obj->Set(Nan::New("emHeightDescent").ToLocalChecked(), - Nan::New(PANGO_DESCENT(logical_rect) + y_offset)); + Nan::New(PANGO_DESCENT(logical_rect) - y_offset)); obj->Set(Nan::New("alphabeticBaseline").ToLocalChecked(), - Nan::New((pango_font_metrics_get_ascent(metrics) / PANGO_SCALE) - + y_offset)); + Nan::New(-(pango_font_metrics_get_ascent(metrics) / PANGO_SCALE - y_offset))); pango_font_metrics_unref(metrics); diff --git a/test/canvas.test.js b/test/canvas.test.js index daf4f2fa0..6bc81004c 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -746,13 +746,37 @@ describe('Canvas', function () { }); }); - it('Context2d#measureText().width', function () { - var canvas = createCanvas(20, 20) - , ctx = canvas.getContext('2d'); + describe('Context2d#measureText()', function () { + it('Context2d#measureText().width', function () { + var canvas = createCanvas(20, 20) + , ctx = canvas.getContext('2d'); - assert.ok(ctx.measureText('foo').width); - assert.ok(ctx.measureText('foo').width != ctx.measureText('foobar').width); - assert.ok(ctx.measureText('foo').width != ctx.measureText(' foo').width); + assert.ok(ctx.measureText('foo').width); + assert.ok(ctx.measureText('foo').width != ctx.measureText('foobar').width); + assert.ok(ctx.measureText('foo').width != ctx.measureText(' foo').width); + }); + + it('works', function () { + var canvas = createCanvas(20, 20) + var ctx = canvas.getContext('2d') + ctx.font = "20px Arial" + + ctx.textBaseline = "alphabetic" + var metrics = ctx.measureText("Alphabet") + // Zero if the given baseline is the alphabetic baseline + assert.equal(metrics.alphabeticBaseline, 0) + // Positive = going up from the baseline + assert.ok(metrics.actualBoundingBoxAscent > 0) + // Positive = going down from the baseline + assert.ok(metrics.actualBoundingBoxDescent > 0) // ~4-5 + + ctx.textBaseline = "bottom" + metrics = ctx.measureText("Alphabet") + assert.ok(metrics.alphabeticBaseline > 0) // ~4-5 + assert.ok(metrics.actualBoundingBoxAscent > 0) + // On the baseline or slightly above + assert.ok(metrics.actualBoundingBoxDescent <= 0) + }); }); it('Context2d#createImageData(ImageData)', function () { diff --git a/test/public/tests.js b/test/public/tests.js index e5f107dad..eebf78d88 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -2163,3 +2163,95 @@ tests['textBaseline and scale'] = function (ctx) { ctx.textAlign = 'center' ctx.fillText('bottom', 1000, 1500) } + +tests['rotated baseline'] = function (ctx) { + ctx.font = '12px Arial' + ctx.fillStyle = 'black' + ctx.textAlign = 'center' + ctx.textBaseline = 'bottom' + ctx.translate(100, 100) + + for (var i = 0; i < 16; i++) { + ctx.fillText('Hello world!', -50, -50) + ctx.rotate(-Math.PI / 8) + } +} + +tests['rotated and scaled baseline'] = function (ctx) { + ctx.font = '120px Arial' + ctx.fillStyle = 'black' + ctx.textAlign = 'center' + ctx.textBaseline = 'bottom' + ctx.translate(100, 100) + ctx.scale(0.1, 0.2) + + for (var i = 0; i < 16; i++) { + ctx.fillText('Hello world!', -50 / 0.1, -50 / 0.2) + ctx.rotate(-Math.PI / 8) + } +} + +tests['rotated and skewed baseline'] = function (ctx) { + ctx.font = '12px Arial' + ctx.fillStyle = 'black' + ctx.textAlign = 'center' + ctx.textBaseline = 'bottom' + ctx.translate(100, 100) + ctx.transform(1, 1, 0, 1, 1, 1) + + for (var i = 0; i < 16; i++) { + ctx.fillText('Hello world!', -50, -50) + ctx.rotate(-Math.PI / 8) + } +} + +tests['rotated, scaled and skewed baseline'] = function (ctx) { + // Known issue: we don't have a way to decompose the cairo matrix into the + // skew and rotation separately. + ctx.font = '120px Arial' + ctx.fillStyle = 'black' + ctx.textAlign = 'center' + ctx.textBaseline = 'bottom' + ctx.translate(100, 100) + ctx.scale(0.1, 0.2) + ctx.transform(1, 1, 0, 1, 1, 1) + + for (var i = 0; i < 16; i++) { + ctx.fillText('Hello world!', -50 / 0.1, -50 / 0.2) + ctx.rotate(-Math.PI / 8) + } +} + +tests['measureText()'] = function (ctx) { + // Note: As of Sep 2017, Chrome is the only browser with advanced TextMetrics, + // and they're behind a flag, and a few of them are missing and others are + // wrong. + function drawWithBBox (text, x, y) { + ctx.fillText(text, x, y) + ctx.strokeStyle = 'red' + ctx.beginPath(); ctx.moveTo(0, y + 0.5); ctx.lineTo(200, y + 0.5); ctx.stroke() + var metrics = ctx.measureText(text) + ctx.strokeStyle = 'blue' + ctx.strokeRect( + x - metrics.actualBoundingBoxLeft + 0.5, + y - metrics.actualBoundingBoxAscent + 0.5, + metrics.width, + metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent + ) + } + + ctx.font = '20px Arial' + ctx.textBaseline = 'alphabetic' + drawWithBBox('Alphabet alphabetic', 20, 50) + + drawWithBBox('weruoasnm', 50, 175) // no ascenders/descenders + + drawWithBBox(',', 100, 125) // tiny height + + ctx.textBaseline = 'bottom' + drawWithBBox('Alphabet bottom', 20, 90) + + ctx.textBaseline = 'alphabetic' + ctx.rotate(Math.PI / 8) + drawWithBBox('Alphabet', 50, 100) +} From a1cd6ec96434f76b45b02fec741d0d73cd715f71 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Mon, 4 Sep 2017 15:01:52 -0400 Subject: [PATCH 060/474] fix Windows build (broken by 71584789) --- src/Canvas.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Canvas.cc b/src/Canvas.cc index cebbaf2ee..02601cf4d 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -752,10 +752,12 @@ Canvas::ResolveFontDescription(const PangoFontDescription *desc) { for (; it != _font_face_list.end(); ++it) { if (g_ascii_strcasecmp(families[i], pango_font_description_get_family(it->user_desc)) == 0) { char *name = g_strdup(pango_font_description_get_family(it->sys_desc)); + bool unseen = g_hash_table_lookup(seen_families, name) == NULL; // Avoid sending duplicate SFNT font names due to a bug in Pango for macOS: // https://bugzilla.gnome.org/show_bug.cgi?id=762873 - if (g_hash_table_add(seen_families, name)) { + if (unseen) { + g_hash_table_replace(seen_families, name, name); if (renamed_families->len) g_string_append(renamed_families, ","); g_string_append(renamed_families, name); } From 10a82ec416f5ee82a935b7ca1207465f1c3f36c3 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Mon, 4 Sep 2017 14:13:01 -0700 Subject: [PATCH 061/474] Support #RGBA, #RRGGBBAA hex colors From CSS Color Module Level 4 https://drafts.csswg.org/css-color/#hex-notation --- src/color.cc | 40 ++++++++++++++++++++++++++++++++++++++-- test/canvas.test.js | 10 ++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/color.cc b/src/color.cc index bede494e9..144e64cb8 100644 --- a/src/color.cc +++ b/src/color.cc @@ -550,6 +550,20 @@ rgba_from_rgb(uint8_t r, uint8_t g, uint8_t b) { return rgba_from_rgba(r, g, b, 255); } +/* + * Return rgba from #RRGGBBAA + */ + +static int32_t +rgba_from_hex8_string(const char *str) { + return rgba_from_rgba( + (h(str[0]) << 4) + h(str[1]), + (h(str[2]) << 4) + h(str[3]), + (h(str[4]) << 4) + h(str[5]), + (h(str[6]) << 4) + h(str[7]) + ); +} + /* * Return rgb from "#RRGGBB". */ @@ -563,6 +577,20 @@ rgba_from_hex6_string(const char *str) { ); } +/* +* Return rgba from #RGBA +*/ + +static int32_t +rgba_from_hex4_string(const char *str) { + return rgba_from_rgba( + (h(str[0]) << 4) + h(str[0]), + (h(str[1]) << 4) + h(str[1]), + (h(str[2]) << 4) + h(str[2]), + (h(str[3]) << 4) + h(str[3]) + ); +} + /* * Return rgb from "#RGB" */ @@ -673,7 +701,9 @@ rgba_from_hsl_string(const char *str, short *ok) { * Return rgb from: * * - "#RGB" + * - "#RGBA" * - "#RRGGBB" + * - "#RRGGBBAA" * */ @@ -681,8 +711,12 @@ static int32_t rgba_from_hex_string(const char *str, short *ok) { size_t len = strlen(str); *ok = 1; - if (6 == len) return rgba_from_hex6_string(str); - if (3 == len) return rgba_from_hex3_string(str); + switch (len) { + case 8: return rgba_from_hex8_string(str); + case 6: return rgba_from_hex6_string(str); + case 4: return rgba_from_hex4_string(str); + case 3: return rgba_from_hex3_string(str); + } return *ok = 0; } @@ -705,7 +739,9 @@ rgba_from_name_string(const char *str, short *ok) { * Return rgb from: * * - #RGB + * - #RGBA * - #RRGGBB + * - #RRGGBBAA * - rgb(r,g,b) * - rgba(r,g,b,a) * - hsl(h,s,l) diff --git a/test/canvas.test.js b/test/canvas.test.js index e0074923a..1500822fb 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -140,6 +140,16 @@ describe('Canvas', function () { ctx.fillStyle = 'afasdfasdf'; assert.equal('#ffffff', ctx.fillStyle); + // #rgba and #rrggbbaa + ctx.fillStyle = '#ffccaa80' + assert.equal('rgba(255, 204, 170, 0.50)', ctx.fillStyle) + + ctx.fillStyle = '#acf8' + assert.equal('rgba(170, 204, 255, 0.53)', ctx.fillStyle) + + ctx.fillStyle = '#BEAD' + assert.equal('rgba(187, 238, 170, 0.87)', ctx.fillStyle) + ctx.fillStyle = 'rgb(255,255,255)'; assert.equal('#ffffff', ctx.fillStyle); From d1b9d1933b738d2b0dbe02dc070ec62e375f0a17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Tue, 5 Sep 2017 10:49:02 +0100 Subject: [PATCH 062/474] 2.0.0-alpha.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b06ea5ace..dbb7d2884 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.0.0-alpha.4", + "version": "2.0.0-alpha.5", "author": "TJ Holowaychuk ", "browser": "browser.js", "contributors": [ From 6a4c0ab75a8225278d58fa86667aa6aec59ce646 Mon Sep 17 00:00:00 2001 From: Carlos de la Torre Date: Wed, 6 Sep 2017 14:25:21 -0300 Subject: [PATCH 063/474] Export CanvasGradient As suggested in https://github.com/Automattic/node-canvas/issues/990 --- index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/index.js b/index.js index 00eb3979a..a1e2ab25a 100644 --- a/index.js +++ b/index.js @@ -50,6 +50,7 @@ module.exports = { Canvas, Context2d: CanvasRenderingContext2D, // Legacy/compat export CanvasRenderingContext2D, + CanvasGradient: bindings.CanvasGradient, CanvasPattern: bindings.CanvasPattern, Image, ImageData: bindings.ImageData, From d6714ee6e79a2d3d1aba67dfd3f0791f6399f76d Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 7 Sep 2017 21:30:51 -0700 Subject: [PATCH 064/474] Support currentTransform Fixes #658 --- index.js | 4 + lib/DOMMatrix.js | 479 ++++++++++++++++++++++++++++++++ lib/context2d.js | 15 + src/CanvasRenderingContext2d.cc | 17 ++ src/CanvasRenderingContext2d.h | 1 + test/dommatrix.test.js | 469 +++++++++++++++++++++++++++++++ 6 files changed, 985 insertions(+) create mode 100644 lib/DOMMatrix.js create mode 100644 test/dommatrix.test.js diff --git a/index.js b/index.js index 00eb3979a..1ee7d817e 100644 --- a/index.js +++ b/index.js @@ -8,6 +8,8 @@ const fs = require('fs') const PNGStream = require('./lib/pngstream') const PDFStream = require('./lib/pdfstream') const JPEGStream = require('./lib/jpegstream') +const DOMMatrix = require('./lib/DOMMatrix').DOMMatrix +const DOMPoint = require('./lib/DOMMatrix').DOMPoint function createCanvas (width, height, type) { return new Canvas(width, height, type) @@ -56,6 +58,8 @@ module.exports = { PNGStream, PDFStream, JPEGStream, + DOMMatrix, + DOMPoint, registerFont, parseFont, diff --git a/lib/DOMMatrix.js b/lib/DOMMatrix.js new file mode 100644 index 000000000..3c5262449 --- /dev/null +++ b/lib/DOMMatrix.js @@ -0,0 +1,479 @@ +'use strict' + +// DOMMatrix per https://drafts.fxtf.org/geometry/#DOMMatrix + +function DOMPoint(x, y, z, w) { + if (!(this instanceof DOMPoint)) { + throw new TypeError("Class constructors cannot be invoked without 'new'") + } + + if (typeof x === 'object') { + w = x.w + z = x.z + y = x.y + x = x.x + } + this.x = typeof x === 'number' ? x : 0 + this.y = typeof y === 'number' ? y : 0 + this.z = typeof z === 'number' ? z : 0 + this.w = typeof w === 'number' ? w : 1 +} + +// Constants to index into _values (col-major) +const M11 = 0, M12 = 1, M13 = 2, M14 = 3 +const M21 = 4, M22 = 5, M23 = 6, M24 = 7 +const M31 = 8, M32 = 9, M33 = 10, M34 = 11 +const M41 = 12, M42 = 13, M43 = 14, M44 = 15 + +const DEGREE_PER_RAD = 180 / Math.PI +const RAD_PER_DEGREE = Math.PI / 180 + +function parseMatrix(init) { + var parsed = init.replace(/matrix\(/, '') + parsed = parsed.split(/,/, 7) // 6 + 1 to handle too many params + if (parsed.length !== 6) throw new Error(`Failed to parse ${init}`) + parsed = parsed.map(parseFloat) + return [ + parsed[0], parsed[1], 0, 0, + parsed[2], parsed[3], 0, 0, + 0, 0, 1, 0, + parsed[4], parsed[5], 0, 1 + ] +} + +function parseMatrix3d(init) { + var parsed = init.replace(/matrix3d\(/, '') + parsed = parsed.split(/,/, 17) // 16 + 1 to handle too many params + if (parsed.length !== 16) throw new Error(`Failed to parse ${init}`) + return parsed.map(parseFloat) +} + +function parseTransform(tform) { + var type = tform.split(/\(/, 1)[0] + switch (type) { + case 'matrix': + return parseMatrix(tform) + case 'matrix3d': + return parseMatrix3d(tform) + // TODO This is supposed to support any CSS transform value. + default: + throw new Error(`${type} parsing not implemented`) + } +} + +function DOMMatrix (init) { + if (!(this instanceof DOMMatrix)) { + throw new TypeError("Class constructors cannot be invoked without 'new'") + } + + this._is2D = true + this._values = new Float64Array([ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ]) + + var i + + if (typeof init === 'string') { // parse CSS transformList + if (init === '') return // default identity matrix + var tforms = init.split(/\)\s+/, 20).map(parseTransform) + if (tforms.length === 0) return + init = tforms[0] + for (i = 1; i < tforms.length; i++) init = multiply(tforms[i], init) + } + + i = 0 + if (init && init.length === 6) { + setNumber2D(this, M11, init[i++]) + setNumber2D(this, M12, init[i++]) + setNumber2D(this, M21, init[i++]) + setNumber2D(this, M22, init[i++]) + setNumber2D(this, M41, init[i++]) + setNumber2D(this, M42, init[i++]) + } else if (init && init.length === 16) { + setNumber2D(this, M11, init[i++]) + setNumber2D(this, M12, init[i++]) + setNumber3D(this, M13, init[i++]) + setNumber3D(this, M14, init[i++]) + setNumber2D(this, M21, init[i++]) + setNumber2D(this, M22, init[i++]) + setNumber3D(this, M23, init[i++]) + setNumber3D(this, M24, init[i++]) + setNumber3D(this, M31, init[i++]) + setNumber3D(this, M32, init[i++]) + setNumber3D(this, M33, init[i++]) + setNumber3D(this, M34, init[i++]) + setNumber2D(this, M41, init[i++]) + setNumber2D(this, M42, init[i++]) + setNumber3D(this, M43, init[i++]) + setNumber3D(this, M44, init[i]) + } else if (init !== undefined) { + throw new TypeError('Expected string or array.') + } +} + +DOMMatrix.fromMatrix = function (init) { + if (!(init instanceof DOMMatrix)) throw new TypeError('Expected DOMMatrix') + return new DOMMatrix(init._values) +} +DOMMatrix.fromFloat32Array = function (init) { + if (!(init instanceof Float32Array)) throw new TypeError('Expected Float32Array') + return new DOMMatrix(init) +} +DOMMatrix.fromFloat64Array = function (init) { + if (!(init instanceof Float64Array)) throw new TypeError('Expected Float64Array') + return new DOMMatrix(init) +} + +DOMMatrix.prototype.inspect = function (depth, options) { + if (depth < 0) return '[DOMMatrix]' + + return `DOMMatrix [ + a: ${this.a} + b: ${this.b} + c: ${this.c} + d: ${this.d} + e: ${this.e} + f: ${this.f} + m11: ${this.m11} + m12: ${this.m12} + m13: ${this.m13} + m14: ${this.m14} + m21: ${this.m21} + m22: ${this.m22} + m23: ${this.m23} + m23: ${this.m23} + m31: ${this.m31} + m32: ${this.m32} + m33: ${this.m33} + m34: ${this.m34} + m41: ${this.m41} + m42: ${this.m42} + m43: ${this.m43} + m44: ${this.m44} + is2D: ${this.is2D} + isIdentity: ${this.isIdentity} ]` +} + +DOMMatrix.prototype.toString = function () { + return this.is2D ? + `matrix(${this.a}, ${this.b}, ${this.c}, ${this.d}, ${this.e}, ${this.f})` : + `matrix3d(${this._values.join(', ')})` +} + +/** + * Checks that `value` is a number and sets the value. + */ +function setNumber2D(receiver, index, value) { + if (typeof value !== 'number') throw new TypeError('Expected number') + return receiver._values[index] = value +} + +/** + * Checks that `value` is a number, sets `_is2D = false` if necessary and sets + * the value. + */ +function setNumber3D(receiver, index, value) { + if (typeof value !== 'number') throw new TypeError('Expected number') + if (index === M33 || index === M44) { + if (value !== 1) receiver._is2D = false + } else if (value !== 0) receiver._is2D = false + return receiver._values[index] = value +} + +Object.defineProperties(DOMMatrix.prototype, { + m11: {get: function () { return this._values[M11] }, set: function (v) { return setNumber2D(this, M11, v) }}, + m12: {get: function () { return this._values[M12] }, set: function (v) { return setNumber2D(this, M12, v) }}, + m13: {get: function () { return this._values[M13] }, set: function (v) { return setNumber3D(this, M13, v) }}, + m14: {get: function () { return this._values[M14] }, set: function (v) { return setNumber3D(this, M14, v) }}, + m21: {get: function () { return this._values[M21] }, set: function (v) { return setNumber2D(this, M21, v) }}, + m22: {get: function () { return this._values[M22] }, set: function (v) { return setNumber2D(this, M22, v) }}, + m23: {get: function () { return this._values[M23] }, set: function (v) { return setNumber3D(this, M23, v) }}, + m24: {get: function () { return this._values[M24] }, set: function (v) { return setNumber3D(this, M24, v) }}, + m31: {get: function () { return this._values[M31] }, set: function (v) { return setNumber3D(this, M31, v) }}, + m32: {get: function () { return this._values[M32] }, set: function (v) { return setNumber3D(this, M32, v) }}, + m33: {get: function () { return this._values[M33] }, set: function (v) { return setNumber3D(this, M33, v) }}, + m34: {get: function () { return this._values[M34] }, set: function (v) { return setNumber3D(this, M34, v) }}, + m41: {get: function () { return this._values[M41] }, set: function (v) { return setNumber2D(this, M41, v) }}, + m42: {get: function () { return this._values[M42] }, set: function (v) { return setNumber2D(this, M42, v) }}, + m43: {get: function () { return this._values[M43] }, set: function (v) { return setNumber3D(this, M43, v) }}, + m44: {get: function () { return this._values[M44] }, set: function (v) { return setNumber3D(this, M44, v) }}, + + a: {get: function () { return this.m11 }, set: function (v) { return this.m11 = v }}, + b: {get: function () { return this.m12 }, set: function (v) { return this.m12 = v }}, + c: {get: function () { return this.m21 }, set: function (v) { return this.m21 = v }}, + d: {get: function () { return this.m22 }, set: function (v) { return this.m22 = v }}, + e: {get: function () { return this.m41 }, set: function (v) { return this.m41 = v }}, + f: {get: function () { return this.m42 }, set: function (v) { return this.m42 = v }}, + + is2D: {get: function () { return this._is2D }}, // read-only + + isIdentity: { + get: function () { + var values = this._values + return values[M11] === 1 && values[M12] === 0 && values[M13] === 0 && values[M14] === 0 && + values[M21] === 0 && values[M22] === 1 && values[M23] === 0 && values[M24] === 0 && + values[M31] === 0 && values[M32] === 0 && values[M33] === 1 && values[M34] === 0 && + values[M41] === 0 && values[M42] === 0 && values[M43] === 0 && values[M44] === 1 + } + } +}) + +/** + * Instantiates a DOMMatrix, bypassing the constructor. + * @param {Float64Array} values Value to assign to `_values`. This is assigned + * without copying (okay because all usages are followed by a multiply). + */ +function newInstance(values) { + var instance = Object.create(DOMMatrix.prototype) + instance.constructor = DOMMatrix + instance._is2D = true + instance._values = values + return instance +} + +function multiply(A, B) { + var dest = new Float64Array(16) + for (var i = 0; i < 4; i++) { + for (var j = 0; j < 4; j++) { + var sum = 0 + for (var k = 0; k < 4; k++) { + sum += A[i * 4 + k] * B[k * 4 + j] + } + dest[i * 4 + j] = sum + } + } + return dest +} + +DOMMatrix.prototype.multiply = function (other) { + return newInstance(this._values).multiplySelf(other) +} +DOMMatrix.prototype.multiplySelf = function (other) { + this._values = multiply(other._values, this._values) + if (!other.is2D) this._is2D = false + return this +} +DOMMatrix.prototype.preMultiplySelf = function (other) { + this._values = multiply(this._values, other._values) + if (!other.is2D) this._is2D = false + return this +} + +DOMMatrix.prototype.translate = function (tx, ty, tz) { + return newInstance(this._values).translateSelf(tx, ty, tz) +} +DOMMatrix.prototype.translateSelf = function (tx, ty, tz) { + if (typeof tx !== 'number') tx = 0 + if (typeof ty !== 'number') ty = 0 + if (typeof tz !== 'number') tz = 0 + this._values = multiply([ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + tx, ty, tz, 1 + ], this._values) + if (tz !== 0) this._is2D = false + return this +} + +DOMMatrix.prototype.scale = function (scaleX, scaleY, scaleZ, originX, originY, originZ) { + return newInstance(this._values).scaleSelf(scaleX, scaleY, scaleZ, originX, originY, originZ) +} +DOMMatrix.prototype.scale3d = function (scale, originX, originY, originZ) { + return newInstance(this._values).scale3dSelf(scale, originX, originY, originZ) +} +DOMMatrix.prototype.scale3dSelf = function (scale, originX, originY, originZ) { + return this.scaleSelf(scale, scale, scale, originX, originY, originZ) +} +DOMMatrix.prototype.scaleSelf = function (scaleX, scaleY, scaleZ, originX, originY, originZ) { + // Not redundant with translate's checks because we need to negate the values later. + if (typeof originX !== 'number') originX = 0 + if (typeof originY !== 'number') originY = 0 + if (typeof originZ !== 'number') originZ = 0 + this.translateSelf(originX, originY, originZ) + if (typeof scaleX !== 'number') scaleX = 1 + if (typeof scaleY !== 'number') scaleY = scaleX + if (typeof scaleZ !== 'number') scaleZ = 1 + this._values = multiply([ + scaleX, 0, 0, 0, + 0, scaleY, 0, 0, + 0, 0, scaleZ, 0, + 0, 0, 0, 1 + ], this._values) + this.translateSelf(-originX, -originY, -originZ) + if (scaleZ !== 1 || originZ !== 0) this._is2D = false + return this +} + +DOMMatrix.prototype.rotateFromVector = function (x, y) { + return newInstance(this._values).rotateFromVectorSelf(x, y) +} +DOMMatrix.prototype.rotateFromVectorSelf = function (x, y) { + if (typeof x !== 'number') x = 0 + if (typeof y !== 'number') y = 0 + var theta = (x === 0 && y === 0) ? 0 : Math.atan2(y, x) * DEGREE_PER_RAD + return this.rotateSelf(theta) +} +DOMMatrix.prototype.rotate = function (rotX, rotY, rotZ) { + return newInstance(this._values).rotateSelf(rotX, rotY, rotZ) +} +DOMMatrix.prototype.rotateSelf = function (rotX, rotY, rotZ) { + if (rotY === undefined && rotZ === undefined) { + rotZ = rotX + rotX = rotY = 0 + } + if (typeof rotY !== 'number') rotY = 0 + if (typeof rotZ !== 'number') rotZ = 0 + if (rotX !== 0 || rotY !== 0) this._is2D = false + rotX *= RAD_PER_DEGREE + rotY *= RAD_PER_DEGREE + rotZ *= RAD_PER_DEGREE + var c, s + c = Math.cos(rotZ) + s = Math.sin(rotZ) + this._values = multiply([ + c, s, 0, 0, + -s, c, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ], this._values) + c = Math.cos(rotY) + s = Math.sin(rotY) + this._values = multiply([ + c, 0, -s, 0, + 0, 1, 0, 0, + s, 0, c, 0, + 0, 0, 0, 1 + ], this._values) + c = Math.cos(rotX) + s = Math.sin(rotX) + this._values = multiply([ + 1, 0, 0, 0, + 0, c, s, 0, + 0, -s, c, 0, + 0, 0, 0, 1 + ], this._values) + return this +} + +DOMMatrix.prototype.rotateAxisAngle = function (x, y, z, angle) { + return newInstance(this._values).rotateAxisAngleSelf(x, y, z, angle) +} +DOMMatrix.prototype.rotateAxisAngleSelf = function (x, y, z, angle) { + if (typeof x !== 'number') x = 0 + if (typeof y !== 'number') y = 0 + if (typeof z !== 'number') z = 0 + // Normalize axis + var length = Math.sqrt(x * x + y * y + z * z) + if (length === 0) return this + if (length !== 1) { + x /= length + y /= length + z /= length + } + angle *= RAD_PER_DEGREE + var c = Math.cos(angle) + var s = Math.sin(angle) + var t = 1 - c + var tx = t * x + var ty = t * y + // NB: This is the generic transform. If the axis is a major axis, there are + // faster transforms. + this._values = multiply([ + tx * x + c, tx * y + s * z, tx * z - s * y, 0, + tx * y - s * z, ty * y + c, ty * z + s * x, 0, + tx * z + s * y, ty * z - s * x, t * z * z + c, 0, + 0, 0, 0, 1 + ], this._values) + if (x !== 0 || y !== 0) this._is2D = false + return this +} + +DOMMatrix.prototype.skewX = function (sx) { + return newInstance(this._values).skewXSelf(sx) +} +DOMMatrix.prototype.skewXSelf = function (sx) { + if (typeof sx !== 'number') return this + var t = Math.tan(sx * RAD_PER_DEGREE) + this._values = multiply([ + 1, 0, 0, 0, + t, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ], this._values) + return this +} + +DOMMatrix.prototype.skewY = function (sy) { + return newInstance(this._values).skewYSelf(sy) +} +DOMMatrix.prototype.skewYSelf = function (sy) { + if (typeof sy !== 'number') return this + var t = Math.tan(sy * RAD_PER_DEGREE) + this._values = multiply([ + 1, t, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ], this._values) + return this +} + +DOMMatrix.prototype.flipX = function () { + return newInstance(multiply([ + -1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ], this._values)) +} +DOMMatrix.prototype.flipY = function () { + return newInstance(multiply([ + 1, 0, 0, 0, + 0, -1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ], this._values)) +} + +DOMMatrix.prototype.inverse = function () { + return newInstance(this._values).invertSelf() +} +DOMMatrix.prototype.invertSelf = function () { + // If not invertible, set all attributes to NaN and is2D to false + throw new Error('Not implemented') +} + +DOMMatrix.prototype.setMatrixValue = function (transformList) { + var temp = new DOMMatrix(transformList) + this._values = temp._values + this._is2D = temp._is2D + return this +} + +DOMMatrix.prototype.transformPoint = function (point) { + point = new DOMPoint(point) + var x = point.x + var y = point.y + var z = point.z + var w = point.w + var values = this._values + var nx = values[M11] * x + values[M21] * y + values[M31] * z + values[M41] * w + var ny = values[M12] * x + values[M22] * y + values[M32] * z + values[M42] * w + var nz = values[M13] * x + values[M23] * y + values[M33] * z + values[M43] * w + var nw = values[M14] * x + values[M24] * y + values[M34] * z + values[M44] * w + return new DOMPoint(nx, ny, nz, nw) +} + +DOMMatrix.prototype.toFloat32Array = function () { + return Float32Array.from(this._values) +} + +DOMMatrix.prototype.toFloat64Array = function () { + return this._values.slice(0) +} + +module.exports = {DOMMatrix, DOMPoint} diff --git a/lib/context2d.js b/lib/context2d.js index f26e06c2a..da7384a82 100644 --- a/lib/context2d.js +++ b/lib/context2d.js @@ -16,6 +16,7 @@ const Context2d = module.exports = bindings.CanvasRenderingContext2d const CanvasGradient = bindings.CanvasGradient const CanvasPattern = bindings.CanvasPattern const ImageData = bindings.ImageData +const DOMMatrix = require('./DOMMatrix') /** * Text baselines. @@ -103,6 +104,20 @@ Context2d.prototype.setTransform = function(){ this.transform.apply(this, arguments); }; +Object.defineProperty(Context2d.prototype, 'currentTransform', { + get: function () { + var values = new Float64Array(6) + this._getMatrix(values) + return new DOMMatrix(values) + }, + set: function (m) { + if (!(m instanceof DOMMatrix)) { + throw new TypeError('Expected DOMMatrix') + } + this.setTransform(m.a, m.b, m.c, m.d, m.e, m.f) + } +}) + /** * Set the fill style with the given css color string. * diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 767573167..ecfb6f5f9 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -126,6 +126,7 @@ Context2d::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { Nan::SetPrototypeMethod(ctor, "_setStrokePattern", SetStrokePattern); Nan::SetPrototypeMethod(ctor, "_setTextBaseline", SetTextBaseline); Nan::SetPrototypeMethod(ctor, "_setTextAlignment", SetTextAlignment); + Nan::SetPrototypeMethod(ctor, "_getMatrix", GetMatrix); Nan::SetAccessor(proto, Nan::New("pixelFormat").ToLocalChecked(), GetFormat); Nan::SetAccessor(proto, Nan::New("patternQuality").ToLocalChecked(), GetPatternQuality, SetPatternQuality); Nan::SetAccessor(proto, Nan::New("globalCompositeOperation").ToLocalChecked(), GetGlobalCompositeOperation, SetGlobalCompositeOperation); @@ -1791,6 +1792,22 @@ NAN_METHOD(Context2d::ResetTransform) { cairo_identity_matrix(context->context()); } +NAN_METHOD(Context2d::GetMatrix) { + Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); + // Accept a receiver array to avoid an alloc (this small array will be + // a view of a larger, shared ArrayBuffer). + Local destTa = info[0].As(); + Nan::TypedArrayContents dest(destTa); + cairo_matrix_t matrix; + cairo_get_matrix(context->context(), &matrix); + (*dest)[0] = matrix.xx; + (*dest)[1] = matrix.yx; + (*dest)[2] = matrix.xy; + (*dest)[3] = matrix.yy; + (*dest)[4] = matrix.x0; + (*dest)[5] = matrix.y0; +} + /* * Translate transformation. */ diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index af373bfd3..055a0b069 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -99,6 +99,7 @@ class Context2d: public Nan::ObjectWrap { static NAN_METHOD(ArcTo); static NAN_METHOD(Ellipse); static NAN_METHOD(GetImageData); + static NAN_METHOD(GetMatrix); static NAN_GETTER(GetFormat); static NAN_GETTER(GetPatternQuality); static NAN_GETTER(GetGlobalCompositeOperation); diff --git a/test/dommatrix.test.js b/test/dommatrix.test.js new file mode 100644 index 000000000..212ce40a4 --- /dev/null +++ b/test/dommatrix.test.js @@ -0,0 +1,469 @@ +/* eslint-env mocha */ + +'use stricit' + +const DOMMatrix = require('../').DOMMatrix + +const assert = require('assert') + +// This doesn't need to be precise; we're not testing the engine's trig +// implementations. +const TOLERANCE = 0.001 +function assertApprox(actual, expected, tolerance) { + if (typeof tolerance !== 'number') tolerance = TOLERANCE + assert.ok(expected > actual - tolerance && expected < actual + tolerance, + `Expected ${expected} to equal ${actual} +/- ${tolerance}`) +} +function assertApproxDeep(actual, expected, tolerance) { + expected.forEach(function (value, index) { + assertApprox(actual[index], value) + }) +} + +describe('DOMMatrix', function () { + var Avals = [4,5,1,8, 0,3,6,1, 3,5,0,9, 2,4,6,1] + var Bvals = [1,5,1,0, 0,3,6,1, 3,5,7,2, 2,0,6,1] + var AxB = [7,25,31,22, 20,43,24,58, 37,73,45,94, 28,44,8,71] + var BxA = [23,40,89,15, 20,39,66,16, 21,30,87,14, 22,52,74,17] + + describe('constructor, general', function () { + it('aliases a,b,c,d,e,f properly', function () { + var y = new DOMMatrix(Avals) + assert.strictEqual(y.a, y.m11) + assert.strictEqual(y.b, y.m12) + assert.strictEqual(y.c, y.m21) + assert.strictEqual(y.d, y.m22) + assert.strictEqual(y.e, y.m41) + assert.strictEqual(y.f, y.m42) + }) + + it('parses lists of transforms per spec', function () { + var y = new DOMMatrix('matrix(1, -2, 3.2, 4.5e2, 3.5E-1, +2) matrix(1, 2, 4, 1, 0, 0)') + assert.strictEqual(y.a, 7.4) + assert.strictEqual(y.b, 898) + assert.strictEqual(y.c, 7.2) + assert.strictEqual(y.d, 442) + assert.strictEqual(y.e, 0.35) + assert.strictEqual(y.f, 2) + assert.strictEqual(y.is2D, true) + }) + + it('parses matrix2d(<16 numbers>) per spec', function () { + var y = new DOMMatrix('matrix3d(1, -0, 0, 0, -2.12, 1, 0, 0, 3e2, 0, +1, 1.252, 0, 0, 0, 1)') + assert.deepEqual(y.toFloat64Array(), [ + 1, 0, 0, 0, + -2.12, 1, 0, 0, + 300, 0, 1, 1.252, + 0, 0, 0, 1 + ]) + assert.strictEqual(y.is2D, false) + }) + + it('sets is2D to true if matrix2d(<16 numbers>) is 2D', function () { + var y = new DOMMatrix('matrix3d(1, 2, 0, 0, 3, 4, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)') + assert.deepEqual(y.toFloat64Array(), [ + 1, 2, 0, 0, + 3, 4, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ]) + assert.strictEqual(y.is2D, true) + }) + }) + + describe('fromMatrix', function () {}) + describe('fromFloat32Array', function () {}) + describe('fromFloat64Array', function () {}) + + describe('multiply', function () { + it('performs self.other, returning a new DOMMatrix', function () { + var A = new DOMMatrix(Avals) + var B = new DOMMatrix(Bvals) + var C = B.multiply(A) + assert.deepEqual(C.toFloat64Array(), BxA) + assert.notStrictEqual(A, C) + assert.notStrictEqual(B, C) + }) + }) + + describe('multiplySelf', function () { + it('performs self.other, mutating self', function () { + var A = new DOMMatrix(Avals) + var B = new DOMMatrix(Bvals) + var C = B.multiplySelf(A) + assert.deepEqual(C.toFloat64Array(), BxA) + assert.strictEqual(C, B) + }) + }) + + describe('preMultiplySelf', function () { + it('performs other.self, mutating self', function () { + var A = new DOMMatrix(Avals) + var B = new DOMMatrix(Bvals) + var C = B.preMultiplySelf(A) + assert.deepEqual(C.toFloat64Array(), AxB) + assert.strictEqual(C, B) + }) + }) + + describe('translate', function () {}) + + describe('translateSelf', function () { + it('works, 1 arg', function () { + var A = new DOMMatrix() + A.translateSelf(1) + assert.deepEqual(A.toFloat64Array(), [ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 1, 0, 0, 1 + ]) + }) + + it('works, 2 args', function () { + var A = new DOMMatrix(Avals) + var C = A.translateSelf(2, 5) + assert.deepEqual(C.toFloat64Array(), [ + 4, 5, 1, 8, + 0, 3, 6, 1, + 3, 5, 0, 9, + 10, 29, 38, 22 + ]) + }) + + it('works, 3 args', function () { + var A = new DOMMatrix() + A.translateSelf(1, 2, 3) + assert.deepEqual(A.toFloat64Array(), [ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 1, 2, 3, 1 + ]) + }) + }) + + describe('scale', function () { + var x = new DOMMatrix() + it('works, 1 arg', function () { + assert.deepEqual(x.scale(2).toFloat64Array(), [ + 2, 0, 0, 0, + 0, 2, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ]) + }) + + it('works, 2 args', function () { + assert.deepEqual(x.scale(2, 3).toFloat64Array(), [ + 2, 0, 0, 0, + 0, 3, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ]) + }) + + it('works, 3 args', function () { + assert.deepEqual(x.scale(2, 3, 4).toFloat64Array(), [ + 2, 0, 0, 0, + 0, 3, 0, 0, + 0, 0, 4, 0, + 0, 0, 0, 1 + ]) + }) + + it('works, 4 args', function () { + assert.deepEqual(x.scale(2, 3, 4, 5).toFloat64Array(), [ + 2, 0, 0, 0, + 0, 3, 0, 0, + 0, 0, 4, 0, + -5, 0, 0, 1 + ]) + }) + + it('works, 5 args', function () { + assert.deepEqual(x.scale(2, 3, 4, 5, 6).toFloat64Array(), [ + 2, 0, 0, 0, + 0, 3, 0, 0, + 0, 0, 4, 0, + -5, -12, 0, 1 + ]) + }) + + it('works, 6 args', function () { + assert.deepEqual(x.scale(2, 1, 1, 0, 0, 0).toFloat64Array(), [ + 2, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ]) + + assert.deepEqual(x.scale(2, 3, 2, 0, 0, 0).toFloat64Array(), [ + 2, 0, 0, 0, + 0, 3, 0, 0, + 0, 0, 2, 0, + 0, 0, 0, 1 + ]) + + assert.deepEqual(x.scale(5, -1, 2, 1, 3, 2).toFloat64Array(), [ + 5, 0, 0, 0, + 0, -1, 0, 0, + 0, 0, 2, 0, + -4, 6, -2, 1 + ]) + }) + }) + + describe('scaleSelf', function () {}) + + describe('scale3d', function () { + var x = new DOMMatrix(Avals) + + it('works, 0 args', function () { + assert.deepEqual(x.scale3d().toFloat64Array(), Avals) + }) + + it('works, 1 arg', function () { + assert.deepEqual(x.scale3d(2).toFloat64Array(), [ + 8, 10, 2, 16, + 0, 6, 12, 2, + 6, 10, 0, 18, + 2, 4, 6, 1 + ]) + }) + + it('works, 2 args', function () { + assert.deepEqual(x.scale3d(2, 3).toFloat64Array(), [ + 8, 10, 2, 16, + 0, 6, 12, 2, + 6, 10, 0, 18, + -10, -11, 3, -23 + ]) + }) + + it('works, 3 args', function () { + assert.deepEqual(x.scale3d(2, 3, 4).toFloat64Array(), [ + 8, 10, 2, 16, + 0, 6, 12, 2, + 6, 10, 0, 18, + -10, -23, -21, -27 + ]) + }) + + it('works, 4 args', function () { + assert.deepEqual(x.scale3d(2, 3, 4, 5).toFloat64Array(), [ + 8, 10, 2, 16, + 0, 6, 12, 2, + 6, 10, 0, 18, + -25, -48, -21, -72 + ]) + }) + }) + + describe('scale3dSelf', function () {}) + + describe('rotate', function () { + it('works, 1 arg', function () { + var x = new DOMMatrix() + var y = x.rotate(70) + assertApproxDeep(y.toFloat64Array(), [ + 0.3420201, 0.9396926, 0, 0, + -0.939692, 0.3420201, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ]) + }) + + it('works, 2 args', function () { + var x = new DOMMatrix() + var y = x.rotate(70, 30) + assertApproxDeep(y.toFloat64Array(), [ + 0.8660254, 0, -0.5, 0, + 0.4698463, 0.3420201, 0.8137976, 0, + 0.1710100, -0.9396926, 0.2961981, 0, + 0, 0, 0, 1 + ]) + assert.strictEqual(y.is2D, false) + }) + + it('works, 3 args', function () { + var x = new DOMMatrix() + var y = x.rotate(70, 30, 50) + assertApproxDeep(y.toFloat64Array(), [ + 0.5566703, 0.6634139, -0.5, 0, + 0.0400087, 0.5797694, 0.8137976, 0, + 0.8297694, -0.4730214, 0.2961981, 0, + 0, 0, 0, 1]) + }) + }) + + describe('rotateSelf', function () {}) + + describe('rotateFromVector', function () { + var x = new DOMMatrix(Avals) + it('works, no args and x/y=0', function () { + assert.deepEqual(x.rotateFromVector().toFloat64Array(), Avals) + assert.deepEqual(x.rotateFromVector(5).toFloat64Array(), Avals) + assert.deepEqual(x.rotateFromVector(0, 0).toFloat64Array(), Avals) + }) + + it('works', function () { + var y = x.rotateFromVector(4, 2).toFloat64Array() + var expect = [ + 3.5777087, 5.8137767, 3.5777087, 7.6026311, + -1.7888543, 0.4472135, 4.9193495, -2.6832815, + 3, 5, 0, 9, + 2, 4, 6, 1 + ] + assertApproxDeep(expect, y) + }) + }) + + describe('rotateFromVectorSelf', function () {}) + + describe('rotateAxisAngle', function () { + it('works, 0 args', function () { + var x = new DOMMatrix(Avals) + var y = x.rotateAxisAngle().toFloat64Array() + assert.deepEqual(y, Avals) + }) + + it('works, 4 args', function () { + var x = new DOMMatrix(Avals) + var y = x.rotateAxisAngle(2, 4, 1, 35).toFloat64Array() + var expect = [ + 1.9640922, 2.4329989, 2.0179538, 2.6719387, + 0.6292488, 4.0133545, 5.6853755, 3.0697681, + 4.5548203, 6.0805840, -0.7774101, 11.3770500, + 2, 4, 6, 1 + ] + assertApproxDeep(expect, y) + }) + }) + + describe('rotateFromAngleSelf', function () {}) + + describe('skewX', function () { + it('works', function () { + var x = new DOMMatrix(Avals) + var y = x.skewX(30).toFloat64Array() + var expect = [ + 4, 5, 1, 8, + 2.3094010, 5.8867513, 6.5773502, 5.6188021, + 3, 5, 0, 9, + 2, 4, 6, 1 + ] + assertApproxDeep(expect, y) + }) + }) + + describe('skewXSelf', function () {}) + + describe('skewY', function () { + it('works', function () { + var x = new DOMMatrix(Avals) + var y = x.skewY(30).toFloat64Array() + var expect = [ + 4, 6.7320508, 4.4641016, 8.5773502, + 0, 3, 6, 1, + 3, 5, 0, 9, + 2, 4, 6, 1 + ] + assertApproxDeep(expect, y) + }) + }) + + describe('skewYSelf', function () {}) + + describe('flipX', function () { + it('works', function () { + var x = new DOMMatrix() + x.rotateSelf(70) + var y = x.flipX() + assertApprox(y.a, -0.34202) + assertApprox(y.b, -0.93969) + assertApprox(y.c, -0.93969) + assertApprox(y.d, 0.34202) + assert.strictEqual(y.e, 0) + assert.strictEqual(y.f, 0) + }) + }) + + describe('flipY', function () { + it('works', function () { + var x = new DOMMatrix() + x.rotateSelf(70) + var y = x.flipY() + assertApprox(y.a, 0.34202) + assertApprox(y.b, 0.93969) + assertApprox(y.c, 0.93969) + assertApprox(y.d, -0.34202) + assert.strictEqual(y.e, 0) + assert.strictEqual(y.f, 0) + }) + }) + + describe('inverse', function () {}) + describe('invertSelf', function () {}) + + describe('transformPoint', function () { + it('works', function () { + var x = new DOMMatrix() + var r = x.transformPoint({x: 1, y: 2, z: 3}) + assert.strictEqual(r.x, 1) + assert.strictEqual(r.y, 2) + assert.strictEqual(r.z, 3) + assert.strictEqual(r.w, 1) + + x.rotateSelf(70) + r = x.transformPoint({x: 2, y: 3, z: 4}) + assertApprox(r.x, -2.13503) + assertApprox(r.y, 2.905445) + assert.strictEqual(r.z, 4) + assert.strictEqual(r.w, 1) + }) + }) + + describe('toFloat32Array', function () { + it('works', function () { + var x = new DOMMatrix() + var y = x.toFloat32Array() + assert.ok(y instanceof Float32Array) + assert.deepEqual(y, [ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ]) + }) + }) + + describe('toFloat64Array', function () { + it('works', function () { + var x = new DOMMatrix() + var y = x.toFloat64Array() + assert.ok(y instanceof Float64Array) + assert.deepEqual(y, [ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ]) + }) + }) + + describe('toString', function () { + it('works, 2d', function () { + var x = new DOMMatrix() + assert.equal(x.toString(), 'matrix(1, 0, 0, 1, 0, 0)') + }) + + it('works, 3d', function () { + var x = new DOMMatrix() + x.m31 = 1 + assert.equal(x.is2D, false) + assert.equal(x.toString(), + 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1)') + }) + }) +}) From d4b6f6771722f39f12f39e824c53a2f217efdf53 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sun, 3 Sep 2017 15:28:55 -0700 Subject: [PATCH 065/474] Fix assertion failure from accessors Accessors should not attempt to unwrap the non-existent 'this' when accessed via the prototype. Fixes #803 Fixes #847 Fixes #885 Fixes nodejs/node#15099 --- src/Canvas.cc | 9 ++++---- src/CanvasRenderingContext2d.cc | 37 +++++++++++++++++---------------- src/Image.cc | 19 +++++++++-------- src/ImageData.cc | 5 +++-- src/Util.h | 25 ++++++++++++++++++++++ test/canvas.test.js | 9 ++++++++ test/image.test.js | 9 ++++++++ test/imageData.test.js | 5 +++++ 8 files changed, 85 insertions(+), 33 deletions(-) create mode 100644 src/Util.h diff --git a/src/Canvas.cc b/src/Canvas.cc index 02601cf4d..db3321ff1 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -13,6 +13,7 @@ #include #include +#include "Util.h" #include "Canvas.h" #include "PNG.h" #include "CanvasRenderingContext2d.h" @@ -57,10 +58,10 @@ Canvas::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { #ifdef HAVE_JPEG Nan::SetPrototypeMethod(ctor, "streamJPEGSync", StreamJPEGSync); #endif - Nan::SetAccessor(proto, Nan::New("type").ToLocalChecked(), GetType); - Nan::SetAccessor(proto, Nan::New("stride").ToLocalChecked(), GetStride); - Nan::SetAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, SetWidth); - Nan::SetAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, SetHeight); + SetProtoAccessor(proto, Nan::New("type").ToLocalChecked(), GetType, NULL, ctor); + SetProtoAccessor(proto, Nan::New("stride").ToLocalChecked(), GetStride, NULL, ctor); + SetProtoAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, SetWidth, ctor); + SetProtoAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, SetHeight, ctor); Nan::SetTemplate(proto, "PNG_NO_FILTERS", Nan::New(PNG_NO_FILTERS)); Nan::SetTemplate(proto, "PNG_FILTER_NONE", Nan::New(PNG_FILTER_NONE)); diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index ecfb6f5f9..20a3aa6f4 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -12,6 +12,7 @@ #include #include +#include "Util.h" #include "Canvas.h" #include "Point.h" #include "Image.h" @@ -127,24 +128,24 @@ Context2d::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { Nan::SetPrototypeMethod(ctor, "_setTextBaseline", SetTextBaseline); Nan::SetPrototypeMethod(ctor, "_setTextAlignment", SetTextAlignment); Nan::SetPrototypeMethod(ctor, "_getMatrix", GetMatrix); - Nan::SetAccessor(proto, Nan::New("pixelFormat").ToLocalChecked(), GetFormat); - Nan::SetAccessor(proto, Nan::New("patternQuality").ToLocalChecked(), GetPatternQuality, SetPatternQuality); - Nan::SetAccessor(proto, Nan::New("globalCompositeOperation").ToLocalChecked(), GetGlobalCompositeOperation, SetGlobalCompositeOperation); - Nan::SetAccessor(proto, Nan::New("globalAlpha").ToLocalChecked(), GetGlobalAlpha, SetGlobalAlpha); - Nan::SetAccessor(proto, Nan::New("shadowColor").ToLocalChecked(), GetShadowColor, SetShadowColor); - Nan::SetAccessor(proto, Nan::New("fillColor").ToLocalChecked(), GetFillColor); - Nan::SetAccessor(proto, Nan::New("strokeColor").ToLocalChecked(), GetStrokeColor); - Nan::SetAccessor(proto, Nan::New("miterLimit").ToLocalChecked(), GetMiterLimit, SetMiterLimit); - Nan::SetAccessor(proto, Nan::New("lineWidth").ToLocalChecked(), GetLineWidth, SetLineWidth); - Nan::SetAccessor(proto, Nan::New("lineCap").ToLocalChecked(), GetLineCap, SetLineCap); - Nan::SetAccessor(proto, Nan::New("lineJoin").ToLocalChecked(), GetLineJoin, SetLineJoin); - Nan::SetAccessor(proto, Nan::New("lineDashOffset").ToLocalChecked(), GetLineDashOffset, SetLineDashOffset); - Nan::SetAccessor(proto, Nan::New("shadowOffsetX").ToLocalChecked(), GetShadowOffsetX, SetShadowOffsetX); - Nan::SetAccessor(proto, Nan::New("shadowOffsetY").ToLocalChecked(), GetShadowOffsetY, SetShadowOffsetY); - Nan::SetAccessor(proto, Nan::New("shadowBlur").ToLocalChecked(), GetShadowBlur, SetShadowBlur); - Nan::SetAccessor(proto, Nan::New("antialias").ToLocalChecked(), GetAntiAlias, SetAntiAlias); - Nan::SetAccessor(proto, Nan::New("textDrawingMode").ToLocalChecked(), GetTextDrawingMode, SetTextDrawingMode); - Nan::SetAccessor(proto, Nan::New("filter").ToLocalChecked(), GetFilter, SetFilter); + SetProtoAccessor(proto, Nan::New("pixelFormat").ToLocalChecked(), GetFormat, NULL, ctor); + SetProtoAccessor(proto, Nan::New("patternQuality").ToLocalChecked(), GetPatternQuality, SetPatternQuality, ctor); + SetProtoAccessor(proto, Nan::New("globalCompositeOperation").ToLocalChecked(), GetGlobalCompositeOperation, SetGlobalCompositeOperation, ctor); + SetProtoAccessor(proto, Nan::New("globalAlpha").ToLocalChecked(), GetGlobalAlpha, SetGlobalAlpha, ctor); + SetProtoAccessor(proto, Nan::New("shadowColor").ToLocalChecked(), GetShadowColor, SetShadowColor, ctor); + SetProtoAccessor(proto, Nan::New("fillColor").ToLocalChecked(), GetFillColor, NULL, ctor); + SetProtoAccessor(proto, Nan::New("strokeColor").ToLocalChecked(), GetStrokeColor, NULL, ctor); + SetProtoAccessor(proto, Nan::New("miterLimit").ToLocalChecked(), GetMiterLimit, SetMiterLimit, ctor); + SetProtoAccessor(proto, Nan::New("lineWidth").ToLocalChecked(), GetLineWidth, SetLineWidth, ctor); + SetProtoAccessor(proto, Nan::New("lineCap").ToLocalChecked(), GetLineCap, SetLineCap, ctor); + SetProtoAccessor(proto, Nan::New("lineJoin").ToLocalChecked(), GetLineJoin, SetLineJoin, ctor); + SetProtoAccessor(proto, Nan::New("lineDashOffset").ToLocalChecked(), GetLineDashOffset, SetLineDashOffset, ctor); + SetProtoAccessor(proto, Nan::New("shadowOffsetX").ToLocalChecked(), GetShadowOffsetX, SetShadowOffsetX, ctor); + SetProtoAccessor(proto, Nan::New("shadowOffsetY").ToLocalChecked(), GetShadowOffsetY, SetShadowOffsetY, ctor); + SetProtoAccessor(proto, Nan::New("shadowBlur").ToLocalChecked(), GetShadowBlur, SetShadowBlur, ctor); + SetProtoAccessor(proto, Nan::New("antialias").ToLocalChecked(), GetAntiAlias, SetAntiAlias, ctor); + SetProtoAccessor(proto, Nan::New("textDrawingMode").ToLocalChecked(), GetTextDrawingMode, SetTextDrawingMode, ctor); + SetProtoAccessor(proto, Nan::New("filter").ToLocalChecked(), GetFilter, SetFilter, ctor); Nan::Set(target, Nan::New("CanvasRenderingContext2d").ToLocalChecked(), ctor->GetFunction()); } diff --git a/src/Image.cc b/src/Image.cc index 73bfcc77e..8e0dbff19 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -4,6 +4,7 @@ // Copyright (c) 2010 LearnBoost // +#include "Util.h" #include "Canvas.h" #include "Image.h" #include @@ -45,16 +46,16 @@ Image::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { // Prototype Local proto = ctor->PrototypeTemplate(); - Nan::SetAccessor(proto, Nan::New("source").ToLocalChecked(), GetSource, SetSource); - Nan::SetAccessor(proto, Nan::New("complete").ToLocalChecked(), GetComplete); - Nan::SetAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, SetWidth); - Nan::SetAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, SetHeight); - Nan::SetAccessor(proto, Nan::New("naturalWidth").ToLocalChecked(), GetNaturalWidth); - Nan::SetAccessor(proto, Nan::New("naturalHeight").ToLocalChecked(), GetNaturalHeight); - Nan::SetAccessor(proto, Nan::New("onload").ToLocalChecked(), GetOnload, SetOnload); - Nan::SetAccessor(proto, Nan::New("onerror").ToLocalChecked(), GetOnerror, SetOnerror); + SetProtoAccessor(proto, Nan::New("source").ToLocalChecked(), GetSource, SetSource, ctor); + SetProtoAccessor(proto, Nan::New("complete").ToLocalChecked(), GetComplete, NULL, ctor); + SetProtoAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, NULL, ctor); + SetProtoAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, NULL, ctor); + SetProtoAccessor(proto, Nan::New("naturalWidth").ToLocalChecked(), GetNaturalWidth, NULL, ctor); + SetProtoAccessor(proto, Nan::New("naturalHeight").ToLocalChecked(), GetNaturalHeight, NULL, ctor); + SetProtoAccessor(proto, Nan::New("onload").ToLocalChecked(), GetOnload, SetOnload, ctor); + SetProtoAccessor(proto, Nan::New("onerror").ToLocalChecked(), GetOnerror, SetOnerror, ctor); #if CAIRO_VERSION_MINOR >= 10 - Nan::SetAccessor(proto, Nan::New("dataMode").ToLocalChecked(), GetDataMode, SetDataMode); + SetProtoAccessor(proto, Nan::New("dataMode").ToLocalChecked(), GetDataMode, SetDataMode, ctor); ctor->Set(Nan::New("MODE_IMAGE").ToLocalChecked(), Nan::New(DATA_IMAGE)); ctor->Set(Nan::New("MODE_MIME").ToLocalChecked(), Nan::New(DATA_MIME)); #endif diff --git a/src/ImageData.cc b/src/ImageData.cc index 1d47a01ed..e8e030ab5 100644 --- a/src/ImageData.cc +++ b/src/ImageData.cc @@ -5,6 +5,7 @@ // Copyright (c) 2010 LearnBoost // +#include "Util.h" #include "ImageData.h" Nan::Persistent ImageData::constructor; @@ -25,8 +26,8 @@ ImageData::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { // Prototype Local proto = ctor->PrototypeTemplate(); - Nan::SetAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth); - Nan::SetAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight); + SetProtoAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, NULL, ctor); + SetProtoAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, NULL, ctor); Nan::Set(target, Nan::New("ImageData").ToLocalChecked(), ctor->GetFunction()); } diff --git a/src/Util.h b/src/Util.h new file mode 100644 index 000000000..f0c703516 --- /dev/null +++ b/src/Util.h @@ -0,0 +1,25 @@ +#include +#include + +// Wrapper around Nan::SetAccessor that makes it easier to change the last +// argument (signature). Getters/setters must be accessed only when there is +// actually an instance, i.e. MyClass.prototype.getter1 should not try to +// unwrap the non-existent 'this'. See #803, #847, #885, nodejs/node#15099, ... +inline void SetProtoAccessor( + v8::Local tpl, + v8::Local name, + Nan::GetterCallback getter, + Nan::SetterCallback setter, + v8::Local ctor + ) { + Nan::SetAccessor( + tpl, + name, + getter, + setter, + v8::Local(), + v8::DEFAULT, + v8::None, + v8::AccessorSignature::New(v8::Isolate::GetCurrent(), ctor) + ); +} diff --git a/test/canvas.test.js b/test/canvas.test.js index 1500822fb..2b2fc58f8 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -17,6 +17,15 @@ const os = require('os') const Readable = require('stream').Readable describe('Canvas', function () { + it('Prototype and ctor are well-shaped, don\'t hit asserts on accessors (GH-803)', function () { + const Canvas = require('../').Canvas; + var c = new Canvas(10, 10); + assert.throws(function () { Canvas.prototype.width; }, /incompatible receiver/); + assert(!c.hasOwnProperty('width')); + assert('width' in c); + assert(Canvas.prototype.hasOwnProperty('width')); + }); + it('.parseFont()', function () { var tests = [ '20px Arial' diff --git a/test/image.test.js b/test/image.test.js index ae0fd5eac..875b8eb83 100644 --- a/test/image.test.js +++ b/test/image.test.js @@ -7,6 +7,7 @@ */ const loadImage = require('../').loadImage +const Image = require('../').Image; const assert = require('assert') const assertRejects = require('assert-rejects') @@ -16,6 +17,14 @@ const png_clock = `${__dirname}/fixtures/clock.png` const jpg_face = `${__dirname}/fixtures/face.jpeg` describe('Image', function () { + it('Prototype and ctor are well-shaped, don\'t hit asserts on accessors (GH-803)', function () { + var img = new Image(); + assert.throws(function () { Image.prototype.width; }, /incompatible receiver/); + assert(!img.hasOwnProperty('width')); + assert('width' in img); + assert(Image.prototype.hasOwnProperty('width')); + }); + it('loads JPEG image', function () { return loadImage(jpg_face).then((img) => { assert.strictEqual(img.onerror, null) diff --git a/test/imageData.test.js b/test/imageData.test.js index 30e60bbcd..cfffe9aaf 100644 --- a/test/imageData.test.js +++ b/test/imageData.test.js @@ -3,10 +3,15 @@ 'use strict' const createImageData = require('../').createImageData +const ImageData = require('../').ImageData; const assert = require('assert') describe('ImageData', function () { + it('Prototype and ctor are well-shaped, don\'t hit asserts on accessors (GH-803)', function () { + assert.throws(function () { ImageData.prototype.width; }, /incompatible receiver/); + }); + it('should throw with invalid numeric arguments', function () { assert.throws(() => { createImageData(0, 0) }, /width is zero/) assert.throws(() => { createImageData(1, 0) }, /height is zero/) From a7fb0412853fbdd99d7418518a28c891a2e1972e Mon Sep 17 00:00:00 2001 From: Benjamin Byholm Date: Mon, 11 Sep 2017 15:46:28 +0300 Subject: [PATCH 066/474] Correct external memory adjustment Fixes #979 --- src/backend/ImageBackend.cc | 11 ++++++----- src/backend/ImageBackend.h | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/backend/ImageBackend.cc b/src/backend/ImageBackend.cc index 2aa91a108..d74250b2a 100644 --- a/src/backend/ImageBackend.cc +++ b/src/backend/ImageBackend.cc @@ -8,14 +8,15 @@ ImageBackend::ImageBackend(int width, int height) ImageBackend::~ImageBackend() { - destroySurface(); - - Nan::AdjustExternalMemory(-1 * approxBytesPerPixel() * width * height); + if (surface) { + destroySurface(); + Nan::AdjustExternalMemory(-approxBytesPerPixel() * width * height); + } } // This returns an approximate value only, suitable for Nan::AdjustExternalMemory. // The formats that don't map to intrinsic types (RGB30, A1) round up. -uint32_t ImageBackend::approxBytesPerPixel() { +int32_t ImageBackend::approxBytesPerPixel() { switch (format) { case CAIRO_FORMAT_ARGB32: case CAIRO_FORMAT_RGB24: @@ -51,7 +52,7 @@ cairo_surface_t* ImageBackend::recreateSurface() int old_width = cairo_image_surface_get_width(this->surface); int old_height = cairo_image_surface_get_height(this->surface); this->destroySurface(); - Nan::AdjustExternalMemory(-1 * approxBytesPerPixel() * old_width * old_height); + Nan::AdjustExternalMemory(-approxBytesPerPixel() * old_width * old_height); } return createSurface(); diff --git a/src/backend/ImageBackend.h b/src/backend/ImageBackend.h index 2e5a82a7a..cfd1285e3 100644 --- a/src/backend/ImageBackend.h +++ b/src/backend/ImageBackend.h @@ -21,7 +21,7 @@ class ImageBackend : public Backend cairo_format_t getFormat(); void setFormat(cairo_format_t format); - uint32_t approxBytesPerPixel(); + int32_t approxBytesPerPixel(); static Nan::Persistent constructor; static void Initialize(v8::Handle target); From d1562b6747ceb28eb8845541784d4bf7fbf86672 Mon Sep 17 00:00:00 2001 From: Benjamin Byholm Date: Mon, 11 Sep 2017 15:48:36 +0300 Subject: [PATCH 067/474] Avoid setjmp local clobbering --- src/PNG.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/PNG.h b/src/PNG.h index 944795f24..2de130c3a 100644 --- a/src/PNG.h +++ b/src/PNG.h @@ -90,6 +90,12 @@ struct canvas_png_write_closure_t { closure_t *closure; }; +#ifdef PNG_SETJMP_SUPPORTED +bool setjmp_wrapper(png_structp png) { + return setjmp(png_jmpbuf(png)); +} +#endif + static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr write_func, canvas_png_write_closure_t *closure) { unsigned int i; cairo_status_t status = CAIRO_STATUS_SUCCESS; @@ -148,7 +154,7 @@ static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr writ } #ifdef PNG_SETJMP_SUPPORTED - if (setjmp (png_jmpbuf (png))) { + if (setjmp_wrapper(png)) { png_destroy_write_struct(&png, &info); free(rows); return status; From 8d87ab13e3a681dc95ebc87a49126594387b70af Mon Sep 17 00:00:00 2001 From: Benjamin Byholm Date: Mon, 11 Sep 2017 15:49:47 +0300 Subject: [PATCH 068/474] Prefer static_cast --- src/Image.cc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Image.cc b/src/Image.cc index 8e0dbff19..81bcf5f10 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -913,8 +913,9 @@ Image::decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len) { void clearMimeData(void *closure) { - Nan::AdjustExternalMemory(-static_cast(((read_closure_t *)closure)->len)); - free(((read_closure_t *) closure)->buf); + Nan::AdjustExternalMemory( + -static_cast((static_cast(closure)->len))); + free(static_cast(closure)->buf); free(closure); } From a522cfa80fbd22f9ce4e668574285f30a8f1abf5 Mon Sep 17 00:00:00 2001 From: Benjamin Byholm Date: Mon, 11 Sep 2017 15:50:40 +0300 Subject: [PATCH 069/474] Correct type in typecast --- src/closure.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/closure.cc b/src/closure.cc index 940210028..968da90a5 100644 --- a/src/closure.cc +++ b/src/closure.cc @@ -25,6 +25,6 @@ void closure_destroy(closure_t *closure) { if (closure->len) { free(closure->data); - Nan::AdjustExternalMemory(-((intptr_t) closure->max_len)); + Nan::AdjustExternalMemory(-static_cast(closure->max_len)); } } From ddd761eb80b483e4f837024630ab91aca3334f1a Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sun, 3 Sep 2017 18:25:39 -0400 Subject: [PATCH 070/474] node-pre-gyp install script for canvas-prebuilt --- .travis.yml | 2 ++ Readme.md | 6 +++++- package.json | 11 ++++++++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 747f4c97b..9b1276f57 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,6 @@ language: node_js +install: + - npm install --build-from-source node_js: - '8' - '6' diff --git a/Readme.md b/Readme.md index 66a649316..e278a75c8 100644 --- a/Readme.md +++ b/Readme.md @@ -27,10 +27,14 @@ $ npm install canvas ``` -Unless previously installed you'll _need_ __Cairo__ and __Pango__. For system-specific installation view the [Wiki](https://github.com/Automattic/node-canvas/wiki/_pages). +By default, binaries for macOS, Linux and Windows will be downloaded. If you want to build from source, use `npm install --build-from-source`. Currently the minimum version of node required is __4.0.0__ +### Compiling + +If you don't have a supported OS or processor architecture, or you use `--build-from-source`, the module will be compiled on your system. Unless previously installed you'll _need_ __Cairo__ and __Pango__. For system-specific installation view the [Wiki](https://github.com/Automattic/node-canvas/wiki/_pages). + You can quickly install the dependencies by using the command for your OS: OS | Command diff --git a/package.json b/package.json index dbb7d2884..9548c3ded 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "description": "Canvas graphics API backed by Cairo", "version": "2.0.0-alpha.5", "author": "TJ Holowaychuk ", + "main": "index.js", "browser": "browser.js", "contributors": [ "Nathan Rajlich ", @@ -27,9 +28,17 @@ "pretest": "node-gyp build", "test": "standard examples/*.js test/server.js test/public/*.js benchmark/run.js util/has_lib.js browser.js index.js && mocha test/*.test.js", "pretest-server": "node-gyp build", - "test-server": "node test/server.js" + "test-server": "node test/server.js", + "install": "node-pre-gyp install" + }, + "binary": { + "module_name": "canvas-prebuilt", + "module_path": "build/Release", + "host": "https://github.com/node-gfx/node-canvas-prebuilt/releases/download/", + "remote_path": "v{version}" }, "dependencies": { + "node-pre-gyp": "^0.6.36", "nan": "^2.4.0" }, "devDependencies": { From 1f4b646b9108542be9d6a984bd08eed0577f1cdf Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Fri, 15 Sep 2017 22:40:04 -0700 Subject: [PATCH 071/474] Fix memory leak when img CBs reference img Storing the onerror/onload callbacks in Persistent handles means they will never be GC'ed. If those functions have references to the image, then the image will never be GC'ed either. Fixes #785 Fixes #150 (for real) --- src/Image.cc | 105 ++++++--------------------------------------------- src/Image.h | 6 --- 2 files changed, 12 insertions(+), 99 deletions(-) diff --git a/src/Image.cc b/src/Image.cc index 81bcf5f10..6df2b06d9 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -52,8 +52,6 @@ Image::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { SetProtoAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, NULL, ctor); SetProtoAccessor(proto, Nan::New("naturalWidth").ToLocalChecked(), GetNaturalWidth, NULL, ctor); SetProtoAccessor(proto, Nan::New("naturalHeight").ToLocalChecked(), GetNaturalHeight, NULL, ctor); - SetProtoAccessor(proto, Nan::New("onload").ToLocalChecked(), GetOnload, SetOnload, ctor); - SetProtoAccessor(proto, Nan::New("onerror").ToLocalChecked(), GetOnerror, SetOnerror, ctor); #if CAIRO_VERSION_MINOR >= 10 SetProtoAccessor(proto, Nan::New("dataMode").ToLocalChecked(), GetDataMode, SetDataMode, ctor); ctor->Set(Nan::New("MODE_IMAGE").ToLocalChecked(), Nan::New(DATA_IMAGE)); @@ -74,6 +72,8 @@ NAN_METHOD(Image::New) { Image *img = new Image; img->data_mode = DATA_IMAGE; img->Wrap(info.This()); + info.This()->Set(Nan::New("onload").ToLocalChecked(), Nan::Null()); + info.This()->Set(Nan::New("onerror").ToLocalChecked(), Nan::Null()); info.GetReturnValue().Set(info.This()); } @@ -233,9 +233,17 @@ NAN_SETTER(Image::SetSource) { // check status if (status) { - img->error(Canvas::Error(status)); + Local onerrorFn = info.This()->Get(Nan::New("onerror").ToLocalChecked()); + if (onerrorFn->IsFunction()) { + Local argv[1] = { Canvas::Error(status) }; + onerrorFn.As()->Call(Isolate::GetCurrent()->GetCurrentContext()->Global(), 1, argv); + } } else { img->loaded(); + Local onloadFn = info.This()->Get(Nan::New("onload").ToLocalChecked()); + if (onloadFn->IsFunction()) { + onloadFn.As()->Call(Isolate::GetCurrent()->GetCurrentContext()->Global(), 0, NULL); + } } } @@ -305,66 +313,6 @@ Image::readPNG(void *c, uint8_t *data, unsigned int len) { return CAIRO_STATUS_SUCCESS; } -/* - * Get onload callback. - */ - -NAN_GETTER(Image::GetOnload) { - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - if (img->onload) { - info.GetReturnValue().Set(img->onload->GetFunction()); - } else { - info.GetReturnValue().SetNull(); - } -} - -/* - * Set onload callback. - */ - -NAN_SETTER(Image::SetOnload) { - if (value->IsFunction()) { - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - img->onload = new Nan::Callback(value.As()); - } else if (value->IsNull()) { - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - if (img->onload) { - delete img->onload; - } - img->onload = NULL; - } -} - -/* - * Get onerror callback. - */ - -NAN_GETTER(Image::GetOnerror) { - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - if (img->onerror) { - info.GetReturnValue().Set(img->onerror->GetFunction()); - } else { - info.GetReturnValue().SetNull(); - } -} - -/* - * Set onerror callback. - */ - -NAN_SETTER(Image::SetOnerror) { - if (value->IsFunction()) { - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - img->onerror = new Nan::Callback(value.As()); - } else if (value->IsNull()) { - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - if (img->onerror) { - delete img->onerror; - } - img->onerror = NULL; - } -} - /* * Initialize a new Image. */ @@ -377,8 +325,6 @@ Image::Image() { width = height = 0; naturalWidth = naturalHeight = 0; state = DEFAULT; - onload = NULL; - onerror = NULL; #ifdef HAVE_RSVG _rsvg = NULL; _is_svg = false; @@ -392,16 +338,6 @@ Image::Image() { Image::~Image() { clearData(); - - if (onerror) { - delete onerror; - onerror = NULL; - } - - if (onload) { - delete onload; - onload = NULL; - } } /* @@ -418,7 +354,7 @@ Image::load() { } /* - * Invoke onload (when assigned) and assign dimensions. + * Set state, assign dimensions. */ void @@ -430,23 +366,6 @@ Image::loaded() { height = naturalHeight = cairo_image_surface_get_height(_surface); _data_len = naturalHeight * cairo_image_surface_get_stride(_surface); Nan::AdjustExternalMemory(_data_len); - - if (onload != NULL) { - onload->Call(0, NULL); - } -} - -/* - * Invoke onerror (when assigned) with the given err. - */ - -void -Image::error(Local err) { - Nan::HandleScope scope; - if (onerror != NULL) { - Local argv[1] = { err }; - onerror->Call(1, argv); - } } /* diff --git a/src/Image.h b/src/Image.h index 054295cc7..d2e7e086d 100644 --- a/src/Image.h +++ b/src/Image.h @@ -40,14 +40,10 @@ class Image: public Nan::ObjectWrap { char *filename; int width, height; int naturalWidth, naturalHeight; - Nan::Callback *onload; - Nan::Callback *onerror; static Nan::Persistent constructor; static void Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target); static NAN_METHOD(New); static NAN_GETTER(GetSource); - static NAN_GETTER(GetOnload); - static NAN_GETTER(GetOnerror); static NAN_GETTER(GetComplete); static NAN_GETTER(GetWidth); static NAN_GETTER(GetHeight); @@ -55,8 +51,6 @@ class Image: public Nan::ObjectWrap { static NAN_GETTER(GetNaturalHeight); static NAN_GETTER(GetDataMode); static NAN_SETTER(SetSource); - static NAN_SETTER(SetOnload); - static NAN_SETTER(SetOnerror); static NAN_SETTER(SetDataMode); static NAN_SETTER(SetWidth); static NAN_SETTER(SetHeight); From 6a6e9636696ce35e63d0f4eed3a85fe1cdfeb14c Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Thu, 12 Oct 2017 11:39:44 -0400 Subject: [PATCH 072/474] no need to send unused ink_rect to Pango --- src/CanvasRenderingContext2d.cc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 767573167..3dca564ee 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -1850,8 +1850,8 @@ NAN_METHOD(Context2d::Stroke) { double get_text_scale(Context2d *context, char *str, double maxWidth) { PangoLayout *layout = context->layout(); - PangoRectangle ink_rect, logical_rect; - pango_layout_get_pixel_extents(layout, &ink_rect, &logical_rect); + PangoRectangle logical_rect; + pango_layout_get_pixel_extents(layout, NULL, &logical_rect); if (logical_rect.width > maxWidth) { return maxWidth / logical_rect.width; @@ -1951,7 +1951,7 @@ inline double getBaselineAdjustment(PangoFontMetrics* metrics, cairo_matrix_t ma void Context2d::setTextPath(const char *str, double x, double y) { - PangoRectangle ink_rect, logical_rect; + PangoRectangle logical_rect; cairo_matrix_t matrix; pango_layout_set_text(_layout, str, -1); @@ -1962,12 +1962,12 @@ Context2d::setTextPath(const char *str, double x, double y) { switch (state->textAlignment) { // center case 0: - pango_layout_get_pixel_extents(_layout, &ink_rect, &logical_rect); + pango_layout_get_pixel_extents(_layout, NULL, &logical_rect); x -= logical_rect.width / 2; break; // right case 1: - pango_layout_get_pixel_extents(_layout, &ink_rect, &logical_rect); + pango_layout_get_pixel_extents(_layout, NULL, &logical_rect); x -= logical_rect.width; break; } From bb25d01bdf7e575df7521aaa5f933e2356a5a3a0 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Thu, 12 Oct 2017 11:44:00 -0400 Subject: [PATCH 073/474] fix incorrect path to cairo --- src/backend/Backend.h | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/backend/Backend.h b/src/backend/Backend.h index a20215355..5cf6b3d86 100644 --- a/src/backend/Backend.h +++ b/src/backend/Backend.h @@ -7,12 +7,7 @@ #include #include - -#if HAVE_PANGO - #include -#else - #include -#endif +#include class Canvas; From 2bbfec501713657214a90e2c316bf61faa9b1034 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Thu, 12 Oct 2017 11:40:16 -0400 Subject: [PATCH 074/474] don't round values for measureText Return fractional software pixels instead of integers like the browser does. --- src/CanvasRenderingContext2d.cc | 23 ++++++++++++++++++++--- src/CanvasRenderingContext2d.h | 14 ++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 3dca564ee..ca25a7c12 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -2073,14 +2073,31 @@ NAN_METHOD(Context2d::MeasureText) { String::Utf8Value str(info[0]->ToString()); Local obj = Nan::New(); - PangoRectangle ink_rect, logical_rect; + PangoRectangle _ink_rect, _logical_rect; + float_rectangle ink_rect, logical_rect; PangoFontMetrics *metrics; PangoLayout *layout = context->layout(); pango_layout_set_text(layout, *str, -1); pango_cairo_update_layout(ctx, layout); - pango_layout_get_pixel_extents(layout, &ink_rect, &logical_rect); + // Normally you could use pango_layout_get_pixel_extents and be done, or use + // pango_extents_to_pixels, but both of those round the pixels, so we have to + // divide by PANGO_SCALE manually + pango_layout_get_extents(layout, &_ink_rect, &_logical_rect); + + float inverse_pango_scale = 1. / PANGO_SCALE; + + logical_rect.x = _logical_rect.x * inverse_pango_scale; + logical_rect.y = _logical_rect.y * inverse_pango_scale; + logical_rect.width = _logical_rect.width * inverse_pango_scale; + logical_rect.height = _logical_rect.height * inverse_pango_scale; + + ink_rect.x = _ink_rect.x * inverse_pango_scale; + ink_rect.y = _ink_rect.y * inverse_pango_scale; + ink_rect.width = _ink_rect.width * inverse_pango_scale; + ink_rect.height = _ink_rect.height * inverse_pango_scale; + metrics = PANGO_LAYOUT_GET_METRICS(layout); double x_offset; @@ -2114,7 +2131,7 @@ NAN_METHOD(Context2d::MeasureText) { obj->Set(Nan::New("emHeightDescent").ToLocalChecked(), Nan::New(PANGO_DESCENT(logical_rect) - y_offset)); obj->Set(Nan::New("alphabeticBaseline").ToLocalChecked(), - Nan::New(-(pango_font_metrics_get_ascent(metrics) / PANGO_SCALE - y_offset))); + Nan::New(-(pango_font_metrics_get_ascent(metrics) * inverse_pango_scale - y_offset))); pango_font_metrics_unref(metrics); diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index af373bfd3..6f439e0c1 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -48,6 +48,20 @@ typedef struct { PangoFontDescription *fontDescription; } canvas_state_t; +/* + * Equivalent to a PangoRectangle but holds floats instead of ints + * (software pixels are stored here instead of pango units) + * + * Should be compatible with PANGO_ASCENT, PANGO_LBEARING, etc. + */ + +typedef struct { + float x; + float y; + float width; + float height; +} float_rectangle; + void state_assign_fontFamily(canvas_state_t *state, const char *str); class Context2d: public Nan::ObjectWrap { From a7adc38b657b08b7b14ab4516eec069d9dfd006b Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Mon, 16 Oct 2017 15:24:55 -0700 Subject: [PATCH 075/474] Add info about installing 2.0 alpha --- Readme.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Readme.md b/Readme.md index 66a649316..68d6da4c1 100644 --- a/Readme.md +++ b/Readme.md @@ -2,9 +2,10 @@ ----- -## This is the documentation for the unreleased version 2.0 +## This is the documentation for version 2.0.0-alpha +Alpha versions of 2.0 can be installed using `npm install canvas@next`. -**For the current version 1.x documentation, see [the v1.x branch](https://github.com/Automattic/node-canvas/tree/v1.x)** +**For version 1.x documentation, see [the v1.x branch](https://github.com/Automattic/node-canvas/tree/v1.x)** ----- From e07fece29e5dcf615ed9f967702715e0299c8a9e Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Wed, 1 Nov 2017 15:44:34 -0700 Subject: [PATCH 076/474] Fix typo in example --- examples/image-src.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/image-src.js b/examples/image-src.js index 8d10aec58..49d7901b2 100644 --- a/examples/image-src.js +++ b/examples/image-src.js @@ -1,5 +1,5 @@ var fs = require('fs') -var path = require(path) +var path = require('path') var Canvas = require('..') var Image = Canvas.Image From 72f949aba90ea59cac3b2f6e47d057c43a460140 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Wed, 1 Nov 2017 15:49:00 -0700 Subject: [PATCH 077/474] Fix require statement for currentTransform --- lib/context2d.js | 2 +- test/canvas.test.js | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/context2d.js b/lib/context2d.js index da7384a82..7174355fc 100644 --- a/lib/context2d.js +++ b/lib/context2d.js @@ -16,7 +16,7 @@ const Context2d = module.exports = bindings.CanvasRenderingContext2d const CanvasGradient = bindings.CanvasGradient const CanvasPattern = bindings.CanvasPattern const ImageData = bindings.ImageData -const DOMMatrix = require('./DOMMatrix') +const DOMMatrix = require('./DOMMatrix').DOMMatrix /** * Text baselines. diff --git a/test/canvas.test.js b/test/canvas.test.js index 2b2fc58f8..f3fb3ddfe 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -799,6 +799,20 @@ describe('Canvas', function () { }); }); + it('Context2d#currentTransform', function () { + var canvas = createCanvas(20, 20); + var ctx = canvas.getContext('2d'); + + ctx.scale(0.1, 0.3); + var actual = ctx.currentTransform; + assert.equal(actual.a, 0.1); + assert.equal(actual.b, 0); + assert.equal(actual.c, 0); + assert.equal(actual.d, 0.3); + assert.equal(actual.e, 0); + assert.equal(actual.f, 0); + }); + it('Context2d#createImageData(ImageData)', function () { var canvas = createCanvas(20, 20) , ctx = canvas.getContext('2d'); From f284dc5dffba44962deb39194ed86163aa4eeb51 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sun, 5 Nov 2017 12:08:02 -0800 Subject: [PATCH 078/474] Add appveyor testing for windows. (#672) --- appveyor.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 appveyor.yml diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..fe7d2e13c --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,29 @@ +version: 0.0.{build} +environment: + matrix: + - nodejs_version: "6" +image: + - Visual Studio 2013 + - Visual Studio 2015 +install: + - ps: Install-Product node $env:nodejs_version x64 + # Sets Windows 7.1 SDK env vars. + - '"C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.cmd" /x64' + # Prepend 2015 and 2013 tools. Harmless when the path for the other version doesn't exist. + - set "PATH=%ProgramFiles(x86)%\MSBuild\14.0\Bin;%ProgramFiles(x86)%\MSBuild\12.0\Bin;%PATH%" + # Upgrade npm to latest + - npm install --loglevel error -g npm + - set "PATH=%APPDATA%\npm;%PATH%" + - node -v + - npm -v + - npm install -g --loglevel error node-gyp + # Put GTK in C:/ + - curl -fLsS -o "gtk.zip" "http://ftp.gnome.org/pub/GNOME/binaries/win64/gtk+/2.22/gtk+-bundle_2.22.1-20101229_win64.zip" + - 7z x gtk.zip -oGTK > nul + - mv GTK/ C:/ + - curl -fLsS -o "libjpeg.exe" "https://downloads.sourceforge.net/project/libjpeg-turbo/1.5.2/libjpeg-turbo-1.5.2-vc64.exe" + - .\libjpeg.exe /S + - npm install +build: off +test_script: + - cmd: npm test From bd72c0ce85cbb3da3dfd72dd404244411d5dd4eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Mon, 6 Nov 2017 12:15:44 +0000 Subject: [PATCH 079/474] 2.0.0-alpha.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dbb7d2884..01239dc93 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.0.0-alpha.5", + "version": "2.0.0-alpha.6", "author": "TJ Holowaychuk ", "browser": "browser.js", "contributors": [ From af2d6e75d1a41d4ed4ca320e9379840ac2ca6b7b Mon Sep 17 00:00:00 2001 From: Michael Heuberger Date: Tue, 14 Nov 2017 12:33:01 +1300 Subject: [PATCH 080/474] Add unit test for valid jpeg EOI marker --- test/canvas.test.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/canvas.test.js b/test/canvas.test.js index f3fb3ddfe..0de8c026d 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -1289,6 +1289,26 @@ describe('Canvas', function () { }); }); + // based on https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format + // end of image marker (FF D9) must exist to maintain JPEG standards + it('EOI at end of Canvas#jpegStream()', function (done) { + var canvas = createCanvas(640, 480); + var stream = canvas.jpegStream(); + var chunks = [] + stream.on('data', function(chunk){ + chunks.push(chunk) + }); + stream.on('end', function(){ + var lastTwoBytes = chunks.pop().slice(-2) + assert.equal(0xFF, lastTwoBytes[0]); + assert.equal(0xD9, lastTwoBytes[1]); + done(); + }); + stream.on('error', function(err) { + done(err); + }); + }); + it('Canvas#jpegStream() should clamp buffer size (#674)', function (done) { var c = createCanvas(10, 10); var SIZE = 10 * 1024 * 1024; From f950be3e282e62243f78f137577f79e836ed7db8 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Thu, 16 Nov 2017 14:27:14 -0500 Subject: [PATCH 081/474] use the PangoLayout to get baseline Instead of getting it via the PangoContext, which seems to be buggy Fixes #1037 --- src/CanvasRenderingContext2d.cc | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 9469d5c32..d7c496632 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -1945,19 +1945,23 @@ NAN_METHOD(Context2d::StrokeText) { } /* - * Gets the baseline adjustment in device pixels, taking into account the - * transformation matrix. TODO This does not handle skew (which cannot easily - * be extracted from the matrix separately from rotation). + * Gets the baseline adjustment in device pixels */ -inline double getBaselineAdjustment(PangoFontMetrics* metrics, cairo_matrix_t matrix, short baseline) { - double yScale = sqrt(matrix.yx * matrix.yx + matrix.yy * matrix.yy); +inline double getBaselineAdjustment(PangoLayout* layout, short baseline) { + PangoRectangle logical_rect; + pango_layout_line_get_extents(pango_layout_get_line(layout, 0), NULL, &logical_rect); + + double scale = 1.0 / PANGO_SCALE; + double ascent = scale * pango_layout_get_baseline(layout); + double descent = scale * logical_rect.height - ascent; + switch (baseline) { case TEXT_BASELINE_ALPHABETIC: - return (pango_font_metrics_get_ascent(metrics) / PANGO_SCALE) * yScale; + return ascent; case TEXT_BASELINE_MIDDLE: - return ((pango_font_metrics_get_ascent(metrics) + pango_font_metrics_get_descent(metrics)) / (2.0 * PANGO_SCALE)) * yScale; + return (ascent + descent) / 2.0; case TEXT_BASELINE_BOTTOM: - return ((pango_font_metrics_get_ascent(metrics) + pango_font_metrics_get_descent(metrics)) / PANGO_SCALE) * yScale; + return ascent + descent; default: return 0; } @@ -1970,13 +1974,10 @@ inline double getBaselineAdjustment(PangoFontMetrics* metrics, cairo_matrix_t ma void Context2d::setTextPath(const char *str, double x, double y) { PangoRectangle logical_rect; - cairo_matrix_t matrix; pango_layout_set_text(_layout, str, -1); pango_cairo_update_layout(_context, _layout); - cairo_get_matrix(_context, &matrix); - switch (state->textAlignment) { // center case 0: @@ -1990,9 +1991,7 @@ Context2d::setTextPath(const char *str, double x, double y) { break; } - PangoFontMetrics *metrics = PANGO_LAYOUT_GET_METRICS(_layout); - y -= getBaselineAdjustment(metrics, matrix, state->textBaseline); - pango_font_metrics_unref(metrics); + y -= getBaselineAdjustment(_layout, state->textBaseline); cairo_move_to(_context, x, y); if (state->textDrawingMode == TEXT_DRAW_PATHS) { @@ -2132,7 +2131,7 @@ NAN_METHOD(Context2d::MeasureText) { cairo_matrix_t matrix; cairo_get_matrix(ctx, &matrix); - double y_offset = getBaselineAdjustment(metrics, matrix, context->state->textBaseline); + double y_offset = getBaselineAdjustment(layout, context->state->textBaseline); obj->Set(Nan::New("width").ToLocalChecked(), Nan::New(logical_rect.width)); From 4c00a704a2a46a6e0012b08ba1cf66135906b947 Mon Sep 17 00:00:00 2001 From: Mathias Bynens Date: Fri, 24 Nov 2017 13:28:31 +0100 Subject: [PATCH 082/474] Update README example per #802 --- Readme.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Readme.md b/Readme.md index 68d6da4c1..24c3d9ec6 100644 --- a/Readme.md +++ b/Readme.md @@ -123,7 +123,7 @@ If image data is not tracked, and the Image is drawn to an image rather than a P ### Canvas#pngStream(options) - To create a `PNGStream` simply call `canvas.pngStream()`, and the stream will start to emit _data_ events, finally emitting _end_ when finished. If an exception occurs the _error_ event is emitted. + To create a `PNGStream` simply call `canvas.pngStream()`, and the stream will start to emit _data_ events, emitting _end_ when the data stream ends. If an exception occurs the _error_ event is emitted. ```javascript var fs = require('fs') @@ -135,7 +135,11 @@ stream.on('data', function(chunk){ }); stream.on('end', function(){ - console.log('saved png'); + console.log('The PNG stream ended'); +}); + +out.on('finish', function(){ + console.log('The PNG file was created.'); }); ``` From b180ea5d88793072cf4b661279d812d125667acd Mon Sep 17 00:00:00 2001 From: Andrey Azov Date: Wed, 29 Nov 2017 15:20:12 +0300 Subject: [PATCH 083/474] Add path for libjpeg detection on CentOS --- util/has_lib.js | 1 + 1 file changed, 1 insertion(+) diff --git a/util/has_lib.js b/util/has_lib.js index 00e0ebfa8..2cfbbc916 100644 --- a/util/has_lib.js +++ b/util/has_lib.js @@ -5,6 +5,7 @@ var childProcess = require('child_process') var SYSTEM_PATHS = [ '/lib', '/usr/lib', + '/usr/lib64', '/usr/local/lib', '/opt/local/lib', '/usr/lib/x86_64-linux-gnu', From d1bb195528cbbb8c46744bf14a11324e28882186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Mon, 4 Dec 2017 16:34:45 +0000 Subject: [PATCH 084/474] 2.0.0-alpha.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 01239dc93..9a1fcbcb9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.0.0-alpha.6", + "version": "2.0.0-alpha.7", "author": "TJ Holowaychuk ", "browser": "browser.js", "contributors": [ From c5f74e68be9b1f845d66e4e67d4c609a31c9249f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Zasso?= Date: Mon, 11 Dec 2017 15:27:18 +0100 Subject: [PATCH 085/474] Make Context2d's currentTransform configurable This allows the library to be used with the jest test runner. --- lib/context2d.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/context2d.js b/lib/context2d.js index 7174355fc..e2e97b2d6 100644 --- a/lib/context2d.js +++ b/lib/context2d.js @@ -115,7 +115,8 @@ Object.defineProperty(Context2d.prototype, 'currentTransform', { throw new TypeError('Expected DOMMatrix') } this.setTransform(m.a, m.b, m.c, m.d, m.e, m.f) - } + }, + configurable: true }) /** From e79f0a9f5f69ecbcf64ed7a27969b9ff72e8a2eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Tue, 12 Dec 2017 15:04:28 +0000 Subject: [PATCH 086/474] 2.0.0-alpha.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9a1fcbcb9..b5349ccad 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.0.0-alpha.7", + "version": "2.0.0-alpha.8", "author": "TJ Holowaychuk ", "browser": "browser.js", "contributors": [ From c20a296138cbca5ae933b4ac6d71c0188e2447cb Mon Sep 17 00:00:00 2001 From: Nicholas Sherlock Date: Sat, 16 Dec 2017 09:55:20 +1300 Subject: [PATCH 087/474] Add install instructions for MacPorts on OS X --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 24c3d9ec6..ec402d251 100644 --- a/Readme.md +++ b/Readme.md @@ -36,7 +36,7 @@ You can quickly install the dependencies by using the command for your OS: OS | Command ----- | ----- -OS X | `brew install pkg-config cairo pango libpng jpeg giflib` +OS X | Using [Homebrew](https://brew.sh/):
`brew install pkg-config cairo pango libpng jpeg giflib`

Using [MacPorts](https://www.macports.org/):
`port install pkgconfig cairo pango libpng jpeg giflib` Ubuntu | `sudo apt-get install libcairo2-dev libjpeg8-dev libpango1.0-dev libgif-dev build-essential g++` Fedora | `sudo yum install cairo cairo-devel cairomm-devel libjpeg-turbo-devel pango pango-devel pangomm pangomm-devel giflib-devel` Solaris | `pkgin install cairo pango pkg-config xproto renderproto kbproto xextproto` From 39a8e19f8a8d0aa785a9c216f273b9b69a47e8c5 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Tue, 19 Dec 2017 10:15:50 -0800 Subject: [PATCH 088/474] Update readme to use libjpeg-dev dummy package Resolves to libjpeg8 for pre-Jessie and libjpeg62-turbo for Jessie+ --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index ec402d251..41da890ff 100644 --- a/Readme.md +++ b/Readme.md @@ -37,7 +37,7 @@ You can quickly install the dependencies by using the command for your OS: OS | Command ----- | ----- OS X | Using [Homebrew](https://brew.sh/):
`brew install pkg-config cairo pango libpng jpeg giflib`

Using [MacPorts](https://www.macports.org/):
`port install pkgconfig cairo pango libpng jpeg giflib` -Ubuntu | `sudo apt-get install libcairo2-dev libjpeg8-dev libpango1.0-dev libgif-dev build-essential g++` +Ubuntu | `sudo apt-get install libcairo2-dev libjpeg-dev libpango1.0-dev libgif-dev build-essential g++` Fedora | `sudo yum install cairo cairo-devel cairomm-devel libjpeg-turbo-devel pango pango-devel pangomm pangomm-devel giflib-devel` Solaris | `pkgin install cairo pango pkg-config xproto renderproto kbproto xextproto` Windows | [Instructions on our wiki](https://github.com/Automattic/node-canvas/wiki/Installation---Windows) From 7c9ec58db59d4b9d857d644cf5ed3a0b6a52cc1f Mon Sep 17 00:00:00 2001 From: Paul Morelle Date: Sun, 17 Dec 2017 22:35:10 +0100 Subject: [PATCH 089/474] Sanitize some parameters before HasInstance calls Some parameters were passed to HasInstance without being checked first. If they were null, this would crash the entire nodejs engine. Throw a proper exception instead. --- src/CanvasRenderingContext2d.cc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index d7c496632..01f15c40c 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -503,6 +503,8 @@ NAN_METHOD(Context2d::New) { return Nan::ThrowTypeError("Class constructors cannot be invoked without 'new'"); } + if (!info[0]->IsObject()) + return Nan::ThrowTypeError("Canvas expected"); Local obj = info[0]->ToObject(); if (!Nan::New(Canvas::constructor)->HasInstance(obj)) return Nan::ThrowTypeError("Canvas expected"); @@ -947,6 +949,10 @@ NAN_METHOD(Context2d::GetImageData) { NAN_METHOD(Context2d::DrawImage) { if (info.Length() < 3) return Nan::ThrowTypeError("invalid arguments"); + if (!info[0]->IsObject() + || !info[1]->IsNumber() + || !info[2]->IsNumber()) + return Nan::ThrowTypeError("Expected object, number and number"); float sx = 0 , sy = 0 @@ -1555,6 +1561,8 @@ NAN_METHOD(Context2d::IsPointInPath) { */ NAN_METHOD(Context2d::SetFillPattern) { + if (!info[0]->IsObject()) + return Nan::ThrowTypeError("Gradient or Pattern expected"); Local obj = info[0]->ToObject(); if (Nan::New(Gradient::constructor)->HasInstance(obj)){ Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -1574,6 +1582,8 @@ NAN_METHOD(Context2d::SetFillPattern) { */ NAN_METHOD(Context2d::SetStrokePattern) { + if (!info[0]->IsObject()) + return Nan::ThrowTypeError("Gradient or Pattern expected"); Local obj = info[0]->ToObject(); if (Nan::New(Gradient::constructor)->HasInstance(obj)){ Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); From 727cb2cd71325998a394fdb53b5fe5a561e29c90 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Thu, 21 Dec 2017 19:43:18 +0100 Subject: [PATCH 090/474] Make setLineDash able to handle full zeroed dashes (#1060) * added fix and test Fixes #1055 --- src/CanvasRenderingContext2d.cc | 10 ++++++++-- test/public/tests.js | 5 ++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 01f15c40c..5ee7037f3 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -2193,12 +2193,13 @@ NAN_METHOD(Context2d::SetLineDash) { if (!info[0]->IsArray()) return; Local dash = Local::Cast(info[0]); uint32_t dashes = dash->Length() & 1 ? dash->Length() * 2 : dash->Length(); - + uint32_t zero_dashes = 0; std::vector a(dashes); for (uint32_t i=0; i d = dash->Get(i % dash->Length()); if (!d->IsNumber()) return; a[i] = d->NumberValue(); + if (a[i] == 0) zero_dashes++; if (a[i] < 0 || isnan(a[i]) || isinf(a[i])) return; } @@ -2206,7 +2207,12 @@ NAN_METHOD(Context2d::SetLineDash) { cairo_t *ctx = context->context(); double offset; cairo_get_dash(ctx, NULL, &offset); - cairo_set_dash(ctx, a.data(), dashes, offset); + if (zero_dashes == dashes) { + std::vector b(0); + cairo_set_dash(ctx, b.data(), 0, offset); + } else { + cairo_set_dash(ctx, a.data(), dashes, offset); + } } /* diff --git a/test/public/tests.js b/test/public/tests.js index 6b823cdc3..055825a8b 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -2069,7 +2069,7 @@ tests['putImageData() png data 3'] = function (ctx, done) { tests['setLineDash'] = function (ctx) { ctx.setLineDash([10, 5, 25, 15]) - ctx.lineWidth = 17 + ctx.lineWidth = 14 var y = 5 var line = function (lineDash, color) { @@ -2096,6 +2096,9 @@ tests['setLineDash'] = function (ctx) { a.push(20) return a })(), 'orange') + line([0, 0], 'purple') // should be full + line([0, 0, 3, 0], 'orange') // should be full + line([0, 3, 0, 0], 'green') // should be empty } tests['lineDashOffset'] = function (ctx) { From b18ae158f2ddcfbd9c349382b33e53202c34151f Mon Sep 17 00:00:00 2001 From: David Caldwell Date: Thu, 26 Oct 2017 20:09:57 -0700 Subject: [PATCH 091/474] Fix reading fillStyle after setting it from gradient to color --- lib/context2d.js | 1 + test/public/tests.js | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/lib/context2d.js b/lib/context2d.js index e2e97b2d6..7d86ca9ac 100644 --- a/lib/context2d.js +++ b/lib/context2d.js @@ -132,6 +132,7 @@ Context2d.prototype.__defineSetter__('fillStyle', function(val){ this.lastFillStyle = val; this._setFillPattern(val); } else if ('string' == typeof val) { + this.lastFillStyle = undefined; this._setFillColor(val); } }); diff --git a/test/public/tests.js b/test/public/tests.js index 055825a8b..2a1830271 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -447,6 +447,10 @@ tests['createLinearGradient()'] = function (ctx) { ctx.fillRect(10, 10, 130, 130) ctx.strokeRect(50, 50, 50, 50) + + ctx.fillStyle = '#13b575' + ctx.fillStyle = ctx.fillStyle + ctx.fillRect(65, 65, 20, 20) } tests['createRadialGradient()'] = function (ctx) { From f26446087871ed27787b8be4a85601ec4de61376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Do=C4=9Fa=20Budak?= Date: Thu, 21 Dec 2017 16:13:51 -0800 Subject: [PATCH 092/474] Update Readme.md --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 41da890ff..9b55691a0 100644 --- a/Readme.md +++ b/Readme.md @@ -42,7 +42,7 @@ Fedora | `sudo yum install cairo cairo-devel cairomm-devel libjpeg-turbo-devel p Solaris | `pkgin install cairo pango pkg-config xproto renderproto kbproto xextproto` Windows | [Instructions on our wiki](https://github.com/Automattic/node-canvas/wiki/Installation---Windows) -**El Capitan users:** If you have recently updated to El Capitan and are experiencing trouble when compiling, run the following command: `xcode-select --install`. Read more about the problem [on Stack Overflow](http://stackoverflow.com/a/32929012/148072). +**Mac OS X v10.11+:** If you have recently updated to Mac OS X v10.11+ and are experiencing trouble when compiling, run the following command: `xcode-select --install`. Read more about the problem [on Stack Overflow](http://stackoverflow.com/a/32929012/148072). ## Screencasts From 6345b0e70d8963b89e690ae0836d05cf3b14489e Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Tue, 26 Dec 2017 22:46:51 -0800 Subject: [PATCH 093/474] Define CAIRO_FORMAT_INVALID for old Cairo versions Fixes #1067 Fixes #1039 Fixes #1012? --- src/backend/Backend.h | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/backend/Backend.h b/src/backend/Backend.h index 5cf6b3d86..dbb91c00f 100644 --- a/src/backend/Backend.h +++ b/src/backend/Backend.h @@ -50,7 +50,14 @@ class Backend : public Nan::ObjectWrap virtual void setHeight(int height); // Overridden by ImageBackend. SVG and PDF thus always return INVALID. - virtual cairo_format_t getFormat() { return CAIRO_FORMAT_INVALID; } + virtual cairo_format_t getFormat() { +#ifndef CAIRO_FORMAT_INVALID + // For old Cairo (CentOS) support + return static_cast(-1); +#else + return CAIRO_FORMAT_INVALID; +#endif + } }; From 1eb1aa0ecd367b6d2afc33952316d1398e4e39c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Wed, 27 Dec 2017 11:08:29 +0100 Subject: [PATCH 094/474] 2.0.0-alpha.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b5349ccad..50ec481ca 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.0.0-alpha.8", + "version": "2.0.0-alpha.9", "author": "TJ Holowaychuk ", "browser": "browser.js", "contributors": [ From 5c28c0981c98d9dd859144a736531bc3ed5d02bb Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Wed, 27 Dec 2017 19:27:07 +0100 Subject: [PATCH 095/474] Add support for pattern repeat and no-repeat (#1066) --- lib/context2d.js | 3 +-- src/CanvasPattern.cc | 24 +++++++++++++++++++----- src/CanvasPattern.h | 19 ++++++++++++++++--- src/CanvasRenderingContext2d.cc | 16 +++++++++++++--- test/public/tests.js | 17 +++++++++++++++++ 5 files changed, 66 insertions(+), 13 deletions(-) diff --git a/lib/context2d.js b/lib/context2d.js index 7d86ca9ac..61c29efa5 100644 --- a/lib/context2d.js +++ b/lib/context2d.js @@ -55,8 +55,7 @@ Context2d.prototype.__defineGetter__('imageSmoothingEnabled', function(val){ */ Context2d.prototype.createPattern = function(image, repetition){ - // TODO Use repetition (currently always 'repeat') - return new CanvasPattern(image); + return new CanvasPattern(image, repetition || 'repeat'); }; /** diff --git a/src/CanvasPattern.cc b/src/CanvasPattern.cc index ed64f9ae7..e3c97a129 100644 --- a/src/CanvasPattern.cc +++ b/src/CanvasPattern.cc @@ -9,6 +9,8 @@ #include "Image.h" #include "CanvasPattern.h" +const cairo_user_data_key_t *pattern_repeat_key; + Nan::Persistent Pattern::constructor; /* @@ -57,24 +59,36 @@ NAN_METHOD(Pattern::New) { } else if (Nan::New(Canvas::constructor)->HasInstance(obj)) { Canvas *canvas = Nan::ObjectWrap::Unwrap(obj); surface = canvas->surface(); - // Invalid } else { return Nan::ThrowTypeError("Image or Canvas expected"); } - - Pattern *pattern = new Pattern(surface); + repeat_type_t repeat = REPEAT; + if (0 == strcmp("no-repeat", *String::Utf8Value(info[1]))) { + repeat = NO_REPEAT; + } else if (0 == strcmp("repeat-x", *String::Utf8Value(info[1]))) { + repeat = REPEAT_X; + } else if (0 == strcmp("repeat-y", *String::Utf8Value(info[1]))) { + repeat = REPEAT_Y; + } + Pattern *pattern = new Pattern(surface, repeat); pattern->Wrap(info.This()); info.GetReturnValue().Set(info.This()); } - /* * Initialize linear gradient. */ -Pattern::Pattern(cairo_surface_t *surface) { +Pattern::Pattern(cairo_surface_t *surface, repeat_type_t repeat) { _pattern = cairo_pattern_create_for_surface(surface); + _repeat = repeat; + cairo_pattern_set_user_data(_pattern, pattern_repeat_key, &_repeat, NULL); +} + +repeat_type_t Pattern::get_repeat_type_for_cairo_pattern(cairo_pattern_t *pattern) { + void *ud = cairo_pattern_get_user_data(pattern, pattern_repeat_key); + return *reinterpret_cast(ud); } /* diff --git a/src/CanvasPattern.h b/src/CanvasPattern.h index 102c74f7a..afbd4703e 100644 --- a/src/CanvasPattern.h +++ b/src/CanvasPattern.h @@ -10,18 +10,31 @@ #include "Canvas.h" +/* + * Canvas types. + */ + +typedef enum { + NO_REPEAT, // match CAIRO_EXTEND_NONE + REPEAT, // match CAIRO_EXTEND_REPEAT + REPEAT_X, // needs custom processing + REPEAT_Y // needs custom processing +} repeat_type_t; + +extern const cairo_user_data_key_t *pattern_repeat_key; + class Pattern: public Nan::ObjectWrap { public: static Nan::Persistent constructor; static void Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target); static NAN_METHOD(New); - Pattern(cairo_surface_t *surface); + static repeat_type_t get_repeat_type_for_cairo_pattern(cairo_pattern_t *pattern); + Pattern(cairo_surface_t *surface, repeat_type_t repeat); inline cairo_pattern_t *pattern(){ return _pattern; } - private: ~Pattern(); - // TODO REPEAT/REPEAT_X/REPEAT_Y cairo_pattern_t *_pattern; + repeat_type_t _repeat; }; #endif diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 5ee7037f3..0108bab9f 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -263,8 +263,13 @@ void Context2d::fill(bool preserve) { if (state->fillPattern) { cairo_set_source(_context, state->fillPattern); - cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_REPEAT); - // TODO repeat/repeat-x/repeat-y + repeat_type_t repeat = Pattern::get_repeat_type_for_cairo_pattern(state->fillPattern); + if (NO_REPEAT == repeat) { + cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_NONE); + } else { + cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_REPEAT); + } + // TODO repeat-x/repeat-y } else if (state->fillGradient) { cairo_pattern_set_filter(state->fillGradient, state->patternQuality); cairo_set_source(_context, state->fillGradient); @@ -291,7 +296,12 @@ void Context2d::stroke(bool preserve) { if (state->strokePattern) { cairo_set_source(_context, state->strokePattern); - cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_REPEAT); + repeat_type_t repeat = Pattern::get_repeat_type_for_cairo_pattern(state->strokePattern); + if (NO_REPEAT == repeat) { + cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_NONE); + } else { + cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_REPEAT); + } } else if (state->strokeGradient) { cairo_pattern_set_filter(state->strokeGradient, state->patternQuality); cairo_set_source(_context, state->strokeGradient); diff --git a/test/public/tests.js b/test/public/tests.js index 2a1830271..80fdf2a5e 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -431,6 +431,23 @@ tests['clip() 2'] = function (ctx) { } } +tests['createPattern() no-repeat'] = function (ctx, done) { + var img = new Image() + img.onload = function () { + ctx.scale(0.1, 0.1) + ctx.lineStyle = 'black' + ctx.lineWidth = 10 + ctx.fillStyle = ctx.createPattern(img, 'no-repeat') + ctx.fillRect(0, 0, 900, 900) + ctx.strokeRect(0, 0, 900, 900) + ctx.fillStyle = ctx.createPattern(img, 'repeat') + ctx.fillRect(1000, 1000, 900, 900) + ctx.strokeRect(1000, 1000, 900, 900) + done() + } + img.src = imageSrc('face.jpeg') +} + tests['createLinearGradient()'] = function (ctx) { var lingrad = ctx.createLinearGradient(0, 0, 0, 150) lingrad.addColorStop(0, '#00ABEB') From d17e9ce9f5e1c0855f9b94ca09e4bc2fef7efc35 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Thu, 28 Dec 2017 02:21:19 +0100 Subject: [PATCH 096/474] Add support for context globalAlpha for gradients and patterns (#1064) Fixes #1061 --- src/CanvasPattern.cc | 2 +- src/CanvasRenderingContext2d.cc | 119 +++++++++++++++++++++++++++++--- test/public/tests.js | 82 ++++++++++++++++++++++ 3 files changed, 194 insertions(+), 9 deletions(-) diff --git a/src/CanvasPattern.cc b/src/CanvasPattern.cc index e3c97a129..a2c7671d3 100644 --- a/src/CanvasPattern.cc +++ b/src/CanvasPattern.cc @@ -77,7 +77,7 @@ NAN_METHOD(Pattern::New) { } /* - * Initialize linear gradient. + * Initialize pattern. */ Pattern::Pattern(cairo_surface_t *surface, repeat_type_t repeat) { diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 0108bab9f..7212cc020 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -243,6 +243,67 @@ Context2d::restorePath() { cairo_path_destroy(_path); } +/* + * Create temporary surface for gradient or pattern transparency + */ +cairo_pattern_t* +create_transparent_gradient(cairo_pattern_t *source, float alpha) { + double x0; + double y0; + double x1; + double y1; + double r0; + double r1; + int count; + int i; + double offset; + double r; + double g; + double b; + double a; + cairo_pattern_t *newGradient; + cairo_pattern_type_t type = cairo_pattern_get_type(source); + cairo_pattern_get_color_stop_count(source, &count); + if (type == CAIRO_PATTERN_TYPE_LINEAR) { + cairo_pattern_get_linear_points (source, &x0, &y0, &x1, &y1); + newGradient = cairo_pattern_create_linear(x0, y0, x1, y1); + } else if (type == CAIRO_PATTERN_TYPE_RADIAL) { + cairo_pattern_get_radial_circles(source, &x0, &y0, &r0, &x1, &y1, &r1); + newGradient = cairo_pattern_create_radial(x0, y0, r0, x1, y1, r1); + } else { + Nan::ThrowError("Unexpected gradient type"); + return NULL; + } + for ( i = 0; i < count; i++ ) { + cairo_pattern_get_color_stop_rgba(source, i, &offset, &r, &g, &b, &a); + cairo_pattern_add_color_stop_rgba(newGradient, offset, r, g, b, a * alpha); + } + return newGradient; +} + +cairo_pattern_t* +create_transparent_pattern(cairo_pattern_t *source, float alpha) { + cairo_surface_t *surface; + cairo_pattern_get_surface(source, &surface); + int width = cairo_image_surface_get_width(surface); + int height = cairo_image_surface_get_height(surface); + cairo_surface_t *mask_surface = cairo_image_surface_create( + CAIRO_FORMAT_ARGB32, + width, + height); + cairo_t *mask_context = cairo_create(mask_surface); + if (cairo_status(mask_context) != CAIRO_STATUS_SUCCESS) { + Nan::ThrowError("Failed to initialize context"); + return NULL; + } + cairo_set_source(mask_context, source); + cairo_paint_with_alpha(mask_context, alpha); + cairo_destroy(mask_context); + cairo_pattern_t* newPattern = cairo_pattern_create_for_surface(mask_surface); + cairo_surface_destroy(mask_surface); + return newPattern; +} + /* * Fill and apply shadow. */ @@ -261,22 +322,42 @@ Context2d::setFillRule(v8::Local value) { void Context2d::fill(bool preserve) { + cairo_pattern_t *new_pattern; if (state->fillPattern) { - cairo_set_source(_context, state->fillPattern); + if (state->globalAlpha < 1) { + new_pattern = create_transparent_pattern(state->fillPattern, state->globalAlpha); + if (new_pattern == NULL) { + // failed to allocate; Nan::ThrowError has already been called, so return from this fn. + return; + } + cairo_set_source(_context, new_pattern); + cairo_pattern_destroy(new_pattern); + } else { + cairo_set_source(_context, state->fillPattern); + } repeat_type_t repeat = Pattern::get_repeat_type_for_cairo_pattern(state->fillPattern); if (NO_REPEAT == repeat) { cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_NONE); } else { cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_REPEAT); } - // TODO repeat-x/repeat-y } else if (state->fillGradient) { - cairo_pattern_set_filter(state->fillGradient, state->patternQuality); - cairo_set_source(_context, state->fillGradient); + if (state->globalAlpha < 1) { + new_pattern = create_transparent_gradient(state->fillGradient, state->globalAlpha); + if (new_pattern == NULL) { + // failed to recognize gradient; Nan::ThrowError has already been called, so return from this fn. + return; + } + cairo_pattern_set_filter(new_pattern, state->patternQuality); + cairo_set_source(_context, new_pattern); + cairo_pattern_destroy(new_pattern); + } else { + cairo_pattern_set_filter(state->fillGradient, state->patternQuality); + cairo_set_source(_context, state->fillGradient); + } } else { setSourceRGBA(state->fill); } - if (preserve) { hasShadow() ? shadow(cairo_fill_preserve) @@ -294,8 +375,19 @@ Context2d::fill(bool preserve) { void Context2d::stroke(bool preserve) { + cairo_pattern_t *new_pattern; if (state->strokePattern) { - cairo_set_source(_context, state->strokePattern); + if (state->globalAlpha < 1) { + new_pattern = create_transparent_pattern(state->strokePattern, state->globalAlpha); + if (new_pattern == NULL) { + // failed to allocate; Nan::ThrowError has already been called, so return from this fn. + return; + } + cairo_set_source(_context, new_pattern); + cairo_pattern_destroy(new_pattern); + } else { + cairo_set_source(_context, state->strokePattern); + } repeat_type_t repeat = Pattern::get_repeat_type_for_cairo_pattern(state->strokePattern); if (NO_REPEAT == repeat) { cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_NONE); @@ -303,8 +395,19 @@ Context2d::stroke(bool preserve) { cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_REPEAT); } } else if (state->strokeGradient) { - cairo_pattern_set_filter(state->strokeGradient, state->patternQuality); - cairo_set_source(_context, state->strokeGradient); + if (state->globalAlpha < 1) { + new_pattern = create_transparent_gradient(state->strokeGradient, state->globalAlpha); + if (new_pattern == NULL) { + // failed to recognize gradient; Nan::ThrowError has already been called, so return from this fn. + return; + } + cairo_pattern_set_filter(new_pattern, state->patternQuality); + cairo_set_source(_context, new_pattern); + cairo_pattern_destroy(new_pattern); + } else { + cairo_pattern_set_filter(state->strokeGradient, state->patternQuality); + cairo_set_source(_context, state->strokeGradient); + } } else { setSourceRGBA(state->stroke); } diff --git a/test/public/tests.js b/test/public/tests.js index 80fdf2a5e..b3d07a70b 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -431,6 +431,38 @@ tests['clip() 2'] = function (ctx) { } } +tests['createPattern()'] = function (ctx, done) { + var img = new Image() + img.onload = function () { + var pattern = ctx.createPattern(img, 'repeat') + ctx.scale(0.1, 0.1) + ctx.fillStyle = pattern + ctx.fillRect(100, 100, 800, 800) + ctx.strokeStyle = pattern + ctx.lineWidth = 200 + ctx.strokeRect(1100, 1100, 800, 800) + done() + } + img.src = imageSrc('face.jpeg') +} + +tests['createPattern() with globalAlpha'] = function (ctx, done) { + var img = new Image() + img.onload = function () { + var pattern = ctx.createPattern(img, 'repeat') + ctx.scale(0.1, 0.1) + ctx.globalAlpha = 0.6 + ctx.fillStyle = pattern + ctx.fillRect(100, 100, 800, 800) + ctx.globalAlpha = 0.2 + ctx.strokeStyle = pattern + ctx.lineWidth = 200 + ctx.strokeRect(1100, 1100, 800, 800) + done() + } + img.src = imageSrc('face.jpeg') +} + tests['createPattern() no-repeat'] = function (ctx, done) { var img = new Image() img.onload = function () { @@ -468,6 +500,56 @@ tests['createLinearGradient()'] = function (ctx) { ctx.fillStyle = '#13b575' ctx.fillStyle = ctx.fillStyle ctx.fillRect(65, 65, 20, 20) + + var lingrad3 = ctx.createLinearGradient(0, 0, 200, 0) + lingrad3.addColorStop(0, 'rgba(0,255,0,0.5)') + lingrad3.addColorStop(0.33, 'rgba(255,255,0,0.5)') + lingrad3.addColorStop(0.66, 'rgba(0,255,255,0.5)') + lingrad3.addColorStop(1, 'rgba(255,0,255,0.5)') + ctx.fillStyle = lingrad3 + ctx.fillRect(0, 170, 200, 30) +} + +tests['createLinearGradient() with opacity'] = function (ctx) { + var lingrad = ctx.createLinearGradient(0, 0, 0, 200) + lingrad.addColorStop(0, '#00FF00') + lingrad.addColorStop(0.33, '#FF0000') + lingrad.addColorStop(0.66, '#0000FF') + lingrad.addColorStop(1, '#00FFFF') + ctx.fillStyle = lingrad + ctx.strokeStyle = lingrad + ctx.lineWidth = 10 + ctx.globalAlpha = 0.4 + ctx.strokeRect(5, 5, 190, 190) + ctx.fillRect(0, 0, 50, 50) + ctx.globalAlpha = 0.6 + ctx.strokeRect(35, 35, 130, 130) + ctx.fillRect(50, 50, 50, 50) + ctx.globalAlpha = 0.8 + ctx.strokeRect(65, 65, 70, 70) + ctx.fillRect(100, 100, 50, 50) + ctx.globalAlpha = 0.95 + ctx.fillRect(150, 150, 50, 50) +} + +tests['createLinearGradient() and transforms'] = function (ctx) { + var lingrad = ctx.createLinearGradient(0, -100, 0, 100) + lingrad.addColorStop(0, '#00FF00') + lingrad.addColorStop(0.33, '#FF0000') + lingrad.addColorStop(0.66, '#0000FF') + lingrad.addColorStop(1, '#00FFFF') + ctx.fillStyle = lingrad + ctx.translate(100, 100) + ctx.beginPath() + ctx.moveTo(-100, -100) + ctx.lineTo(100, -100) + ctx.lineTo(100, 100) + ctx.lineTo(-100, 100) + ctx.closePath() + ctx.globalAlpha = 0.5 + ctx.rotate(1.570795) + ctx.scale(0.6, 0.6) + ctx.fill() } tests['createRadialGradient()'] = function (ctx) { From 67cf24a22c61a4a4cb6fdd9059069be3c5f35791 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Wed, 14 Feb 2018 15:37:06 -0500 Subject: [PATCH 097/474] correctly sample the edge of images when scaling Thanks to @ikokostya for finding the fix: When image is downscaling color of pixels is computed using color of surrounding pixels, but color outside of the source pattern is black (default canvas fill). That's why boundary pixels of the downscaled image is more dark. Fixes #1084 --- src/CanvasRenderingContext2d.cc | 1 + test/fixtures/halved-1.jpeg | Bin 0 -> 15636 bytes test/fixtures/halved-2.jpeg | Bin 0 -> 8768 bytes test/public/tests.js | 29 +++++++++++++++++++++++++++++ 4 files changed, 30 insertions(+) create mode 100644 test/fixtures/halved-1.jpeg create mode 100644 test/fixtures/halved-2.jpeg diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 7212cc020..4e32866f0 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -1190,6 +1190,7 @@ NAN_METHOD(Context2d::DrawImage) { // Paint cairo_set_source_surface(ctx, surface, dx - sx, dy - sy); cairo_pattern_set_filter(cairo_get_source(ctx), context->state->patternQuality); + cairo_pattern_set_extend(cairo_get_source(ctx), CAIRO_EXTEND_REFLECT); cairo_paint_with_alpha(ctx, context->state->globalAlpha); cairo_restore(ctx); diff --git a/test/fixtures/halved-1.jpeg b/test/fixtures/halved-1.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..d7c7e0f79b75cb44fc79ebc0ec491ed36d180920 GIT binary patch literal 15636 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/halved-2.jpeg b/test/fixtures/halved-2.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..056e2eaf49c3d9a1ec43b7d0768aa93c27a9fa1b GIT binary patch literal 8768 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 { + loaded1 = true + ctx.drawImage(img1, -170 - 100, -203, 352, 352) + if (loaded2) done() + } + + img1.onerror = function () { + done(new Error('Failed to load image')) + } + + img2.onload = () => { + loaded2 = true + ctx.drawImage(img2, 182 - 100, -203, 352, 352) + if (loaded1) done() + } + + img2.onerror = function () { + done(new Error('Failed to load image')) + } + + img1.src = imageSrc('halved-1.jpeg') + img2.src = imageSrc('halved-2.jpeg') +} From 238f5a41a54043d9305461d602dde59b8647d230 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Wed, 14 Feb 2018 12:45:43 -0500 Subject: [PATCH 098/474] create a new Cairo instance in one place Fixes #1095 --- src/Canvas.cc | 13 ++++++++++++- src/Canvas.h | 1 + src/CanvasRenderingContext2d.cc | 3 +-- test/canvas.test.js | 5 ++++- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/Canvas.cc b/src/Canvas.cc index db3321ff1..6dc62f47e 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -800,11 +800,22 @@ Canvas::resurface(Local canvas) { if (!context->IsUndefined()) { Context2d *context2d = ObjectWrap::Unwrap(context->ToObject()); cairo_t *prev = context2d->context(); - context2d->setContext(cairo_create(surface())); + context2d->setContext(createCairoContext()); cairo_destroy(prev); } } +/** + * Wrapper around cairo_create() + * (do not call cairo_create directly, call this instead) + */ +cairo_t* +Canvas::createCairoContext() { + cairo_t* ret = cairo_create(surface()); + cairo_set_line_width(ret, 1); // Cairo defaults to 2 + return ret; +} + /* * Construct an Error from the given cairo status. */ diff --git a/src/Canvas.h b/src/Canvas.h index 14f399059..69b78d173 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -71,6 +71,7 @@ class Canvas: public Nan::ObjectWrap { inline Backend* backend() { return _backend; } inline cairo_surface_t* surface(){ return backend()->getSurface(); } + cairo_t* createCairoContext(); inline uint8_t *data(){ return cairo_image_surface_get_data(surface()); } inline int stride(){ return cairo_image_surface_get_stride(surface()); } diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 4e32866f0..38f24d807 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -155,9 +155,8 @@ Context2d::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { Context2d::Context2d(Canvas *canvas) { _canvas = canvas; - _context = cairo_create(canvas->surface()); + _context = canvas->createCairoContext(); _layout = pango_cairo_create_layout(_context); - cairo_set_line_width(_context, 1); state = states[stateno = 0] = (canvas_state_t *) malloc(sizeof(canvas_state_t)); state->shadowBlur = 0; state->shadowOffsetX = state->shadowOffsetY = 0; diff --git a/test/canvas.test.js b/test/canvas.test.js index 0de8c026d..2760662c1 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -308,11 +308,13 @@ describe('Canvas', function () { }); it('Canvas#{width,height}=', function () { - var canvas = createCanvas(100, 200); + var context, canvas = createCanvas(100, 200); + assert.equal(100, canvas.width); assert.equal(200, canvas.height); canvas = createCanvas(); + context = canvas.getContext("2d"); assert.equal(0, canvas.width); assert.equal(0, canvas.height); @@ -320,6 +322,7 @@ describe('Canvas', function () { canvas.height = 50; assert.equal(50, canvas.width); assert.equal(50, canvas.height); + assert.equal(1, context.lineWidth); // #1095 }); it('Canvas#stride', function() { From 180b7fa20c4404fa5aa0a52619858b7490e54c8f Mon Sep 17 00:00:00 2001 From: Thomas Date: Thu, 22 Feb 2018 23:45:09 +0100 Subject: [PATCH 099/474] Perform some checks to avoid assertion fault (#1100) * Perform some checks to avoid assertion fault * Check if the passed argument is an instance of some derived dackend class --- src/Canvas.cc | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Canvas.cc b/src/Canvas.cc index 6dc62f47e..40be152dc 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -104,8 +104,13 @@ NAN_METHOD(Canvas::New) { backend = new ImageBackend(width, height); } else if (info[0]->IsObject()) { - // TODO need to check if this is actually an instance of a Backend to avoid a fault - backend = Nan::ObjectWrap::Unwrap(info[0]->ToObject()); + if (Nan::New(ImageBackend::constructor)->HasInstance(info[0]) || + Nan::New(PdfBackend::constructor)->HasInstance(info[0]) || + Nan::New(SvgBackend::constructor)->HasInstance(info[0])) { + backend = Nan::ObjectWrap::Unwrap(info[0]->ToObject()); + }else{ + return Nan::ThrowTypeError("Invalid arguments"); + } } else { backend = new ImageBackend(0, 0); From 15dda364e036778138cfa250266d9a65c51c33fb Mon Sep 17 00:00:00 2001 From: Hakerh400 Date: Fri, 2 Mar 2018 19:26:29 +0100 Subject: [PATCH 100/474] Deallocate library structure at get_pango_font_description function to prevent memory leaks. --- src/register_font.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/register_font.cc b/src/register_font.cc index 75fc87cc8..706b15d65 100644 --- a/src/register_font.cc +++ b/src/register_font.cc @@ -212,6 +212,7 @@ get_pango_font_description(unsigned char* filepath) { pango_font_description_set_style(desc, get_pango_style(face->style_flags)); FT_Done_Face(face); + FT_Done_FreeType(library); return desc; } From 06fbaf9e1bb51009c5170a337d67c3cbdf70dd6d Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 6 Mar 2018 15:02:52 +0100 Subject: [PATCH 101/474] Prevent segfault caused by invalid fonts (#1105) * Prevent segfault caused by invalid fonts * Detect invalid fonts --- src/register_font.cc | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/register_font.cc b/src/register_font.cc index 706b15d65..3a81f3cf8 100644 --- a/src/register_font.cc +++ b/src/register_font.cc @@ -206,7 +206,15 @@ get_pango_font_description(unsigned char* filepath) { if (table) { char *family = get_family_name(face); - if (family) pango_font_description_set_family_static(desc, family); + if (!family) { + pango_font_description_free(desc); + FT_Done_Face(face); + FT_Done_FreeType(library); + + return NULL; + } + + pango_font_description_set_family_static(desc, family); pango_font_description_set_weight(desc, get_pango_weight(table->usWeightClass)); pango_font_description_set_stretch(desc, get_pango_stretch(table->usWidthClass)); pango_font_description_set_style(desc, get_pango_style(face->style_flags)); From ffcb8f8c1923ec58fe41ad4ed97924aef3ee9ef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Wed, 7 Mar 2018 09:57:05 +0000 Subject: [PATCH 102/474] 2.0.0-alpha.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 60a13a581..91b5d1a2b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.0.0-alpha.9", + "version": "2.0.0-alpha.10", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 692ed805aeab2c2817ab8efb3dc9abdfc11df72c Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Thu, 8 Mar 2018 13:44:31 -0500 Subject: [PATCH 103/474] node-pre-gyp should fallback to node-gyp (#1109) Fixes #1108 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 91b5d1a2b..27c90e5f1 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "test": "standard examples/*.js test/server.js test/public/*.js benchmark/run.js util/has_lib.js browser.js index.js && mocha test/*.test.js", "pretest-server": "node-gyp build", "test-server": "node test/server.js", - "install": "node-pre-gyp install" + "install": "node-pre-gyp install --fallback-to-build" }, "binary": { "module_name": "canvas-prebuilt", From 871bc70cbcb15539ca4b0714b5cf511ae385e209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Fri, 9 Mar 2018 11:44:42 +0000 Subject: [PATCH 104/474] 2.0.0-alpha.11 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 27c90e5f1..2d28e5f44 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.0.0-alpha.10", + "version": "2.0.0-alpha.11", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From d1cc1afae0aa0fecaa09ab054cbfe2b3b58a59c2 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Wed, 14 Mar 2018 02:56:59 +0000 Subject: [PATCH 105/474] Re-add Image SetWidth, SetHeight These were added in 83f295a, but got trampled in d4b6f67 Fixes #1080 --- src/Image.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Image.cc b/src/Image.cc index 6df2b06d9..c997825be 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -48,8 +48,8 @@ Image::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { Local proto = ctor->PrototypeTemplate(); SetProtoAccessor(proto, Nan::New("source").ToLocalChecked(), GetSource, SetSource, ctor); SetProtoAccessor(proto, Nan::New("complete").ToLocalChecked(), GetComplete, NULL, ctor); - SetProtoAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, NULL, ctor); - SetProtoAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, NULL, ctor); + SetProtoAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, SetWidth, ctor); + SetProtoAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, SetHeight, ctor); SetProtoAccessor(proto, Nan::New("naturalWidth").ToLocalChecked(), GetNaturalWidth, NULL, ctor); SetProtoAccessor(proto, Nan::New("naturalHeight").ToLocalChecked(), GetNaturalHeight, NULL, ctor); #if CAIRO_VERSION_MINOR >= 10 From 3dabbfd4a9ac3d9744b3fa54d48bed19f6f195bf Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Tue, 13 Mar 2018 20:30:05 -0700 Subject: [PATCH 106/474] Update changelog Not sure I got absolutely everything, but got most of it, and I think I got all breaking changes. Fixes #1112 --- History.md | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index 1dcf3a0a0..5cc0a8126 100644 --- a/History.md +++ b/History.md @@ -1,10 +1,106 @@ -Unreleased / patch +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) and this +project adheres to [Semantic Versioning](http://semver.org/). + +2.0.0 (unreleased -- encompasses all alpha versions) ================== +### Breaking + * Drop support for Node.js <4.x + * Remove sync streams (bc53059). Note that all or most streams are still + synchronous to some degree; this change just removed `syncPNGStream` and + friends. + * Pango is now *required* on all platforms (7716ae4). + +### Fixed + * Prevent segfaults caused by loading invalid fonts (#1105) + * Fix memory leak in font loading * Port has_lib.sh to javascript (#872) + * Correctly sample the edge of images when scaling (#1084) + * Detect CentOS libjpeg path (b180ea5) + * Improve measureText accuracy (2bbfec5) + * Fix memory leak when image callbacks reference the image (1f4b646) + * Fix putImageData(data, negative, negative) (2102e25) + * Fix SVG recognition when loading from buffer (77749e6) + * Re-rasterize SVG when drawing to a context and dimensions changed (79bf232) + +### Added + * Prebuilds (#992) * Support canvas.getContext("2d", {alpha: boolean}) and canvas.getContext("2d", {pixelFormat: "..."}) * Support indexed PNG encoding. + * Support `currentTransform` (d6714ee) + * Export `CanvasGradient` (6a4c0ab) + * Support #RGBA , #RRGGBBAA hex colors (10a82ec) + * Support maxWidth arg for fill/strokeText (175b40d) + * Support image.naturalWidth/Height (a5915f8) + * Render SVG img elements when librsvg is available (1baf00e) + * Support ellipse method (4d4a726) + * Browser-compatible API (6a29a23) + * Support for jpeg on Windows (42e9a74) + * Support for backends (1a6dffe) + +1.6.x (unreleased) +================== +### Fixed + * Make setLineDash able to handle full zeroed dashes (b8cf1d7) + * Fix reading fillStyle after setting it from gradient to color (a84b2bc) + +### Added + * Support for pattern repeat and no-repeat (#1066) + * Support for context globalAlpha for gradients and patterns (#1064) + +1.6.9 / 2017-12-20 +================== +### Fixed + * Fix some instances of crashes (7c9ec58, 8b792c3) + * Fix node 0.x compatibility (dca33f7) + +1.6.8 / 2017-12-12 +================== +### Fixed + * Faster, more compliant parseFont (4625efa, 37cd969) + +1.6.7 / 2017-09-08 +================== +### Fixed + * Minimal backport of #985 (rotated text baselines) (c19edb8) + +1.6.6 / 2017-05-03 +================== +### Fixed + * Use .node extension for requiring native module so webpack works (1b05599) + * Correct text baseline calculation (#1037) + +1.6.5 / 2017-03-18 +================== +### Changed + * Parse font using parse-css-font and units-css (d316416) + +1.6.4 / 2017-02-26 +================== +### Fixed + * Make sure Canvas#toDataURL is always async if callback is passed (8586d72) + +1.6.3 / 2017-02-14 +================== +### Fixed + * Fix isnan() and isinf() on clang (5941e13) + +1.6.2 / 2016-10-30 +================== +### Fixed + * Fix deprecation warnings (c264879) + * Bump nan (e4aea20) + +1.6.1 / 2016-10-23 +================== + +### Fixed + * Make has_lib.sh work on BSD OSes (1727d66) 1.6.0 / 2016-10-16 ================== From 9885e55d698cc60845f7826b818fb4540e7c1932 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Tue, 13 Mar 2018 20:30:27 -0700 Subject: [PATCH 107/474] Rename history -> changelog Easier to find? --- History.md => CHANGELOG.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename History.md => CHANGELOG.md (100%) diff --git a/History.md b/CHANGELOG.md similarity index 100% rename from History.md rename to CHANGELOG.md From 918926f6cfea3505e517239d06dec1cf32e395e9 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Tue, 13 Mar 2018 20:34:50 -0700 Subject: [PATCH 108/474] Add PR template to remind people to update the changelog --- .github/PULL_REQUEST_TEMPLATE.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..c308e167e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,3 @@ +Thanks for contributing! + +- [ ] Have you updated CHANGELOG.md? From 13f357011754f72259232a3226ea0e4ec5e7ffe9 Mon Sep 17 00:00:00 2001 From: Plusb Preco Date: Thu, 15 Mar 2018 01:29:55 +0900 Subject: [PATCH 109/474] Add caption overlay on image example (#1076) * Add caption overlay on image example * Fix stylistic issue --- examples/image-caption-overlay.js | 87 +++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 examples/image-caption-overlay.js diff --git a/examples/image-caption-overlay.js b/examples/image-caption-overlay.js new file mode 100644 index 000000000..9e765703e --- /dev/null +++ b/examples/image-caption-overlay.js @@ -0,0 +1,87 @@ +import {createWriteStream} from 'fs' +import pify from 'pify' +import imageSizeOf from 'image-size' +import {createCanvas, loadImage, Image} from 'canvas' + +const imageSizeOfP = pify(imageSizeOf) + +function createImageFromBuffer (buffer) { + const image = new Image() + image.src = buffer + + return image +} + +function createCaptionOverlay ({ + text, + width, + height, + font = 'Arial', + fontSize = 48, + captionHeight = 120, + decorateCaptionTextFillStyle = null, + decorateCaptionFillStyle = null, + offsetX = 0, + offsetY = 0 +}) { + const canvas = createCanvas(width, height) + const ctx = canvas.getContext('2d') + + const createGradient = (first, second) => { + const grd = ctx.createLinearGradient(width, captionY, width, height) + grd.addColorStop(0, first) + grd.addColorStop(1, second) + + return grd + } + + // Hold computed caption position + const captionX = offsetX + const captionY = offsetY + height - captionHeight + const captionTextX = captionX + (width / 2) + const captionTextY = captionY + (captionHeight / 2) + + // Fill caption rect + ctx.fillStyle = decorateCaptionFillStyle + ? decorateCaptionFillStyle(ctx) + : createGradient('rgba(0, 0, 0, 0)', 'rgba(0, 0, 0, 0.45)') + ctx.fillRect(captionX, captionY, width, captionHeight) + + // Fill caption text + ctx.textBaseline = 'middle' + ctx.textAlign = 'center' + ctx.font = `${fontSize}px ${font}` + ctx.fillStyle = decorateCaptionTextFillStyle + ? decorateCaptionTextFillStyle(ctx) + : 'white' + ctx.fillText(text, captionTextX, captionTextY) + + return createImageFromBuffer(canvas.toBuffer()) +} + +(async () => { + try { + const source = 'images/lime-cat.jpg' + const {width, height} = await imageSizeOfP(source) + const canvas = createCanvas(width, height) + const ctx = canvas.getContext('2d') + + // Draw base image + const image = await loadImage(source) + ctx.drawImage(image, 0, 0) + + // Draw caption overlay + const overlay = await createCaptionOverlay({ + text: 'Hello!', + width, + height + }) + ctx.drawImage(overlay, 0, 0) + + // Output to `.png` file + canvas.createPNGStream().pipe(createWriteStream('foo.png')) + } catch (err) { + console.log(err) + process.exit(1) + } +})() From d463e38f190a3b1038cdf41e8c497a03d351781b Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 14 Mar 2018 17:37:34 +0100 Subject: [PATCH 110/474] Change --enable-pdf=yes wording in Readme. (#521) Addresses issue #519. --- Readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Readme.md b/Readme.md index 1f782e012..69418671c 100644 --- a/Readme.md +++ b/Readme.md @@ -304,8 +304,8 @@ ctx.antialias = 'none'; ## PDF Support - Basic PDF support was added in 0.11.0. Make sure to install cairo with `--enable-pdf=yes` for the PDF backend. node-canvas must know that it is creating - a PDF on initialization, using the "pdf" string: + Basic PDF support was added in 0.11.0. If you are building cairo from source, be sure to use `--enable-pdf=yes` for the PDF backend. + node-canvas must know that it is creating a PDF on initialization, using the "pdf" string: ```js var canvas = createCanvas(200, 500, 'pdf'); From 0e72c99e4852c2f811777948dc794c6b95f0b44e Mon Sep 17 00:00:00 2001 From: Daniel Odendahl Jr Date: Sat, 17 Mar 2018 13:08:22 +0000 Subject: [PATCH 111/474] Update node-pre-gyp to 0.9.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2d28e5f44..888440a53 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "remote_path": "v{version}" }, "dependencies": { - "node-pre-gyp": "^0.6.36", + "node-pre-gyp": "^0.9.0", "nan": "^2.4.0" }, "devDependencies": { From 453e93571f98acd9e97b69436813c5ab114cdbd2 Mon Sep 17 00:00:00 2001 From: Hakerh400 Date: Mon, 19 Mar 2018 14:50:23 +0100 Subject: [PATCH 112/474] Return if the rotation angle is NaN or infinite --- src/CanvasRenderingContext2d.cc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 38f24d807..a7ce1f1c1 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -1884,6 +1884,11 @@ NAN_METHOD(Context2d::ClosePath) { */ NAN_METHOD(Context2d::Rotate) { + double angle = info[0]->NumberValue(); + + if (isnan(angle) || isinf(angle)) + return; + Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_rotate(context->context() , info[0]->IsNumber() ? info[0]->NumberValue() : 0); From 6145ded614d4fc97d177c8f1571a87ef1b1a69fd Mon Sep 17 00:00:00 2001 From: Hakerh400 Date: Tue, 20 Mar 2018 11:38:27 +0100 Subject: [PATCH 113/474] Remove unnecessary checks --- src/CanvasRenderingContext2d.cc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index a7ce1f1c1..3383fd54e 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -1890,8 +1890,7 @@ NAN_METHOD(Context2d::Rotate) { return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_rotate(context->context() - , info[0]->IsNumber() ? info[0]->NumberValue() : 0); + cairo_rotate(context->context(), angle); } /* From 74e7fe8f77e7d875fde5a1a20b71eb1eb90fce65 Mon Sep 17 00:00:00 2001 From: Hakerh400 Date: Tue, 20 Mar 2018 15:07:38 +0100 Subject: [PATCH 114/474] Added tests for ctx.rotate --- test/canvas.test.js | 49 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/test/canvas.test.js b/test/canvas.test.js index 2760662c1..247279a27 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -1492,4 +1492,53 @@ describe('Canvas', function () { assert.ok(ctx.isPointInPath(3, 3, 'evenodd')); }); + it('Context2d#rotate(angle)', function () { + var canvas = createCanvas(4, 4); + var ctx = canvas.getContext('2d'); + + // Number + ctx.resetTransform(); + testAngle(1.23); + + // String + ctx.resetTransform(); + testAngle('-4.56e-1', -0.456); + + // Boolean + ctx.resetTransform(); + testAngle(true, 1); + + // Array + ctx.resetTransform(); + testAngle([7.8], 7.8); + + // Object + var obj = {[Symbol.toPrimitive](){ return 0.89; }}; + ctx.resetTransform(); + testAngle(obj, 0.89); + + // NaN + ctx.resetTransform(); + ctx.rotate(0.91); + testAngle(NaN, 0.91); + + // Infinite value + ctx.resetTransform(); + ctx.rotate(0.94); + testAngle(-Infinity, 0.94); + + function testAngle(angle, expected = angle){ + ctx.rotate(angle); + + var mat = ctx.currentTransform; + var sin = Math.sin(expected); + var cos = Math.cos(expected); + + assert.strictEqual(mat.m11, cos); + assert.strictEqual(mat.m12, sin); + assert.strictEqual(mat.m21, -sin); + assert.strictEqual(mat.m22, cos); + } + }); + }); From 2495a092ece6538e8e3f1dfc9d04441fa151e9dd Mon Sep 17 00:00:00 2001 From: Hakerh400 Date: Tue, 20 Mar 2018 15:34:14 +0100 Subject: [PATCH 115/474] Fix test for node4 --- test/canvas.test.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/canvas.test.js b/test/canvas.test.js index 247279a27..0868e6bc7 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -1498,7 +1498,7 @@ describe('Canvas', function () { // Number ctx.resetTransform(); - testAngle(1.23); + testAngle(1.23, 1.23); // String ctx.resetTransform(); @@ -1513,7 +1513,11 @@ describe('Canvas', function () { testAngle([7.8], 7.8); // Object - var obj = {[Symbol.toPrimitive](){ return 0.89; }}; + var obj = Object.create(null); + if (+process.version.match(/\d+/) >= 6) + obj[Symbol.toPrimitive] = function () { return 0.89; }; + else + obj.valueOf = function () { return 0.89; }; ctx.resetTransform(); testAngle(obj, 0.89); @@ -1527,7 +1531,7 @@ describe('Canvas', function () { ctx.rotate(0.94); testAngle(-Infinity, 0.94); - function testAngle(angle, expected = angle){ + function testAngle(angle, expected){ ctx.rotate(angle); var mat = ctx.currentTransform; From 236e719541158250732f86f9c275c0a0201dfcea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Tue, 20 Mar 2018 15:50:48 +0000 Subject: [PATCH 116/474] Prevent JPEG errors from crashing process --- CHANGELOG.md | 1 + src/Image.cc | 58 +++++++++++++++++++++++++++++++++++++-- test/fixtures/chrome.jpg | Bin 0 -> 5750 bytes test/image.test.js | 33 +++++++++++++++++++++- 4 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 test/fixtures/chrome.jpg diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cc0a8126..ccac0f1ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * Fix putImageData(data, negative, negative) (2102e25) * Fix SVG recognition when loading from buffer (77749e6) * Re-rasterize SVG when drawing to a context and dimensions changed (79bf232) + * Prevent JPEG errors from crashing process (#1124) ### Added * Prebuilds (#992) diff --git a/src/Image.cc b/src/Image.cc index c997825be..68090c6c6 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -20,6 +20,14 @@ typedef struct { } gif_data_t; #endif +#ifdef HAVE_JPEG +#include + +struct canvas_jpeg_error_mgr: jpeg_error_mgr { + jmp_buf setjmp_buffer; +}; +#endif + /* * Read closure used by loadFromBuffer. */ @@ -774,6 +782,17 @@ Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args) { return CAIRO_STATUS_SUCCESS; } +/* + * Callback to recover from jpeg errors + */ + +METHODDEF(void) canvas_jpeg_error_exit (j_common_ptr cinfo) { + canvas_jpeg_error_mgr *cjerr = static_cast(cinfo->err); + + // Return control to the setjmp point + longjmp(cjerr->setjmp_buffer, 1); +} + #if CAIRO_VERSION_MINOR >= 10 /* @@ -786,8 +805,19 @@ Image::decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len) { // TODO: remove this duplicate logic // JPEG setup struct jpeg_decompress_struct args; - struct jpeg_error_mgr err; + struct canvas_jpeg_error_mgr err; + args.err = jpeg_std_error(&err); + args.err->error_exit = canvas_jpeg_error_exit; + + // Establish the setjmp return context for canvas_jpeg_error_exit to use + if (setjmp(err.setjmp_buffer)) { + // If we get here, the JPEG code has signaled an error. + // We need to clean up the JPEG object, close the input file, and return. + jpeg_destroy_decompress(&args); + return CAIRO_STATUS_READ_ERROR; + } + jpeg_create_decompress(&args); jpeg_mem_src(&args, buf, len); @@ -881,8 +911,19 @@ Image::loadJPEGFromBuffer(uint8_t *buf, unsigned len) { // TODO: remove this duplicate logic // JPEG setup struct jpeg_decompress_struct args; - struct jpeg_error_mgr err; + struct canvas_jpeg_error_mgr err; + args.err = jpeg_std_error(&err); + args.err->error_exit = canvas_jpeg_error_exit; + + // Establish the setjmp return context for canvas_jpeg_error_exit to use + if (setjmp(err.setjmp_buffer)) { + // If we get here, the JPEG code has signaled an error. + // We need to clean up the JPEG object, close the input file, and return. + jpeg_destroy_decompress(&args); + return CAIRO_STATUS_READ_ERROR; + } + jpeg_create_decompress(&args); jpeg_mem_src(&args, buf, len); @@ -910,8 +951,19 @@ Image::loadJPEG(FILE *stream) { #endif // JPEG setup struct jpeg_decompress_struct args; - struct jpeg_error_mgr err; + struct canvas_jpeg_error_mgr err; + args.err = jpeg_std_error(&err); + args.err->error_exit = canvas_jpeg_error_exit; + + // Establish the setjmp return context for canvas_jpeg_error_exit to use + if (setjmp(err.setjmp_buffer)) { + // If we get here, the JPEG code has signaled an error. + // We need to clean up the JPEG object, close the input file, and return. + jpeg_destroy_decompress(&args); + return CAIRO_STATUS_READ_ERROR; + } + jpeg_create_decompress(&args); jpeg_stdio_src(&args, stream); diff --git a/test/fixtures/chrome.jpg b/test/fixtures/chrome.jpg new file mode 100644 index 0000000000000000000000000000000000000000..29fd36ae808b6f80dd97050474c5f581bed07e80 GIT binary patch literal 5750 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 N?EjB{Eg<{v{{i+a-=F{h literal 0 HcmV?d00001 diff --git a/test/image.test.js b/test/image.test.js index 875b8eb83..94fa249ac 100644 --- a/test/image.test.js +++ b/test/image.test.js @@ -7,13 +7,15 @@ */ const loadImage = require('../').loadImage -const Image = require('../').Image; +const Image = require('../').Image const assert = require('assert') const assertRejects = require('assert-rejects') +const fs = require('fs') const png_checkers = `${__dirname}/fixtures/checkers.png` const png_clock = `${__dirname}/fixtures/clock.png` +const jpg_chrome = `${__dirname}/fixtures/chrome.jpg` const jpg_face = `${__dirname}/fixtures/face.jpeg` describe('Image', function () { @@ -187,4 +189,33 @@ describe('Image', function () { assert.strictEqual(onerrorCalled, 0) }) }) + + it('does not crash on invalid images', function () { + function withIncreasedByte (source, index) { + const copy = source.slice(0) + + copy[index] += 1 + + return copy + } + + const source = fs.readFileSync(jpg_chrome) + + const corruptSources = [ + withIncreasedByte(source, 0), + withIncreasedByte(source, 1), + withIncreasedByte(source, 1060), + withIncreasedByte(source, 1061), + withIncreasedByte(source, 1062), + withIncreasedByte(source, 1063), + withIncreasedByte(source, 1064), + withIncreasedByte(source, 1065), + withIncreasedByte(source, 1066), + withIncreasedByte(source, 1067), + withIncreasedByte(source, 1068), + withIncreasedByte(source, 1069) + ] + + return Promise.all(corruptSources.map(src => loadImage(src).catch(() => null))) + }) }) From 23ad11f0bde637aeff90c1c4640bdde5860b118e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Wed, 21 Mar 2018 21:57:37 +0000 Subject: [PATCH 117/474] Cleanup header inclusion (#1125) --- src/CanvasRenderingContext2d.cc | 2 +- src/Image.cc | 11 ++++++----- src/ImageData.h | 5 +++-- src/PNG.h | 6 ++++-- src/color.cc | 21 +++++++++++---------- src/color.h | 8 ++++---- src/init.cc | 2 +- src/toBuffer.cc | 2 +- 8 files changed, 31 insertions(+), 26 deletions(-) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 38f24d807..7d67e2226 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -6,7 +6,7 @@ // #include -#include +#include #include #include #include diff --git a/src/Image.cc b/src/Image.cc index 68090c6c6..fa512ab25 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -4,13 +4,14 @@ // Copyright (c) 2010 LearnBoost // +#include +#include +#include +#include + #include "Util.h" #include "Canvas.h" #include "Image.h" -#include -#include -#include -#include #ifdef HAVE_GIF typedef struct { @@ -21,7 +22,7 @@ typedef struct { #endif #ifdef HAVE_JPEG -#include +#include struct canvas_jpeg_error_mgr: jpeg_error_mgr { jmp_buf setjmp_buffer; diff --git a/src/ImageData.h b/src/ImageData.h index 8007f4d31..392b1faca 100644 --- a/src/ImageData.h +++ b/src/ImageData.h @@ -8,10 +8,11 @@ #ifndef __NODE_IMAGE_DATA_H__ #define __NODE_IMAGE_DATA_H__ -#include "Canvas.h" -#include +#include #include +#include "Canvas.h" + class ImageData: public Nan::ObjectWrap { public: static Nan::Persistent constructor; diff --git a/src/PNG.h b/src/PNG.h index 2de130c3a..d202f78f8 100644 --- a/src/PNG.h +++ b/src/PNG.h @@ -1,10 +1,12 @@ #ifndef _CANVAS_PNG_H #define _CANVAS_PNG_H + +#include +#include #include #include #include -#include -#include + #include "closure.h" #if defined(__GNUC__) && (__GNUC__ > 2) && defined(__OPTIMIZE__) diff --git a/src/color.cc b/src/color.cc index 144e64cb8..30d9fcd35 100644 --- a/src/color.cc +++ b/src/color.cc @@ -5,11 +5,12 @@ // Copyright (c) 2010 LearnBoost // -#include "color.h" -#include +#include #include #include +#include "color.h" + // Compatibility with Visual Studio versions prior to VS2015 #if defined(_MSC_VER) && _MSC_VER < 1900 #define snprintf _snprintf @@ -60,7 +61,7 @@ parse_css_number(const char** pStr, parsed_t *pParsed) { const char*& str = *pStr; const char* startStr = str; if (!str || !*str) - return false; + return false; parsed_t integerPart = 0; parsed_t fractionPart = 0; int divisorForFraction = 1; @@ -68,7 +69,7 @@ parse_css_number(const char** pStr, parsed_t *pParsed) { int exponent = 0; int digits = 0; bool inFraction = false; - + if (*str == '-') { ++str; sign = -1; @@ -83,7 +84,7 @@ parse_css_number(const char** pStr, parsed_t *pParsed) { } else { ++digits; - + if (inFraction) { fractionPart = fractionPart*10 + (*str - '0'); divisorForFraction *= 10; @@ -137,7 +138,7 @@ clip(T value, T minValue, T maxValue) { /* * Wrap value to the range [0, limit] */ - + template static T wrap_float(T value, T limit) { @@ -186,7 +187,7 @@ parse_degrees(const char** pStr, float *pDegrees) { */ static bool -parse_clipped_percentage(const char** pStr, float *pFraction) { +parse_clipped_percentage(const char** pStr, float *pFraction) { float percentage; bool result = parse_css_number(pStr,&percentage); const char*& str = *pStr; @@ -699,7 +700,7 @@ rgba_from_hsl_string(const char *str, short *ok) { /* * Return rgb from: - * + * * - "#RGB" * - "#RGBA" * - "#RRGGBB" @@ -737,7 +738,7 @@ rgba_from_name_string(const char *str, short *ok) { /* * Return rgb from: - * + * * - #RGB * - #RGBA * - #RRGGBB @@ -752,7 +753,7 @@ rgba_from_name_string(const char *str, short *ok) { int32_t rgba_from_string(const char *str, short *ok) { - if ('#' == str[0]) + if ('#' == str[0]) return rgba_from_hex_string(++str, ok); if (str == strstr(str, "rgba")) return rgba_from_rgba_string(str, ok); diff --git a/src/color.h b/src/color.h index c570c4a45..fc0474d6e 100644 --- a/src/color.h +++ b/src/color.h @@ -8,10 +8,10 @@ #ifndef __COLOR_PARSER_H__ #define __COLOR_PARSER_H__ -#include -#include -#include -#include +#include +#include +#include +#include /* * RGBA struct. diff --git a/src/init.cc b/src/init.cc index 9cf3ef370..1e3a2f3cf 100644 --- a/src/init.cc +++ b/src/init.cc @@ -5,7 +5,7 @@ // Copyright (c) 2010 LearnBoost // -#include +#include #include #include diff --git a/src/toBuffer.cc b/src/toBuffer.cc index 10e0a56cd..3660532b7 100644 --- a/src/toBuffer.cc +++ b/src/toBuffer.cc @@ -1,4 +1,4 @@ -#include +#include #include "closure.h" #include "toBuffer.h" From 566df0c0db87c4daa6d13fcc525aca27d8cfe8e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Thu, 22 Mar 2018 10:33:48 +0000 Subject: [PATCH 118/474] Fix JSDoc syntax in util --- util/has_lib.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/util/has_lib.js b/util/has_lib.js index 2cfbbc916..71b4dd8ea 100644 --- a/util/has_lib.js +++ b/util/has_lib.js @@ -15,8 +15,8 @@ var SYSTEM_PATHS = [ /** * Checks for lib using ldconfig if present, or searching SYSTEM_PATHS * otherwise. - * @param String library name, e.g. 'jpeg' in 'libjpeg64.so' (see first line) - * @return Boolean exists + * @param {string} lib - library name, e.g. 'jpeg' in 'libjpeg64.so' (see first line) + * @return {boolean} exists */ function hasSystemLib (lib) { var libName = 'lib' + lib + '.+(so|dylib)' @@ -48,7 +48,7 @@ function hasSystemLib (lib) { /** * Checks for ldconfig on the path and /sbin - * @return Boolean exists + * @return {boolean} exists */ function hasLdconfig () { try { @@ -81,8 +81,8 @@ function hasFreetype () { /** * Checks for lib using pkg-config. - * @param String library name - * @return Boolean exists + * @param {string} lib - library name + * @return {boolean} exists */ function hasPkgconfigLib (lib) { try { From 4df773ac2a9ae2b9f07c40c5165e883ed954fdc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Thu, 22 Mar 2018 11:24:16 +0000 Subject: [PATCH 119/474] Return error for too large GIF-files --- src/Image.cc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Image.cc b/src/Image.cc index fa512ab25..759c41f6b 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -558,6 +558,14 @@ Image::loadGIFFromBuffer(uint8_t *buf, unsigned len) { width = naturalWidth = gif->SWidth; height = naturalHeight = gif->SHeight; + /* Cairo limit: + * https://lists.cairographics.org/archives/cairo/2010-December/021422.html + */ + if (width > 32767 || height > 32767) { + GIF_CLOSE_FILE(gif); + return CAIRO_STATUS_INVALID_SIZE; + } + uint8_t *data = (uint8_t *) malloc(naturalWidth * naturalHeight * 4); if (!data) { GIF_CLOSE_FILE(gif); From e84bf678a93b9efb1f5e4b35b286fb6486f99192 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Thu, 22 Mar 2018 11:24:32 +0000 Subject: [PATCH 120/474] Return error when missing color map in GIF --- src/Image.cc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Image.cc b/src/Image.cc index 759c41f6b..a78f026f1 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -579,6 +579,11 @@ Image::loadGIFFromBuffer(uint8_t *buf, unsigned len) { ? img->ColorMap : gif->SColorMap; + if (colormap == nullptr) { + GIF_CLOSE_FILE(gif); + return CAIRO_STATUS_READ_ERROR; + } + int bgColor = 0; int alphaColor = get_gif_transparent_color(gif, i); if (gif->SColorMap) bgColor = (uint8_t) gif->SBackGroundColor; From 536c0633b1c1b2770d764c27ed940b88ccd074b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Thu, 22 Mar 2018 11:25:44 +0000 Subject: [PATCH 121/474] Cache computation of background pixel --- src/Image.cc | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Image.cc b/src/Image.cc index a78f026f1..316e03219 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -610,13 +610,16 @@ Image::loadGIFFromBuffer(uint8_t *buf, unsigned len) { int bottom = img->Top + img->Height; int right = img->Left + img->Width; + uint32_t bgPixel = + ((bgColor == alphaColor) ? 0 : 255) << 24 + | colormap->Colors[bgColor].Red << 16 + | colormap->Colors[bgColor].Green << 8 + | colormap->Colors[bgColor].Blue; + for (int y = 0; y < naturalHeight; ++y) { for (int x = 0; x < naturalWidth; ++x) { if (y < img->Top || y >= bottom || x < img->Left || x >= right) { - *dst_data = ((bgColor == alphaColor) ? 0 : 255) << 24 - | colormap->Colors[bgColor].Red << 16 - | colormap->Colors[bgColor].Green << 8 - | colormap->Colors[bgColor].Blue; + *dst_data = bgPixel; } else { *dst_data = ((*src_data == alphaColor) ? 0 : 255) << 24 | colormap->Colors[*src_data].Red << 16 From 689538217f12ae45b9b947c708e2f3fb205cbb20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Thu, 22 Mar 2018 11:26:20 +0000 Subject: [PATCH 122/474] Only increment src pointer when reading from source --- src/Image.cc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Image.cc b/src/Image.cc index 316e03219..9ff95d12f 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -620,15 +620,15 @@ Image::loadGIFFromBuffer(uint8_t *buf, unsigned len) { for (int x = 0; x < naturalWidth; ++x) { if (y < img->Top || y >= bottom || x < img->Left || x >= right) { *dst_data = bgPixel; + dst_data++; } else { *dst_data = ((*src_data == alphaColor) ? 0 : 255) << 24 | colormap->Colors[*src_data].Red << 16 | colormap->Colors[*src_data].Green << 8 | colormap->Colors[*src_data].Blue; + dst_data++; + src_data++; } - - dst_data++; - src_data++; } } } From a4906ee719225654a2146484b5cfdba0fb5b60e0 Mon Sep 17 00:00:00 2001 From: aquiire Date: Thu, 22 Mar 2018 23:00:56 +0530 Subject: [PATCH 123/474] Updated examples/crop.js (#1130) --- examples/crop.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/crop.js b/examples/crop.js index 5d14354a8..d58e3b08f 100644 --- a/examples/crop.js +++ b/examples/crop.js @@ -1,8 +1,8 @@ var fs = require('fs') var path = require('path') -var {createCanvas, Image} = require('..') +var Canvas = require('canvas') -var img = new Image() +var img = new Canvas.Image() img.onerror = function (err) { throw err @@ -11,12 +11,12 @@ img.onerror = function (err) { img.onload = function () { var w = img.width / 2 var h = img.height / 2 - var canvas = createCanvas(w, h) + var canvas = Canvas.createCanvas(w, h) var ctx = canvas.getContext('2d') ctx.drawImage(img, 0, 0, w, h, 0, 0, w, h) - var out = fs.createWriteStream(path.join(__dirname, 'crop.png')) + var out = fs.createWriteStream(path.join(__dirname, 'crop.jpg')) var stream = canvas.createJPEGStream({ bufsize: 2048, quality: 80 From 92b192447e9b9ae98da0f801e4e34afdd1dc1ef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Thu, 22 Mar 2018 19:32:47 +0000 Subject: [PATCH 124/474] 2.0.0-alpha.12 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 888440a53..dc5b8a7cc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.0.0-alpha.11", + "version": "2.0.0-alpha.12", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From e740695397bb68ba389920c9adb6dc79b589a893 Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 26 Mar 2018 21:41:15 +0200 Subject: [PATCH 125/474] Systematically check for NaNs in ctx methods (#1129) * Systematically check for NaNs in ctx methods * Add comments * Return the actual value * Fix out-of-bounds error and few warnings * Fix float rounding error PR #1129 --- CHANGELOG.md | 1 + src/CanvasRenderingContext2d.cc | 258 +++++++++++++++++--------------- test/canvas.test.js | 8 +- 3 files changed, 146 insertions(+), 121 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccac0f1ea..5a5f0d0c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * Fix SVG recognition when loading from buffer (77749e6) * Re-rasterize SVG when drawing to a context and dimensions changed (79bf232) * Prevent JPEG errors from crashing process (#1124) + * Improve handling of invalid arguments (#1129) ### Added * Prebuilds (#992) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 560fcf53e..49bf01d8e 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -40,14 +40,13 @@ Nan::Persistent Context2d::constructor; */ #define RECT_ARGS \ - if (!info[0]->IsNumber() \ - ||!info[1]->IsNumber() \ - ||!info[2]->IsNumber() \ - ||!info[3]->IsNumber()) return; \ - double x = info[0]->NumberValue(); \ - double y = info[1]->NumberValue(); \ - double width = info[2]->NumberValue(); \ - double height = info[3]->NumberValue(); + double args[4]; \ + if(!checkArgs(info, args, 4)) \ + return; \ + double x = args[0]; \ + double y = args[1]; \ + double width = args[2]; \ + double height = args[3]; /* * Text baselines. @@ -71,6 +70,31 @@ enum { pango_layout_get_font_description(LAYOUT), \ pango_context_get_language(pango_layout_get_context(LAYOUT))) +inline static bool checkArgs(const Nan::FunctionCallbackInfo &info, double *args, int argsNum, int offset = 0){ + int argsEnd = offset + argsNum; + bool areArgsValid = true; + + for (int i = offset; i < argsEnd; i++) { + double val = info[i]->NumberValue(); + + if (areArgsValid) { + if (val != val || + val == std::numeric_limits::infinity() || + val == -std::numeric_limits::infinity()) { + // We should continue the loop instead of returning immediately + // See https://html.spec.whatwg.org/multipage/canvas.html + + areArgsValid = false; + continue; + } + + args[i - offset] = val; + } + } + + return areArgsValid; +} + /* * Initialize Context2d. */ @@ -1059,18 +1083,25 @@ NAN_METHOD(Context2d::GetImageData) { */ NAN_METHOD(Context2d::DrawImage) { - if (info.Length() < 3) - return Nan::ThrowTypeError("invalid arguments"); - if (!info[0]->IsObject() - || !info[1]->IsNumber() - || !info[2]->IsNumber()) - return Nan::ThrowTypeError("Expected object, number and number"); + int infoLen = info.Length(); + if (infoLen != 3 && infoLen != 5 && infoLen != 9) + return Nan::ThrowTypeError("Invalid arguments"); + + if (!info[0]->IsObject()) + return Nan::ThrowTypeError("The first argument must be an object"); + + double args[8]; + if(!checkArgs(info, args, infoLen - 1, 1)) + return; float sx = 0 , sy = 0 , sw = 0 , sh = 0 - , dx, dy, dw, dh; + , dx = 0 + , dy = 0 + , dw = 0 + , dh = 0; cairo_surface_t *surface; @@ -1102,34 +1133,32 @@ NAN_METHOD(Context2d::DrawImage) { cairo_t *ctx = context->context(); // Arguments - switch (info.Length()) { + switch (infoLen) { // img, sx, sy, sw, sh, dx, dy, dw, dh case 9: - sx = info[1]->NumberValue(); - sy = info[2]->NumberValue(); - sw = info[3]->NumberValue(); - sh = info[4]->NumberValue(); - dx = info[5]->NumberValue(); - dy = info[6]->NumberValue(); - dw = info[7]->NumberValue(); - dh = info[8]->NumberValue(); + sx = args[0]; + sy = args[1]; + sw = args[2]; + sh = args[3]; + dx = args[4]; + dy = args[5]; + dw = args[6]; + dh = args[7]; break; // img, dx, dy, dw, dh case 5: - dx = info[1]->NumberValue(); - dy = info[2]->NumberValue(); - dw = info[3]->NumberValue(); - dh = info[4]->NumberValue(); + dx = args[0]; + dy = args[1]; + dw = args[2]; + dh = args[3]; break; // img, dx, dy case 3: - dx = info[1]->NumberValue(); - dy = info[2]->NumberValue(); + dx = args[0]; + dy = args[1]; dw = sw; dh = sh; break; - default: - return Nan::ThrowTypeError("invalid arguments"); } // Start draw @@ -1793,21 +1822,18 @@ NAN_GETTER(Context2d::GetStrokeColor) { */ NAN_METHOD(Context2d::BezierCurveTo) { - if (!info[0]->IsNumber() - ||!info[1]->IsNumber() - ||!info[2]->IsNumber() - ||!info[3]->IsNumber() - ||!info[4]->IsNumber() - ||!info[5]->IsNumber()) return; + double args[6]; + if(!checkArgs(info, args, 6)) + return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_curve_to(context->context() - , info[0]->NumberValue() - , info[1]->NumberValue() - , info[2]->NumberValue() - , info[3]->NumberValue() - , info[4]->NumberValue() - , info[5]->NumberValue()); + , args[0] + , args[1] + , args[2] + , args[3] + , args[4] + , args[5]); } /* @@ -1815,19 +1841,18 @@ NAN_METHOD(Context2d::BezierCurveTo) { */ NAN_METHOD(Context2d::QuadraticCurveTo) { - if (!info[0]->IsNumber() - ||!info[1]->IsNumber() - ||!info[2]->IsNumber() - ||!info[3]->IsNumber()) return; + double args[4]; + if(!checkArgs(info, args, 4)) + return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); double x, y - , x1 = info[0]->NumberValue() - , y1 = info[1]->NumberValue() - , x2 = info[2]->NumberValue() - , y2 = info[3]->NumberValue(); + , x1 = args[0] + , y1 = args[1] + , x2 = args[2] + , y2 = args[3]; cairo_get_current_point(ctx, &x, &y); @@ -1884,13 +1909,12 @@ NAN_METHOD(Context2d::ClosePath) { */ NAN_METHOD(Context2d::Rotate) { - double angle = info[0]->NumberValue(); - - if (isnan(angle) || isinf(angle)) + double args[1]; + if(!checkArgs(info, args, 1)) return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_rotate(context->context(), angle); + cairo_rotate(context->context(), args[0]); } /* @@ -1898,14 +1922,18 @@ NAN_METHOD(Context2d::Rotate) { */ NAN_METHOD(Context2d::Transform) { + double args[6]; + if(!checkArgs(info, args, 6)) + return; + cairo_matrix_t matrix; cairo_matrix_init(&matrix - , info[0]->IsNumber() ? info[0]->NumberValue() : 0 - , info[1]->IsNumber() ? info[1]->NumberValue() : 0 - , info[2]->IsNumber() ? info[2]->NumberValue() : 0 - , info[3]->IsNumber() ? info[3]->NumberValue() : 0 - , info[4]->IsNumber() ? info[4]->NumberValue() : 0 - , info[5]->IsNumber() ? info[5]->NumberValue() : 0); + , args[0] + , args[1] + , args[2] + , args[3] + , args[4] + , args[5]); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_transform(context->context(), &matrix); @@ -1941,10 +1969,12 @@ NAN_METHOD(Context2d::GetMatrix) { */ NAN_METHOD(Context2d::Translate) { + double args[2]; + if(!checkArgs(info, args, 2)) + return; + Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_translate(context->context() - , info[0]->IsNumber() ? info[0]->NumberValue() : 0 - , info[1]->IsNumber() ? info[1]->NumberValue() : 0); + cairo_translate(context->context(), args[0], args[1]); } /* @@ -1952,10 +1982,12 @@ NAN_METHOD(Context2d::Translate) { */ NAN_METHOD(Context2d::Scale) { + double args[2]; + if(!checkArgs(info, args, 2)) + return; + Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_scale(context->context() - , info[0]->IsNumber() ? info[0]->NumberValue() : 0 - , info[1]->IsNumber() ? info[1]->NumberValue() : 0); + cairo_scale(context->context(), args[0], args[1]); } /* @@ -2010,18 +2042,20 @@ get_text_scale(Context2d *context, char *str, double maxWidth) { */ NAN_METHOD(Context2d::FillText) { - if (!info[1]->IsNumber() - || !info[2]->IsNumber()) return; + int argsNum = info.Length() >= 4 ? 3 : 2; + double args[3]; + if(!checkArgs(info, args, argsNum, 1)) + return; String::Utf8Value str(info[0]->ToString()); - double x = info[1]->NumberValue(); - double y = info[2]->NumberValue(); + double x = args[0]; + double y = args[1]; double scaled_by = 1; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - if (info[3]->IsNumber()) { - scaled_by = get_text_scale(context, *str, info[3]->NumberValue()); + if (argsNum == 3) { + scaled_by = get_text_scale(context, *str, args[2]); cairo_scale(context->context(), scaled_by, 1); } @@ -2043,18 +2077,20 @@ NAN_METHOD(Context2d::FillText) { */ NAN_METHOD(Context2d::StrokeText) { - if (!info[1]->IsNumber() - || !info[2]->IsNumber()) return; + int argsNum = info.Length() >= 4 ? 3 : 2; + double args[3]; + if(!checkArgs(info, args, argsNum, 1)) + return; String::Utf8Value str(info[0]->ToString()); - double x = info[1]->NumberValue(); - double y = info[2]->NumberValue(); + double x = args[0]; + double y = args[1]; double scaled_by = 1; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - if (info[3]->IsNumber()) { - scaled_by = get_text_scale(context, *str, info[3]->NumberValue()); + if (argsNum == 3) { + scaled_by = get_text_scale(context, *str, args[2]); cairo_scale(context->context(), scaled_by, 1); } @@ -2133,15 +2169,12 @@ Context2d::setTextPath(const char *str, double x, double y) { */ NAN_METHOD(Context2d::LineTo) { - if (!info[0]->IsNumber()) - return Nan::ThrowTypeError("lineTo() x must be a number"); - if (!info[1]->IsNumber()) - return Nan::ThrowTypeError("lineTo() y must be a number"); + double args[2]; + if(!checkArgs(info, args, 2)) + return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_line_to(context->context() - , info[0]->NumberValue() - , info[1]->NumberValue()); + cairo_line_to(context->context(), args[0], args[1]); } /* @@ -2149,15 +2182,12 @@ NAN_METHOD(Context2d::LineTo) { */ NAN_METHOD(Context2d::MoveTo) { - if (!info[0]->IsNumber()) - return Nan::ThrowTypeError("moveTo() x must be a number"); - if (!info[1]->IsNumber()) - return Nan::ThrowTypeError("moveTo() y must be a number"); + double args[2]; + if(!checkArgs(info, args, 2)) + return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_move_to(context->context() - , info[0]->NumberValue() - , info[1]->NumberValue()); + cairo_move_to(context->context(), args[0], args[1]); } /* @@ -2487,11 +2517,9 @@ NAN_METHOD(Context2d::Arc) { */ NAN_METHOD(Context2d::ArcTo) { - if (!info[0]->IsNumber() - || !info[1]->IsNumber() - || !info[2]->IsNumber() - || !info[3]->IsNumber() - || !info[4]->IsNumber()) return; + double args[5]; + if(!checkArgs(info, args, 5)) + return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); @@ -2502,12 +2530,12 @@ NAN_METHOD(Context2d::ArcTo) { Point p0(x, y); // Point (x0,y0) - Point p1(info[0]->NumberValue(), info[1]->NumberValue()); + Point p1(args[0], args[1]); // Point (x1,y1) - Point p2(info[2]->NumberValue(), info[3]->NumberValue()); + Point p2(args[2], args[3]); - float radius = info[4]->NumberValue(); + float radius = args[4]; if ((p1.x == p0.x && p1.y == p0.y) || (p1.x == p2.x && p1.y == p2.y) @@ -2594,24 +2622,20 @@ NAN_METHOD(Context2d::ArcTo) { */ NAN_METHOD(Context2d::Ellipse) { - if (!info[0]->IsNumber() - || !info[1]->IsNumber() - || !info[2]->IsNumber() - || !info[3]->IsNumber() - || !info[4]->IsNumber() - || !info[5]->IsNumber() - || !info[6]->IsNumber()) return; + double args[7]; + if(!checkArgs(info, args, 7)) + return; - double radiusX = info[2]->NumberValue(); - double radiusY = info[3]->NumberValue(); + double radiusX = args[2]; + double radiusY = args[3]; if (radiusX == 0 || radiusY == 0) return; - double x = info[0]->NumberValue(); - double y = info[1]->NumberValue(); - double rotation = info[4]->NumberValue(); - double startAngle = info[5]->NumberValue(); - double endAngle = info[6]->NumberValue(); + double x = args[0]; + double y = args[1]; + double rotation = args[4]; + double startAngle = args[5]; + double endAngle = args[6]; bool anticlockwise = info[7]->BooleanValue(); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -2626,7 +2650,7 @@ NAN_METHOD(Context2d::Ellipse) { cairo_rotate(ctx, rotation); cairo_scale(ctx, xRatio, 1.0); cairo_translate(ctx, -x, -y); - if (anticlockwise && M_PI * 2 != info[4]->NumberValue()) { + if (anticlockwise && M_PI * 2 != args[4]) { cairo_arc_negative(ctx, x, y, diff --git a/test/canvas.test.js b/test/canvas.test.js index 0868e6bc7..b15ade3ef 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -1538,10 +1538,10 @@ describe('Canvas', function () { var sin = Math.sin(expected); var cos = Math.cos(expected); - assert.strictEqual(mat.m11, cos); - assert.strictEqual(mat.m12, sin); - assert.strictEqual(mat.m21, -sin); - assert.strictEqual(mat.m22, cos); + assert.ok(Math.abs(mat.m11 - cos) < Number.EPSILON); + assert.ok(Math.abs(mat.m12 - sin) < Number.EPSILON); + assert.ok(Math.abs(mat.m21 + sin) < Number.EPSILON); + assert.ok(Math.abs(mat.m22 - cos) < Number.EPSILON); } }); From 952f4e10e7a397c53aeaf823a3301bb334dab375 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Mon, 26 Mar 2018 15:53:01 -0400 Subject: [PATCH 126/474] old-style stdint #include to support macOS + node < 7 (#1135) Fixes #1134 --- src/color.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/color.h b/src/color.h index fc0474d6e..f4123e1b6 100644 --- a/src/color.h +++ b/src/color.h @@ -9,7 +9,7 @@ #define __COLOR_PARSER_H__ #include -#include +#include // node < 7 uses libstdc++ on macOS which lacks complete c++11 #include #include From 29087745722c17fb1894a1fc73bf30c0a48b0c11 Mon Sep 17 00:00:00 2001 From: Benjamin Byholm Date: Mon, 26 Mar 2018 22:53:55 +0300 Subject: [PATCH 127/474] Propagate async context (#1115) --- package.json | 2 +- src/Canvas.cc | 17 ++++++++++------- src/JPEGStream.h | 8 +++++--- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index dc5b8a7cc..8ec6fc8bc 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ }, "dependencies": { "node-pre-gyp": "^0.9.0", - "nan": "^2.4.0" + "nan": "^2.9.2" }, "devDependencies": { "assert-rejects": "^0.1.1", diff --git a/src/Canvas.cc b/src/Canvas.cc index 40be152dc..682a6f87b 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -204,17 +204,18 @@ Canvas::ToBufferAsync(uv_work_t *req) { void Canvas::ToBufferAsyncAfter(uv_work_t *req) { Nan::HandleScope scope; + Nan::AsyncResource async("canvas:ToBufferAsyncAfter"); closure_t *closure = (closure_t *) req->data; delete req; if (closure->status) { Local argv[1] = { Canvas::Error(closure->status) }; - closure->pfn->Call(1, argv); + closure->pfn->Call(1, argv, &async); } else { Local buf = Nan::CopyBuffer((char*)closure->data, closure->len).ToLocalChecked(); memcpy(Buffer::Data(buf), closure->data, closure->len); Local argv[2] = { Nan::Null(), buf }; - closure->pfn->Call(2, argv); + closure->pfn->Call(sizeof argv / sizeof *argv, argv, &async); } closure->canvas->Unref(); @@ -353,13 +354,14 @@ NAN_METHOD(Canvas::ToBuffer) { static cairo_status_t streamPNG(void *c, const uint8_t *data, unsigned len) { Nan::HandleScope scope; + Nan::AsyncResource async("canvas:StreamPNG"); closure_t *closure = (closure_t *) c; Local buf = Nan::CopyBuffer((char *)data, len).ToLocalChecked(); Local argv[3] = { Nan::Null() , buf , Nan::New(len) }; - Nan::MakeCallback(Nan::GetCurrentContext()->Global(), (v8::Local)closure->fn, 3, argv); + async.runInAsyncScope(Nan::GetCurrentContext()->Global(), closure->fn, sizeof argv / sizeof *argv, argv); return CAIRO_STATUS_SUCCESS; } @@ -462,13 +464,13 @@ NAN_METHOD(Canvas::StreamPNGSync) { return; } else if (status) { Local argv[1] = { Canvas::Error(status) }; - Nan::MakeCallback(Nan::GetCurrentContext()->Global(), (v8::Local)closure.fn, 1, argv); + Nan::Call(closure.fn, Nan::GetCurrentContext()->Global(), sizeof argv / sizeof *argv, argv); } else { Local argv[3] = { Nan::Null() , Nan::Null() , Nan::New(0) }; - Nan::MakeCallback(Nan::GetCurrentContext()->Global(), (v8::Local)closure.fn, 3, argv); + Nan::Call(closure.fn, Nan::GetCurrentContext()->Global(), sizeof argv / sizeof *argv, argv); } return; } @@ -486,13 +488,14 @@ void stream_pdf_free(char *, void *) {} static cairo_status_t streamPDF(void *c, const uint8_t *data, unsigned len) { Nan::HandleScope scope; + Nan::AsyncResource async("canvas:StreamPDF"); closure_t *closure = static_cast(c); Local buf = Nan::NewBuffer(const_cast(reinterpret_cast(data)), len, stream_pdf_free, 0).ToLocalChecked(); Local argv[3] = { Nan::Null() , buf , Nan::New(len) }; - Nan::MakeCallback(Nan::GetCurrentContext()->Global(), closure->fn, 3, argv); + async.runInAsyncScope(Nan::GetCurrentContext()->Global(), closure->fn, sizeof argv / sizeof *argv, argv); return CAIRO_STATUS_SUCCESS; } @@ -547,7 +550,7 @@ NAN_METHOD(Canvas::StreamPDFSync) { Nan::Null() , Nan::Null() , Nan::New(0) }; - Nan::Call(closure.fn, Nan::GetCurrentContext()->Global(), 3, argv); + Nan::Call(closure.fn, Nan::GetCurrentContext()->Global(), sizeof argv / sizeof *argv, argv); } } diff --git a/src/JPEGStream.h b/src/JPEGStream.h index edc887e78..4b0787a8b 100644 --- a/src/JPEGStream.h +++ b/src/JPEGStream.h @@ -30,6 +30,7 @@ init_closure_destination(j_compress_ptr cinfo){ boolean empty_closure_output_buffer(j_compress_ptr cinfo){ Nan::HandleScope scope; + Nan::AsyncResource async("canvas:empty_closure_output_buffer"); closure_destination_mgr *dest = (closure_destination_mgr *) cinfo->dest; Local buf = Nan::NewBuffer((char *)dest->buffer, dest->bufsize).ToLocalChecked(); @@ -39,7 +40,7 @@ empty_closure_output_buffer(j_compress_ptr cinfo){ Nan::Null() , buf }; - Nan::MakeCallback(Nan::GetCurrentContext()->Global(), (v8::Local)dest->closure->fn, 2, argv); + async.runInAsyncScope(Nan::GetCurrentContext()->Global(), dest->closure->fn, sizeof argv / sizeof *argv, argv); dest->buffer = (JOCTET *)malloc(dest->bufsize); cinfo->dest->next_output_byte = dest->buffer; @@ -50,6 +51,7 @@ empty_closure_output_buffer(j_compress_ptr cinfo){ void term_closure_destination(j_compress_ptr cinfo){ Nan::HandleScope scope; + Nan::AsyncResource async("canvas:term_closure_destination"); closure_destination_mgr *dest = (closure_destination_mgr *) cinfo->dest; /* emit remaining data */ @@ -60,7 +62,7 @@ term_closure_destination(j_compress_ptr cinfo){ , buf }; - Nan::MakeCallback(Nan::GetCurrentContext()->Global(), (v8::Local)dest->closure->fn, 2, data_argv); + async.runInAsyncScope(Nan::GetCurrentContext()->Global(), dest->closure->fn, sizeof data_argv / sizeof *data_argv, data_argv); // emit "end" Local end_argv[2] = { @@ -68,7 +70,7 @@ term_closure_destination(j_compress_ptr cinfo){ , Nan::Null() }; - Nan::MakeCallback(Nan::GetCurrentContext()->Global(), (v8::Local)dest->closure->fn, 2, end_argv); + async.runInAsyncScope(Nan::GetCurrentContext()->Global(), dest->closure->fn, sizeof end_argv / sizeof *end_argv, end_argv); } void From 8d5b4dd67953126ae27ac95b59c86d7fcec4c2b2 Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 2 Apr 2018 20:58:45 +0200 Subject: [PATCH 128/474] Copy canvas to a new surface when drawing to itself (#1137) Fix: #1136 --- CHANGELOG.md | 1 + src/CanvasRenderingContext2d.cc | 24 ++++++++++++++++++++++++ test/canvas.test.js | 22 ++++++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a5f0d0c4..90afd1d98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * Re-rasterize SVG when drawing to a context and dimensions changed (79bf232) * Prevent JPEG errors from crashing process (#1124) * Improve handling of invalid arguments (#1129) + * Fix repeating patterns when drawing a canvas to itself (#1136) ### Added * Prebuilds (#992) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 49bf01d8e..f53d884e8 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -1210,6 +1210,25 @@ NAN_METHOD(Context2d::DrawImage) { } } + bool sameCanvas = surface == context->canvas()->surface(); + cairo_surface_t *surfTemp; + cairo_t *ctxTemp; + + if (sameCanvas) { + int width = context->canvas()->getWidth(); + int height = context->canvas()->getHeight(); + + surfTemp = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + ctxTemp = cairo_create(surfTemp); + + cairo_set_source_surface(ctxTemp, surface, 0, 0); + cairo_pattern_set_filter(cairo_get_source(ctxTemp), context->state->patternQuality); + cairo_pattern_set_extend(cairo_get_source(ctxTemp), CAIRO_EXTEND_REFLECT); + cairo_paint_with_alpha(ctxTemp, 1); + + surface = surfTemp; + } + context->savePath(); cairo_rectangle(ctx, dx, dy, dw, dh); cairo_clip(ctx); @@ -1222,6 +1241,11 @@ NAN_METHOD(Context2d::DrawImage) { cairo_paint_with_alpha(ctx, context->state->globalAlpha); cairo_restore(ctx); + + if (sameCanvas) { + cairo_destroy(ctxTemp); + cairo_surface_destroy(surfTemp); + } } /* diff --git a/test/canvas.test.js b/test/canvas.test.js index b15ade3ef..ed4e3694a 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -1545,4 +1545,26 @@ describe('Canvas', function () { } }); + it('Context2d#drawImage()', function () { + var canvas = createCanvas(500, 500); + var ctx = canvas.getContext('2d'); + + ctx.fillStyle = 'white'; + ctx.fillRect(0, 0, 500, 500); + ctx.fillStyle = 'black'; + ctx.fillRect(5, 5, 10, 10); + ctx.drawImage(ctx.canvas, 20, 20); + + var imgd = ctx.getImageData(0, 0, 500, 500); + var data = imgd.data; + var count = 0; + + for(var i = 0; i < 500 * 500; i += 4){ + if(data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0) + count++; + } + + assert.strictEqual(count, 10 * 10 * 2); + }); + }); From 21e90df094cb51451a51d807e7fed52d92b83fdc Mon Sep 17 00:00:00 2001 From: Grey Vugrin Date: Mon, 16 Apr 2018 13:07:42 -0700 Subject: [PATCH 129/474] Fix order of fill text arguments `registerFont` example has reversed arguments, which leads to silent failure. Seen in: https://github.com/Automattic/node-canvas/issues/1117 --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 69418671c..fc9b3e95e 100644 --- a/Readme.md +++ b/Readme.md @@ -240,7 +240,7 @@ var canvas = createCanvas(500, 500), ctx = canvas.getContext('2d'); ctx.font = '12px "Comic Sans"'; -ctx.fillText(250, 10, 'Everyone hates this font :('); +ctx.fillText('Everyone hates this font :(', 250, 10); ``` The second argument is an object with properties that resemble the CSS properties that are specified in `@font-face` rules. You must specify at least `family`. `weight`, and `style` are optional (and default to "normal"). From 77140fdd34a50a2a48b82f54f7d7066e422f0642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20P=C3=B3lit?= Date: Fri, 20 Apr 2018 18:44:50 -0500 Subject: [PATCH 130/474] Fixed the stream instructions. (#1150) The pngStream options have been updated to stream.pipe(out) and now the out.on('finish'... event is actually called. --- Readme.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Readme.md b/Readme.md index fc9b3e95e..332b26687 100644 --- a/Readme.md +++ b/Readme.md @@ -134,13 +134,7 @@ var fs = require('fs') , out = fs.createWriteStream(__dirname + '/text.png') , stream = canvas.pngStream(); -stream.on('data', function(chunk){ - out.write(chunk); -}); - -stream.on('end', function(){ - console.log('The PNG stream ended'); -}); +stream.pipe(out); out.on('finish', function(){ console.log('The PNG file was created.'); From 731989eec9232b7e8fbe923d3681ef5610c40764 Mon Sep 17 00:00:00 2001 From: Nicholas Sherlock Date: Sun, 22 Apr 2018 06:37:27 +1200 Subject: [PATCH 131/474] Support disabling chroma subsampling (#1092) --- Readme.md | 1 + lib/canvas.js | 2 ++ lib/jpegstream.js | 4 +++- src/Canvas.cc | 10 +++++++--- src/JPEGStream.h | 5 ++++- 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Readme.md b/Readme.md index 332b26687..ff92a8548 100644 --- a/Readme.md +++ b/Readme.md @@ -171,6 +171,7 @@ var stream = canvas.jpegStream({ bufsize: 4096 // output buffer size in bytes, default: 4096 , quality: 75 // JPEG quality (0-100) default: 75 , progressive: false // true for progressive compression, default: false + , disableChromaSubsampling: false // true to disable 2x2 subsampling of the chroma components, default: false }); ``` diff --git a/lib/canvas.js b/lib/canvas.js index 918a33f40..8f2d74a42 100644 --- a/lib/canvas.js +++ b/lib/canvas.js @@ -94,6 +94,8 @@ Canvas.prototype.createJPEGStream = function(options){ bufsize: clampedBufSize , quality: options.quality || 75 , progressive: options.progressive || false + , chromaHSampFactor: options.disableChromaSubsampling ? 1 : 2 + , chromaVSampFactor: options.disableChromaSubsampling ? 1 : 2 }); }; diff --git a/lib/jpegstream.js b/lib/jpegstream.js index efae2f776..f6cba3713 100644 --- a/lib/jpegstream.js +++ b/lib/jpegstream.js @@ -54,7 +54,9 @@ JPEGStream.prototype._read = function _read() { var bufsize = this.options.bufsize; var quality = this.options.quality; var progressive = this.options.progressive; - self.canvas.streamJPEGSync(bufsize, quality, progressive, function(err, chunk){ + var chromaHSampFactor = this.options.chromaHSampFactor; + var chromaVSampFactor = this.options.chromaVSampFactor; + self.canvas.streamJPEGSync(bufsize, quality, progressive, chromaHSampFactor, chromaVSampFactor, function(err, chunk){ if (err) { self.emit('error', err); } else if (chunk) { diff --git a/src/Canvas.cc b/src/Canvas.cc index 682a6f87b..5a64870fd 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -568,15 +568,19 @@ NAN_METHOD(Canvas::StreamJPEGSync) { return Nan::ThrowTypeError("quality setting required"); if (!info[2]->IsBoolean()) return Nan::ThrowTypeError("progressive setting required"); - if (!info[3]->IsFunction()) + if (!info[3]->IsNumber()) + return Nan::ThrowTypeError("chromaHSampFactor required"); + if (!info[4]->IsNumber()) + return Nan::ThrowTypeError("chromaVSampFactor required"); + if (!info[5]->IsFunction()) return Nan::ThrowTypeError("callback function required"); Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); closure_t closure; - closure.fn = Local::Cast(info[3]); + closure.fn = Local::Cast(info[5]); Nan::TryCatch try_catch; - write_to_jpeg_stream(canvas->surface(), info[0]->NumberValue(), info[1]->NumberValue(), info[2]->BooleanValue(), &closure); + write_to_jpeg_stream(canvas->surface(), info[0]->NumberValue(), info[1]->NumberValue(), info[2]->BooleanValue(), info[3]->NumberValue(), info[4]->NumberValue(), &closure); if (try_catch.HasCaught()) { try_catch.ReThrow(); diff --git a/src/JPEGStream.h b/src/JPEGStream.h index 4b0787a8b..ff6c3f6d0 100644 --- a/src/JPEGStream.h +++ b/src/JPEGStream.h @@ -101,7 +101,7 @@ jpeg_closure_dest(j_compress_ptr cinfo, closure_t * closure, int bufsize){ } void -write_to_jpeg_stream(cairo_surface_t *surface, int bufsize, int quality, bool progressive, closure_t *closure){ +write_to_jpeg_stream(cairo_surface_t *surface, int bufsize, int quality, bool progressive, int chromaHSampFactor, int chromaVSampFactor, closure_t *closure){ int w = cairo_image_surface_get_width(surface); int h = cairo_image_surface_get_height(surface); struct jpeg_compress_struct cinfo; @@ -118,6 +118,9 @@ write_to_jpeg_stream(cairo_surface_t *surface, int bufsize, int quality, bool pr if (progressive) jpeg_simple_progression(&cinfo); jpeg_set_quality(&cinfo, quality, (quality<25)?0:1); + cinfo.comp_info[0].h_samp_factor = chromaHSampFactor; + cinfo.comp_info[0].v_samp_factor = chromaVSampFactor; + jpeg_closure_dest(&cinfo, closure, bufsize); jpeg_start_compress(&cinfo, TRUE); From 7695fb28404dcfc8435319a4151b56d3b352356f Mon Sep 17 00:00:00 2001 From: Techborn Date: Sun, 22 Apr 2018 03:22:22 -0500 Subject: [PATCH 132/474] Add ARM support to SYSTEM_PATHS (#1149) --- util/has_lib.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/util/has_lib.js b/util/has_lib.js index 71b4dd8ea..c8aa5a417 100644 --- a/util/has_lib.js +++ b/util/has_lib.js @@ -9,7 +9,9 @@ var SYSTEM_PATHS = [ '/usr/local/lib', '/opt/local/lib', '/usr/lib/x86_64-linux-gnu', - '/usr/lib/i386-linux-gnu' + '/usr/lib/i386-linux-gnu', + '/usr/lib/arm-linux-gnueabihf', + '/usr/lib/arm-linux-gnueabi' ] /** From f49a011cd4522d0ea24896b0127168dca488ffe6 Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 30 Apr 2018 02:07:26 +0200 Subject: [PATCH 133/474] Prevent segfaults caused by creating a too large canvas (#1151) * Fix warning * Detect bad allocations * Fix zero-width canvas edge case * Add test * Update changelog * Fix compiler error * Improve error handling * Update tests * Fix tests for node 4 * Add new line --- CHANGELOG.md | 1 + src/Canvas.cc | 5 +++ src/CanvasRenderingContext2d.cc | 19 +++++--- src/backend/Backend.cc | 79 ++++++++++++++++++++------------- src/backend/Backend.h | 4 ++ src/backend/ImageBackend.cc | 10 ++--- test/canvas.test.js | 36 +++++++++++++++ 7 files changed, 112 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90afd1d98..ec1926e47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * Prevent JPEG errors from crashing process (#1124) * Improve handling of invalid arguments (#1129) * Fix repeating patterns when drawing a canvas to itself (#1136) + * Prevent segfaults caused by creating a too large canvas ### Added * Prebuilds (#992) diff --git a/src/Canvas.cc b/src/Canvas.cc index 5a64870fd..944c75530 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -116,6 +116,11 @@ NAN_METHOD(Canvas::New) { backend = new ImageBackend(0, 0); } + if (!backend->isSurfaceValid()) { + delete backend; + return Nan::ThrowError(backend->getError()); + } + Canvas* canvas = new Canvas(backend); canvas->Wrap(info.This()); diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index f53d884e8..06b11f89f 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -675,6 +675,7 @@ NAN_METHOD(Context2d::New) { } Context2d *context = new Context2d(canvas); + context->Wrap(info.This()); info.GetReturnValue().Set(info.This()); } @@ -825,7 +826,7 @@ NAN_METHOD(Context2d::PutImageData) { dst += dstStride; src += srcStride; } - break; + break; } case CAIRO_FORMAT_RGB24: { src += sy * srcStride + sx * 4; @@ -923,6 +924,14 @@ NAN_METHOD(Context2d::GetImageData) { if (!sh) return Nan::ThrowError("IndexSizeError: The source height is 0."); + int width = canvas->getWidth(); + int height = canvas->getHeight(); + + if (!width) + return Nan::ThrowTypeError("Canvas width is 0"); + if (!height) + return Nan::ThrowTypeError("Canvas height is 0"); + // WebKit and Firefox have this behavior: // Flip the coordinates so the origin is top/left-most: if (sw < 0) { @@ -934,8 +943,6 @@ NAN_METHOD(Context2d::GetImageData) { sh = -sh; } - int width = canvas->getWidth(); - int height = canvas->getHeight(); if (sx + sw > width) sw = width - sx; if (sy + sh > height) sh = height - sy; @@ -955,7 +962,7 @@ NAN_METHOD(Context2d::GetImageData) { } int srcStride = canvas->stride(); - int bpp = srcStride / canvas->getWidth(); + int bpp = srcStride / width; int size = sw * sh * bpp; int dstStride = sw * bpp; @@ -1211,8 +1218,8 @@ NAN_METHOD(Context2d::DrawImage) { } bool sameCanvas = surface == context->canvas()->surface(); - cairo_surface_t *surfTemp; - cairo_t *ctxTemp; + cairo_surface_t *surfTemp = NULL; + cairo_t *ctxTemp = NULL; if (sameCanvas) { int width = context->canvas()->getWidth(); diff --git a/src/backend/Backend.cc b/src/backend/Backend.cc index b3e3dac45..6075f9544 100644 --- a/src/backend/Backend.cc +++ b/src/backend/Backend.cc @@ -2,88 +2,105 @@ Backend::Backend(string name, int width, int height) - : name(name) - , width(width) - , height(height) - , surface(NULL) - , canvas(NULL) - , _closure(NULL) + : name(name) + , width(width) + , height(height) + , surface(NULL) + , canvas(NULL) + , _closure(NULL) {} Backend::~Backend() { - this->destroySurface(); + this->destroySurface(); } void Backend::setCanvas(Canvas* _canvas) { - this->canvas = _canvas; + this->canvas = _canvas; } cairo_surface_t* Backend::recreateSurface() { - this->destroySurface(); + this->destroySurface(); - return this->createSurface(); + return this->createSurface(); } cairo_surface_t* Backend::getSurface() { - if (!surface) createSurface(); - return surface; + if (!surface) createSurface(); + return surface; } void Backend::destroySurface() { - if(this->surface) - { - cairo_surface_destroy(this->surface); - this->surface = NULL; - } + if(this->surface) + { + cairo_surface_destroy(this->surface); + this->surface = NULL; + } } string Backend::getName() { - return name; + return name; } int Backend::getWidth() { - return this->width; + return this->width; } void Backend::setWidth(int width_) { - this->width = width_; - this->recreateSurface(); + this->width = width_; + this->recreateSurface(); } int Backend::getHeight() { - return this->height; + return this->height; } void Backend::setHeight(int height_) { - this->height = height_; - this->recreateSurface(); + this->height = height_; + this->recreateSurface(); +} + +bool Backend::isSurfaceValid(){ + bool hadSurface = surface != NULL; + bool isValid = true; + + cairo_status_t status = cairo_surface_status(getSurface()); + + if (status != CAIRO_STATUS_SUCCESS) { + error = cairo_status_to_string(status); + isValid = false; + } + + if (!hadSurface) + destroySurface(); + + return isValid; } BackendOperationNotAvailable::BackendOperationNotAvailable(Backend* backend, - string operation_name) - : backend(backend) - , operation_name(operation_name) + string operation_name) + : backend(backend) + , operation_name(operation_name) {}; BackendOperationNotAvailable::~BackendOperationNotAvailable() throw() {}; const char* BackendOperationNotAvailable::what() const throw() { - std::ostringstream o; + std::ostringstream o; - o << "operation " << this->operation_name; - o << " not supported by backend " + backend->getName(); + o << "operation " << this->operation_name; + o << " not supported by backend " + backend->getName(); - return o.str().c_str(); + return o.str().c_str(); }; diff --git a/src/backend/Backend.h b/src/backend/Backend.h index dbb91c00f..bcee4ddb1 100644 --- a/src/backend/Backend.h +++ b/src/backend/Backend.h @@ -17,6 +17,7 @@ class Backend : public Nan::ObjectWrap { private: const string name; + const char* error = NULL; protected: int width; @@ -58,6 +59,9 @@ class Backend : public Nan::ObjectWrap return CAIRO_FORMAT_INVALID; #endif } + + bool isSurfaceValid(); + inline const char* getError(){ return error; } }; diff --git a/src/backend/ImageBackend.cc b/src/backend/ImageBackend.cc index d74250b2a..f63f7d7c4 100644 --- a/src/backend/ImageBackend.cc +++ b/src/backend/ImageBackend.cc @@ -37,12 +37,12 @@ int32_t ImageBackend::approxBytesPerPixel() { cairo_surface_t* ImageBackend::createSurface() { - assert(!this->surface); - this->surface = cairo_image_surface_create(this->format, width, height); - assert(this->surface); - Nan::AdjustExternalMemory(approxBytesPerPixel() * width * height); + assert(!this->surface); + this->surface = cairo_image_surface_create(this->format, width, height); + assert(this->surface); + Nan::AdjustExternalMemory(approxBytesPerPixel() * width * height); - return this->surface; + return this->surface; } cairo_surface_t* ImageBackend::recreateSurface() diff --git a/test/canvas.test.js b/test/canvas.test.js index ed4e3694a..2a173c351 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -257,6 +257,42 @@ describe('Canvas', function () { assert.ok('object' == typeof ctx); assert.equal(canvas, ctx.canvas, 'context.canvas is not canvas'); assert.equal(ctx, canvas.context, 'canvas.context is not context'); + + const MAX_IMAGE_SIZE = 32767; + + [ + [0, 0, 1], + [1, 0, 1], + [MAX_IMAGE_SIZE, 0, 1], + [MAX_IMAGE_SIZE + 1, 0, 3], + [MAX_IMAGE_SIZE, MAX_IMAGE_SIZE, null], + [MAX_IMAGE_SIZE + 1, MAX_IMAGE_SIZE, 3], + [MAX_IMAGE_SIZE + 1, MAX_IMAGE_SIZE + 1, 3], + [Math.pow(2, 30), 0, 3], + [Math.pow(2, 30), 1, 3], + [Math.pow(2, 32), 0, 1], + [Math.pow(2, 32), 1, 1], + ].forEach(params => { + var width = params[0]; + var height = params[1]; + var errorLevel = params[2]; + + var level = 3; + + try { + var canvas = createCanvas(width, height); + level--; + + var ctx = canvas.getContext('2d'); + level--; + + ctx.getImageData(0, 0, 1, 1); + level--; + } catch (err) {} + + if (errorLevel !== null) + assert.strictEqual(level, errorLevel); + }); }); it('Canvas#getContext("2d", {pixelFormat: string})', function () { From 6ef44c3ed2a3aaa76d9fd5935464a6fae48e651b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaras=C5=82a=C5=AD=20Viktor=C4=8Dyk?= Date: Fri, 4 May 2018 16:27:07 +0100 Subject: [PATCH 134/474] add {libc} to prebuilt binary package name (#1156) * add {libc} to prebuilt binary package name * update changelog with libc binary --- CHANGELOG.md | 2 +- package.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec1926e47..3fb5fdff1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,7 +32,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * Prevent segfaults caused by creating a too large canvas ### Added - * Prebuilds (#992) + * Prebuilds (#992) with different libc versions to the prebuilt binary (#1140) * Support canvas.getContext("2d", {alpha: boolean}) and canvas.getContext("2d", {pixelFormat: "..."}) * Support indexed PNG encoding. diff --git a/package.json b/package.json index 8ec6fc8bc..a46b9ca93 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "module_name": "canvas-prebuilt", "module_path": "build/Release", "host": "https://github.com/node-gfx/node-canvas-prebuilt/releases/download/", - "remote_path": "v{version}" + "remote_path": "v{version}", + "package_name": "{module_name}-v{version}-{node_abi}-{platform}-{libc}-{arch}.tar.gz" }, "dependencies": { "node-pre-gyp": "^0.9.0", From c8c420b449aa3f21f4027c41fe844489bdeb7d5e Mon Sep 17 00:00:00 2001 From: DaniDror <37115972+DaniDror@users.noreply.github.com> Date: Tue, 29 May 2018 18:25:38 +0300 Subject: [PATCH 135/474] Parse font regex font family fix (#1170) * Fix FontFamily regex to allow whitespace in name Current regex breaks for fonts with whitespaces in their font family * Update Changelog --- CHANGELOG.md | 1 + lib/parse-font.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fb5fdff1..83d6caedd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * Improve handling of invalid arguments (#1129) * Fix repeating patterns when drawing a canvas to itself (#1136) * Prevent segfaults caused by creating a too large canvas + * Fix parse-font regex to allow for whitespaces. ### Added * Prebuilds (#992) with different libc versions to the prebuilt binary (#1140) diff --git a/lib/parse-font.js b/lib/parse-font.js index cf91b6e61..a149c709c 100644 --- a/lib/parse-font.js +++ b/lib/parse-font.js @@ -9,7 +9,7 @@ const weights = 'bold|bolder|lighter|[1-9]00' , variants = 'small-caps' , stretches = 'ultra-condensed|extra-condensed|condensed|semi-condensed|semi-expanded|expanded|extra-expanded|ultra-expanded' , units = 'px|pt|pc|in|cm|mm|%|em|ex|ch|rem|q' - , string = '\'([^\']+)\'|"([^"]+)"|[\\w-]+' + , string = '\'([^\']+)\'|"([^"]+)"|[\\w\\s-]+' // [ [ <‘font-style’> || || <‘font-weight’> || <‘font-stretch’> ]? // <‘font-size’> [ / <‘line-height’> ]? <‘font-family’> ] From d0716a22ab8d459b2c4cd8b3a0a61c17df06fed1 Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 29 May 2018 22:22:55 +0200 Subject: [PATCH 136/474] Allow assigning non-string values to fillStyle and strokeStyle (#1171) * Allow assigning non-string values to fillStyle and strokeStyle * Fix error in tests * Fixed some edge cases * Replace v8::TryCatch with Nan::TryCatch * . * Move checks to JS * Fix the order of calling stringifying functions --- CHANGELOG.md | 1 + lib/context2d.js | 18 ++++++++---------- src/CanvasRenderingContext2d.cc | 4 ++++ test/canvas.test.js | 20 ++++++++++++++++++++ 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83d6caedd..ee963ac7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * Fix repeating patterns when drawing a canvas to itself (#1136) * Prevent segfaults caused by creating a too large canvas * Fix parse-font regex to allow for whitespaces. + * Allow assigning non-string values to fillStyle and strokeStyle ### Added * Prebuilds (#992) with different libc versions to the prebuilt binary (#1140) diff --git a/lib/context2d.js b/lib/context2d.js index 61c29efa5..3b9f37b36 100644 --- a/lib/context2d.js +++ b/lib/context2d.js @@ -125,14 +125,13 @@ Object.defineProperty(Context2d.prototype, 'currentTransform', { */ Context2d.prototype.__defineSetter__('fillStyle', function(val){ - if (!val) return; - if ('CanvasGradient' == val.constructor.name - || 'CanvasPattern' == val.constructor.name) { + if (val instanceof CanvasGradient + || val instanceof CanvasPattern) { this.lastFillStyle = val; this._setFillPattern(val); - } else if ('string' == typeof val) { + } else { this.lastFillStyle = undefined; - this._setFillColor(val); + this._setFillColor(String(val)); } }); @@ -154,13 +153,12 @@ Context2d.prototype.__defineGetter__('fillStyle', function(){ */ Context2d.prototype.__defineSetter__('strokeStyle', function(val){ - if (!val) return; - if ('CanvasGradient' == val.constructor.name - || 'CanvasPattern' == val.constructor.name) { + if (val instanceof CanvasGradient + || val instanceof CanvasPattern) { this.lastStrokeStyle = val; this._setStrokePattern(val); - } else if ('string' == typeof val) { - this._setStrokeColor(val); + } else { + this._setStrokeColor(String(val)); } }); diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 06b11f89f..954468f32 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -1802,8 +1802,10 @@ NAN_GETTER(Context2d::GetShadowColor) { NAN_METHOD(Context2d::SetFillColor) { short ok; + if (!info[0]->IsString()) return; String::Utf8Value str(info[0]); + uint32_t rgba = rgba_from_string(*str, &ok); if (!ok) return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -1828,8 +1830,10 @@ NAN_GETTER(Context2d::GetFillColor) { NAN_METHOD(Context2d::SetStrokeColor) { short ok; + if (!info[0]->IsString()) return; String::Utf8Value str(info[0]); + uint32_t rgba = rgba_from_string(*str, &ok); if (!ok) return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); diff --git a/test/canvas.test.js b/test/canvas.test.js index 2a173c351..5c4aa020c 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -1603,4 +1603,24 @@ describe('Canvas', function () { assert.strictEqual(count, 10 * 10 * 2); }); + it('Context2d#SetFillColor()', function () { + var canvas = createCanvas(2, 2); + var ctx = canvas.getContext('2d'); + + ctx.fillStyle = ['#808080']; + ctx.fillRect(0, 0, 2, 2); + var data = ctx.getImageData(0, 0, 2, 2).data; + + data.forEach(function (byte, index) { + if (index + 1 & 3) + assert.strictEqual(byte, 128); + else + assert.strictEqual(byte, 255); + }); + + assert.throws(function () { + ctx.fillStyle = Object.create(null); + }); + }); + }); From 140e6eb428c80267273ba8580d8fe87485e3c976 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 21 Apr 2018 12:24:27 -0700 Subject: [PATCH 137/474] disableChromaSubsampling: false -> chromaSubsampling: true --- Readme.md | 2 +- lib/canvas.js | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Readme.md b/Readme.md index ff92a8548..8a9d87e59 100644 --- a/Readme.md +++ b/Readme.md @@ -171,7 +171,7 @@ var stream = canvas.jpegStream({ bufsize: 4096 // output buffer size in bytes, default: 4096 , quality: 75 // JPEG quality (0-100) default: 75 , progressive: false // true for progressive compression, default: false - , disableChromaSubsampling: false // true to disable 2x2 subsampling of the chroma components, default: false + , chromaSubsampling: true // false to disable 2x2 subsampling of the chroma components, default: true }); ``` diff --git a/lib/canvas.js b/lib/canvas.js index 8f2d74a42..a589a2e56 100644 --- a/lib/canvas.js +++ b/lib/canvas.js @@ -90,12 +90,21 @@ Canvas.prototype.createJPEGStream = function(options){ // Don't allow the buffer size to exceed the size of the canvas (#674) var maxBufSize = this.width * this.height * 4; var clampedBufSize = Math.min(options.bufsize || 4096, maxBufSize); + var chromaFactor; + if (typeof options.chromaSubsampling === "number") { + // libjpeg-turbo seems to complain about values above 2, but hopefully this + // can be supported in the future. For now 1 and 2 are valid. + // https://github.com/Automattic/node-canvas/pull/1092#issuecomment-366558028 + chromaFactor = options.chromaSubsampling; + } else { + chromaFactor = options.chromaSubsampling === false ? 1 : 2; + } return new JPEGStream(this, { bufsize: clampedBufSize , quality: options.quality || 75 , progressive: options.progressive || false - , chromaHSampFactor: options.disableChromaSubsampling ? 1 : 2 - , chromaVSampFactor: options.disableChromaSubsampling ? 1 : 2 + , chromaHSampFactor: chromaFactor + , chromaVSampFactor: chromaFactor }); }; From 81f4517745b282d92c0d13c5b461ee9da41eaa9f Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 19 Apr 2018 22:15:24 -0700 Subject: [PATCH 138/474] Support toBuffer("image/jpeg"), unify encoding configs See updated Readme.md and CHANGELOG.md Still needs more testing and possibly cleanup of the closure mess. --- CHANGELOG.md | 44 +++- Readme.md | 170 ++++++++++----- binding.gyp | 9 +- lib/canvas.js | 54 ++--- lib/jpegstream.js | 11 +- lib/pngstream.js | 24 +-- package.json | 4 +- src/Canvas.cc | 424 +++++++++++++++++++++----------------- src/Canvas.h | 3 +- src/JPEGStream.h | 58 ++++-- src/PNG.h | 8 +- src/backend/Backend.cc | 1 - src/backend/Backend.h | 4 - src/backend/PdfBackend.cc | 69 +++---- src/backend/PdfBackend.h | 4 + src/backend/SvgBackend.cc | 73 +++---- src/backend/SvgBackend.h | 4 + src/closure.cc | 41 ++-- src/closure.h | 55 ++++- src/toBuffer.cc | 4 +- test/canvas.test.js | 224 +++++++++++--------- 21 files changed, 745 insertions(+), 543 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee963ac7a..b7473174c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,43 @@ project adheres to [Semantic Versioning](http://semver.org/). 2.0.0 (unreleased -- encompasses all alpha versions) ================== +**Upgrading from 1.x** +```js +// (1) The quality argument for canvas.jpegStream now goes from 0 to 1 instead +// of from 0 to 100: +canvas.jpegStream({quality: 50}) // old +canvas.jpegStream({quality: 0.5}) // new + +// (2) The ZLIB compression level and PNG filter options for canvas.toBuffer are +// now named instead of positional arguments: +canvas.toBuffer(undefined, 3, canvas.PNG_FILTER_NONE) // old +canvas.toBuffer(undefined, {compressionLevel: 3, filters: canvas.PNG_FILTER_NONE}) // new +// or specify the mime type explicitly: +canvas.toBuffer("image/png", {compressionLevel: 3, filters: canvas.PNG_FILTER_NONE}) // new + +// (3) #2 also applies for canvas.pngStream, although these arguments were not +// documented: +canvas.pngStream(3, canvas.PNG_FILTER_NONE) // old +canvas.pngStream({compressionLevel: 3, filters: canvas.PNG_FILTER_NONE}) // new + +// (4) canvas.syncPNGStream() and canvas.syncJPEGStream() have been removed: +canvas.syncPNGStream() // old +canvas.pngStream() // new + +canvas.syncJPEGStream() // old +canvas.jpegStream() // new +``` + ### Breaking * Drop support for Node.js <4.x - * Remove sync streams (bc53059). Note that all or most streams are still - synchronous to some degree; this change just removed `syncPNGStream` and - friends. + * Remove sync stream functions (bc53059). Note that most streams are still + synchronous (run in the main thread); this change just removed `syncPNGStream` + and `syncJPEGStream`. * Pango is now *required* on all platforms (7716ae4). + * Make the `quality` argument for JPEG output go from 0 to 1 to match HTML spec. + * Make the `compressionLevel` and `filters` arguments for `canvas.toBuffer()` + named instead of positional. Same for `canvas.pngStream()`, although these + arguments were not documented. ### Fixed * Prevent segfaults caused by loading invalid fonts (#1105) @@ -35,8 +66,8 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Added * Prebuilds (#992) with different libc versions to the prebuilt binary (#1140) - * Support canvas.getContext("2d", {alpha: boolean}) and - canvas.getContext("2d", {pixelFormat: "..."}) + * Support `canvas.getContext("2d", {alpha: boolean})` and + `canvas.getContext("2d", {pixelFormat: "..."})` * Support indexed PNG encoding. * Support `currentTransform` (d6714ee) * Export `CanvasGradient` (6a4c0ab) @@ -48,6 +79,9 @@ project adheres to [Semantic Versioning](http://semver.org/). * Browser-compatible API (6a29a23) * Support for jpeg on Windows (42e9a74) * Support for backends (1a6dffe) + * Support for `canvas.toBuffer("image/jpeg")` + * Unified configuration options for `canvas.toBuffer()`, `canvas.pngStream()` + and `canvas.jpegStream()` 1.6.x (unreleased) ================== diff --git a/Readme.md b/Readme.md index 8a9d87e59..a5f5f9475 100644 --- a/Readme.md +++ b/Readme.md @@ -5,6 +5,9 @@ ## This is the documentation for version 2.0.0-alpha Alpha versions of 2.0 can be installed using `npm install canvas@next`. +See the [changelog](https://github.com/Automattic/node-canvas/blob/master/CHANGELOG.md) +for a guide to upgrading from 1.x to 2.x. + **For version 1.x documentation, see [the v1.x branch](https://github.com/Automattic/node-canvas/tree/v1.x)** ----- @@ -80,9 +83,11 @@ loadImage('examples/images/lime-cat.jpg').then((image) => { }) ``` -## Non-Standard API +## Non-Standard APIs - node-canvas extends the canvas API to provide interfacing with node, for example streaming PNG data, converting to a `Buffer` instance, etc. Among the interfacing API, in some cases the drawing API has been extended for SSJS image manipulation / creation usage, however keep in mind these additions may fail to render properly within browsers. +node-canvas implements the [HTML Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) as closely as possible. +(See [Compatibility Status](https://github.com/Automattic/node-canvas/wiki/Compatibility-Status) +for the current API compliance.) All non-standard APIs are documented below. ### Image#src=Buffer @@ -125,84 +130,145 @@ 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#pngStream(options) +### Canvas#toBuffer() - To create a `PNGStream` simply call `canvas.pngStream()`, and the stream will start to emit _data_ events, emitting _end_ when the data stream ends. If an exception occurs the _error_ event is emitted. +Creates a [`Buffer`](https://nodejs.org/api/buffer.html) object representing the +image contained in the canvas. + +> `canvas.toBuffer((err: Error|null, result: Buffer) => void[, mimeType[, config]]) => void` +> `canvas.toBuffer([mimeType[, config]]) => Buffer` + +* **callback** If provided, the buffer will be provided in the callback instead + of being returned by the function. Invoked with an error as the first argument + if encoding failed, or the resulting buffer as the second argument if it + succeeded. Not supported for mimeType `raw` or for PDF or SVG canvases (there + is no async work to do in those cases). +* **mimeType** A string indicating the image format. Valid options are `image/png`, + `image/jpeg` (if node-canvas was built with JPEG support) and `raw` (unencoded + ARGB32 data in native-endian byte order, top-to-bottom). Defaults to + `image/png`. If the canvas is a PDF or SVG canvas, this argument is ignored + and a PDF or SVG is returned always. +* **config** + * For `image/jpeg` an object specifying the quality (0 to 1), if progressive + compression should be used and/or if chroma subsampling should be used: + `{quality: 0.75, progressive: false, chromaSubsampling: true}`. All + properties are optional. + * For `image/png`, an object specifying the ZLIB compression level (between 0 + and 9), the compression filter(s), the palette (indexed PNGs only) and/or + the background palette index (indexed PNGs only): + `{compressionLevel: 6, filters: canvas.PNG_ALL_FILTERS, palette: undefined, backgroundIndex: 0}`. + All properties are optional. + +**Return value** + +If no callback is provided, a [`Buffer`](https://nodejs.org/api/buffer.html). +If a callback is provided, none. + +#### Examples ```javascript -var fs = require('fs') - , out = fs.createWriteStream(__dirname + '/text.png') - , stream = canvas.pngStream(); +// Default: buf contains a PNG-encoded image +const buf = canvas.toBuffer() -stream.pipe(out); +// PNG-encoded, zlib compression level 3 for faster compression but bigger files, no filtering +const buf2 = canvas.toBuffer('image/png', {compressionLevel: 3, filters: canvas.PNG_FILTER_NONE}) -out.on('finish', function(){ - console.log('The PNG file was created.'); -}); +// JPEG-encoded, 50% quality +const buf3 = canvas.toBuffer('image/jpeg', {quality: 0.5}) + +// Asynchronous PNG +canvas.toBuffer((err, buf) => { + if (err) throw err; // encoding failed + // buf is PNG-encoded image +}) + +canvas.toBuffer((err, buf) => { + if (err) throw err; // encoding failed + // buf is JPEG-encoded image at 95% quality +}, 'image/jpeg', {quality: 0.95}) + +// ARGB32 pixel values, native-endian +const buf4 = canvas.toBuffer('raw') +const {stride, width} = canvas +// In memory, this is `canvas.height * canvas.stride` bytes long. +// The top row of pixels, in ARGB order, left-to-right, is: +const topPixelsARGBLeftToRight = buf4.slice(0, width * 4) +// And the third row is: +const row3 = buf4.slice(2 * stride, 2 * stride + width * 4) + +// SVG and PDF canvases ignore the mimeType argument +const myCanvas = createCanvas(w, h, 'pdf') +myCanvas.toBuffer() // returns a buffer containing a PDF-encoded canvas +``` + +### Canvas#pngStream(options) + +Creates a [`ReadableStream`](https://nodejs.org/api/stream.html#stream_class_stream_readable) +that emits PNG-encoded data. + +> `canvas.pngStream([config]) => ReadableStream` + +* `config` An object specifying the ZLIB compression level (between 0 and 9), + the compression filter(s), the palette (indexed PNGs only) and/or the + background palette index (indexed PNGs only): + `{compressionLevel: 6, filters: canvas.PNG_ALL_FILTERS, palette: undefined, backgroundIndex: 0}`. + All properties are optional. + +#### Examples + +```javascript +const fs = require('fs') +const out = fs.createWriteStream(__dirname + '/test.png') +const stream = canvas.pngStream() +stream.pipe(out) +out.on('finish', () => console.log('The PNG file was created.')) ``` To encode indexed PNGs from canvases with `pixelFormat: 'A8'` or `'A1'`, provide an options object: ```js -var palette = new Uint8ClampedArray([ +const palette = new Uint8ClampedArray([ //r g b a 0, 50, 50, 255, // index 1 10, 90, 90, 255, // index 2 127, 127, 255, 255 // ... -]); +]) canvas.pngStream({ palette: palette, backgroundIndex: 0 // optional, defaults to 0 }) ``` -### Canvas#jpegStream() and Canvas#syncJPEGStream() - -You can likewise create a `JPEGStream` by calling `canvas.jpegStream()` with -some optional parameters; functionality is otherwise identical to -`pngStream()`. See `examples/crop.js` for an example. - -_Note: At the moment, `jpegStream()` is the same as `syncJPEGStream()`, both -are synchronous_ - -```javascript -var stream = canvas.jpegStream({ - bufsize: 4096 // output buffer size in bytes, default: 4096 - , quality: 75 // JPEG quality (0-100) default: 75 - , progressive: false // true for progressive compression, default: false - , chromaSubsampling: true // false to disable 2x2 subsampling of the chroma components, default: true -}); -``` - -### Canvas#toBuffer() +### Canvas#jpegStream() -A call to `Canvas#toBuffer()` will return a node `Buffer` instance containing image data. +Creates a [`ReadableStream`](https://nodejs.org/api/stream.html#stream_class_stream_readable) +that emits JPEG-encoded data. -```javascript -// PNG Buffer, default settings -var buf = canvas.toBuffer(); +_Note: At the moment, `jpegStream()` is synchronous under the hood. That is, it +runs in the main thread, not in the libuv threadpool._ -// PNG Buffer, zlib compression level 3 (from 0-9), faster but bigger -var buf2 = canvas.toBuffer(undefined, 3, canvas.PNG_FILTER_NONE); +> `canvas.pngStream([config]) => ReadableStream` -// ARGB32 Buffer, native-endian -var buf3 = canvas.toBuffer('raw'); -var stride = canvas.stride; -// In memory, this is `canvas.height * canvas.stride` bytes long. -// The top row of pixels, in ARGB order, left-to-right, is: -var topPixelsARGBLeftToRight = buf3.slice(0, canvas.width * 4); -var row3 = buf3.slice(2 * canvas.stride, 2 * canvas.stride + canvas.width * 4); -``` +* `config` an object specifying the quality (0 to 1), if progressive compression + should be used and/or if chroma subsampling should be used: + `{quality: 0.75, progressive: false, chromaSubsampling: true}`. All properties + are optional. -### Canvas#toBuffer() async - -Optionally we may pass a callback function to `Canvas#toBuffer()`, and this process will be performed asynchronously, and will `callback(err, buf)`. +#### Examples ```javascript -canvas.toBuffer(function(err, buf){ - -}); +const fs = require('fs') +const out = fs.createWriteStream(__dirname + '/test.jpeg') +const stream = canvas.jpegStream() +stream.pipe(out) +out.on('finish', () => console.log('The JPEG file was created.')) + +// Disable 2x2 chromaSubsampling for deeper colors and use a higher quality +const stream = canvas.jpegStream({ + quality: 95, + chromaSubsampling: false +}) ``` ### Canvas#toDataURL() sync and async diff --git a/binding.gyp b/binding.gyp index 23e2316da..79ea2fdf2 100644 --- a/binding.gyp +++ b/binding.gyp @@ -135,7 +135,14 @@ 'data; +Canvas::ToPngBufferAsync(uv_work_t *req) { + PngClosure* closure = static_cast(req->data); closure->status = canvas_write_to_png_stream( - closure->canvas->surface() - , toBuffer - , closure); + closure->canvas->surface(), + toBuffer, + closure); +} + +void +Canvas::ToJpegBufferAsync(uv_work_t *req) { + JpegClosure* closure = static_cast(req->data); + + write_to_jpeg_buffer(closure->canvas->surface(), closure, &closure->data, &closure->len); } /* @@ -210,7 +217,7 @@ void Canvas::ToBufferAsyncAfter(uv_work_t *req) { Nan::HandleScope scope; Nan::AsyncResource async("canvas:ToBufferAsyncAfter"); - closure_t *closure = (closure_t *) req->data; + Closure* closure = static_cast(req->data); delete req; if (closure->status) { @@ -218,41 +225,128 @@ Canvas::ToBufferAsyncAfter(uv_work_t *req) { closure->pfn->Call(1, argv, &async); } else { Local buf = Nan::CopyBuffer((char*)closure->data, closure->len).ToLocalChecked(); - memcpy(Buffer::Data(buf), closure->data, closure->len); Local argv[2] = { Nan::Null(), buf }; closure->pfn->Call(sizeof argv / sizeof *argv, argv, &async); } closure->canvas->Unref(); - delete closure->pfn; - closure_destroy(closure); - free(closure); + delete closure->pfn; // TODO move to destructor + delete closure; +} + +static void parsePNGArgs(Local arg, PngClosure& pngargs) { + if (arg->IsObject()) { + Local obj = arg->ToObject(); + + Local cLevel = obj->Get(Nan::New("compressionLevel").ToLocalChecked()); + if (cLevel->IsUint32()) { + uint32_t val = cLevel->Uint32Value(); + // See quote below from spec section 4.12.5.5. + if (val <= 9) pngargs.compressionLevel = val; + } + + Local filters = obj->Get(Nan::New("filters").ToLocalChecked()); + if (filters->IsUint32()) pngargs.filters = filters->Uint32Value(); + + Local palette = obj->Get(Nan::New("palette").ToLocalChecked()); + if (palette->IsUint8ClampedArray()) { + Local palette_ta = palette.As(); + pngargs.nPaletteColors = palette_ta->Length(); + if (pngargs.nPaletteColors % 4 != 0) { + throw "Palette length must be a multiple of 4."; + } + pngargs.nPaletteColors /= 4; + Nan::TypedArrayContents _paletteColors(palette_ta); + pngargs.palette = *_paletteColors; + // Optional background color index: + Local backgroundIndexVal = obj->Get(Nan::New("backgroundIndex").ToLocalChecked()); + if (backgroundIndexVal->IsUint32()) { + pngargs.backgroundIndex = static_cast(backgroundIndexVal->Uint32Value()); + } + } + } +} + +static void parseJPEGArgs(Local arg, JpegClosure& jpegargs) { + // "If Type(quality) is not Number, or if quality is outside that range, the + // user agent must use its default quality value, as if the quality argument + // had not been given." - 4.12.5.5 + if (arg->IsObject()) { + Local obj = arg->ToObject(); + + Local qual = obj->Get(Nan::New("quality").ToLocalChecked()); + if (qual->IsNumber()) { + double quality = qual->NumberValue(); + if (quality >= 0.0 && quality <= 1.0) { + jpegargs.quality = static_cast(100.0 * quality); + } + } + + Local chroma = obj->Get(Nan::New("chromaSubsampling").ToLocalChecked()); + if (chroma->IsBoolean()) { + bool subsample = chroma->BooleanValue(); + jpegargs.chromaSubsampling = subsample ? 2 : 1; + } else if (chroma->IsNumber()) { + jpegargs.chromaSubsampling = chroma->Uint32Value(); + } + + Local progressive = obj->Get(Nan::New("progressive").ToLocalChecked()); + if (!progressive->IsUndefined()) { + jpegargs.progressive = progressive->BooleanValue(); + } + } +} + +static uint32_t getSafeBufSize(Canvas* canvas) { + // Don't allow the buffer size to exceed the size of the canvas (#674) + // TODO not sure if this is really correct, but it fixed #674 + return min(canvas->getWidth() * canvas->getHeight() * 4, static_cast(PAGE_SIZE)); } /* - * Convert PNG data to a node::Buffer, async when a - * callback function is passed. + * Converts/encodes data to a Buffer. Async when a callback function is passed. + + * PDF/SVG canvases: + (any) => Buffer + + * ARGB data: + ("raw") => Buffer + + * PNG-encoded + () => Buffer + (undefined|"image/png", {compressionLevel?: number, filter?: number}) => Buffer + ((err: null|Error, buffer) => any) + ((err: null|Error, buffer) => any, undefined|"image/png", {compressionLevel?: number, filter?: number}) + + * JPEG-encoded + ("image/jpeg") => Buffer + ("image/jpeg", {quality?: number, progressive?: Boolean, chromaSubsampling?: Boolean|number}) => Buffer + ((err: null|Error, buffer) => any, "image/jpeg") + ((err: null|Error, buffer) => any, "image/jpeg", {quality?: number, progressive?: Boolean, chromaSubsampling?: Boolean|number}) */ NAN_METHOD(Canvas::ToBuffer) { cairo_status_t status; - uint32_t compression_level = 6; - uint32_t filter = PNG_ALL_FILTERS; Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - // TODO: async / move this out + // Vector canvases, sync only const string name = canvas->backend()->getName(); if (name == "pdf" || name == "svg") { cairo_surface_finish(canvas->surface()); - closure_t *closure = (closure_t *) canvas->backend()->closure(); + PdfSvgClosure* closure; + if (name == "pdf") { + closure = static_cast(canvas->backend())->closure(); + } else { + closure = static_cast(canvas->backend())->closure(); + } Local buf = Nan::CopyBuffer((char*) closure->data, closure->len).ToLocalChecked(); info.GetReturnValue().Set(buf); return; } - if (info.Length() >= 1 && info[0]->StrictEquals(Nan::New("raw").ToLocalChecked())) { - // Return raw ARGB data -- just a memcpy() + // Raw ARGB data -- just a memcpy() + if (info[0]->StrictEquals(Nan::New("raw").ToLocalChecked())) { cairo_surface_t *surface = canvas->surface(); cairo_surface_flush(surface); const unsigned char *data = cairo_image_surface_get_data(surface); @@ -261,54 +355,49 @@ NAN_METHOD(Canvas::ToBuffer) { return; } - if (info.Length() > 1 && !(info[1]->IsUndefined() && info[2]->IsUndefined())) { - if (!info[1]->IsUndefined()) { - bool good = true; - if (info[1]->IsNumber()) { - compression_level = info[1]->Uint32Value(); - } else if (info[1]->IsString()) { - if (info[1]->StrictEquals(Nan::New("0").ToLocalChecked())) { - compression_level = 0; - } else { - uint32_t tmp = info[1]->Uint32Value(); - if (tmp == 0) { - good = false; - } else { - compression_level = tmp; - } - } - } else { - good = false; - } - - if (good) { - if (compression_level > 9) { - return Nan::ThrowRangeError("Allowed compression levels lie in the range [0, 9]."); - } - } else { - return Nan::ThrowTypeError("Compression level must be a number."); - } - } + // Sync PNG, default + if (info[0]->IsUndefined() || info[0]->StrictEquals(Nan::New("image/png").ToLocalChecked())) { + try { + PngClosure closure(canvas); + parsePNGArgs(info[1], closure); + if (closure.nPaletteColors == 0xFFFFFFFF) { + Nan::ThrowError("Palette length must be a multiple of 4."); + return; + } + + Nan::TryCatch try_catch; + status = canvas_write_to_png_stream(canvas->surface(), toBuffer, &closure); - if (!info[2]->IsUndefined()) { - if (info[2]->IsUint32()) { - filter = info[2]->Uint32Value(); + if (try_catch.HasCaught()) { + try_catch.ReThrow(); + } else if (status) { + throw status; } else { - return Nan::ThrowTypeError("Invalid filter value."); + Local buf = Nan::CopyBuffer((char *)closure.data, closure.len).ToLocalChecked(); + info.GetReturnValue().Set(buf); } + } catch (cairo_status_t ex) { + Nan::ThrowError(Canvas::Error(ex)); + } catch (const char* ex) { + Nan::ThrowError(ex); } + return; } - // Async - if (info[0]->IsFunction()) { - closure_t *closure = (closure_t *) malloc(sizeof(closure_t)); - status = closure_init(closure, canvas, compression_level, filter); + // Async PNG + if (info[0]->IsFunction() && + (info[1]->IsUndefined() || info[1]->StrictEquals(Nan::New("image/png").ToLocalChecked()))) { - // ensure closure is ok - if (status) { - closure_destroy(closure); - free(closure); - return Nan::ThrowError(Canvas::Error(status)); + PngClosure* closure; + try { + closure = new PngClosure(canvas); + parsePNGArgs(info[2], *closure); + } catch (cairo_status_t ex) { + Nan::ThrowError(Canvas::Error(ex)); + return; + } catch (const char* ex) { + Nan::ThrowError(ex); + return; } // TODO: only one callback fn in closure @@ -319,37 +408,62 @@ NAN_METHOD(Canvas::ToBuffer) { req->data = closure; // Make sure the surface exists since we won't have an isolate context in the async block: canvas->surface(); - uv_queue_work(uv_default_loop(), req, ToBufferAsync, (uv_after_work_cb)ToBufferAsyncAfter); + uv_queue_work(uv_default_loop(), req, ToPngBufferAsync, (uv_after_work_cb)ToBufferAsyncAfter); return; - // Sync - } else { - closure_t closure; - status = closure_init(&closure, canvas, compression_level, filter); + } - // ensure closure is ok - if (status) { - closure_destroy(&closure); - return Nan::ThrowError(Canvas::Error(status)); +#ifdef HAVE_JPEG + // Sync JPEG + Local jpegStr = Nan::New("image/jpeg").ToLocalChecked(); + if (info[0]->StrictEquals(jpegStr)) { + try { + JpegClosure closure(canvas); + parseJPEGArgs(info[1], closure); + + Nan::TryCatch try_catch; + unsigned char *outbuff = NULL; + uint32_t outsize = 0; + write_to_jpeg_buffer(canvas->surface(), &closure, &outbuff, &outsize); + + if (try_catch.HasCaught()) { + try_catch.ReThrow(); + } else { + char *signedOutBuff = reinterpret_cast(outbuff); + Local buf = Nan::CopyBuffer(signedOutBuff, outsize).ToLocalChecked(); + info.GetReturnValue().Set(buf); + } + } catch (cairo_status_t ex) { + Nan::ThrowError(Canvas::Error(ex)); } + return; + } - Nan::TryCatch try_catch; - status = canvas_write_to_png_stream(canvas->surface(), toBuffer, &closure); - - if (try_catch.HasCaught()) { - closure_destroy(&closure); - try_catch.ReThrow(); - return; - } else if (status) { - closure_destroy(&closure); - return Nan::ThrowError(Canvas::Error(status)); - } else { - Local buf = Nan::CopyBuffer((char *)closure.data, closure.len).ToLocalChecked(); - closure_destroy(&closure); - info.GetReturnValue().Set(buf); + // Async JPEG + if (info[0]->IsFunction() && info[1]->StrictEquals(jpegStr)) { + JpegClosure* closure; + try { + closure = new JpegClosure(canvas); + } catch (cairo_status_t ex) { + Nan::ThrowError(Canvas::Error(ex)); return; } + + parseJPEGArgs(info[1], *closure); + + // TODO: only one callback fn in closure // TODO what does this comment mean? + canvas->Ref(); + closure->pfn = new Nan::Callback(info[0].As()); + + uv_work_t* req = new uv_work_t; + req->data = closure; + // Make sure the surface exists since we won't have an isolate context in the async block: + canvas->surface(); + uv_queue_work(uv_default_loop(), req, ToJpegBufferAsync, (uv_after_work_cb)ToBufferAsyncAfter); + + return; } +#endif } /* @@ -360,7 +474,7 @@ static cairo_status_t streamPNG(void *c, const uint8_t *data, unsigned len) { Nan::HandleScope scope; Nan::AsyncResource async("canvas:StreamPNG"); - closure_t *closure = (closure_t *) c; + PngClosure* closure = (PngClosure*) c; Local buf = Nan::CopyBuffer((char *)data, len).ToLocalChecked(); Local argv[3] = { Nan::Null() @@ -371,94 +485,20 @@ streamPNG(void *c, const uint8_t *data, unsigned len) { } /* - * Stream PNG data synchronously. - * TODO the compression level and filter args don't seem to be documented. - * Maybe move them to named properties in the options object? - * StreamPngSync(this, options: {palette?: Uint8ClampedArray}) - * StreamPngSync(this, compression_level?: uint32, filter?: uint32) + * Stream PNG data synchronously. TODO async + * StreamPngSync(this, options: {palette?: Uint8ClampedArray, backgroundIndex?: uint32, compressionLevel: uint32, filters: uint32}) */ NAN_METHOD(Canvas::StreamPNGSync) { - uint32_t compression_level = 6; - uint32_t filter = PNG_ALL_FILTERS; - // TODO: async as well if (!info[0]->IsFunction()) return Nan::ThrowTypeError("callback function required"); - + Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - uint8_t* paletteColors = NULL; - size_t nPaletteColors = 0; - uint8_t backgroundIndex = 0; - - if (info.Length() > 1 && !(info[1]->IsUndefined() && info[2]->IsUndefined())) { - if (!info[1]->IsUndefined()) { - bool good = true; - if (info[1]->IsNumber()) { - compression_level = info[1]->Uint32Value(); - } else if (info[1]->IsString()) { - if (info[1]->StrictEquals(Nan::New("0").ToLocalChecked())) { - compression_level = 0; - } else { - uint32_t tmp = info[1]->Uint32Value(); - if (tmp == 0) { - good = false; - } else { - compression_level = tmp; - } - } - } else if (info[1]->IsObject()) { - // If canvas is A8 or A1 and options obj has Uint8ClampedArray palette, - // encode as indexed PNG. - cairo_format_t format = canvas->backend()->getFormat(); - if (format == CAIRO_FORMAT_A8 || format == CAIRO_FORMAT_A1) { - Local attrs = info[1]->ToObject(); - Local palette = attrs->Get(Nan::New("palette").ToLocalChecked()); - if (palette->IsUint8ClampedArray()) { - Local palette_ta = palette.As(); - nPaletteColors = palette_ta->Length(); - if (nPaletteColors % 4 != 0) { - Nan::ThrowError("Palette length must be a multiple of 4."); - } - nPaletteColors /= 4; - Nan::TypedArrayContents _paletteColors(palette_ta); - paletteColors = *_paletteColors; - // Optional background color index: - Local backgroundIndexVal = attrs->Get(Nan::New("backgroundIndex").ToLocalChecked()); - if (backgroundIndexVal->IsUint32()) { - backgroundIndex = static_cast(backgroundIndexVal->Uint32Value()); - } - } - } - } else { - good = false; - } - if (good) { - if (compression_level > 9) { - return Nan::ThrowRangeError("Allowed compression levels lie in the range [0, 9]."); - } - } else { - return Nan::ThrowTypeError("Compression level must be a number."); - } - } + PngClosure closure(canvas); + parsePNGArgs(info[1], closure); - if (!info[2]->IsUndefined()) { - if (info[2]->IsUint32()) { - filter = info[2]->Uint32Value(); - } else { - return Nan::ThrowTypeError("Invalid filter value."); - } - } - } - - - closure_t closure; closure.fn = Local::Cast(info[0]); - closure.compression_level = compression_level; - closure.filter = filter; - closure.palette = paletteColors; - closure.nPaletteColors = nPaletteColors; - closure.backgroundIndex = backgroundIndex; Nan::TryCatch try_catch; @@ -480,6 +520,14 @@ NAN_METHOD(Canvas::StreamPNGSync) { return; } + +struct PdfStreamInfo { + Local fn; + uint32_t len; + uint8_t* data; +}; + + /* * Canvas::StreamPDF FreeCallback */ @@ -494,28 +542,27 @@ static cairo_status_t streamPDF(void *c, const uint8_t *data, unsigned len) { Nan::HandleScope scope; Nan::AsyncResource async("canvas:StreamPDF"); - closure_t *closure = static_cast(c); + PdfStreamInfo* streaminfo = static_cast(c); Local buf = Nan::NewBuffer(const_cast(reinterpret_cast(data)), len, stream_pdf_free, 0).ToLocalChecked(); Local argv[3] = { Nan::Null() , buf , Nan::New(len) }; - async.runInAsyncScope(Nan::GetCurrentContext()->Global(), closure->fn, sizeof argv / sizeof *argv, argv); + async.runInAsyncScope(Nan::GetCurrentContext()->Global(), streaminfo->fn, sizeof argv / sizeof *argv, argv); return CAIRO_STATUS_SUCCESS; } -cairo_status_t canvas_write_to_pdf_stream(cairo_surface_t *surface, cairo_write_func_t write_func, void *closure) { - closure_t *pdf_closure = static_cast(closure); - size_t whole_chunks = pdf_closure->len / PAGE_SIZE; - size_t remainder = pdf_closure->len - whole_chunks * PAGE_SIZE; +cairo_status_t canvas_write_to_pdf_stream(cairo_surface_t *surface, cairo_write_func_t write_func, PdfStreamInfo* streaminfo) { + size_t whole_chunks = streaminfo->len / PAGE_SIZE; + size_t remainder = streaminfo->len - whole_chunks * PAGE_SIZE; for (size_t i = 0; i < whole_chunks; ++i) { - write_func(pdf_closure, &pdf_closure->data[i * PAGE_SIZE], PAGE_SIZE); + write_func(streaminfo, &streaminfo->data[i * PAGE_SIZE], PAGE_SIZE); } if (remainder) { - write_func(pdf_closure, &pdf_closure->data[whole_chunks * PAGE_SIZE], remainder); + write_func(streaminfo, &streaminfo->data[whole_chunks * PAGE_SIZE], remainder); } return CAIRO_STATUS_SUCCESS; @@ -536,26 +583,28 @@ NAN_METHOD(Canvas::StreamPDFSync) { cairo_surface_finish(canvas->surface()); - closure_t closure; - closure.data = static_cast(canvas->backend()->closure())->data; - closure.len = static_cast(canvas->backend()->closure())->len; - closure.fn = info[0].As(); + PdfSvgClosure* closure = static_cast(canvas->backend())->closure(); + Local fn = info[0].As(); + PdfStreamInfo streaminfo; + streaminfo.fn = fn; + streaminfo.data = closure->data; + streaminfo.len = closure->len; Nan::TryCatch try_catch; - cairo_status_t status = canvas_write_to_pdf_stream(canvas->surface(), streamPDF, &closure); + cairo_status_t status = canvas_write_to_pdf_stream(canvas->surface(), streamPDF, &streaminfo); if (try_catch.HasCaught()) { try_catch.ReThrow(); } else if (status) { Local error = Canvas::Error(status); - Nan::Call(closure.fn, Nan::GetCurrentContext()->Global(), 1, &error); + Nan::Call(fn, Nan::GetCurrentContext()->Global(), 1, &error); } else { Local argv[3] = { Nan::Null() , Nan::Null() , Nan::New(0) }; - Nan::Call(closure.fn, Nan::GetCurrentContext()->Global(), sizeof argv / sizeof *argv, argv); + Nan::Call(fn, Nan::GetCurrentContext()->Global(), sizeof argv / sizeof *argv, argv); } } @@ -566,26 +615,17 @@ NAN_METHOD(Canvas::StreamPDFSync) { #ifdef HAVE_JPEG NAN_METHOD(Canvas::StreamJPEGSync) { - // TODO: async as well - if (!info[0]->IsNumber()) - return Nan::ThrowTypeError("buffer size required"); - if (!info[1]->IsNumber()) - return Nan::ThrowTypeError("quality setting required"); - if (!info[2]->IsBoolean()) - return Nan::ThrowTypeError("progressive setting required"); - if (!info[3]->IsNumber()) - return Nan::ThrowTypeError("chromaHSampFactor required"); - if (!info[4]->IsNumber()) - return Nan::ThrowTypeError("chromaVSampFactor required"); - if (!info[5]->IsFunction()) + if (!info[1]->IsFunction()) return Nan::ThrowTypeError("callback function required"); Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - closure_t closure; - closure.fn = Local::Cast(info[5]); + JpegClosure closure(canvas); + parseJPEGArgs(info[0], closure); + closure.fn = Local::Cast(info[1]); Nan::TryCatch try_catch; - write_to_jpeg_stream(canvas->surface(), info[0]->NumberValue(), info[1]->NumberValue(), info[2]->BooleanValue(), info[3]->NumberValue(), info[4]->NumberValue(), &closure); + uint32_t bufsize = getSafeBufSize(canvas); + write_to_jpeg_stream(canvas->surface(), bufsize, &closure); if (try_catch.HasCaught()) { try_catch.ReThrow(); diff --git a/src/Canvas.h b/src/Canvas.h index 69b78d173..847da4a00 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -63,7 +63,8 @@ class Canvas: public Nan::ObjectWrap { static NAN_METHOD(StreamJPEGSync); static NAN_METHOD(RegisterFont); static Local Error(cairo_status_t status); - static void ToBufferAsync(uv_work_t *req); + static void ToPngBufferAsync(uv_work_t *req); + static void ToJpegBufferAsync(uv_work_t *req); static void ToBufferAsyncAfter(uv_work_t *req); static PangoWeight GetWeightFromCSSString(const char *weight); static PangoStyle GetStyleFromCSSString(const char *style); diff --git a/src/JPEGStream.h b/src/JPEGStream.h index ff6c3f6d0..473be72fc 100644 --- a/src/JPEGStream.h +++ b/src/JPEGStream.h @@ -6,6 +6,7 @@ #ifndef __NODE_JPEG_STREAM_H__ #define __NODE_JPEG_STREAM_H__ +#include "closure.h" #include "Canvas.h" #include #include @@ -17,7 +18,7 @@ typedef struct { struct jpeg_destination_mgr pub; - closure_t *closure; + JpegClosure* closure; JOCTET *buffer; int bufsize; } closure_destination_mgr; @@ -74,7 +75,7 @@ term_closure_destination(j_compress_ptr cinfo){ } void -jpeg_closure_dest(j_compress_ptr cinfo, closure_t * closure, int bufsize){ +jpeg_closure_dest(j_compress_ptr cinfo, JpegClosure* closure, int bufsize){ closure_destination_mgr * dest; /* The destination object is made permanent so that multiple JPEG images @@ -100,34 +101,27 @@ jpeg_closure_dest(j_compress_ptr cinfo, closure_t * closure, int bufsize){ cinfo->dest->free_in_buffer = dest->bufsize; } -void -write_to_jpeg_stream(cairo_surface_t *surface, int bufsize, int quality, bool progressive, int chromaHSampFactor, int chromaVSampFactor, closure_t *closure){ +void encode_jpeg(jpeg_compress_struct cinfo, cairo_surface_t *surface, int quality, bool progressive, int chromaHSampFactor, int chromaVSampFactor) { int w = cairo_image_surface_get_width(surface); int h = cairo_image_surface_get_height(surface); - struct jpeg_compress_struct cinfo; - struct jpeg_error_mgr jerr; - JSAMPROW slr; - cinfo.err = jpeg_std_error(&jerr); - jpeg_create_compress(&cinfo); cinfo.in_color_space = JCS_RGB; cinfo.input_components = 3; cinfo.image_width = w; cinfo.image_height = h; jpeg_set_defaults(&cinfo); if (progressive) - jpeg_simple_progression(&cinfo); - jpeg_set_quality(&cinfo, quality, (quality<25)?0:1); + jpeg_simple_progression(&cinfo); + jpeg_set_quality(&cinfo, quality, (quality < 25) ? 0 : 1); cinfo.comp_info[0].h_samp_factor = chromaHSampFactor; cinfo.comp_info[0].v_samp_factor = chromaVSampFactor; - jpeg_closure_dest(&cinfo, closure, bufsize); - + JSAMPROW slr; jpeg_start_compress(&cinfo, TRUE); unsigned char *dst; - unsigned int *src = (unsigned int *) cairo_image_surface_get_data(surface); + unsigned int *src = (unsigned int *)cairo_image_surface_get_data(surface); int sl = 0; - dst = (unsigned char *) malloc(w * 3); + dst = (unsigned char *)malloc(w * 3); while (sl < h) { unsigned char *dp = dst; int x = 0; @@ -148,4 +142,38 @@ write_to_jpeg_stream(cairo_surface_t *surface, int bufsize, int quality, bool pr jpeg_destroy_compress(&cinfo); } +void +write_to_jpeg_stream(cairo_surface_t *surface, int bufsize, JpegClosure* closure) { + struct jpeg_compress_struct cinfo; + struct jpeg_error_mgr jerr; + cinfo.err = jpeg_std_error(&jerr); + jpeg_create_compress(&cinfo); + jpeg_closure_dest(&cinfo, closure, bufsize); + encode_jpeg( + cinfo, + surface, + closure->quality, + closure->progressive, + closure->chromaSubsampling, + closure->chromaSubsampling); +} + +void +write_to_jpeg_buffer(cairo_surface_t* surface, JpegClosure* closure, unsigned char** outbuff, uint32_t* outsize) { + struct jpeg_compress_struct cinfo; + struct jpeg_error_mgr jerr; + cinfo.err = jpeg_std_error(&jerr); + jpeg_create_compress(&cinfo); + unsigned long ulOutsize; + jpeg_mem_dest(&cinfo, outbuff, &ulOutsize); + encode_jpeg( + cinfo, + surface, + closure->quality, + closure->progressive, + closure->chromaSubsampling, + closure->chromaSubsampling); + *outsize = static_cast(ulOutsize); +} + #endif diff --git a/src/PNG.h b/src/PNG.h index d202f78f8..e35f24f09 100644 --- a/src/PNG.h +++ b/src/PNG.h @@ -89,7 +89,7 @@ static void canvas_convert_565_to_888(png_structp png, png_row_infop row_info, p struct canvas_png_write_closure_t { cairo_write_func_t write_func; - closure_t *closure; + PngClosure* closure; }; #ifdef PNG_SETJMP_SUPPORTED @@ -164,8 +164,8 @@ static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr writ #endif png_set_write_fn(png, closure, write_func, canvas_png_flush); - png_set_compression_level(png, closure->closure->compression_level); - png_set_filter(png, 0, closure->closure->filter); + png_set_compression_level(png, closure->closure->compressionLevel); + png_set_filter(png, 0, closure->closure->filters); cairo_format_t format = cairo_image_surface_get_format(surface); @@ -279,7 +279,7 @@ static void canvas_stream_write_func(png_structp png, png_bytep data, png_size_t } } -static cairo_status_t canvas_write_to_png_stream(cairo_surface_t *surface, cairo_write_func_t write_func, closure_t *closure) { +static cairo_status_t canvas_write_to_png_stream(cairo_surface_t *surface, cairo_write_func_t write_func, PngClosure* closure) { struct canvas_png_write_closure_t png_closure; if (cairo_surface_status(surface)) { diff --git a/src/backend/Backend.cc b/src/backend/Backend.cc index 6075f9544..74e05c17c 100644 --- a/src/backend/Backend.cc +++ b/src/backend/Backend.cc @@ -7,7 +7,6 @@ Backend::Backend(string name, int width, int height) , height(height) , surface(NULL) , canvas(NULL) - , _closure(NULL) {} Backend::~Backend() diff --git a/src/backend/Backend.h b/src/backend/Backend.h index bcee4ddb1..d3871c3cf 100644 --- a/src/backend/Backend.h +++ b/src/backend/Backend.h @@ -30,10 +30,6 @@ class Backend : public Nan::ObjectWrap public: virtual ~Backend(); - // TODO Used only by SVG and PDF, move there - void* _closure; - inline void* closure(){ return _closure; } - void setCanvas(Canvas* canvas); virtual cairo_surface_t* createSurface() = 0; diff --git a/src/backend/PdfBackend.cc b/src/backend/PdfBackend.cc index a976b6f86..03b70e38e 100644 --- a/src/backend/PdfBackend.cc +++ b/src/backend/PdfBackend.cc @@ -11,63 +11,50 @@ using namespace v8; PdfBackend::PdfBackend(int width, int height) - : Backend("pdf", width, height) -{ - _closure = malloc(sizeof(closure_t)); - assert(_closure); - createSurface(); + : Backend("pdf", width, height) { + createSurface(); } -PdfBackend::~PdfBackend() -{ - cairo_surface_finish(this->surface); - closure_destroy((closure_t*)_closure); - free(_closure); - - destroySurface(); +PdfBackend::~PdfBackend() { + cairo_surface_finish(surface); + if (_closure) delete _closure; + destroySurface(); } -cairo_surface_t* PdfBackend::createSurface() -{ - cairo_status_t status = closure_init((closure_t*)_closure, this->canvas, 0, PNG_NO_FILTERS); - assert(status == CAIRO_STATUS_SUCCESS); - - this->surface = cairo_pdf_surface_create_for_stream(toBuffer, _closure, width, height); - - return this->surface; +cairo_surface_t* PdfBackend::createSurface() { + if (!_closure) _closure = new PdfSvgClosure(canvas); + surface = cairo_pdf_surface_create_for_stream(toBuffer, _closure, width, height); + return surface; } -cairo_surface_t* PdfBackend::recreateSurface() -{ - cairo_pdf_surface_set_size(this->surface, width, height); +cairo_surface_t* PdfBackend::recreateSurface() { + cairo_pdf_surface_set_size(surface, width, height); - return this->surface; + return surface; } Nan::Persistent PdfBackend::constructor; -void PdfBackend::Initialize(Handle target) -{ - Nan::HandleScope scope; +void PdfBackend::Initialize(Handle target) { + Nan::HandleScope scope; - Local ctor = Nan::New(PdfBackend::New); - PdfBackend::constructor.Reset(ctor); - ctor->InstanceTemplate()->SetInternalFieldCount(1); - ctor->SetClassName(Nan::New("PdfBackend").ToLocalChecked()); - target->Set(Nan::New("PdfBackend").ToLocalChecked(), ctor->GetFunction()); + Local ctor = Nan::New(PdfBackend::New); + PdfBackend::constructor.Reset(ctor); + ctor->InstanceTemplate()->SetInternalFieldCount(1); + ctor->SetClassName(Nan::New("PdfBackend").ToLocalChecked()); + target->Set(Nan::New("PdfBackend").ToLocalChecked(), ctor->GetFunction()); } -NAN_METHOD(PdfBackend::New) -{ - int width = 0; - int height = 0; - if (info[0]->IsNumber()) width = info[0]->Uint32Value(); - if (info[1]->IsNumber()) height = info[1]->Uint32Value(); +NAN_METHOD(PdfBackend::New) { + int width = 0; + int height = 0; + if (info[0]->IsNumber()) width = info[0]->Uint32Value(); + if (info[1]->IsNumber()) height = info[1]->Uint32Value(); - PdfBackend* backend = new PdfBackend(width, height); + PdfBackend* backend = new PdfBackend(width, height); - backend->Wrap(info.This()); - info.GetReturnValue().Set(info.This()); + backend->Wrap(info.This()); + info.GetReturnValue().Set(info.This()); } diff --git a/src/backend/PdfBackend.h b/src/backend/PdfBackend.h index 9287c0fd5..0e41157d9 100644 --- a/src/backend/PdfBackend.h +++ b/src/backend/PdfBackend.h @@ -3,6 +3,7 @@ #include +#include "../closure.h" #include "Backend.h" using namespace std; @@ -14,6 +15,9 @@ class PdfBackend : public Backend cairo_surface_t* recreateSurface(); public: + PdfSvgClosure* _closure = NULL; + inline PdfSvgClosure* closure() { return _closure; } + PdfBackend(int width, int height); ~PdfBackend(); diff --git a/src/backend/SvgBackend.cc b/src/backend/SvgBackend.cc index 71249a16b..26f5df360 100644 --- a/src/backend/SvgBackend.cc +++ b/src/backend/SvgBackend.cc @@ -11,65 +11,52 @@ using namespace v8; SvgBackend::SvgBackend(int width, int height) - : Backend("svg", width, height) -{ - _closure = malloc(sizeof(closure_t)); - assert(_closure); - createSurface(); + : Backend("svg", width, height) { + createSurface(); } -SvgBackend::~SvgBackend() -{ - cairo_surface_finish(this->surface); - closure_destroy((closure_t*)_closure); - free(_closure); - - destroySurface(); +SvgBackend::~SvgBackend() { + cairo_surface_finish(surface); + if (_closure) delete _closure; + destroySurface(); } -cairo_surface_t* SvgBackend::createSurface() -{ - cairo_status_t status = closure_init((closure_t*)_closure, this->canvas, 0, PNG_NO_FILTERS); - assert(status == CAIRO_STATUS_SUCCESS); - - this->surface = cairo_svg_surface_create_for_stream(toBuffer, _closure, width, height); - - return this->surface; +cairo_surface_t* SvgBackend::createSurface() { + if (!_closure) _closure = new PdfSvgClosure(canvas); + surface = cairo_svg_surface_create_for_stream(toBuffer, _closure, width, height); + return surface; } -cairo_surface_t* SvgBackend::recreateSurface() -{ - cairo_surface_finish(this->surface); - closure_destroy((closure_t*)_closure); - cairo_surface_destroy(this->surface); +cairo_surface_t* SvgBackend::recreateSurface() { + cairo_surface_finish(surface); + delete _closure; + cairo_surface_destroy(surface); - return createSurface(); + return createSurface(); } Nan::Persistent SvgBackend::constructor; -void SvgBackend::Initialize(Handle target) -{ - Nan::HandleScope scope; +void SvgBackend::Initialize(Handle target) { + Nan::HandleScope scope; - Local ctor = Nan::New(SvgBackend::New); - SvgBackend::constructor.Reset(ctor); - ctor->InstanceTemplate()->SetInternalFieldCount(1); - ctor->SetClassName(Nan::New("SvgBackend").ToLocalChecked()); - target->Set(Nan::New("SvgBackend").ToLocalChecked(), ctor->GetFunction()); + Local ctor = Nan::New(SvgBackend::New); + SvgBackend::constructor.Reset(ctor); + ctor->InstanceTemplate()->SetInternalFieldCount(1); + ctor->SetClassName(Nan::New("SvgBackend").ToLocalChecked()); + target->Set(Nan::New("SvgBackend").ToLocalChecked(), ctor->GetFunction()); } -NAN_METHOD(SvgBackend::New) -{ - int width = 0; - int height = 0; - if (info[0]->IsNumber()) width = info[0]->Uint32Value(); - if (info[1]->IsNumber()) height = info[1]->Uint32Value(); +NAN_METHOD(SvgBackend::New) { + int width = 0; + int height = 0; + if (info[0]->IsNumber()) width = info[0]->Uint32Value(); + if (info[1]->IsNumber()) height = info[1]->Uint32Value(); - SvgBackend* backend = new SvgBackend(width, height); + SvgBackend* backend = new SvgBackend(width, height); - backend->Wrap(info.This()); - info.GetReturnValue().Set(info.This()); + backend->Wrap(info.This()); + info.GetReturnValue().Set(info.This()); } diff --git a/src/backend/SvgBackend.h b/src/backend/SvgBackend.h index fda10d66e..47391ba89 100644 --- a/src/backend/SvgBackend.h +++ b/src/backend/SvgBackend.h @@ -4,6 +4,7 @@ #include #include "Backend.h" +#include "../closure.h" using namespace std; @@ -14,6 +15,9 @@ class SvgBackend : public Backend cairo_surface_t* recreateSurface(); public: + PdfSvgClosure* _closure = NULL; + inline PdfSvgClosure* closure() { return _closure; } + SvgBackend(int width, int height); ~SvgBackend(); diff --git a/src/closure.cc b/src/closure.cc index 968da90a5..86a46d9b5 100644 --- a/src/closure.cc +++ b/src/closure.cc @@ -1,30 +1,27 @@ #include "closure.h" +PdfSvgClosure::PdfSvgClosure(Canvas* canvas) : Closure(canvas) { + //data = new (std::nothrow) uint8_t[max_len]; // toBuffer.cc uses realloc + data = static_cast(malloc(max_len)); + if (!data) throw CAIRO_STATUS_NO_MEMORY; +} -/* - * Initialize the given closure. - */ - -cairo_status_t -closure_init(closure_t *closure, Canvas *canvas, unsigned int compression_level, unsigned int filter) { - closure->len = 0; - closure->canvas = canvas; - closure->data = (uint8_t *) malloc(closure->max_len = PAGE_SIZE); - if (!closure->data) return CAIRO_STATUS_NO_MEMORY; - closure->compression_level = compression_level; - closure->filter = filter; - return CAIRO_STATUS_SUCCESS; +PdfSvgClosure::~PdfSvgClosure() { + if (len) { + //delete[] data; + free(data); + Nan::AdjustExternalMemory(-static_cast(max_len)); + } } -/* - * Free the given closure's data, - * and hint V8 at the memory dealloc. - */ +PngClosure::PngClosure(Canvas* canvas) : Closure(canvas) { + data = static_cast(malloc(max_len)); + if (!data) throw CAIRO_STATUS_NO_MEMORY; +} -void -closure_destroy(closure_t *closure) { - if (closure->len) { - free(closure->data); - Nan::AdjustExternalMemory(-static_cast(closure->max_len)); +PngClosure::~PngClosure() { + if (len) { + free(data); + Nan::AdjustExternalMemory(-static_cast(max_len)); } } diff --git a/src/closure.h b/src/closure.h index 76cd2fe15..cdfa91214 100644 --- a/src/closure.h +++ b/src/closure.h @@ -17,11 +17,57 @@ #endif #include +#include #include "Canvas.h" /* - * PNG stream closure. + * Image encoding closures. + */ + +struct Closure { + Nan::Callback *pfn; + Local fn; + unsigned len = 0; + unsigned max_len = PAGE_SIZE; + uint8_t* data = NULL; + Canvas* canvas = NULL; + cairo_status_t status = CAIRO_STATUS_SUCCESS; + + Closure(Canvas* canvas) : canvas(canvas) {}; +}; + +struct PdfSvgClosure : Closure { + PdfSvgClosure(Canvas* canvas); + ~PdfSvgClosure(); +}; + +struct PngClosure : Closure { + uint32_t compressionLevel = 6; + uint32_t filters = PNG_ALL_FILTERS; + // Indexed PNGs: + uint32_t nPaletteColors = 0; + uint8_t* palette = NULL; + uint8_t backgroundIndex = 0; + + PngClosure(Canvas* canvas); + ~PngClosure(); +}; + +struct JpegClosure : Closure { + uint32_t quality = 75; + uint32_t chromaSubsampling = 2; + bool progressive = false; + + JpegClosure(Canvas* canvas) : Closure(canvas) {}; + ~JpegClosure() { + // jpeg_mem_dest mallocs 'data' + if (data) free(data); + } +}; + +/* + * Image encoding closure. */ typedef struct { @@ -32,11 +78,6 @@ typedef struct { uint8_t *data; Canvas *canvas; cairo_status_t status; - uint32_t compression_level; - uint32_t filter; - uint8_t *palette; - size_t nPaletteColors; - uint8_t backgroundIndex; } closure_t; /* @@ -44,7 +85,7 @@ typedef struct { */ cairo_status_t -closure_init(closure_t *closure, Canvas *canvas, unsigned int compression_level, unsigned int filter); +closure_init(closure_t *closure, Canvas *canvas); /* * Free the given closure's data, diff --git a/src/toBuffer.cc b/src/toBuffer.cc index 3660532b7..d0a038894 100644 --- a/src/toBuffer.cc +++ b/src/toBuffer.cc @@ -8,9 +8,11 @@ * Canvas::ToBuffer callback. */ +// TODO try to use std::vector instead + cairo_status_t toBuffer(void *c, const uint8_t *odata, unsigned len) { - closure_t *closure = (closure_t *) c; + Closure* closure = (Closure*)c; if (closure->len + len > closure->max_len) { uint8_t *data; diff --git a/test/canvas.test.js b/test/canvas.test.js index 5c4aa020c..d3338e24b 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -487,100 +487,148 @@ describe('Canvas', function () { assert.equal('end', ctx.textAlign); }); - it('Canvas#toBuffer()', function () { - var buf = createCanvas(200,200).toBuffer(); - assert.equal('PNG', buf.slice(1,4).toString()); - }); - - it('Canvas#toBuffer() async', function (done) { - createCanvas(200, 200).toBuffer(function(err, buf){ - assert.ok(!err); + describe('#toBuffer', function () { + it('Canvas#toBuffer()', function () { + var buf = createCanvas(200,200).toBuffer(); assert.equal('PNG', buf.slice(1,4).toString()); - done(); }); - }); - - describe('#toBuffer("raw")', function() { - var canvas = createCanvas(11, 10) - , ctx = canvas.getContext('2d'); - - ctx.clearRect(0, 0, 11, 10); - - ctx.fillStyle = 'rgba(200, 200, 200, 0.505)'; - ctx.fillRect(0, 0, 5, 5); - - ctx.fillStyle = 'red'; - ctx.fillRect(5, 0, 5, 5); - - ctx.fillStyle = '#00ff00'; - ctx.fillRect(0, 5, 5, 5); - - ctx.fillStyle = 'black'; - ctx.fillRect(5, 5, 4, 5); - - /** Output: - * *****RRRRR- - * *****RRRRR- - * *****RRRRR- - * *****RRRRR- - * *****RRRRR- - * GGGGGBBBB-- - * GGGGGBBBB-- - * GGGGGBBBB-- - * GGGGGBBBB-- - * GGGGGBBBB-- - */ - - var buf = canvas.toBuffer('raw'); - var stride = canvas.stride; - - var endianness = os.endianness(); - - function assertPixel(u32, x, y, message) { - var expected = '0x' + u32.toString(16); - - // Buffer doesn't have readUInt32(): it only has readUInt32LE() and - // readUInt32BE(). - var px = buf['readUInt32' + endianness](y * stride + x * 4); - var actual = '0x' + px.toString(16); - - assert.equal(actual, expected, message); - } - - it('should have the correct size', function() { - assert.equal(buf.length, stride * 10); - }); - - it('does not premultiply alpha', function() { - assertPixel(0x80646464, 0, 0, 'first semitransparent pixel'); - assertPixel(0x80646464, 4, 4, 'last semitransparent pixel'); + + it('Canvas#toBuffer("image/png")', function () { + var buf = createCanvas(200,200).toBuffer('image/png'); + assert.equal('PNG', buf.slice(1,4).toString()); }); - - it('draws red', function() { - assertPixel(0xffff0000, 5, 0, 'first red pixel'); - assertPixel(0xffff0000, 9, 4, 'last red pixel'); + + it('Canvas#toBuffer("image/png", {compressionLevel: 5})', function () { + var buf = createCanvas(200,200).toBuffer('image/png', {compressionLevel: 5}); + assert.equal('PNG', buf.slice(1,4).toString()); }); - it('draws green', function() { - assertPixel(0xff00ff00, 0, 5, 'first green pixel'); - assertPixel(0xff00ff00, 4, 9, 'last green pixel'); + it('Canvas#toBuffer("image/jpeg")', function () { + var buf = createCanvas(200,200).toBuffer('image/jpeg'); + assert.equal(buf[0], 0xff); + assert.equal(buf[1], 0xd8); + assert.equal(buf[buf.byteLength - 2], 0xff); + assert.equal(buf[buf.byteLength - 1], 0xd9); }); - it('draws black', function() { - assertPixel(0xff000000, 5, 5, 'first black pixel'); - assertPixel(0xff000000, 8, 9, 'last black pixel'); + it('Canvas#toBuffer("image/jpeg", {quality: 0.95})', function () { + var buf = createCanvas(200,200).toBuffer('image/jpeg', {quality: 0.95}); + assert.equal(buf[0], 0xff); + assert.equal(buf[1], 0xd8); + assert.equal(buf[buf.byteLength - 2], 0xff); + assert.equal(buf[buf.byteLength - 1], 0xd9); }); - it('leaves undrawn pixels black, transparent', function() { - assertPixel(0x0, 9, 5, 'first undrawn pixel'); - assertPixel(0x0, 9, 9, 'last undrawn pixel'); + it('Canvas#toBuffer(callback)', function (done) { + createCanvas(200, 200).toBuffer(function(err, buf){ + assert.ok(!err); + assert.equal('PNG', buf.slice(1,4).toString()); + done(); + }); }); - it('is immutable', function() { - ctx.fillStyle = 'white'; - ctx.fillRect(0, 0, 10, 10); - canvas.toBuffer('raw'); // (side-effect: flushes canvas) - assertPixel(0xffff0000, 5, 0, 'first red pixel'); + it('Canvas#toBuffer(callback, "image/jpeg")', function () { + var buf = createCanvas(200,200).toBuffer(function (err, buff) { + assert.ok(!err); + assert.equal(buf[0], 0xff); + assert.equal(buf[1], 0xd8); + assert.equal(buf[buf.byteLength - 2], 0xff); + assert.equal(buf[buf.byteLength - 1], 0xd9); + }, 'image/jpeg'); + }); + + it('Canvas#toBuffer(callback, "image/jpeg", {quality: 0.95})', function () { + var buf = createCanvas(200,200).toBuffer(function (err, buff) { + assert.ok(!err); + assert.equal(buf[0], 0xff); + assert.equal(buf[1], 0xd8); + assert.equal(buf[buf.byteLength - 2], 0xff); + assert.equal(buf[buf.byteLength - 1], 0xd9); + }, 'image/jpeg', {quality: 0.95}); + }); + + describe('#toBuffer("raw")', function() { + var canvas = createCanvas(11, 10) + , ctx = canvas.getContext('2d'); + + ctx.clearRect(0, 0, 11, 10); + + ctx.fillStyle = 'rgba(200, 200, 200, 0.505)'; + ctx.fillRect(0, 0, 5, 5); + + ctx.fillStyle = 'red'; + ctx.fillRect(5, 0, 5, 5); + + ctx.fillStyle = '#00ff00'; + ctx.fillRect(0, 5, 5, 5); + + ctx.fillStyle = 'black'; + ctx.fillRect(5, 5, 4, 5); + + /** Output: + * *****RRRRR- + * *****RRRRR- + * *****RRRRR- + * *****RRRRR- + * *****RRRRR- + * GGGGGBBBB-- + * GGGGGBBBB-- + * GGGGGBBBB-- + * GGGGGBBBB-- + * GGGGGBBBB-- + */ + + var buf = canvas.toBuffer('raw'); + var stride = canvas.stride; + + var endianness = os.endianness(); + + function assertPixel(u32, x, y, message) { + var expected = '0x' + u32.toString(16); + + // Buffer doesn't have readUInt32(): it only has readUInt32LE() and + // readUInt32BE(). + var px = buf['readUInt32' + endianness](y * stride + x * 4); + var actual = '0x' + px.toString(16); + + assert.equal(actual, expected, message); + } + + it('should have the correct size', function() { + assert.equal(buf.length, stride * 10); + }); + + it('does not premultiply alpha', function() { + assertPixel(0x80646464, 0, 0, 'first semitransparent pixel'); + assertPixel(0x80646464, 4, 4, 'last semitransparent pixel'); + }); + + it('draws red', function() { + assertPixel(0xffff0000, 5, 0, 'first red pixel'); + assertPixel(0xffff0000, 9, 4, 'last red pixel'); + }); + + it('draws green', function() { + assertPixel(0xff00ff00, 0, 5, 'first green pixel'); + assertPixel(0xff00ff00, 4, 9, 'last green pixel'); + }); + + it('draws black', function() { + assertPixel(0xff000000, 5, 5, 'first black pixel'); + assertPixel(0xff000000, 8, 9, 'last black pixel'); + }); + + it('leaves undrawn pixels black, transparent', function() { + assertPixel(0x0, 9, 5, 'first undrawn pixel'); + assertPixel(0x0, 9, 9, 'last undrawn pixel'); + }); + + it('is immutable', function() { + ctx.fillStyle = 'white'; + ctx.fillRect(0, 0, 10, 10); + canvas.toBuffer('raw'); // (side-effect: flushes canvas) + assertPixel(0xffff0000, 5, 0, 'first red pixel'); + }); }); }); @@ -1348,16 +1396,6 @@ describe('Canvas', function () { }); }); - it('Canvas#jpegStream() should clamp buffer size (#674)', function (done) { - var c = createCanvas(10, 10); - var SIZE = 10 * 1024 * 1024; - var s = c.jpegStream({bufsize: SIZE}); - s.on('data', function (chunk) { - assert(chunk.length < SIZE); - }); - s.on('end', done); - }); - it('Context2d#fill()', function() { var canvas = createCanvas(2, 2); var ctx = canvas.getContext('2d'); From 80a5a0de5f80aa8e02ab31352da1d7b0ac2b2917 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 31 May 2018 11:19:51 -0700 Subject: [PATCH 139/474] Standardize on createPNGStream/createJPEGStream Vs. canvas.pngStream(). Follows node's core fs.createReadStream, etc. --- CHANGELOG.md | 14 ++++++++------ Readme.md | 22 +++++++++++----------- examples/ray.js | 2 +- examples/resize.js | 2 +- test/canvas.test.js | 8 ++++---- 5 files changed, 25 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7473174c..88215e7f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,10 @@ project adheres to [Semantic Versioning](http://semver.org/). **Upgrading from 1.x** ```js -// (1) The quality argument for canvas.jpegStream now goes from 0 to 1 instead -// of from 0 to 100: -canvas.jpegStream({quality: 50}) // old -canvas.jpegStream({quality: 0.5}) // new +// (1) The quality argument for canvas.createJPEGStream/canvas.jpegStream now +// goes from 0 to 1 instead of from 0 to 100: +canvas.createJPEGStream({quality: 50}) // old +canvas.createJPEGStream({quality: 0.5}) // new // (2) The ZLIB compression level and PNG filter options for canvas.toBuffer are // now named instead of positional arguments: @@ -29,10 +29,12 @@ canvas.pngStream({compressionLevel: 3, filters: canvas.PNG_FILTER_NONE}) // new // (4) canvas.syncPNGStream() and canvas.syncJPEGStream() have been removed: canvas.syncPNGStream() // old -canvas.pngStream() // new +canvas.createSyncPNGStream() // old +canvas.createPNGStream() // new canvas.syncJPEGStream() // old -canvas.jpegStream() // new +canvas.createSyncJPEGStream() // old +canvas.createJPEGStream() // new ``` ### Breaking diff --git a/Readme.md b/Readme.md index a5f5f9475..994afa94e 100644 --- a/Readme.md +++ b/Readme.md @@ -201,12 +201,12 @@ const myCanvas = createCanvas(w, h, 'pdf') myCanvas.toBuffer() // returns a buffer containing a PDF-encoded canvas ``` -### Canvas#pngStream(options) +### Canvas#createPNGStream(options) Creates a [`ReadableStream`](https://nodejs.org/api/stream.html#stream_class_stream_readable) that emits PNG-encoded data. -> `canvas.pngStream([config]) => ReadableStream` +> `canvas.createPNGStream([config]) => ReadableStream` * `config` An object specifying the ZLIB compression level (between 0 and 9), the compression filter(s), the palette (indexed PNGs only) and/or the @@ -219,7 +219,7 @@ that emits PNG-encoded data. ```javascript const fs = require('fs') const out = fs.createWriteStream(__dirname + '/test.png') -const stream = canvas.pngStream() +const stream = canvas.createPNGStream() stream.pipe(out) out.on('finish', () => console.log('The PNG file was created.')) ``` @@ -234,21 +234,21 @@ const palette = new Uint8ClampedArray([ 127, 127, 255, 255 // ... ]) -canvas.pngStream({ +canvas.createPNGStream({ palette: palette, backgroundIndex: 0 // optional, defaults to 0 }) ``` -### Canvas#jpegStream() +### Canvas#createJPEGStream() -Creates a [`ReadableStream`](https://nodejs.org/api/stream.html#stream_class_stream_readable) +Creates a [`createJPEGStream`](https://nodejs.org/api/stream.html#stream_class_stream_readable) that emits JPEG-encoded data. -_Note: At the moment, `jpegStream()` is synchronous under the hood. That is, it +_Note: At the moment, `createJPEGStream()` is synchronous under the hood. That is, it runs in the main thread, not in the libuv threadpool._ -> `canvas.pngStream([config]) => ReadableStream` +> `canvas.createJPEGStream([config]) => ReadableStream` * `config` an object specifying the quality (0 to 1), if progressive compression should be used and/or if chroma subsampling should be used: @@ -260,12 +260,12 @@ runs in the main thread, not in the libuv threadpool._ ```javascript const fs = require('fs') const out = fs.createWriteStream(__dirname + '/test.jpeg') -const stream = canvas.jpegStream() +const stream = canvas.createJPEGStream() stream.pipe(out) out.on('finish', () => console.log('The JPEG file was created.')) // Disable 2x2 chromaSubsampling for deeper colors and use a higher quality -const stream = canvas.jpegStream({ +const stream = canvas.createJPEGStream({ quality: 95, chromaSubsampling: false }) @@ -281,7 +281,7 @@ var dataUrl = canvas.toDataURL('image/png'); canvas.toDataURL(function(err, png){ }); // defaults to PNG canvas.toDataURL('image/png', function(err, png){ }); canvas.toDataURL('image/jpeg', function(err, jpeg){ }); // sync JPEG is not supported -canvas.toDataURL('image/jpeg', {opts...}, function(err, jpeg){ }); // see Canvas#jpegStream for valid options +canvas.toDataURL('image/jpeg', {opts...}, function(err, jpeg){ }); // see Canvas#createJPEGStream for valid options canvas.toDataURL('image/jpeg', quality, function(err, jpeg){ }); // spec-following; quality from 0 to 1 ``` diff --git a/examples/ray.js b/examples/ray.js index 93fd80184..68c85246a 100644 --- a/examples/ray.js +++ b/examples/ray.js @@ -82,4 +82,4 @@ render(1) console.log('Rendered in %s seconds', (new Date() - start) / 1000) -canvas.pngStream().pipe(fs.createWriteStream(path.join(__dirname, 'ray.png'))) +canvas.createPNGStream().pipe(fs.createWriteStream(path.join(__dirname, 'ray.png'))) diff --git a/examples/resize.js b/examples/resize.js index 151630ed5..d50aa9231 100644 --- a/examples/resize.js +++ b/examples/resize.js @@ -21,7 +21,7 @@ img.onload = function () { ctx.imageSmoothingEnabled = true ctx.drawImage(img, 0, 0, width, height) - canvas.pngStream().pipe(out) + canvas.createPNGStream().pipe(out) out.on('finish', function () { console.log('Resized and saved in %dms', new Date() - start) diff --git a/test/canvas.test.js b/test/canvas.test.js index d3338e24b..600633bed 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -1352,9 +1352,9 @@ describe('Canvas', function () { }); }); - it('Canvas#jpegStream()', function (done) { + it('Canvas#createJPEGStream()', function (done) { var canvas = createCanvas(640, 480); - var stream = canvas.jpegStream(); + var stream = canvas.createJPEGStream(); assert(stream instanceof Readable); var firstChunk = true; var bytes = 0; @@ -1378,9 +1378,9 @@ describe('Canvas', function () { // based on https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format // end of image marker (FF D9) must exist to maintain JPEG standards - it('EOI at end of Canvas#jpegStream()', function (done) { + it('EOI at end of Canvas#createJPEGStream()', function (done) { var canvas = createCanvas(640, 480); - var stream = canvas.jpegStream(); + var stream = canvas.createJPEGStream(); var chunks = [] stream.on('data', function(chunk){ chunks.push(chunk) From c9df8196735f5a968a7beda29bf9c600252a5b40 Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 11 Jun 2018 19:37:16 +0200 Subject: [PATCH 140/474] Fix drawing zero-width and zero-height images (#1178) * Fix drawing zero-width and zero-height images * Add test and improve previous test * Update the changelog * Add a few more tests --- CHANGELOG.md | 1 + src/CanvasRenderingContext2d.cc | 3 +++ test/canvas.test.js | 23 +++++++++++++++++++++-- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88215e7f9..e013ab5df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,7 @@ canvas.createJPEGStream() // new * Prevent segfaults caused by creating a too large canvas * Fix parse-font regex to allow for whitespaces. * Allow assigning non-string values to fillStyle and strokeStyle + * Fix drawing zero-width and zero-height images. ### Added * Prebuilds (#992) with different libc versions to the prebuilt binary (#1140) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 954468f32..0f104eaca 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -1168,6 +1168,9 @@ NAN_METHOD(Context2d::DrawImage) { break; } + if (!(sw && sh && dw && dh)) + return; + // Start draw cairo_save(ctx); diff --git a/test/canvas.test.js b/test/canvas.test.js index 600633bed..9e92f9934 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -1623,22 +1623,41 @@ describe('Canvas', function () { var canvas = createCanvas(500, 500); var ctx = canvas.getContext('2d'); + // Drawing canvas to itself ctx.fillStyle = 'white'; ctx.fillRect(0, 0, 500, 500); ctx.fillStyle = 'black'; ctx.fillRect(5, 5, 10, 10); - ctx.drawImage(ctx.canvas, 20, 20); + ctx.drawImage(canvas, 20, 20); var imgd = ctx.getImageData(0, 0, 500, 500); var data = imgd.data; var count = 0; - for(var i = 0; i < 500 * 500; i += 4){ + for(var i = 0; i < 500 * 500 * 4; i += 4){ if(data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0) count++; } assert.strictEqual(count, 10 * 10 * 2); + + // Drawing zero-width image + ctx.drawImage(canvas, 0, 0, 0, 0, 0, 0, 0, 0); + ctx.drawImage(canvas, 0, 0, 0, 0, 1, 1, 1, 1); + ctx.drawImage(canvas, 1, 1, 1, 1, 0, 0, 0, 0); + ctx.fillStyle = 'white'; + ctx.fillRect(0, 0, 500, 500); + + imgd = ctx.getImageData(0, 0, 500, 500); + data = imgd.data; + count = 0; + + for(i = 0; i < 500 * 500 * 4; i += 4){ + if(data[i] === 255 && data[i + 1] === 255 && data[i + 2] === 255) + count++; + } + + assert.strictEqual(count, 500 * 500); }); it('Context2d#SetFillColor()', function () { From 147f567fa52713ebb38e594369de14895ae1d2a6 Mon Sep 17 00:00:00 2001 From: "Yuya.Nishida" Date: Tue, 12 Jun 2018 10:20:03 +0900 Subject: [PATCH 141/474] Suppress Buffer deprecation warning on NodeJS v10 (#1177) * Ignore package-lock.json * Fix DEP0005 deprecation warning * Run Travis on Node.js 10 * Drop support for Node.js 4.x --- .gitignore | 1 + .travis.yml | 2 +- CHANGELOG.md | 3 ++- Readme.md | 2 +- lib/image.js | 2 +- package.json | 2 +- test/image.test.js | 2 +- 7 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 04f1c001d..e5e14d5b6 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ out.pdf out.svg .pomo node_modules +package-lock.json # Vim cruft *.swp diff --git a/.travis.yml b/.travis.yml index 9b1276f57..d017940c0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,9 +2,9 @@ language: node_js install: - npm install --build-from-source node_js: + - '10' - '8' - '6' - - '4' addons: apt: sources: diff --git a/CHANGELOG.md b/CHANGELOG.md index e013ab5df..a87bf31b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,7 +38,7 @@ canvas.createJPEGStream() // new ``` ### Breaking - * Drop support for Node.js <4.x + * Drop support for Node.js <6.x * Remove sync stream functions (bc53059). Note that most streams are still synchronous (run in the main thread); this change just removed `syncPNGStream` and `syncJPEGStream`. @@ -66,6 +66,7 @@ canvas.createJPEGStream() // new * Fix parse-font regex to allow for whitespaces. * Allow assigning non-string values to fillStyle and strokeStyle * Fix drawing zero-width and zero-height images. + * Fix DEP0005 deprecation warning ### Added * Prebuilds (#992) with different libc versions to the prebuilt binary (#1140) diff --git a/Readme.md b/Readme.md index 994afa94e..ccc9742e3 100644 --- a/Readme.md +++ b/Readme.md @@ -33,7 +33,7 @@ $ npm install canvas By default, binaries for macOS, Linux and Windows will be downloaded. If you want to build from source, use `npm install --build-from-source`. -Currently the minimum version of node required is __4.0.0__ +Currently the minimum version of node required is __6.0.0__ ### Compiling diff --git a/lib/image.js b/lib/image.js index 33a796b82..e0edc2ab9 100644 --- a/lib/image.js +++ b/lib/image.js @@ -25,7 +25,7 @@ const Image = module.exports = bindings.Image Image.prototype.__defineSetter__('src', function(val){ if ('string' == typeof val && 0 == val.indexOf('data:')) { val = val.slice(val.indexOf(',') + 1); - this.source = new Buffer(val, 'base64'); + this.source = Buffer.from(val, 'base64'); } else { this.source = val; } diff --git a/package.json b/package.json index 976e64126..dc17b88f4 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "standard": "^8.5.0" }, "engines": { - "node": ">=4" + "node": ">=6" }, "license": "MIT" } diff --git a/test/image.test.js b/test/image.test.js index 94fa249ac..a09a65beb 100644 --- a/test/image.test.js +++ b/test/image.test.js @@ -128,7 +128,7 @@ describe('Image', function () { img.onerror = () => { onerrorCalled += 1 } - img.src = new Buffer(0) + img.src = Buffer.allocUnsafe(0) assert.strictEqual(img.width, 0) assert.strictEqual(img.height, 0) From e3119835486933da00ca271f1381a62de702da12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Tue, 12 Jun 2018 15:40:49 +0100 Subject: [PATCH 142/474] Avoid using allocUnsafe (#1180) --- test/image.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/image.test.js b/test/image.test.js index a09a65beb..a4ac3216a 100644 --- a/test/image.test.js +++ b/test/image.test.js @@ -128,7 +128,7 @@ describe('Image', function () { img.onerror = () => { onerrorCalled += 1 } - img.src = Buffer.allocUnsafe(0) + img.src = Buffer.alloc(0) assert.strictEqual(img.width, 0) assert.strictEqual(img.height, 0) From 013223f92afdbb3265a7073368b9800fbacc40cf Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Fri, 15 Jun 2018 15:24:16 -0700 Subject: [PATCH 143/474] Add resolution option for PNG format (#1179) Fixes #766 Fixes #716 --- CHANGELOG.md | 2 ++ Readme.md | 12 ++++++++---- src/PNG.h | 6 +++++- src/closure.h | 1 + test/canvas.test.js | 20 ++++++++++++++++++++ 5 files changed, 36 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a87bf31b2..2c94364e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,6 +86,8 @@ canvas.createJPEGStream() // new * Support for `canvas.toBuffer("image/jpeg")` * Unified configuration options for `canvas.toBuffer()`, `canvas.pngStream()` and `canvas.jpegStream()` + * Added `resolution` option for `canvas.toBuffer("image/png")` and + `canvas.createPNGStream()` 1.6.x (unreleased) ================== diff --git a/Readme.md b/Readme.md index ccc9742e3..a3e1d3848 100644 --- a/Readme.md +++ b/Readme.md @@ -154,10 +154,14 @@ image contained in the canvas. `{quality: 0.75, progressive: false, chromaSubsampling: true}`. All properties are optional. * For `image/png`, an object specifying the ZLIB compression level (between 0 - and 9), the compression filter(s), the palette (indexed PNGs only) and/or - the background palette index (indexed PNGs only): - `{compressionLevel: 6, filters: canvas.PNG_ALL_FILTERS, palette: undefined, backgroundIndex: 0}`. + and 9), the compression filter(s), the palette (indexed PNGs only), the + the background palette index (indexed PNGs only) and/or the resolution (ppi): + `{compressionLevel: 6, filters: canvas.PNG_ALL_FILTERS, palette: undefined, backgroundIndex: 0, resolution: undefined}`. All properties are optional. + + Note that the PNG format encodes the resolution in pixels per meter, so if + you specify `96`, the file will encode 3780 ppm (~96.01 ppi). The resolution + is undefined by default to match common browser behavior. **Return value** @@ -211,7 +215,7 @@ that emits PNG-encoded data. * `config` An object specifying the ZLIB compression level (between 0 and 9), the compression filter(s), the palette (indexed PNGs only) and/or the background palette index (indexed PNGs only): - `{compressionLevel: 6, filters: canvas.PNG_ALL_FILTERS, palette: undefined, backgroundIndex: 0}`. + `{compressionLevel: 6, filters: canvas.PNG_ALL_FILTERS, palette: undefined, backgroundIndex: 0, resolution: undefined}`. All properties are optional. #### Examples diff --git a/src/PNG.h b/src/PNG.h index e35f24f09..e0bd86a66 100644 --- a/src/PNG.h +++ b/src/PNG.h @@ -6,7 +6,7 @@ #include #include #include - +#include // round #include "closure.h" #if defined(__GNUC__) && (__GNUC__ > 2) && defined(__OPTIMIZE__) @@ -166,6 +166,10 @@ static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr writ png_set_write_fn(png, closure, write_func, canvas_png_flush); png_set_compression_level(png, closure->closure->compressionLevel); png_set_filter(png, 0, closure->closure->filters); + if (closure->closure->resolution != 0) { + uint32_t res = static_cast(round(static_cast(closure->closure->resolution) * 39.3701)); + png_set_pHYs(png, info, res, res, PNG_RESOLUTION_METER); + } cairo_format_t format = cairo_image_surface_get_format(surface); diff --git a/src/closure.h b/src/closure.h index cdfa91214..0c2a18075 100644 --- a/src/closure.h +++ b/src/closure.h @@ -45,6 +45,7 @@ struct PdfSvgClosure : Closure { struct PngClosure : Closure { uint32_t compressionLevel = 6; uint32_t filters = PNG_ALL_FILTERS; + uint32_t resolution = 0; // 0 = unspecified // Indexed PNGs: uint32_t nPaletteColors = 0; uint8_t* palette = NULL; diff --git a/test/canvas.test.js b/test/canvas.test.js index 9e92f9934..4afb2f99c 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -497,6 +497,26 @@ describe('Canvas', function () { var buf = createCanvas(200,200).toBuffer('image/png'); assert.equal('PNG', buf.slice(1,4).toString()); }); + + it('Canvas#toBuffer("image/png", {resolution: 96})', function () { + const buf = createCanvas(200, 200).toBuffer('image/png', {resolution: 96}); + // 3780 ppm ~= 96 ppi + for (let i = 0; i < buf.length - 12; i++) { + if (buf[i] === 0x70 && + buf[i + 1] === 0x48 && + buf[i + 2] === 0x59 && + buf[i + 3] === 0x73) { // pHYs + assert.equal(buf[i + 4], 0); + assert.equal(buf[i + 5], 0); + assert.equal(buf[i + 6], 0x0e); + assert.equal(buf[i + 7], 0xc4); // x + assert.equal(buf[i + 8], 0); + assert.equal(buf[i + 9], 0); + assert.equal(buf[i + 10], 0x0e); + assert.equal(buf[i + 11], 0xc4); // y + } + } + }) it('Canvas#toBuffer("image/png", {compressionLevel: 5})', function () { var buf = createCanvas(200,200).toBuffer('image/png', {compressionLevel: 5}); From 83bb5e783257e356f069d055aa0cfc2c4feb69b2 Mon Sep 17 00:00:00 2001 From: pravdomil Date: Thu, 21 Jun 2018 17:38:49 +0200 Subject: [PATCH 144/474] add know issues section --- Readme.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Readme.md b/Readme.md index a3e1d3848..91e4aa992 100644 --- a/Readme.md +++ b/Readme.md @@ -83,6 +83,14 @@ loadImage('examples/images/lime-cat.jpg').then((image) => { }) ``` +## Know issues + +- CMYK images are not supported ([#1183](https://github.com/Automattic/node-canvas/issues/1183), [#425](https://github.com/Automattic/node-canvas/issues/425)) +- `ctx.fillText` `maxWidth` is inconsistent ([#1088](https://github.com/Automattic/node-canvas/issues/1183), [#1088](https://github.com/Automattic/node-canvas/issues/425)) +- Async `canvas.toBuffer` for PDF is not working ([#821](https://github.com/Automattic/node-canvas/issues/821)) + +[See all list of bugs](https://github.com/Automattic/node-canvas/issues?q=is%3Aissue+is%3Aopen+label%3ABug). + ## Non-Standard APIs node-canvas implements the [HTML Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) as closely as possible. From 3ce0f4c77d6f5445e65cc88ec57ebade73ef9e1c Mon Sep 17 00:00:00 2001 From: Tomas Eglinskas Date: Sat, 23 Jun 2018 14:19:58 +0300 Subject: [PATCH 145/474] Documentation fix: async to sync example writeFile should ask for a callback if it's async, in the example, the arguments are for sync, although it is used as async --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index a3e1d3848..10018a0f6 100644 --- a/Readme.md +++ b/Readme.md @@ -401,7 +401,7 @@ ctx.addPage(); ```js var canvas = createCanvas(200, 500, 'svg'); // Use the normal primitives. -fs.writeFile('out.svg', canvas.toBuffer()); +fs.writeFileSync('out.svg', canvas.toBuffer()); ``` ## SVG Image Support From b10d204c69a63b7d5c6f1c60ad30a67a51bc7167 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 29 Jun 2018 18:51:19 +0200 Subject: [PATCH 146/474] Remove deprecation warning for v8::String::Utf8Value (#1191) * Fix deprecation warning related to String::Utf8Value * Fix failing test * Suppress alignment warning * Switch to Nan::Utf8String to make it compatible with v6.x * Fix test --- src/Canvas.cc | 8 +++---- src/CanvasGradient.cc | 2 +- src/CanvasPattern.cc | 6 +++--- src/CanvasRenderingContext2d.cc | 38 ++++++++++++++++----------------- src/Image.cc | 5 +++-- test/canvas.test.js | 10 +++++---- 6 files changed, 36 insertions(+), 33 deletions(-) diff --git a/src/Canvas.cc b/src/Canvas.cc index f1233220e..623d8b0f9 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -93,9 +93,9 @@ NAN_METHOD(Canvas::New) { if (info[1]->IsNumber()) height = info[1]->Uint32Value(); if (info[2]->IsString()) { - if (0 == strcmp("pdf", *String::Utf8Value(info[2]))) + if (0 == strcmp("pdf", *Nan::Utf8String(info[2]))) backend = new PdfBackend(width, height); - else if (0 == strcmp("svg", *String::Utf8Value(info[2]))) + else if (0 == strcmp("svg", *Nan::Utf8String(info[2]))) backend = new SvgBackend(width, height); else backend = new ImageBackend(width, height); @@ -638,7 +638,7 @@ NAN_METHOD(Canvas::StreamJPEGSync) { char * str_value(Local val, const char *fallback, bool can_be_number) { if (val->IsString() || (can_be_number && val->IsNumber())) { - return g_strdup(*String::Utf8Value(val)); + return g_strdup(*Nan::Utf8String(val)); } else if (fallback) { return g_strdup(fallback); } else { @@ -653,7 +653,7 @@ NAN_METHOD(Canvas::RegisterFont) { return Nan::ThrowError(GENERIC_FACE_ERROR); } - String::Utf8Value filePath(info[0]); + Nan::Utf8String filePath(info[0]); PangoFontDescription *sys_desc = get_pango_font_description((unsigned char *) *filePath); if (!sys_desc) return Nan::ThrowError("Could not parse font file"); diff --git a/src/CanvasGradient.cc b/src/CanvasGradient.cc index 8063b6dad..74e3284ad 100644 --- a/src/CanvasGradient.cc +++ b/src/CanvasGradient.cc @@ -80,7 +80,7 @@ NAN_METHOD(Gradient::AddColorStop) { Gradient *grad = Nan::ObjectWrap::Unwrap(info.This()); short ok; - String::Utf8Value str(info[1]); + Nan::Utf8String str(info[1]); uint32_t rgba = rgba_from_string(*str, &ok); if (ok) { diff --git a/src/CanvasPattern.cc b/src/CanvasPattern.cc index a2c7671d3..69ab75a97 100644 --- a/src/CanvasPattern.cc +++ b/src/CanvasPattern.cc @@ -64,11 +64,11 @@ NAN_METHOD(Pattern::New) { return Nan::ThrowTypeError("Image or Canvas expected"); } repeat_type_t repeat = REPEAT; - if (0 == strcmp("no-repeat", *String::Utf8Value(info[1]))) { + if (0 == strcmp("no-repeat", *Nan::Utf8String(info[1]))) { repeat = NO_REPEAT; - } else if (0 == strcmp("repeat-x", *String::Utf8Value(info[1]))) { + } else if (0 == strcmp("repeat-x", *Nan::Utf8String(info[1]))) { repeat = REPEAT_X; - } else if (0 == strcmp("repeat-y", *String::Utf8Value(info[1]))) { + } else if (0 == strcmp("repeat-y", *Nan::Utf8String(info[1]))) { repeat = REPEAT_Y; } Pattern *pattern = new Pattern(surface, repeat); diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 0f104eaca..e71cd5f9e 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -335,7 +335,7 @@ void Context2d::setFillRule(v8::Local value) { cairo_fill_rule_t rule = CAIRO_FILL_RULE_WINDING; if (value->IsString()) { - String::Utf8Value str(value); + Nan::Utf8String str(value); if (std::strcmp(*str, "evenodd") == 0) { rule = CAIRO_FILL_RULE_EVEN_ODD; } @@ -654,7 +654,7 @@ NAN_METHOD(Context2d::New) { Local pixelFormat = ctxAttributes->Get(Nan::New("pixelFormat").ToLocalChecked()); if (pixelFormat->IsString()) { - String::Utf8Value utf8PixelFormat(pixelFormat); + Nan::Utf8String utf8PixelFormat(pixelFormat); if (!strcmp(*utf8PixelFormat, "RGBA32")) format = CAIRO_FORMAT_ARGB32; else if (!strcmp(*utf8PixelFormat, "RGB24")) format = CAIRO_FORMAT_RGB24; else if (!strcmp(*utf8PixelFormat, "A8")) format = CAIRO_FORMAT_A8; @@ -1335,7 +1335,7 @@ NAN_GETTER(Context2d::GetGlobalCompositeOperation) { NAN_SETTER(Context2d::SetPatternQuality) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - String::Utf8Value quality(value->ToString()); + Nan::Utf8String quality(value->ToString()); if (0 == strcmp("fast", *quality)) { context->state->patternQuality = CAIRO_FILTER_FAST; } else if (0 == strcmp("good", *quality)) { @@ -1373,7 +1373,7 @@ NAN_GETTER(Context2d::GetPatternQuality) { NAN_SETTER(Context2d::SetGlobalCompositeOperation) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); - String::Utf8Value type(value->ToString()); + Nan::Utf8String type(value->ToString()); if (0 == strcmp("xor", *type)) { cairo_set_operator(ctx, CAIRO_OPERATOR_XOR); } else if (0 == strcmp("source-atop", *type)) { @@ -1521,7 +1521,7 @@ NAN_GETTER(Context2d::GetAntiAlias) { */ NAN_SETTER(Context2d::SetAntiAlias) { - String::Utf8Value str(value->ToString()); + Nan::Utf8String str(value->ToString()); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); cairo_antialias_t a; @@ -1561,7 +1561,7 @@ NAN_GETTER(Context2d::GetTextDrawingMode) { */ NAN_SETTER(Context2d::SetTextDrawingMode) { - String::Utf8Value str(value->ToString()); + Nan::Utf8String str(value->ToString()); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); if (0 == strcmp("path", *str)) { context->state->textDrawingMode = TEXT_DRAW_PATHS; @@ -1592,7 +1592,7 @@ NAN_GETTER(Context2d::GetFilter) { */ NAN_SETTER(Context2d::SetFilter) { - String::Utf8Value str(value->ToString()); + Nan::Utf8String str(value->ToString()); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_filter_t filter; if (0 == strcmp("fast", *str)) { @@ -1673,7 +1673,7 @@ NAN_GETTER(Context2d::GetLineJoin) { NAN_SETTER(Context2d::SetLineJoin) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); - String::Utf8Value type(value->ToString()); + Nan::Utf8String type(value->ToString()); if (0 == strcmp("round", *type)) { cairo_set_line_join(ctx, CAIRO_LINE_JOIN_ROUND); } else if (0 == strcmp("bevel", *type)) { @@ -1705,7 +1705,7 @@ NAN_GETTER(Context2d::GetLineCap) { NAN_SETTER(Context2d::SetLineCap) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); - String::Utf8Value type(value->ToString()); + Nan::Utf8String type(value->ToString()); if (0 == strcmp("round", *type)) { cairo_set_line_cap(ctx, CAIRO_LINE_CAP_ROUND); } else if (0 == strcmp("square", *type)) { @@ -1780,7 +1780,7 @@ NAN_METHOD(Context2d::SetStrokePattern) { NAN_SETTER(Context2d::SetShadowColor) { short ok; - String::Utf8Value str(value->ToString()); + Nan::Utf8String str(value->ToString()); uint32_t rgba = rgba_from_string(*str, &ok); if (ok) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -1807,7 +1807,7 @@ NAN_METHOD(Context2d::SetFillColor) { short ok; if (!info[0]->IsString()) return; - String::Utf8Value str(info[0]); + Nan::Utf8String str(info[0]); uint32_t rgba = rgba_from_string(*str, &ok); if (!ok) return; @@ -1835,7 +1835,7 @@ NAN_METHOD(Context2d::SetStrokeColor) { short ok; if (!info[0]->IsString()) return; - String::Utf8Value str(info[0]); + Nan::Utf8String str(info[0]); uint32_t rgba = rgba_from_string(*str, &ok); if (!ok) return; @@ -2085,7 +2085,7 @@ NAN_METHOD(Context2d::FillText) { if(!checkArgs(info, args, argsNum, 1)) return; - String::Utf8Value str(info[0]->ToString()); + Nan::Utf8String str(info[0]->ToString()); double x = args[0]; double y = args[1]; double scaled_by = 1; @@ -2120,7 +2120,7 @@ NAN_METHOD(Context2d::StrokeText) { if(!checkArgs(info, args, argsNum, 1)) return; - String::Utf8Value str(info[0]->ToString()); + Nan::Utf8String str(info[0]->ToString()); double x = args[0]; double y = args[1]; double scaled_by = 1; @@ -2245,11 +2245,11 @@ NAN_METHOD(Context2d::SetFont) { || !info[3]->IsString() || !info[4]->IsString()) return; - String::Utf8Value weight(info[0]); - String::Utf8Value style(info[1]); + Nan::Utf8String weight(info[0]); + Nan::Utf8String style(info[1]); double size = info[2]->NumberValue(); - String::Utf8Value unit(info[3]); - String::Utf8Value family(info[4]); + Nan::Utf8String unit(info[3]); + Nan::Utf8String family(info[4]); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -2282,7 +2282,7 @@ NAN_METHOD(Context2d::MeasureText) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); - String::Utf8Value str(info[0]->ToString()); + Nan::Utf8String str(info[0]->ToString()); Local obj = Nan::New(); PangoRectangle _ink_rect, _logical_rect; diff --git a/src/Image.cc b/src/Image.cc index 9ff95d12f..84c6f7e88 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -25,7 +25,8 @@ typedef struct { #include struct canvas_jpeg_error_mgr: jpeg_error_mgr { - jmp_buf setjmp_buffer; + private: char unused[8]; + public: jmp_buf setjmp_buffer; }; #endif @@ -229,7 +230,7 @@ NAN_SETTER(Image::SetSource) { // url string if (value->IsString()) { - String::Utf8Value src(value); + Nan::Utf8String src(value); if (img->filename) free(img->filename); img->filename = strdup(*src); status = img->load(); diff --git a/test/canvas.test.js b/test/canvas.test.js index 4afb2f99c..b5480d155 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -547,23 +547,25 @@ describe('Canvas', function () { }); }); - it('Canvas#toBuffer(callback, "image/jpeg")', function () { - var buf = createCanvas(200,200).toBuffer(function (err, buff) { + it('Canvas#toBuffer(callback, "image/jpeg")', function (done) { + createCanvas(200,200).toBuffer(function (err, buf) { assert.ok(!err); assert.equal(buf[0], 0xff); assert.equal(buf[1], 0xd8); assert.equal(buf[buf.byteLength - 2], 0xff); assert.equal(buf[buf.byteLength - 1], 0xd9); + done(); }, 'image/jpeg'); }); - it('Canvas#toBuffer(callback, "image/jpeg", {quality: 0.95})', function () { - var buf = createCanvas(200,200).toBuffer(function (err, buff) { + it('Canvas#toBuffer(callback, "image/jpeg", {quality: 0.95})', function (done) { + createCanvas(200,200).toBuffer(function (err, buf) { assert.ok(!err); assert.equal(buf[0], 0xff); assert.equal(buf[1], 0xd8); assert.equal(buf[buf.byteLength - 2], 0xff); assert.equal(buf[buf.byteLength - 1], 0xd9); + done(); }, 'image/jpeg', {quality: 0.95}); }); From 0f74eed1eba4611cba649615eb9c220b0ca23a77 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sun, 1 Jul 2018 19:05:21 -0700 Subject: [PATCH 147/474] Support canvas.toDataURL("image/jpeg") (sync) Also fixes a bug I introduced in #1152 (JPEG quality needs to go from 0 to 1, not 0 to 100). Fixes #1146 --- CHANGELOG.md | 1 + lib/canvas.js | 62 +++++++++++++++++---------------------------- test/canvas.test.js | 36 ++++++++------------------ 3 files changed, 35 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c94364e0..076b46516 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,7 @@ canvas.createJPEGStream() // new and `canvas.jpegStream()` * Added `resolution` option for `canvas.toBuffer("image/png")` and `canvas.createPNGStream()` + * Support for `canvas.toDataURI("image/jpeg")` (sync) 1.6.x (unreleased) ================== diff --git a/lib/canvas.js b/lib/canvas.js index 748929df6..ec16b92ea 100644 --- a/lib/canvas.js +++ b/lib/canvas.js @@ -99,15 +99,17 @@ Canvas.prototype.createJPEGStream = function(options){ }; /** - * Return a data url. Pass a function for async support (required for "image/jpeg"). + * Returns a data URI. Pass a function for async operation (non-standard). * - * @param {String} type, optional, one of "image/png" or "image/jpeg", defaults to "image/png" - * @param {Object|Number} encoderOptions, optional, options for jpeg compression (see documentation for Canvas#jpegStream) or the JPEG encoding quality from 0 to 1. - * @param {Function} fn, optional, callback for asynchronous operation. Required for type "image/jpeg". + * @param {"image/png"|"image/jpeg"} [type="image/png"] Type. + * @param {Object|Number} [encoderOptions] A number between 0 and 1 indicating + * image quality if the requested type is image/jpeg (standard), or an options + * object for image encoding (see documentation for Canvas#toBuffer) + * (non-standard). + * @param {Function} [fn] Callback for asynchronous operation (non-standard). * @return {String} data URL if synchronous (callback omitted) * @api public */ - Canvas.prototype.toDataURL = function(a1, a2, a3){ // valid arg patterns (args -> [type, opts, fn]): // [] -> ['image/png', null, null] @@ -123,6 +125,9 @@ Canvas.prototype.toDataURL = function(a1, a2, a3){ // ['image/jpeg', opts, fn] -> ['image/jpeg', opts, fn] // ['image/jpeg', qual, fn] -> ['image/jpeg', {quality: qual}, fn] // ['image/jpeg', undefined, fn] -> ['image/jpeg', null, fn] + // ['image/jpeg'] -> ['image/jpeg', null, fn] + // ['image/jpeg', opts] -> ['image/jpeg', opts, fn] + // ['image/jpeg', qual] -> ['image/jpeg', {quality: qual}, fn] var type = 'image/png'; var opts = {}; @@ -131,7 +136,7 @@ Canvas.prototype.toDataURL = function(a1, a2, a3){ if ('function' === typeof a1) { fn = a1; } else { - if ('string' === typeof a1 && FORMATS.indexOf(a1.toLowerCase()) !== -1) { + if ('string' === typeof a1 && FORMATS.includes(a1.toLowerCase())) { type = a1.toLowerCase(); } @@ -141,7 +146,7 @@ Canvas.prototype.toDataURL = function(a1, a2, a3){ if ('object' === typeof a2) { opts = a2; } else if ('number' === typeof a2) { - opts = {quality: Math.max(0, Math.min(1, a2)) * 100}; + opts = {quality: Math.max(0, Math.min(1, a2))}; } if ('function' === typeof a3) { @@ -156,40 +161,19 @@ Canvas.prototype.toDataURL = function(a1, a2, a3){ // Per spec, if the bitmap has no pixels, return this string: var str = "data:,"; if (fn) { - setTimeout(function() { - fn(null, str); - }); - } - return str; - } - - if ('image/png' === type) { - if (fn) { - this.toBuffer(function(err, buf){ - if (err) return fn(err); - fn(null, 'data:image/png;base64,' + buf.toString('base64')); - }); + setTimeout(() => fn(null, str)); + return; } else { - return 'data:image/png;base64,' + this.toBuffer().toString('base64'); - } - - } else if ('image/jpeg' === type) { - if (undefined === fn) { - throw new Error('Missing required callback function for format "image/jpeg"'); + return str; } + } - var stream = this.jpegStream(opts); - // note that jpegStream is synchronous - var buffers = []; - stream.on('data', function (chunk) { - buffers.push(chunk); - }); - stream.on('error', function (err) { - fn(err); - }); - stream.on('end', function() { - var result = 'data:image/jpeg;base64,' + Buffer.concat(buffers).toString('base64'); - fn(null, result); - }); + if (fn) { + this.toBuffer((err, buf) => { + if (err) return fn(err); + fn(null, `data:${type};base64,${buf.toString('base64')}`); + }, type, opts) + } else { + return `data:${type};base64,${this.toBuffer(type).toString('base64')}` } }; diff --git a/test/canvas.test.js b/test/canvas.test.js index b5480d155..d2f042702 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -663,38 +663,31 @@ describe('Canvas', function () { ctx.fillRect(100,0,100,100); it('toDataURL() works and defaults to PNG', function () { - assert.ok(0 == canvas.toDataURL().indexOf('data:image/png;base64,')); + assert.ok(canvas.toDataURL().startsWith('data:image/png;base64,')); }); it('toDataURL(0.5) works and defaults to PNG', function () { - assert.ok(0 == canvas.toDataURL(0.5).indexOf('data:image/png;base64,')); + assert.ok(canvas.toDataURL(0.5).startsWith('data:image/png;base64,')); }); it('toDataURL(undefined) works and defaults to PNG', function () { - assert.ok(0 == canvas.toDataURL(undefined).indexOf('data:image/png;base64,')); + assert.ok(canvas.toDataURL(undefined).startsWith('data:image/png;base64,')); }); it('toDataURL("image/png") works', function () { - assert.ok(0 == canvas.toDataURL('image/png').indexOf('data:image/png;base64,')); + assert.ok(canvas.toDataURL('image/png').startsWith('data:image/png;base64,')); }); it('toDataURL("image/png", 0.5) works', function () { - assert.ok(0 == canvas.toDataURL('image/png').indexOf('data:image/png;base64,')); + assert.ok(canvas.toDataURL('image/png').startsWith('data:image/png;base64,')); }); it('toDataURL("iMaGe/PNg") works', function () { - assert.ok(0 == canvas.toDataURL('iMaGe/PNg').indexOf('data:image/png;base64,')); + assert.ok(canvas.toDataURL('iMaGe/PNg').startsWith('data:image/png;base64,')); }); - it('toDataURL("image/jpeg") throws', function () { - assert.throws( - function () { - canvas.toDataURL('image/jpeg'); - }, - function (err) { - return err.message === 'Missing required callback function for format "image/jpeg"'; - } - ); + it('toDataURL("image/jpeg") works', function () { + assert.ok(canvas.toDataURL('image/jpeg').startsWith('data:image/jpeg;base64,')); }); it('toDataURL(function (err, str) {...}) works and defaults to PNG', function (done) { @@ -746,18 +739,11 @@ describe('Canvas', function () { }); it('toDataURL("image/png", {}) works', function () { - assert.ok(0 == canvas.toDataURL('image/png', {}).indexOf('data:image/png;base64,')); + assert.ok(canvas.toDataURL('image/png', {}).startsWith('data:image/png;base64,')); }); - it('toDataURL("image/jpeg", {}) throws', function () { - assert.throws( - function () { - canvas.toDataURL('image/jpeg', {}); - }, - function (err) { - return err.message === 'Missing required callback function for format "image/jpeg"'; - } - ); + it('toDataURL("image/jpeg", {}) works', function () { + assert.ok(canvas.toDataURL('image/jpeg', {}).startsWith('data:image/jpeg;base64,')); }); it('toDataURL("image/jpeg", function (err, str) {...}) works', function (done) { From 230b1dba86eda2c48b42b50a29e4c8d740cd4aa7 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 5 Jul 2018 20:50:43 -0700 Subject: [PATCH 148/474] Clean up img.src= (#1193) * Clean up img.src= Fixes #945 - support img.src = Fixes #807 - support for non-base64 `data:` URIs Fixes #1079 - ditto Closes #564 - it works, probably didn't pass a string or buffer Makes all examples and the README use onload, onerror. * image: throw if no onerror listener attached --- CHANGELOG.md | 2 + Readme.md | 56 +++++++++++++++++----------- examples/grayscale-image.js | 26 +++++++------ examples/image-src-url.js | 18 +++++++++ examples/image-src.js | 39 ++++++++++--------- examples/kraken.js | 2 + lib/image.js | 74 ++++++++++++++++++++++++------------- 7 files changed, 142 insertions(+), 75 deletions(-) create mode 100644 examples/image-src-url.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 076b46516..ba2681db5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,7 @@ canvas.createJPEGStream() // new * Allow assigning non-string values to fillStyle and strokeStyle * Fix drawing zero-width and zero-height images. * Fix DEP0005 deprecation warning + * Don't assume `data:` URIs assigned to `img.src` are always base64-encoded ### Added * Prebuilds (#992) with different libc versions to the prebuilt binary (#1140) @@ -89,6 +90,7 @@ canvas.createJPEGStream() // new * Added `resolution` option for `canvas.toBuffer("image/png")` and `canvas.createPNGStream()` * Support for `canvas.toDataURI("image/jpeg")` (sync) + * Support for `img.src = ` to match browsers 1.6.x (unreleased) ================== diff --git a/Readme.md b/Readme.md index 586c498da..7469fb558 100644 --- a/Readme.md +++ b/Readme.md @@ -97,31 +97,42 @@ node-canvas implements the [HTML Canvas API](https://developer.mozilla.org/en-US (See [Compatibility Status](https://github.com/Automattic/node-canvas/wiki/Compatibility-Status) for the current API compliance.) All non-standard APIs are documented below. -### Image#src=Buffer +### Image#src - node-canvas adds `Image#src=Buffer` support, allowing you to read images from disc, redis, etc and apply them via `ctx.drawImage()`. Below we draw scaled down squid png by reading it from the disk with node's I/O. +As in browsers, `img.src` can be set to a `data:` URI or a remote URL. In addition, +node-canvas allows setting `src` to a local file path or to a `Buffer` instance. ```javascript const { Image } = require('canvas'); -fs.readFile(__dirname + '/images/squid.png', function(err, squid){ - if (err) throw err; - img = new Image; - img.src = squid; - ctx.drawImage(img, 0, 0, img.width / 4, img.height / 4); -}); -``` - Below is an example of a canvas drawing it-self as the source several time: +// From a buffer: +fs.readFile('images/squid.png', (err, squid) => { + if (err) throw err + const img = new Image() + img.onload = () => ctx.drawImage(img, 0, 0) + img.onerror = err => { throw err } + img.src = squid +}) -```javascript -const { Image } = require('canvas'); -var img = new Image; -img.src = canvas.toBuffer(); -ctx.drawImage(img, 0, 0, 50, 50); -ctx.drawImage(img, 50, 0, 50, 50); -ctx.drawImage(img, 100, 0, 50, 50); +// From a local file path: +const img = new Image() +img.onload = () => ctx.drawImage(img, 0, 0) +img.onerror = err => { throw err } +img.src = 'images/squid.png' + +// From a remote URL: +img.src = 'http://picsum.photos/200/300' +// ... as above + +// From a `data:` URI: +img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==' +// ... as above ``` +*Note: In some cases, `img.src=` is currently synchronous. However, you should +always use `img.onload` and `img.onerror`, as we intend to make `img.src=` always +asynchronous as it is in browsers. See https://github.com/Automattic/node-canvas/issues/1007.* + ### Image#dataMode node-canvas adds `Image#dataMode` support, which can be used to opt-in to mime data tracking of images (currently only JPEGs). @@ -414,12 +425,15 @@ fs.writeFileSync('out.svg', canvas.toBuffer()); ## SVG Image Support -If librsvg is on your system when node-canvas is installed, node-canvas can render SVG images within your canvas context. Note that this currently works by simply rasterizing the SVG image using librsvg. +If librsvg is available when node-canvas is installed, node-canvas can render +SVG images to your canvas context. This currently works by rasterizing the SVG +image (i.e. drawing an SVG image to an SVG canvas will not preserve the SVG data). ```js -var img = new Image; -img.src = './example.svg'; -ctx.drawImage(img, 0, 0, 100, 100); +const img = new Image() +img.onload = () => ctx.drawImage(img, 0, 0) +img.onerror = err => { throw err } +img.src = './example.svg' ``` ## Image pixel formats (experimental) diff --git a/examples/grayscale-image.js b/examples/grayscale-image.js index ef6bac719..ce3ffa06c 100644 --- a/examples/grayscale-image.js +++ b/examples/grayscale-image.js @@ -1,14 +1,18 @@ -var fs = require('fs') -var path = require('path') -var Canvas = require('..') +const fs = require('fs') +const path = require('path') +const Canvas = require('..') -var Image = Canvas.Image -var canvas = Canvas.createCanvas(288, 288) -var ctx = canvas.getContext('2d') +const Image = Canvas.Image +const canvas = Canvas.createCanvas(288, 288) +const ctx = canvas.getContext('2d') -var img = new Image() -img.src = fs.readFileSync(path.join(__dirname, 'images', 'grayscaleImage.jpg')) +const img = new Image() +img.onload = () => { + ctx.drawImage(img, 0, 0) + canvas.createJPEGStream().pipe(fs.createWriteStream(path.join(__dirname, 'passedThroughGrayscale.jpg'))) +} +img.onerror = err => { + throw err +} -ctx.drawImage(img, 0, 0) - -canvas.createJPEGStream().pipe(fs.createWriteStream(path.join(__dirname, 'passedThroughGrayscale.jpg'))) +img.src = path.join(__dirname, 'images', 'grayscaleImage.jpg') diff --git a/examples/image-src-url.js b/examples/image-src-url.js new file mode 100644 index 000000000..fcad291df --- /dev/null +++ b/examples/image-src-url.js @@ -0,0 +1,18 @@ +const fs = require('fs') +const path = require('path') +const Canvas = require('..') + +const Image = Canvas.Image +const canvas = Canvas.createCanvas(200, 300) +const ctx = canvas.getContext('2d') + +const img = new Image() +img.onload = () => { + ctx.drawImage(img, 0, 0) + canvas.createPNGStream() + .pipe(fs.createWriteStream(path.join(__dirname, 'image-src-url.png'))) +} +img.onerror = err => { + console.log(err) +} +img.src = 'http://picsum.photos/200/300' diff --git a/examples/image-src.js b/examples/image-src.js index 49d7901b2..704b5d39b 100644 --- a/examples/image-src.js +++ b/examples/image-src.js @@ -1,10 +1,10 @@ -var fs = require('fs') -var path = require('path') -var Canvas = require('..') +const fs = require('fs') +const path = require('path') +const Canvas = require('..') -var Image = Canvas.Image -var canvas = Canvas.createCanvas(200, 200) -var ctx = canvas.getContext('2d') +const Image = Canvas.Image +const canvas = Canvas.createCanvas(200, 200) +const ctx = canvas.getContext('2d') ctx.fillRect(0, 0, 150, 150) ctx.save() @@ -23,14 +23,19 @@ ctx.fillRect(45, 45, 60, 60) ctx.restore() ctx.fillRect(60, 60, 30, 30) -var img = new Image() -img.src = canvas.toBuffer() -ctx.drawImage(img, 0, 0, 50, 50) -ctx.drawImage(img, 50, 0, 50, 50) -ctx.drawImage(img, 100, 0, 50, 50) - -img = new Image() -img.src = fs.readFileSync(path.join(__dirname, 'images', 'squid.png')) -ctx.drawImage(img, 30, 50, img.width / 4, img.height / 4) - -canvas.createPNGStream().pipe(fs.createWriteStream(path.join(__dirname, 'image-src.png'))) +const img = new Image() +img.onerror = err => { throw err } +img.onload = () => { + img.src = canvas.toBuffer() + ctx.drawImage(img, 0, 0, 50, 50) + ctx.drawImage(img, 50, 0, 50, 50) + ctx.drawImage(img, 100, 0, 50, 50) + + const img2 = new Image() + img2.onload = () => { + ctx.drawImage(img2, 30, 50, img2.width / 4, img2.height / 4) + canvas.createPNGStream().pipe(fs.createWriteStream(path.join(__dirname, 'image-src.png'))) + } + img2.onerror = err => { throw err } + img2.src = path.join(__dirname, 'images', 'squid.png') +} diff --git a/examples/kraken.js b/examples/kraken.js index 7dca25b8c..c49ef8836 100644 --- a/examples/kraken.js +++ b/examples/kraken.js @@ -12,6 +12,8 @@ img.onload = function () { ctx.drawImage(img, 0, 0) } +img.onerror = err => { throw err } + img.src = path.join(__dirname, 'images', 'squid.png') var sigma = 10 // radius diff --git a/lib/image.js b/lib/image.js index e0edc2ab9..7e79666ec 100644 --- a/lib/image.js +++ b/lib/image.js @@ -12,37 +12,59 @@ const bindings = require('./bindings') const Image = module.exports = bindings.Image +const http = require("http") -/** - * Src setter. - * - * - convert data uri to `Buffer` - * - * @param {String|Buffer} val filename, buffer, data uri - * @api public - */ +Object.defineProperty(Image.prototype, 'src', { + /** + * src setter. Valid values: + * * `data:` URI + * * Local file path + * * HTTP or HTTPS URL + * * Buffer containing image data (i.e. not a `data:` URI stored in a Buffer) + * + * @param {String|Buffer} val filename, buffer, data URI, URL + * @api public + */ + set(val) { + if (typeof val === 'string') { + if (/^\s*data:/.test(val)) { // data: URI + const commaI = val.indexOf(',') + // 'base64' must come before the comma + const isBase64 = val.lastIndexOf('base64', commaI) + val = val.slice(commaI + 1) + this.source = Buffer.from(val, isBase64 ? 'base64' : 'utf8') + } else if (/^\s*http/.test(val)) { // remote URL + const onerror = err => { + if (typeof this.onerror === 'function') { + this.onerror(err) + } else { + throw err + } + } + http.get(val, res => { + if (res.statusCode !== 200) { + return onerror(new Error(`Server responded with ${res.statusCode}`)) + } + const buffers = [] + res.on('data', buffer => buffers.push(buffer)) + res.on('end', () => { + this.source = Buffer.concat(buffers) + }) + }).on('error', onerror) + } else { // local file path assumed + this.source = val + } + } else if (Buffer.isBuffer(val)) { + this.source = val + } + }, -Image.prototype.__defineSetter__('src', function(val){ - if ('string' == typeof val && 0 == val.indexOf('data:')) { - val = val.slice(val.indexOf(',') + 1); - this.source = Buffer.from(val, 'base64'); - } else { - this.source = val; + get() { + // TODO https://github.com/Automattic/node-canvas/issues/118 + return this.source; } }); -/** - * Src getter. - * - * TODO: return buffer - * - * @api public - */ - -Image.prototype.__defineGetter__('src', function(){ - return this.source; -}); - /** * Inspect image. * From e7b193b7fa66b7cdfea7454a51967c5e1076f4f4 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sun, 8 Jul 2018 00:18:56 -0700 Subject: [PATCH 149/474] Fix compilation without libjpeg (#1198) Broken since #1152 Fixes #1196 --- src/Canvas.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Canvas.cc b/src/Canvas.cc index 623d8b0f9..b87fc3b31 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -202,12 +202,14 @@ Canvas::ToPngBufferAsync(uv_work_t *req) { closure); } +#ifdef HAVE_JPEG void Canvas::ToJpegBufferAsync(uv_work_t *req) { JpegClosure* closure = static_cast(req->data); write_to_jpeg_buffer(closure->canvas->surface(), closure, &closure->data, &closure->len); } +#endif /* * EIO after toBuffer callback. From 3167628e81ecc2952297157ad20f03939990f6bf Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Fri, 6 Jul 2018 20:05:40 -0700 Subject: [PATCH 150/474] Fix string formatting of RGBA colors Fixes #251 --- CHANGELOG.md | 1 + src/color.cc | 18 +++++++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba2681db5..8c2e2b555 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,7 @@ canvas.createJPEGStream() // new * Fix drawing zero-width and zero-height images. * Fix DEP0005 deprecation warning * Don't assume `data:` URIs assigned to `img.src` are always base64-encoded + * Fix formatting of color strings (e.g. `ctx.fillStyle`) on 32-bit platforms ### Added * Prebuilds (#992) with different libc versions to the prebuilt binary (#1140) diff --git a/src/color.cc b/src/color.cc index 30d9fcd35..40cc847a8 100644 --- a/src/color.cc +++ b/src/color.cc @@ -458,16 +458,16 @@ rgba_create(uint32_t rgba) { void rgba_to_string(rgba_t rgba, char *buf, size_t len) { if (1 == rgba.a) { - snprintf(buf, len, "#%.2x%.2x%.2x" - , (int) (rgba.r * 255) - , (int) (rgba.g * 255) - , (int) (rgba.b * 255)); + snprintf(buf, len, "#%.2x%.2x%.2x", + static_cast(round(rgba.r * 255)), + static_cast(round(rgba.g * 255)), + static_cast(round(rgba.b * 255))); } else { - snprintf(buf, len, "rgba(%d, %d, %d, %.2f)" - , (int) (rgba.r * 255) - , (int) (rgba.g * 255) - , (int) (rgba.b * 255) - , rgba.a); + snprintf(buf, len, "rgba(%d, %d, %d, %.2f)", + static_cast(round(rgba.r * 255)), + static_cast(round(rgba.g * 255)), + static_cast(round(rgba.b * 255)), + rgba.a); } } From bc4c01b078ed13b2773c0879a8654444aed9c07b Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sun, 8 Jul 2018 10:21:38 -0700 Subject: [PATCH 151/474] Explicitly export some C++ symbols This only affects the C++ API on Windows, where symbols are not always exported by default. Linking against node-canvas used to happen to work, until backends were added and the combination of inlining lead to some missing symbols. This is the correct thing to do when offering a C++ API. For example, node does it in node.h: https://github.com/nodejs/node/blob/602da6492f278c1345f7f2d82014d9248254476b/src/node.h#L25-L33 and v8: https://github.com/nodejs/node/blob/ca480719199d2ff38223aff8e301aced25d7e6f1/deps/v8/src/base/base-export.h. The directives for GCC are unnecessary since we don't use DLL_LOCAL yet and it exports by default, but using DLL_LOCAL in the future can offer some significant performance benefits (see https://gcc.gnu.org/wiki/Visibility). --- CHANGELOG.md | 1 + src/Canvas.h | 15 ++++++++------- src/backend/Backend.cc | 2 +- src/backend/Backend.h | 10 ++++++---- src/dll_visibility.h | 20 ++++++++++++++++++++ 5 files changed, 36 insertions(+), 12 deletions(-) create mode 100644 src/dll_visibility.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c2e2b555..6049a8306 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,7 @@ canvas.createJPEGStream() // new * Fix DEP0005 deprecation warning * Don't assume `data:` URIs assigned to `img.src` are always base64-encoded * Fix formatting of color strings (e.g. `ctx.fillStyle`) on 32-bit platforms + * Explicitly export symbols for the C++ API ### Added * Prebuilds (#992) with different libc versions to the prebuilt binary (#1140) diff --git a/src/Canvas.h b/src/Canvas.h index 847da4a00..adaac1554 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -17,6 +17,7 @@ #include #include +#include "dll_visibility.h" #include "backend/Backend.h" @@ -70,16 +71,16 @@ class Canvas: public Nan::ObjectWrap { static PangoStyle GetStyleFromCSSString(const char *style); static PangoFontDescription *ResolveFontDescription(const PangoFontDescription *desc); - inline Backend* backend() { return _backend; } - inline cairo_surface_t* surface(){ return backend()->getSurface(); } + DLL_PUBLIC inline Backend* backend() { return _backend; } + DLL_PUBLIC inline cairo_surface_t* surface(){ return backend()->getSurface(); } cairo_t* createCairoContext(); - inline uint8_t *data(){ return cairo_image_surface_get_data(surface()); } - inline int stride(){ return cairo_image_surface_get_stride(surface()); } - inline int nBytes(){ return getHeight() * stride(); } + DLL_PUBLIC inline uint8_t *data(){ return cairo_image_surface_get_data(surface()); } + DLL_PUBLIC inline int stride(){ return cairo_image_surface_get_stride(surface()); } + DLL_PUBLIC inline int nBytes(){ return getHeight() * stride(); } - inline int getWidth() { return backend()->getWidth(); } - inline int getHeight() { return backend()->getHeight(); } + DLL_PUBLIC inline int getWidth() { return backend()->getWidth(); } + DLL_PUBLIC inline int getHeight() { return backend()->getHeight(); } Canvas(Backend* backend); void resurface(Local canvas); diff --git a/src/backend/Backend.cc b/src/backend/Backend.cc index 74e05c17c..4a2e0c212 100644 --- a/src/backend/Backend.cc +++ b/src/backend/Backend.cc @@ -28,7 +28,7 @@ cairo_surface_t* Backend::recreateSurface() return this->createSurface(); } -cairo_surface_t* Backend::getSurface() { +DLL_PUBLIC cairo_surface_t* Backend::getSurface() { if (!surface) createSurface(); return surface; } diff --git a/src/backend/Backend.h b/src/backend/Backend.h index d3871c3cf..40d2d328d 100644 --- a/src/backend/Backend.h +++ b/src/backend/Backend.h @@ -9,6 +9,8 @@ #include #include +#include "../dll_visibility.h" + class Canvas; using namespace std; @@ -35,15 +37,15 @@ class Backend : public Nan::ObjectWrap virtual cairo_surface_t* createSurface() = 0; virtual cairo_surface_t* recreateSurface(); - cairo_surface_t* getSurface(); + DLL_PUBLIC cairo_surface_t* getSurface(); void destroySurface(); - string getName(); + DLL_PUBLIC string getName(); - int getWidth(); + DLL_PUBLIC int getWidth(); virtual void setWidth(int width); - int getHeight(); + DLL_PUBLIC int getHeight(); virtual void setHeight(int height); // Overridden by ImageBackend. SVG and PDF thus always return INVALID. diff --git a/src/dll_visibility.h b/src/dll_visibility.h new file mode 100644 index 000000000..7a1f98450 --- /dev/null +++ b/src/dll_visibility.h @@ -0,0 +1,20 @@ +#ifndef DLL_PUBLIC + +#if defined _WIN32 + #ifdef __GNUC__ + #define DLL_PUBLIC __attribute__ ((dllexport)) + #else + #define DLL_PUBLIC __declspec(dllexport) + #endif + #define DLL_LOCAL +#else + #if __GNUC__ >= 4 + #define DLL_PUBLIC __attribute__ ((visibility ("default"))) + #define DLL_LOCAL __attribute__ ((visibility ("hidden"))) + #else + #define DLL_PUBLIC + #define DLL_LOCAL + #endif +#endif + +#endif From f92e091f29b0d3010948f05e2beab61703d87845 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Mon, 9 Jul 2018 21:19:17 -0700 Subject: [PATCH 152/474] named colors are case-insensitive Fixes #235 --- CHANGELOG.md | 1 + src/color.cc | 13 +++++++------ test/canvas.test.js | 4 ++++ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6049a8306..bded3a6e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ canvas.createJPEGStream() // new * Don't assume `data:` URIs assigned to `img.src` are always base64-encoded * Fix formatting of color strings (e.g. `ctx.fillStyle`) on 32-bit platforms * Explicitly export symbols for the C++ API + * Named CSS colors should match case-insensitive ### Added * Prebuilds (#992) with different libc versions to the prebuilt binary (#1140) diff --git a/src/color.cc b/src/color.cc index 40cc847a8..e8f1233b1 100644 --- a/src/color.cc +++ b/src/color.cc @@ -8,7 +8,8 @@ #include #include #include - +#include +#include #include "color.h" // Compatibility with Visual Studio versions prior to VS2015 @@ -398,7 +399,6 @@ static struct named_color { , { "whitesmoke", 0xF5F5F5FF } , { "yellow", 0xFFFF00FF } , { "yellowgreen", 0x9ACD32FF } - , { NULL, 0 } }; /* @@ -727,11 +727,12 @@ rgba_from_hex_string(const char *str, short *ok) { static int32_t rgba_from_name_string(const char *str, short *ok) { - int i = 0; - struct named_color color; - while ((color = named_colors[i++]).name) { - if (*str == *color.name && 0 == strcmp(str, color.name)) + std::string lowered(str); + std::transform(lowered.begin(), lowered.end(), lowered.begin(), tolower); + for (auto color : named_colors) { + if (color.name == lowered) { return *ok = 1, color.val; + } } return *ok = 0; } diff --git a/test/canvas.test.js b/test/canvas.test.js index d2f042702..8dfbb9926 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -238,6 +238,10 @@ describe('Canvas', function () { ctx.fillStyle = 'hsl(1.24e2, 760e-1%, 4.7e1%)'; assert.equal('#1dd329', ctx.fillStyle); + + // case-insensitive (#235) + ctx.fillStyle = "sILveR"; + assert.equal(ctx.fillStyle, "#c0c0c0"); }); it('Canvas#type', function () { From 0ffd400de04d089f77e5e6684ac85d10858ab4ce Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Mon, 9 Jul 2018 21:42:50 -0700 Subject: [PATCH 153/474] Implement rgba_from_name_string with std::map Benchmark (ops/sec): | color | AoS loop | `std::map` | | --- | ---: | ---: | | "whitesmoke" | 668,905 | 2,764,503 | | "transparent" | 4,519,027 | 4,578,934 | --- benchmarks/run.js | 4 ++++ src/color.cc | 14 +++++--------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/benchmarks/run.js b/benchmarks/run.js index 531055524..e088e0692 100644 --- a/benchmarks/run.js +++ b/benchmarks/run.js @@ -64,6 +64,10 @@ function done (benchmark, times, start, isAsync) { // node-canvas +bm('fillStyle= name', function () { + ctx.fillStyle = "transparent"; +}); + bm('lineTo()', function () { ctx.lineTo(0, 50) }) diff --git a/src/color.cc b/src/color.cc index e8f1233b1..c6df4a6ec 100644 --- a/src/color.cc +++ b/src/color.cc @@ -11,6 +11,7 @@ #include #include #include "color.h" +#include // Compatibility with Visual Studio versions prior to VS2015 #if defined(_MSC_VER) && _MSC_VER < 1900 @@ -245,11 +246,7 @@ parse_clipped_percentage(const char** pStr, float *pFraction) { /* * Named colors. */ - -static struct named_color { - const char *name; - uint32_t val; -} named_colors[] = { +static const std::map named_colors = { { "transparent", 0xFFFFFF00} , { "aliceblue", 0xF0F8FFFF } , { "antiquewhite", 0xFAEBD7FF } @@ -729,10 +726,9 @@ static int32_t rgba_from_name_string(const char *str, short *ok) { std::string lowered(str); std::transform(lowered.begin(), lowered.end(), lowered.begin(), tolower); - for (auto color : named_colors) { - if (color.name == lowered) { - return *ok = 1, color.val; - } + auto color = named_colors.find(lowered); + if (color != named_colors.end()) { + return *ok = 1, color->second; } return *ok = 0; } From 0fa6e02f8cd2bd9d049f77028ea39f2fd5e1460b Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Tue, 10 Jul 2018 23:21:08 -0700 Subject: [PATCH 154/474] Correct globalCompositeOperator types See changelog. Now matches the spec. Closes #105 (appears to have been working already) Adds a test case for #416 (not fixed by this PR) --- CHANGELOG.md | 12 ++ src/CanvasRenderingContext2d.cc | 157 +++++++++----------- test/fixtures/existing.png | Bin 0 -> 42514 bytes test/fixtures/newcontent.png | Bin 0 -> 4778 bytes test/public/tests.js | 244 ++++++-------------------------- 5 files changed, 122 insertions(+), 291 deletions(-) create mode 100644 test/fixtures/existing.png create mode 100644 test/fixtures/newcontent.png diff --git a/CHANGELOG.md b/CHANGELOG.md index bded3a6e9..dc761869f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,9 @@ canvas.createJPEGStream() // new * Make the `compressionLevel` and `filters` arguments for `canvas.toBuffer()` named instead of positional. Same for `canvas.pngStream()`, although these arguments were not documented. + * See also: *Correct some of the `globalCompositeOperator` types* under + **Fixed**. These changes were bug-fixes, but will break existing code relying + on the incorrect types. ### Fixed * Prevent segfaults caused by loading invalid fonts (#1105) @@ -71,6 +74,15 @@ canvas.createJPEGStream() // new * Fix formatting of color strings (e.g. `ctx.fillStyle`) on 32-bit platforms * Explicitly export symbols for the C++ API * Named CSS colors should match case-insensitive + * Correct some of the `globalCompositeOperator` types to match the spec: + * "hsl-hue" is now "hue" + * "hsl-saturation" is now "saturation" + * "hsl-color" is now "color" + * "hsl-luminosity" is now "luminosity" + * "darker" is now "darken" + * "dest" is now "destination" + * "add" is removed (but is the same as "lighter") + * "source" is now "copy" ### Added * Prebuilds (#992) with different libc versions to the prebuilt binary (#1140) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index e71cd5f9e..ed7d3ce6d 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -11,6 +11,7 @@ #include #include #include +#include #include "Util.h" #include "Canvas.h" @@ -1289,41 +1290,43 @@ NAN_GETTER(Context2d::GetGlobalCompositeOperation) { const char *op = "source-over"; switch (cairo_get_operator(ctx)) { - case CAIRO_OPERATOR_ATOP: op = "source-atop"; break; + // composite modes: + case CAIRO_OPERATOR_CLEAR: op = "clear"; break; + case CAIRO_OPERATOR_SOURCE: op = "copy"; break; + case CAIRO_OPERATOR_DEST: op = "destination"; break; + case CAIRO_OPERATOR_OVER: op = "source-over"; break; + case CAIRO_OPERATOR_DEST_OVER: op = "destination-over"; break; case CAIRO_OPERATOR_IN: op = "source-in"; break; - case CAIRO_OPERATOR_OUT: op = "source-out"; break; - case CAIRO_OPERATOR_XOR: op = "xor"; break; - case CAIRO_OPERATOR_DEST_ATOP: op = "destination-atop"; break; case CAIRO_OPERATOR_DEST_IN: op = "destination-in"; break; + case CAIRO_OPERATOR_OUT: op = "source-out"; break; case CAIRO_OPERATOR_DEST_OUT: op = "destination-out"; break; - case CAIRO_OPERATOR_DEST_OVER: op = "destination-over"; break; - case CAIRO_OPERATOR_CLEAR: op = "clear"; break; - case CAIRO_OPERATOR_SOURCE: op = "source"; break; - case CAIRO_OPERATOR_DEST: op = "dest"; break; - case CAIRO_OPERATOR_OVER: op = "over"; break; - case CAIRO_OPERATOR_SATURATE: op = "saturate"; break; - // Non-standard - // supported by resent versions of cairo -#if CAIRO_VERSION_MINOR >= 10 - case CAIRO_OPERATOR_LIGHTEN: op = "lighten"; break; - case CAIRO_OPERATOR_ADD: op = "add"; break; - case CAIRO_OPERATOR_DARKEN: op = "darker"; break; + case CAIRO_OPERATOR_ATOP: op = "source-atop"; break; + case CAIRO_OPERATOR_DEST_ATOP: op = "destination-atop"; break; + case CAIRO_OPERATOR_XOR: op = "xor"; break; + case CAIRO_OPERATOR_ADD: op = "lighter"; break; + // blend modes: + // Note: "source-over" and "normal" are synonyms. Chrome and FF both report + // "source-over" after setting gCO to "normal". + // case CAIRO_OPERATOR_OVER: op = "normal"; +#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 10, 0) case CAIRO_OPERATOR_MULTIPLY: op = "multiply"; break; case CAIRO_OPERATOR_SCREEN: op = "screen"; break; case CAIRO_OPERATOR_OVERLAY: op = "overlay"; break; - case CAIRO_OPERATOR_HARD_LIGHT: op = "hard-light"; break; - case CAIRO_OPERATOR_SOFT_LIGHT: op = "soft-light"; break; - case CAIRO_OPERATOR_HSL_HUE: op = "hsl-hue"; break; - case CAIRO_OPERATOR_HSL_SATURATION: op = "hsl-saturation"; break; - case CAIRO_OPERATOR_HSL_COLOR: op = "hsl-color"; break; - case CAIRO_OPERATOR_HSL_LUMINOSITY: op = "hsl-luminosity"; break; + case CAIRO_OPERATOR_DARKEN: op = "darken"; break; + case CAIRO_OPERATOR_LIGHTEN: op = "lighten"; break; case CAIRO_OPERATOR_COLOR_DODGE: op = "color-dodge"; break; case CAIRO_OPERATOR_COLOR_BURN: op = "color-burn"; break; + case CAIRO_OPERATOR_HARD_LIGHT: op = "hard-light"; break; + case CAIRO_OPERATOR_SOFT_LIGHT: op = "soft-light"; break; case CAIRO_OPERATOR_DIFFERENCE: op = "difference"; break; case CAIRO_OPERATOR_EXCLUSION: op = "exclusion"; break; -#else - case CAIRO_OPERATOR_ADD: op = "lighter"; break; + case CAIRO_OPERATOR_HSL_HUE: op = "hue"; break; + case CAIRO_OPERATOR_HSL_SATURATION: op = "saturation"; break; + case CAIRO_OPERATOR_HSL_COLOR: op = "color"; break; + case CAIRO_OPERATOR_HSL_LUMINOSITY: op = "luminosity"; break; #endif + // non-standard: + case CAIRO_OPERATOR_SATURATE: op = "saturate"; break; } info.GetReturnValue().Set(Nan::New(op).ToLocalChecked()); @@ -1373,74 +1376,46 @@ NAN_GETTER(Context2d::GetPatternQuality) { NAN_SETTER(Context2d::SetGlobalCompositeOperation) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); - Nan::Utf8String type(value->ToString()); - if (0 == strcmp("xor", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_XOR); - } else if (0 == strcmp("source-atop", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_ATOP); - } else if (0 == strcmp("source-in", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_IN); - } else if (0 == strcmp("source-out", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_OUT); - } else if (0 == strcmp("destination-atop", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_DEST_ATOP); - } else if (0 == strcmp("destination-in", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_DEST_IN); - } else if (0 == strcmp("destination-out", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_DEST_OUT); - } else if (0 == strcmp("destination-over", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_DEST_OVER); - } else if (0 == strcmp("clear", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_CLEAR); - } else if (0 == strcmp("source", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_SOURCE); - } else if (0 == strcmp("dest", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_DEST); - } else if (0 == strcmp("saturate", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_SATURATE); - } else if (0 == strcmp("over", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_OVER); - // Non-standard - // supported by resent versions of cairo -#if CAIRO_VERSION_MINOR >= 10 - } else if (0 == strcmp("add", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_ADD); - } else if (0 == strcmp("lighten", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_LIGHTEN); - } else if (0 == strcmp("darker", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_DARKEN); - } else if (0 == strcmp("multiply", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_MULTIPLY); - } else if (0 == strcmp("screen", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_SCREEN); - } else if (0 == strcmp("overlay", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_OVERLAY); - } else if (0 == strcmp("hard-light", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_HARD_LIGHT); - } else if (0 == strcmp("soft-light", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_SOFT_LIGHT); - } else if (0 == strcmp("hsl-hue", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_HSL_HUE); - } else if (0 == strcmp("hsl-saturation", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_HSL_SATURATION); - } else if (0 == strcmp("hsl-color", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_HSL_COLOR); - } else if (0 == strcmp("hsl-luminosity", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_HSL_LUMINOSITY); - } else if (0 == strcmp("color-dodge", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_COLOR_DODGE); - } else if (0 == strcmp("color-burn", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_COLOR_BURN); - } else if (0 == strcmp("difference", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_DIFFERENCE); - } else if (0 == strcmp("exclusion", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_EXCLUSION); + Nan::Utf8String opStr(value->ToString()); // Unlike CSS colors, this *is* case-sensitive + const std::map blendmodes = { + // composite modes: + {"clear", CAIRO_OPERATOR_CLEAR}, + {"copy", CAIRO_OPERATOR_SOURCE}, + {"destination", CAIRO_OPERATOR_DEST}, // this seems to have been omitted from the spec + {"source-over", CAIRO_OPERATOR_OVER}, + {"destination-over", CAIRO_OPERATOR_DEST_OVER}, + {"source-in", CAIRO_OPERATOR_IN}, + {"destination-in", CAIRO_OPERATOR_DEST_IN}, + {"source-out", CAIRO_OPERATOR_OUT}, + {"destination-out", CAIRO_OPERATOR_DEST_OUT}, + {"source-atop", CAIRO_OPERATOR_ATOP}, + {"destination-atop", CAIRO_OPERATOR_DEST_ATOP}, + {"xor", CAIRO_OPERATOR_XOR}, + {"lighter", CAIRO_OPERATOR_ADD}, + // blend modes: + {"normal", CAIRO_OPERATOR_OVER}, +#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 10, 0) + {"multiply", CAIRO_OPERATOR_MULTIPLY}, + {"screen", CAIRO_OPERATOR_SCREEN}, + {"overlay", CAIRO_OPERATOR_OVERLAY}, + {"darken", CAIRO_OPERATOR_DARKEN}, + {"lighten", CAIRO_OPERATOR_LIGHTEN}, + {"color-dodge", CAIRO_OPERATOR_COLOR_DODGE}, + {"color-burn", CAIRO_OPERATOR_COLOR_BURN}, + {"hard-light", CAIRO_OPERATOR_HARD_LIGHT}, + {"soft-light", CAIRO_OPERATOR_SOFT_LIGHT}, + {"difference", CAIRO_OPERATOR_DIFFERENCE}, + {"exclusion", CAIRO_OPERATOR_EXCLUSION}, + {"hue", CAIRO_OPERATOR_HSL_HUE}, + {"saturation", CAIRO_OPERATOR_HSL_SATURATION}, + {"color", CAIRO_OPERATOR_HSL_COLOR}, + {"luminosity", CAIRO_OPERATOR_HSL_LUMINOSITY}, #endif - } else if (0 == strcmp("lighter", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_ADD); - } else { - cairo_set_operator(ctx, CAIRO_OPERATOR_OVER); - } + // non-standard: + {"saturate", CAIRO_OPERATOR_SATURATE} + }; + auto op = blendmodes.find(*opStr); + if (op != blendmodes.end()) cairo_set_operator(ctx, op->second); } /* diff --git a/test/fixtures/existing.png b/test/fixtures/existing.png new file mode 100644 index 0000000000000000000000000000000000000000..720a5cfec8c545e40347f80bfad6870516e48ea4 GIT binary patch literal 42514 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 z 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 Ef;|TAb^rhX literal 0 HcmV?d00001 diff --git a/test/public/tests.js b/test/public/tests.js index 5df792804..9441f0f3a 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -1184,207 +1184,51 @@ tests['font style variant weight size family'] = function (ctx) { ctx.fillText('normal normal normal 16px', 100, 100) } -tests['globalCompositeOperation source-over'] = function (ctx) { - ctx.fillStyle = 'blue' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'source-over' - ctx.fillStyle = 'red' - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} - -tests['globalCompositeOperation source-in'] = function (ctx) { - ctx.fillStyle = 'blue' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'source-in' - ctx.fillStyle = 'red' - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} - -tests['globalCompositeOperation source-out'] = function (ctx) { - ctx.fillStyle = 'blue' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'source-out' - ctx.fillStyle = 'red' - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} - -tests['globalCompositeOperation destination-in'] = function (ctx) { - ctx.fillStyle = 'blue' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'destination-in' - ctx.fillStyle = 'red' - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} - -tests['globalCompositeOperation source-atop'] = function (ctx) { - ctx.fillStyle = 'blue' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'source-atop' - ctx.fillStyle = 'red' - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} - -tests['globalCompositeOperation destination-out'] = function (ctx) { - ctx.fillStyle = 'blue' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'destination-out' - ctx.fillStyle = 'red' - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} - -tests['globalCompositeOperation destination-atop'] = function (ctx) { - ctx.fillStyle = 'blue' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'destination-atop' - ctx.fillStyle = 'red' - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} - -tests['globalCompositeOperation xor'] = function (ctx) { - ctx.fillStyle = 'blue' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'xor' - ctx.fillStyle = 'red' - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} - -tests['globalCompositeOperation copy'] = function (ctx) { - ctx.fillStyle = 'blue' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'copy' - ctx.fillStyle = 'red' - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} - -tests['globalCompositeOperation lighter'] = function (ctx) { - ctx.fillStyle = 'blue' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'lighter' - ctx.fillStyle = 'red' - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} - -tests['globalCompositeOperation darker'] = function (ctx) { - ctx.fillStyle = 'blue' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'darker' - ctx.fillStyle = 'red' - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} - -tests['globalCompositeOperation multiply'] = function (ctx) { - ctx.fillStyle = 'rgba(0,0,255,0.6)' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'multiply' - var grad = ctx.createRadialGradient(80, 80, 5, 60, 60, 60) - grad.addColorStop(0, 'yellow') - grad.addColorStop(0.2, 'red') - grad.addColorStop(1, 'black') - ctx.fillStyle = grad - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} - -tests['globalCompositeOperation screen'] = function (ctx) { - ctx.fillStyle = 'rgba(0,0,255,0.6)' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'screen' - var grad = ctx.createRadialGradient(80, 80, 5, 60, 60, 60) - grad.addColorStop(0, 'yellow') - grad.addColorStop(0.2, 'red') - grad.addColorStop(1, 'black') - ctx.fillStyle = grad - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} - -tests['globalCompositeOperation overlay'] = function (ctx) { - ctx.fillStyle = 'rgba(0,0,255,0.6)' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'overlay' - var grad = ctx.createRadialGradient(80, 80, 5, 60, 60, 60) - grad.addColorStop(0, 'yellow') - grad.addColorStop(0.2, 'red') - grad.addColorStop(1, 'black') - ctx.fillStyle = grad - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} - -tests['globalCompositeOperation hard-light'] = function (ctx) { - ctx.fillStyle = 'rgba(0,0,255,0.6)' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'hard-light' - var grad = ctx.createRadialGradient(80, 80, 5, 60, 60, 60) - grad.addColorStop(0, 'yellow') - grad.addColorStop(0.2, 'red') - grad.addColorStop(1, 'black') - ctx.fillStyle = grad - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} - -tests['globalCompositeOperation hsl-hue'] = function (ctx) { - ctx.fillStyle = 'rgba(0,0,255,0.6)' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'hsl-hue' - var grad = ctx.createRadialGradient(80, 80, 5, 60, 60, 60) - grad.addColorStop(0, 'yellow') - grad.addColorStop(0.2, 'red') - grad.addColorStop(1, 'black') - ctx.fillStyle = grad - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} - -tests['globalCompositeOperation hsl-saturation'] = function (ctx) { - ctx.fillStyle = 'rgba(0,0,255,0.6)' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'hsl-saturation' - var grad = ctx.createRadialGradient(80, 80, 5, 60, 60, 60) - grad.addColorStop(0, 'yellow') - grad.addColorStop(0.2, 'red') - grad.addColorStop(1, 'black') - ctx.fillStyle = grad - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} - -tests['globalCompositeOperation hsl-color'] = function (ctx) { - ctx.fillStyle = 'rgba(0,0,255,0.6)' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'hsl-color' - var grad = ctx.createRadialGradient(80, 80, 5, 60, 60, 60) - grad.addColorStop(0, 'yellow') - grad.addColorStop(0.2, 'red') - grad.addColorStop(1, 'black') - ctx.fillStyle = grad - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} +// From https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation +const gco = [ + 'source-over', 'source-in', 'source-out', 'source-atop', + 'destination-over', 'destination-in', 'destination-out', 'destination-atop', + 'lighter', 'copy', 'xor', 'multiply', 'screen', 'overlay', 'darken', + 'lighten', 'color-dodge', 'color-burn', 'hard-light', 'soft-light', + 'difference', 'exclusion', 'hue', 'saturation', 'color', 'luminosity' +] + +gco.forEach(op => { + tests['globalCompositeOperator ' + op] = function (ctx, done) { + var img1 = new Image() + var img2 = new Image() + img1.onload = function () { + img2.onload = function () { + ctx.drawImage(img1, 0, 0) + ctx.globalCompositeOperation = op + ctx.drawImage(img2, 0, 0) + done() + } + img2.src = imageSrc('newcontent.png') + } + img1.src = imageSrc('existing.png') + } +}) -tests['globalCompositeOperation hsl-luminosity'] = function (ctx) { - ctx.fillStyle = 'rgba(0,0,255,0.6)' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'hsl-luminosity' - var grad = ctx.createRadialGradient(80, 80, 5, 60, 60, 60) - grad.addColorStop(0, 'yellow') - grad.addColorStop(0.2, 'red') - grad.addColorStop(1, 'black') - ctx.fillStyle = grad - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() +tests['known bug #416'] = function (ctx, done) { + var img1 = new Image() + var img2 = new Image() + img1.onload = function () { + img2.onload = function () { + ctx.drawImage(img1, 0, 0) + ctx.globalCompositeOperation = 'destination-in' + ctx.save() + ctx.translate(img2.width / 2, img1.height / 2) + ctx.rotate(Math.PI / 4) + ctx.scale(0.5) + ctx.translate(-img2.width / 2, -img1.height / 2) + ctx.drawImage(img2, 0, 0) + ctx.restore() + done() + } + img2.src = imageSrc('newcontent.png') + } + img1.src = imageSrc('existing.png') } tests['shadowBlur'] = function (ctx) { From 1fc447dc96375baa5b3e70e46a46d6f2d85cff5f Mon Sep 17 00:00:00 2001 From: Sergey Ivanov1 Date: Wed, 18 Jul 2018 08:34:57 +0300 Subject: [PATCH 155/474] Replace call to inexistent error method with exception in case of error during SVG rasterization in Image class. --- CHANGELOG.md | 1 + src/Image.cc | 2 +- src/Image.h | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc761869f..0d5b7b312 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ canvas.createJPEGStream() // new on the incorrect types. ### Fixed + * Fix build with SVG support (#1205) * Prevent segfaults caused by loading invalid fonts (#1105) * Fix memory leak in font loading * Port has_lib.sh to javascript (#872) diff --git a/src/Image.cc b/src/Image.cc index 84c6f7e88..22e4e31f5 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -392,7 +392,7 @@ cairo_surface_t *Image::surface() { cairo_status_t status = renderSVGToSurface(); if (status != CAIRO_STATUS_SUCCESS) { g_object_unref(_rsvg); - error(Canvas::Error(status)); + Nan::ThrowError(Canvas::Error(status)); return NULL; } } diff --git a/src/Image.h b/src/Image.h index d2e7e086d..89006b485 100644 --- a/src/Image.h +++ b/src/Image.h @@ -86,7 +86,6 @@ class Image: public Nan::ObjectWrap { cairo_status_t assignDataAsMime(uint8_t *data, int len, const char *mime_type); #endif #endif - void error(Local error); void loaded(); cairo_status_t load(); Image(); From 01491e9b4cdff4c0fd3e93d6ae23ec305289aefc Mon Sep 17 00:00:00 2001 From: Sergey Ivanov1 Date: Wed, 18 Jul 2018 08:46:02 +0300 Subject: [PATCH 156/474] Copy librsvg with dependencies into destination folder on build for win platform. --- binding.gyp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/binding.gyp b/binding.gyp index 79ea2fdf2..c0f39f6d2 100644 --- a/binding.gyp +++ b/binding.gyp @@ -191,6 +191,17 @@ ], 'conditions': [ ['OS=="win"', { + 'copies': [{ + 'destination': '<(PRODUCT_DIR)', + 'files': [ + '<(GTK_Root)/bin/librsvg-2-2.dll', + '<(GTK_Root)/bin/libgdk_pixbuf-2.0-0.dll', + '<(GTK_Root)/bin/libgio-2.0-0.dll', + '<(GTK_Root)/bin/libcroco-0.6-3.dll', + '<(GTK_Root)/bin/libgsf-1-114.dll', + '<(GTK_Root)/bin/libxml2-2.dll' + ] + }], 'libraries': [ '-l<(GTK_Root)/lib/librsvg-2-2.lib' ] From 0b28d484704e325978d7bd54fd91bc26e75208f4 Mon Sep 17 00:00:00 2001 From: Sergey Ivanov1 Date: Wed, 18 Jul 2018 09:03:16 +0300 Subject: [PATCH 157/474] Update issue number in changelog. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d5b7b312..e3e369663 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,7 +52,7 @@ canvas.createJPEGStream() // new on the incorrect types. ### Fixed - * Fix build with SVG support (#1205) + * Fix build with SVG support enabled (#1123) * Prevent segfaults caused by loading invalid fonts (#1105) * Fix memory leak in font loading * Port has_lib.sh to javascript (#872) From 2dce2131cb80ff494ea7cc54f95a8d0df8cde458 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Wed, 18 Jul 2018 09:22:58 -0700 Subject: [PATCH 158/474] Add librsvg2-dev to CI env --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index d017940c0..e7e3fca5f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,7 @@ addons: - libjpeg8-dev - libpango1.0-dev - libgif-dev + - librsvg2-dev - g++-4.9 env: - CXX=g++-4.9 From 986868f5205e3624e2abb9bf7b4c55ccc1fcc56b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Thu, 2 Aug 2018 10:37:44 +0100 Subject: [PATCH 159/474] 2.0.0-alpha.13 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dc17b88f4..54b9388f0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.0.0-alpha.12", + "version": "2.0.0-alpha.13", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 4dcf8a0bf295f9f3b79137211a0628680c616b8e Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Mon, 16 Jul 2018 22:50:53 -0700 Subject: [PATCH 160/474] Better error handling in Image * Provide coded errors (e.g. ENOENT, EACCESS) instead of generic "error while reading from input stream". * Capture libjpeg errors instead of printing to stderr/stdout Closes #1048 (most likely cause) Fixes #283 --- CHANGELOG.md | 3 ++ src/CanvasError.h | 26 ++++++++++++++ src/Image.cc | 67 ++++++++++++++++++++++++++++------- src/Image.h | 2 ++ test/fixtures/159-crash1.jpg | Bin 0 -> 8192 bytes test/image.test.js | 20 +++++++++++ 6 files changed, 106 insertions(+), 12 deletions(-) create mode 100644 src/CanvasError.h create mode 100644 test/fixtures/159-crash1.jpg diff --git a/CHANGELOG.md b/CHANGELOG.md index e3e369663..ab7d3baa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,9 @@ canvas.createJPEGStream() // new * "dest" is now "destination" * "add" is removed (but is the same as "lighter") * "source" is now "copy" + * Provide better, Node.js core-style coded errors for failed sys calls. (For + example, provide an error with code 'ENOENT' if setting `img.src` to a path + that does not exist.) ### Added * Prebuilds (#992) with different libc versions to the prebuilt binary (#1140) diff --git a/src/CanvasError.h b/src/CanvasError.h new file mode 100644 index 000000000..802fcad02 --- /dev/null +++ b/src/CanvasError.h @@ -0,0 +1,26 @@ +#ifndef __CANVAS_ERROR_H__ +#define __CANVAS_ERROR_H__ + +#include + +class CanvasError { + public: + std::string message; + std::string syscall; + std::string path; + int cerrno = 0; + void set(char *iMessage = NULL, char *iSyscall = NULL, int iErrno = 0, char *iPath = NULL) { + if (iMessage) message = std::string(iMessage); + if (iSyscall) syscall = std::string(iSyscall); + cerrno = iErrno; + if (iPath) path = std::string(iPath); + } + void reset() { + message.clear(); + syscall.clear(); + path.clear(); + cerrno = 0; + } +}; + +#endif diff --git a/src/Image.cc b/src/Image.cc index 22e4e31f5..b6175bd0a 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -25,8 +25,8 @@ typedef struct { #include struct canvas_jpeg_error_mgr: jpeg_error_mgr { - private: char unused[8]; - public: jmp_buf setjmp_buffer; + Image* image; + jmp_buf setjmp_buffer; }; #endif @@ -227,6 +227,8 @@ NAN_SETTER(Image::SetSource) { cairo_status_t status = CAIRO_STATUS_READ_ERROR; img->clearData(); + // Clear errno in case some unrelated previous syscall failed + errno = 0; // url string if (value->IsString()) { @@ -241,11 +243,18 @@ NAN_SETTER(Image::SetSource) { status = img->loadFromBuffer(buf, len); } - // check status if (status) { Local onerrorFn = info.This()->Get(Nan::New("onerror").ToLocalChecked()); if (onerrorFn->IsFunction()) { - Local argv[1] = { Canvas::Error(status) }; + Local argv[1]; + CanvasError errorInfo = img->errorInfo; + if (errorInfo.cerrno) { + argv[0] = Nan::ErrnoException(errorInfo.cerrno, errorInfo.syscall.c_str(), errorInfo.message.c_str(), errorInfo.path.c_str()); + } else if (!errorInfo.message.empty()) { + argv[0] = Nan::Error(Nan::New(errorInfo.message).ToLocalChecked()); + } else { + argv[0] = Nan::Error(Nan::New(cairo_status_to_string(status)).ToLocalChecked()); + } onerrorFn.As()->Call(Isolate::GetCurrent()->GetCurrentContext()->Global(), 1, argv); } } else { @@ -410,13 +419,16 @@ cairo_surface_t *Image::surface() { cairo_status_t Image::loadSurface() { FILE *stream = fopen(filename, "rb"); - if (!stream) return CAIRO_STATUS_READ_ERROR; + if (!stream) { + this->errorInfo.set(NULL, "fopen", errno, filename); + return CAIRO_STATUS_READ_ERROR; + } uint8_t buf[5]; if (1 != fread(&buf, 5, 1, stream)) { fclose(stream); return CAIRO_STATUS_READ_ERROR; } - fseek(stream, 0, SEEK_SET); + rewind(stream); // png if (isPNG(buf)) { @@ -448,11 +460,13 @@ Image::loadSurface() { fclose(stream); return CAIRO_STATUS_READ_ERROR; } - fseek(stream, 0, SEEK_SET); + rewind(stream); if (isSVG(head, head_len)) return loadSVG(stream); #endif fclose(stream); + + this->errorInfo.set("Unsupported image type"); return CAIRO_STATUS_READ_ERROR; } @@ -518,6 +532,7 @@ Image::loadGIF(FILE *stream) { if (!buf) { fclose(stream); + this->errorInfo.set(NULL, "malloc", errno); return CAIRO_STATUS_NO_MEMORY; } @@ -570,6 +585,7 @@ Image::loadGIFFromBuffer(uint8_t *buf, unsigned len) { uint8_t *data = (uint8_t *) malloc(naturalWidth * naturalHeight * 4); if (!data) { GIF_CLOSE_FILE(gif); + this->errorInfo.set(NULL, "malloc", errno); return CAIRO_STATUS_NO_MEMORY; } @@ -744,6 +760,7 @@ Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args) { if (!data) { jpeg_abort_decompress(args); jpeg_destroy_decompress(args); + this->errorInfo.set(NULL, "malloc", errno); return CAIRO_STATUS_NO_MEMORY; } @@ -752,6 +769,7 @@ Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args) { free(data); jpeg_abort_decompress(args); jpeg_destroy_decompress(args); + this->errorInfo.set(NULL, "malloc", errno); return CAIRO_STATUS_NO_MEMORY; } @@ -804,13 +822,22 @@ Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args) { * Callback to recover from jpeg errors */ -METHODDEF(void) canvas_jpeg_error_exit (j_common_ptr cinfo) { +static void canvas_jpeg_error_exit(j_common_ptr cinfo) { canvas_jpeg_error_mgr *cjerr = static_cast(cinfo->err); - + cjerr->output_message(cinfo); // Return control to the setjmp point longjmp(cjerr->setjmp_buffer, 1); } +// Capture libjpeg errors instead of writing stdout +static void canvas_jpeg_output_message(j_common_ptr cinfo) { + canvas_jpeg_error_mgr *cjerr = static_cast(cinfo->err); + char buff[JMSG_LENGTH_MAX]; + cjerr->format_message(cinfo, buff); + // (Only the last message will be returned to JS land.) + cjerr->image->errorInfo.set(buff); +} + #if CAIRO_VERSION_MINOR >= 10 /* @@ -825,8 +852,10 @@ Image::decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len) { struct jpeg_decompress_struct args; struct canvas_jpeg_error_mgr err; + err.image = this; args.err = jpeg_std_error(&err); args.err->error_exit = canvas_jpeg_error_exit; + args.err->output_message = canvas_jpeg_output_message; // Establish the setjmp return context for canvas_jpeg_error_exit to use if (setjmp(err.setjmp_buffer)) { @@ -849,7 +878,10 @@ Image::decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len) { // 8 pixels per byte using Alpha Channel format to reduce memory requirement. int buf_size = naturalHeight * cairo_format_stride_for_width(CAIRO_FORMAT_A1, naturalWidth); uint8_t *data = (uint8_t *) malloc(buf_size); - if (!data) return CAIRO_STATUS_NO_MEMORY; + if (!data) { + this->errorInfo.set(NULL, "malloc", errno); + return CAIRO_STATUS_NO_MEMORY; + } // New image surface _surface = cairo_image_surface_create_for_data( @@ -895,11 +927,15 @@ clearMimeData(void *closure) { 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; + if (!mime_data) { + this->errorInfo.set(NULL, "malloc", errno); + return CAIRO_STATUS_NO_MEMORY; + } read_closure_t *mime_closure = (read_closure_t *) malloc(sizeof(read_closure_t)); if (!mime_closure) { free(mime_data); + this->errorInfo.set(NULL, "malloc", errno); return CAIRO_STATUS_NO_MEMORY; } @@ -931,8 +967,10 @@ Image::loadJPEGFromBuffer(uint8_t *buf, unsigned len) { struct jpeg_decompress_struct args; struct canvas_jpeg_error_mgr err; + err.image = this; args.err = jpeg_std_error(&err); args.err->error_exit = canvas_jpeg_error_exit; + args.err->output_message = canvas_jpeg_output_message; // Establish the setjmp return context for canvas_jpeg_error_exit to use if (setjmp(err.setjmp_buffer)) { @@ -971,8 +1009,10 @@ Image::loadJPEG(FILE *stream) { struct jpeg_decompress_struct args; struct canvas_jpeg_error_mgr err; + err.image = this; args.err = jpeg_std_error(&err); args.err->error_exit = canvas_jpeg_error_exit; + args.err->output_message = canvas_jpeg_output_message; // Establish the setjmp return context for canvas_jpeg_error_exit to use if (setjmp(err.setjmp_buffer)) { @@ -1003,7 +1043,10 @@ Image::loadJPEG(FILE *stream) { fseek(stream, 0, SEEK_SET); buf = (uint8_t *) malloc(len); - if (!buf) return CAIRO_STATUS_NO_MEMORY; + if (!buf) { + this->errorInfo.set(NULL, "malloc", errno); + return CAIRO_STATUS_NO_MEMORY; + } if (fread(buf, len, 1, stream) != 1) { status = CAIRO_STATUS_READ_ERROR; diff --git a/src/Image.h b/src/Image.h index 89006b485..2dcbd4759 100644 --- a/src/Image.h +++ b/src/Image.h @@ -9,6 +9,7 @@ #define __NODE_IMAGE_H__ #include "Canvas.h" +#include "CanvasError.h" #ifdef HAVE_JPEG #include @@ -86,6 +87,7 @@ class Image: public Nan::ObjectWrap { cairo_status_t assignDataAsMime(uint8_t *data, int len, const char *mime_type); #endif #endif + CanvasError errorInfo; void loaded(); cairo_status_t load(); Image(); diff --git a/test/fixtures/159-crash1.jpg b/test/fixtures/159-crash1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0f3bbd0a7b88690ec03b0623509533dd4a86bc33 GIT binary patch literal 8192 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 pr+ { + assert.equal(err.code, 'ENOENT') + assert.equal(err.path, 'path/to/nothing') + assert.equal(err.syscall, 'fopen') + done() + } + img.src = 'path/to/nothing' + }) + + it('captures errors from libjpeg', function (done) { + const img = new Image() + img.onerror = err => { + assert.equal(err.message, "JPEG datastream contains no image") + done() + } + img.src = `${__dirname}/fixtures/159-crash1.jpg` + }) + it('calls Image#onerror multiple times', function () { return loadImage(png_clock).then((img) => { let onloadCalled = 0 From c141976bbe553e3160323d402276865b58b68a51 Mon Sep 17 00:00:00 2001 From: "Yuya.Nishida" Date: Tue, 31 Jul 2018 16:34:41 +0900 Subject: [PATCH 161/474] support reading data URL on Canvas.Image#src. --- CHANGELOG.md | 1 + lib/image.js | 10 +++++++--- test/image.test.js | 30 ++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab7d3baa8..f9aec12af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -110,6 +110,7 @@ canvas.createJPEGStream() // new `canvas.createPNGStream()` * Support for `canvas.toDataURI("image/jpeg")` (sync) * Support for `img.src = ` to match browsers + * Support reading data URL on `img.src` 1.6.x (unreleased) ================== diff --git a/lib/image.js b/lib/image.js index 7e79666ec..57d6fde23 100644 --- a/lib/image.js +++ b/lib/image.js @@ -31,8 +31,9 @@ Object.defineProperty(Image.prototype, 'src', { const commaI = val.indexOf(',') // 'base64' must come before the comma const isBase64 = val.lastIndexOf('base64', commaI) - val = val.slice(commaI + 1) - this.source = Buffer.from(val, isBase64 ? 'base64' : 'utf8') + const content = val.slice(commaI + 1) + this.source = Buffer.from(content, isBase64 ? 'base64' : 'utf8') + this.originalSource = val } else if (/^\s*http/.test(val)) { // remote URL const onerror = err => { if (typeof this.onerror === 'function') { @@ -49,19 +50,22 @@ Object.defineProperty(Image.prototype, 'src', { res.on('data', buffer => buffers.push(buffer)) res.on('end', () => { this.source = Buffer.concat(buffers) + this.originalSource = undefined }) }).on('error', onerror) } else { // local file path assumed this.source = val + this.originalSource = undefined } } else if (Buffer.isBuffer(val)) { this.source = val + this.originalSource = undefined } }, get() { // TODO https://github.com/Automattic/node-canvas/issues/118 - return this.source; + return this.originalSource || this.source; } }); diff --git a/test/image.test.js b/test/image.test.js index 5a35fad6e..bac0d6605 100644 --- a/test/image.test.js +++ b/test/image.test.js @@ -39,6 +39,21 @@ describe('Image', function () { }) }) + it('loads JPEG data URL', function () { + const base64Encoded = fs.readFileSync(jpg_face, 'base64') + const dataURL = `data:image/png;base64,${base64Encoded}` + + return loadImage(dataURL).then((img) => { + assert.strictEqual(img.onerror, null) + assert.strictEqual(img.onload, null) + + assert.strictEqual(img.src, dataURL) + assert.strictEqual(img.width, 485) + assert.strictEqual(img.height, 401) + assert.strictEqual(img.complete, true) + }) + }) + it('loads PNG image', function () { return loadImage(png_clock).then((img) => { assert.strictEqual(img.onerror, null) @@ -51,6 +66,21 @@ describe('Image', function () { }) }) + it('loads PNG data URL', function () { + const base64Encoded = fs.readFileSync(png_clock, 'base64') + const dataURL = `data:image/png;base64,${base64Encoded}` + + return loadImage(dataURL).then((img) => { + assert.strictEqual(img.onerror, null) + assert.strictEqual(img.onload, null) + + assert.strictEqual(img.src, dataURL) + assert.strictEqual(img.width, 320) + assert.strictEqual(img.height, 320) + assert.strictEqual(img.complete, true) + }) + }) + it('calls Image#onload multiple times', function () { return loadImage(png_clock).then((img) => { let onloadCalled = 0 From 867bac39d0548324c8b51382c0c1eab2a1063b73 Mon Sep 17 00:00:00 2001 From: "Yuya.Nishida" Date: Fri, 3 Aug 2018 17:45:28 +0900 Subject: [PATCH 162/474] Fix private property name. run: git cococo sed -i -e s/originalSource/_originalSource/g lib/image.js --- lib/image.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/image.js b/lib/image.js index 57d6fde23..830075198 100644 --- a/lib/image.js +++ b/lib/image.js @@ -33,7 +33,7 @@ Object.defineProperty(Image.prototype, 'src', { const isBase64 = val.lastIndexOf('base64', commaI) const content = val.slice(commaI + 1) this.source = Buffer.from(content, isBase64 ? 'base64' : 'utf8') - this.originalSource = val + this._originalSource = val } else if (/^\s*http/.test(val)) { // remote URL const onerror = err => { if (typeof this.onerror === 'function') { @@ -50,22 +50,22 @@ Object.defineProperty(Image.prototype, 'src', { res.on('data', buffer => buffers.push(buffer)) res.on('end', () => { this.source = Buffer.concat(buffers) - this.originalSource = undefined + this._originalSource = undefined }) }).on('error', onerror) } else { // local file path assumed this.source = val - this.originalSource = undefined + this._originalSource = undefined } } else if (Buffer.isBuffer(val)) { this.source = val - this.originalSource = undefined + this._originalSource = undefined } }, get() { // TODO https://github.com/Automattic/node-canvas/issues/118 - return this.originalSource || this.source; + return this._originalSource || this.source; } }); From be1cfe95ce13fdc87c4df9249d86091d6455d651 Mon Sep 17 00:00:00 2001 From: lostfictions Date: Fri, 17 Aug 2018 22:36:51 -0400 Subject: [PATCH 163/474] support https urls, better protocol check regex --- lib/image.js | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/lib/image.js b/lib/image.js index 830075198..faa2a33cd 100644 --- a/lib/image.js +++ b/lib/image.js @@ -13,6 +13,7 @@ const bindings = require('./bindings') const Image = module.exports = bindings.Image const http = require("http") +const https = require("https") Object.defineProperty(Image.prototype, 'src', { /** @@ -34,17 +35,22 @@ Object.defineProperty(Image.prototype, 'src', { const content = val.slice(commaI + 1) this.source = Buffer.from(content, isBase64 ? 'base64' : 'utf8') this._originalSource = val - } else if (/^\s*http/.test(val)) { // remote URL - const onerror = err => { - if (typeof this.onerror === 'function') { - this.onerror(err) - } else { - throw err + } else if (/^\s*https:\/\//.test(val)) { // https URL + https.get(val, res => { + if (res.statusCode !== 200) { + return this.onHttpError(new Error(`Server responded with ${res.statusCode}`)) } - } + const buffers = [] + res.on('data', buffer => buffers.push(buffer)) + res.on('end', () => { + this.source = Buffer.concat(buffers) + this._originalSource = undefined + }) + }).on('error', this.onHttpError) + } else if (/^\s*http:\/\//.test(val)) { // http URL http.get(val, res => { if (res.statusCode !== 200) { - return onerror(new Error(`Server responded with ${res.statusCode}`)) + return this.onHttpError(new Error(`Server responded with ${res.statusCode}`)) } const buffers = [] res.on('data', buffer => buffers.push(buffer)) @@ -52,7 +58,7 @@ Object.defineProperty(Image.prototype, 'src', { this.source = Buffer.concat(buffers) this._originalSource = undefined }) - }).on('error', onerror) + }).on('error', this.onHttpError) } else { // local file path assumed this.source = val this._originalSource = undefined @@ -62,7 +68,13 @@ Object.defineProperty(Image.prototype, 'src', { this._originalSource = undefined } }, - + onHttpError: err => { + if (typeof this.onerror === 'function') { + this.onerror(err) + } else { + throw err + } + }, get() { // TODO https://github.com/Automattic/node-canvas/issues/118 return this._originalSource || this.source; From c5698c8495bbd4a861d2a628251ed50d7bd51c03 Mon Sep 17 00:00:00 2001 From: lostfictions Date: Sat, 18 Aug 2018 04:58:35 -0400 Subject: [PATCH 164/474] reduce duplication, define onHttpError on proto --- lib/image.js | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/lib/image.js b/lib/image.js index faa2a33cd..893b71398 100644 --- a/lib/image.js +++ b/lib/image.js @@ -35,8 +35,9 @@ Object.defineProperty(Image.prototype, 'src', { const content = val.slice(commaI + 1) this.source = Buffer.from(content, isBase64 ? 'base64' : 'utf8') this._originalSource = val - } else if (/^\s*https:\/\//.test(val)) { // https URL - https.get(val, res => { + } else if (/^\s*https?:\/\//.test(val)) { // remote URL + const type = /^\s*https:\/\//.test(val) ? https : http + type.get(val, res => { if (res.statusCode !== 200) { return this.onHttpError(new Error(`Server responded with ${res.statusCode}`)) } @@ -46,19 +47,7 @@ Object.defineProperty(Image.prototype, 'src', { this.source = Buffer.concat(buffers) this._originalSource = undefined }) - }).on('error', this.onHttpError) - } else if (/^\s*http:\/\//.test(val)) { // http URL - http.get(val, res => { - if (res.statusCode !== 200) { - return this.onHttpError(new Error(`Server responded with ${res.statusCode}`)) - } - const buffers = [] - res.on('data', buffer => buffers.push(buffer)) - res.on('end', () => { - this.source = Buffer.concat(buffers) - this._originalSource = undefined - }) - }).on('error', this.onHttpError) + }).on('error', this.onHttpError.bind(this)) } else { // local file path assumed this.source = val this._originalSource = undefined @@ -68,13 +57,6 @@ Object.defineProperty(Image.prototype, 'src', { this._originalSource = undefined } }, - onHttpError: err => { - if (typeof this.onerror === 'function') { - this.onerror(err) - } else { - throw err - } - }, get() { // TODO https://github.com/Automattic/node-canvas/issues/118 return this._originalSource || this.source; @@ -97,3 +79,11 @@ Image.prototype.inspect = function(){ + (this.complete ? ' complete' : '') + ']'; }; + +Image.prototype.onHttpError = function(err) { + if (typeof this.onerror === 'function') { + this.onerror(err) + } else { + throw err + } +}; From c42e73bbd476e6a7337bb0518bb342db085b51a1 Mon Sep 17 00:00:00 2001 From: lostfictions Date: Sat, 18 Aug 2018 16:45:54 -0400 Subject: [PATCH 165/474] restore original onerror code --- lib/image.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/image.js b/lib/image.js index 893b71398..8f0ae075f 100644 --- a/lib/image.js +++ b/lib/image.js @@ -36,10 +36,18 @@ Object.defineProperty(Image.prototype, 'src', { this.source = Buffer.from(content, isBase64 ? 'base64' : 'utf8') this._originalSource = val } else if (/^\s*https?:\/\//.test(val)) { // remote URL + const onerror = err => { + if (typeof this.onerror === 'function') { + this.onerror(err) + } else { + throw err + } + } + const type = /^\s*https:\/\//.test(val) ? https : http type.get(val, res => { if (res.statusCode !== 200) { - return this.onHttpError(new Error(`Server responded with ${res.statusCode}`)) + return onerror(new Error(`Server responded with ${res.statusCode}`)) } const buffers = [] res.on('data', buffer => buffers.push(buffer)) @@ -47,7 +55,7 @@ Object.defineProperty(Image.prototype, 'src', { this.source = Buffer.concat(buffers) this._originalSource = undefined }) - }).on('error', this.onHttpError.bind(this)) + }).on('error', onerror) } else { // local file path assumed this.source = val this._originalSource = undefined @@ -57,6 +65,7 @@ Object.defineProperty(Image.prototype, 'src', { this._originalSource = undefined } }, + get() { // TODO https://github.com/Automattic/node-canvas/issues/118 return this._originalSource || this.source; @@ -79,11 +88,3 @@ Image.prototype.inspect = function(){ + (this.complete ? ' complete' : '') + ']'; }; - -Image.prototype.onHttpError = function(err) { - if (typeof this.onerror === 'function') { - this.onerror(err) - } else { - throw err - } -}; From 5e333383b8e5bc48893a38561dbb85c6299cfd81 Mon Sep 17 00:00:00 2001 From: Andrey Azov Date: Thu, 23 Aug 2018 03:18:56 +0300 Subject: [PATCH 166/474] Add 'configurable: true' option to the descriptor of Image.src property --- lib/image.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/image.js b/lib/image.js index 8f0ae075f..f373d849c 100644 --- a/lib/image.js +++ b/lib/image.js @@ -69,7 +69,9 @@ Object.defineProperty(Image.prototype, 'src', { get() { // TODO https://github.com/Automattic/node-canvas/issues/118 return this._originalSource || this.source; - } + }, + + configurable: true }); /** From 7526664bccfba973f9c9e7f8cdd2e17a9f46a8c9 Mon Sep 17 00:00:00 2001 From: Devin Leaman Date: Thu, 30 Aug 2018 03:57:28 -0500 Subject: [PATCH 167/474] Updated README - Fixed the link to the Windows installation instructions. --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 7469fb558..7af064dea 100644 --- a/Readme.md +++ b/Readme.md @@ -47,7 +47,7 @@ OS X | Using [Homebrew](https://brew.sh/):
`brew install pkg-config cairo pa Ubuntu | `sudo apt-get install libcairo2-dev libjpeg-dev libpango1.0-dev libgif-dev build-essential g++` Fedora | `sudo yum install cairo cairo-devel cairomm-devel libjpeg-turbo-devel pango pango-devel pangomm pangomm-devel giflib-devel` Solaris | `pkgin install cairo pango pkg-config xproto renderproto kbproto xextproto` -Windows | [Instructions on our wiki](https://github.com/Automattic/node-canvas/wiki/Installation---Windows) +Windows | [Instructions on our wiki](https://github.com/Automattic/node-canvas/wiki/Installation:-Windows) **Mac OS X v10.11+:** If you have recently updated to Mac OS X v10.11+ and are experiencing trouble when compiling, run the following command: `xcode-select --install`. Read more about the problem [on Stack Overflow](http://stackoverflow.com/a/32929012/148072). From 92163e0883c46b8857a808bf8dc48668a9022477 Mon Sep 17 00:00:00 2001 From: Delan Azabani Date: Sat, 1 Sep 2018 21:00:24 +1000 Subject: [PATCH 168/474] Readme.md: add dependencies command for OpenBSD --- CHANGELOG.md | 1 + Readme.md | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9aec12af..f741b3a6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -111,6 +111,7 @@ canvas.createJPEGStream() // new * Support for `canvas.toDataURI("image/jpeg")` (sync) * Support for `img.src = ` to match browsers * Support reading data URL on `img.src` + * Readme: add dependencies command for OpenBSD 1.6.x (unreleased) ================== diff --git a/Readme.md b/Readme.md index 7af064dea..8957c2f60 100644 --- a/Readme.md +++ b/Readme.md @@ -47,6 +47,7 @@ OS X | Using [Homebrew](https://brew.sh/):
`brew install pkg-config cairo pa Ubuntu | `sudo apt-get install libcairo2-dev libjpeg-dev libpango1.0-dev libgif-dev build-essential g++` Fedora | `sudo yum install cairo cairo-devel cairomm-devel libjpeg-turbo-devel pango pango-devel pangomm pangomm-devel giflib-devel` Solaris | `pkgin install cairo pango pkg-config xproto renderproto kbproto xextproto` +OpenBSD | `doas pkg_add cairo pango png jpeg giflib` Windows | [Instructions on our wiki](https://github.com/Automattic/node-canvas/wiki/Installation:-Windows) **Mac OS X v10.11+:** If you have recently updated to Mac OS X v10.11+ and are experiencing trouble when compiling, run the following command: `xcode-select --install`. Read more about the problem [on Stack Overflow](http://stackoverflow.com/a/32929012/148072). From 0066986b0d5e864a3bbbd3111ec9e41bd40ba85c Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Mon, 3 Sep 2018 03:38:32 +0200 Subject: [PATCH 169/474] Make imagesmoothing enabled behave more like browser (#1233) * first draft * add unit test * add unit test --- lib/context2d.js | 21 -------------- src/CanvasRenderingContext2d.cc | 26 +++++++++++++++-- src/CanvasRenderingContext2d.h | 3 ++ test/canvas.test.js | 51 ++++++++++++++++++++------------- 4 files changed, 57 insertions(+), 44 deletions(-) diff --git a/lib/context2d.js b/lib/context2d.js index 3b9f37b36..b9c28e6ea 100644 --- a/lib/context2d.js +++ b/lib/context2d.js @@ -24,27 +24,6 @@ const DOMMatrix = require('./DOMMatrix').DOMMatrix var baselines = ['alphabetic', 'top', 'bottom', 'middle', 'ideographic', 'hanging']; -/** - * Enable or disable image smoothing. - * - * @api public - */ - -Context2d.prototype.__defineSetter__('imageSmoothingEnabled', function(val){ - this._imageSmoothing = !! val; - this.patternQuality = val ? 'best' : 'fast'; -}); - -/** - * Get image smoothing value. - * - * @api public - */ - -Context2d.prototype.__defineGetter__('imageSmoothingEnabled', function(val){ - return !! this._imageSmoothing; -}); - /** * Create a pattern from `Image` or `Canvas`. * diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index ed7d3ce6d..38d3cf2ed 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -155,6 +155,7 @@ Context2d::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { Nan::SetPrototypeMethod(ctor, "_getMatrix", GetMatrix); SetProtoAccessor(proto, Nan::New("pixelFormat").ToLocalChecked(), GetFormat, NULL, ctor); SetProtoAccessor(proto, Nan::New("patternQuality").ToLocalChecked(), GetPatternQuality, SetPatternQuality, ctor); + SetProtoAccessor(proto, Nan::New("imageSmoothingEnabled").ToLocalChecked(), GetImageSmoothingEnabled, SetImageSmoothingEnabled, ctor); SetProtoAccessor(proto, Nan::New("globalCompositeOperation").ToLocalChecked(), GetGlobalCompositeOperation, SetGlobalCompositeOperation, ctor); SetProtoAccessor(proto, Nan::New("globalAlpha").ToLocalChecked(), GetGlobalAlpha, SetGlobalAlpha, ctor); SetProtoAccessor(proto, Nan::New("shadowColor").ToLocalChecked(), GetShadowColor, SetShadowColor, ctor); @@ -196,6 +197,7 @@ Context2d::Context2d(Canvas *canvas) { state->stroke = transparent; state->shadow = transparent_black; state->patternQuality = CAIRO_FILTER_GOOD; + state->imageSmoothingEnabled = true; state->textDrawingMode = TEXT_DRAW_PATHS; state->fontDescription = pango_font_description_from_string("sans serif"); pango_font_description_set_absolute_size(state->fontDescription, 10 * PANGO_SCALE); @@ -1233,7 +1235,7 @@ NAN_METHOD(Context2d::DrawImage) { ctxTemp = cairo_create(surfTemp); cairo_set_source_surface(ctxTemp, surface, 0, 0); - cairo_pattern_set_filter(cairo_get_source(ctxTemp), context->state->patternQuality); + cairo_pattern_set_filter(cairo_get_source(ctxTemp), context->state->imageSmoothingEnabled ? context->state->patternQuality : CAIRO_FILTER_NEAREST); cairo_pattern_set_extend(cairo_get_source(ctxTemp), CAIRO_EXTEND_REFLECT); cairo_paint_with_alpha(ctxTemp, 1); @@ -1247,7 +1249,7 @@ NAN_METHOD(Context2d::DrawImage) { // Paint cairo_set_source_surface(ctx, surface, dx - sx, dy - sy); - cairo_pattern_set_filter(cairo_get_source(ctx), context->state->patternQuality); + cairo_pattern_set_filter(cairo_get_source(ctx), context->state->imageSmoothingEnabled ? context->state->patternQuality : CAIRO_FILTER_NEAREST); cairo_pattern_set_extend(cairo_get_source(ctx), CAIRO_EXTEND_REFLECT); cairo_paint_with_alpha(ctx, context->state->globalAlpha); @@ -1369,6 +1371,24 @@ NAN_GETTER(Context2d::GetPatternQuality) { info.GetReturnValue().Set(Nan::New(quality).ToLocalChecked()); } +/* + * Set ImageSmoothingEnabled value. + */ + +NAN_SETTER(Context2d::SetImageSmoothingEnabled) { + Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); + context->state->imageSmoothingEnabled = value->BooleanValue(); +} + +/* + * Get pattern quality. + */ + +NAN_GETTER(Context2d::GetImageSmoothingEnabled) { + Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); + info.GetReturnValue().Set(Nan::New(context->state->imageSmoothingEnabled)); +} + /* * Set global composite operation. */ @@ -1783,7 +1803,7 @@ NAN_METHOD(Context2d::SetFillColor) { if (!info[0]->IsString()) return; Nan::Utf8String str(info[0]); - + uint32_t rgba = rgba_from_string(*str, &ok); if (!ok) return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index 9a5052c5d..adb9661ad 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -46,6 +46,7 @@ typedef struct { double shadowOffsetY; canvas_draw_mode_t textDrawingMode; PangoFontDescription *fontDescription; + bool imageSmoothingEnabled; } canvas_state_t; /* @@ -116,6 +117,7 @@ class Context2d: public Nan::ObjectWrap { static NAN_METHOD(GetMatrix); static NAN_GETTER(GetFormat); static NAN_GETTER(GetPatternQuality); + static NAN_GETTER(GetImageSmoothingEnabled); static NAN_GETTER(GetGlobalCompositeOperation); static NAN_GETTER(GetGlobalAlpha); static NAN_GETTER(GetShadowColor); @@ -133,6 +135,7 @@ class Context2d: public Nan::ObjectWrap { static NAN_GETTER(GetTextDrawingMode); static NAN_GETTER(GetFilter); static NAN_SETTER(SetPatternQuality); + static NAN_SETTER(SetImageSmoothingEnabled); static NAN_SETTER(SetGlobalCompositeOperation); static NAN_SETTER(SetGlobalAlpha); static NAN_SETTER(SetShadowColor); diff --git a/test/canvas.test.js b/test/canvas.test.js index 8dfbb9926..77e012648 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -388,6 +388,17 @@ describe('Canvas', function () { assert.equal('best', ctx.patternQuality); }); + it('Context2d#imageSmoothingEnabled', function () { + var canvas = createCanvas(200, 200) + , ctx = canvas.getContext('2d'); + + assert.equal(true, ctx.imageSmoothingEnabled); + ctx.imageSmoothingEnabled = false; + assert.equal('good', ctx.patternQuality); + assert.equal(false, ctx.imageSmoothingEnabled); + assert.equal('good', ctx.patternQuality); + }); + it('Context2d#font=', function () { var canvas = createCanvas(200, 200) , ctx = canvas.getContext('2d'); @@ -496,7 +507,7 @@ describe('Canvas', function () { var buf = createCanvas(200,200).toBuffer(); assert.equal('PNG', buf.slice(1,4).toString()); }); - + it('Canvas#toBuffer("image/png")', function () { var buf = createCanvas(200,200).toBuffer('image/png'); assert.equal('PNG', buf.slice(1,4).toString()); @@ -521,7 +532,7 @@ describe('Canvas', function () { } } }) - + it('Canvas#toBuffer("image/png", {compressionLevel: 5})', function () { var buf = createCanvas(200,200).toBuffer('image/png', {compressionLevel: 5}); assert.equal('PNG', buf.slice(1,4).toString()); @@ -576,21 +587,21 @@ describe('Canvas', function () { describe('#toBuffer("raw")', function() { var canvas = createCanvas(11, 10) , ctx = canvas.getContext('2d'); - + ctx.clearRect(0, 0, 11, 10); - + ctx.fillStyle = 'rgba(200, 200, 200, 0.505)'; ctx.fillRect(0, 0, 5, 5); - + ctx.fillStyle = 'red'; ctx.fillRect(5, 0, 5, 5); - + ctx.fillStyle = '#00ff00'; ctx.fillRect(0, 5, 5, 5); - + ctx.fillStyle = 'black'; ctx.fillRect(5, 5, 4, 5); - + /** Output: * *****RRRRR- * *****RRRRR- @@ -603,52 +614,52 @@ describe('Canvas', function () { * GGGGGBBBB-- * GGGGGBBBB-- */ - + var buf = canvas.toBuffer('raw'); var stride = canvas.stride; - + var endianness = os.endianness(); - + function assertPixel(u32, x, y, message) { var expected = '0x' + u32.toString(16); - + // Buffer doesn't have readUInt32(): it only has readUInt32LE() and // readUInt32BE(). var px = buf['readUInt32' + endianness](y * stride + x * 4); var actual = '0x' + px.toString(16); - + assert.equal(actual, expected, message); } - + it('should have the correct size', function() { assert.equal(buf.length, stride * 10); }); - + it('does not premultiply alpha', function() { assertPixel(0x80646464, 0, 0, 'first semitransparent pixel'); assertPixel(0x80646464, 4, 4, 'last semitransparent pixel'); }); - + it('draws red', function() { assertPixel(0xffff0000, 5, 0, 'first red pixel'); assertPixel(0xffff0000, 9, 4, 'last red pixel'); }); - + it('draws green', function() { assertPixel(0xff00ff00, 0, 5, 'first green pixel'); assertPixel(0xff00ff00, 4, 9, 'last green pixel'); }); - + it('draws black', function() { assertPixel(0xff000000, 5, 5, 'first black pixel'); assertPixel(0xff000000, 8, 9, 'last black pixel'); }); - + it('leaves undrawn pixels black, transparent', function() { assertPixel(0x0, 9, 5, 'first undrawn pixel'); assertPixel(0x0, 9, 9, 'last undrawn pixel'); }); - + it('is immutable', function() { ctx.fillStyle = 'white'; ctx.fillRect(0, 0, 10, 10); From 8ffaacca3214d838b357d6c07c79ae3ac1111f1e Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Mon, 3 Sep 2018 20:11:00 +0200 Subject: [PATCH 170/474] some code reuse (#1237) --- src/CanvasRenderingContext2d.cc | 52 ++++++++++----------------------- 1 file changed, 16 insertions(+), 36 deletions(-) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 38d3cf2ed..99ffe3b47 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -2070,11 +2070,8 @@ get_text_scale(Context2d *context, char *str, double maxWidth) { } } -/* - * Fill text at (x, y). - */ - -NAN_METHOD(Context2d::FillText) { +void +paintText(const Nan::FunctionCallbackInfo &info, bool stroke) { int argsNum = info.Length() >= 4 ? 3 : 2; double args[3]; if(!checkArgs(info, args, argsNum, 1)) @@ -2089,20 +2086,30 @@ NAN_METHOD(Context2d::FillText) { if (argsNum == 3) { scaled_by = get_text_scale(context, *str, args[2]); + cairo_save(context->context()); cairo_scale(context->context(), scaled_by, 1); } context->savePath(); if (context->state->textDrawingMode == TEXT_DRAW_GLYPHS) { - context->fill(); + if (stroke == true) { context->stroke(); } else { context->fill(); } context->setTextPath(*str, x, y); } else if (context->state->textDrawingMode == TEXT_DRAW_PATHS) { context->setTextPath(*str, x, y); - context->fill(); + if (stroke == true) { context->stroke(); } else { context->fill(); } } context->restorePath(); + if (argsNum == 3) { + cairo_restore(context->context()); + } +} + +/* + * Fill text at (x, y). + */ - cairo_scale(context->context(), 1 / scaled_by, 1); +NAN_METHOD(Context2d::FillText) { + paintText(info, false); } /* @@ -2110,34 +2117,7 @@ NAN_METHOD(Context2d::FillText) { */ NAN_METHOD(Context2d::StrokeText) { - int argsNum = info.Length() >= 4 ? 3 : 2; - double args[3]; - if(!checkArgs(info, args, argsNum, 1)) - return; - - Nan::Utf8String str(info[0]->ToString()); - double x = args[0]; - double y = args[1]; - double scaled_by = 1; - - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - - if (argsNum == 3) { - scaled_by = get_text_scale(context, *str, args[2]); - cairo_scale(context->context(), scaled_by, 1); - } - - context->savePath(); - if (context->state->textDrawingMode == TEXT_DRAW_GLYPHS) { - context->stroke(); - context->setTextPath(*str, x, y); - } else if (context->state->textDrawingMode == TEXT_DRAW_PATHS) { - context->setTextPath(*str, x, y); - context->stroke(); - } - context->restorePath(); - - cairo_scale(context->context(), 1 / scaled_by, 1); + paintText(info, true); } /* From 5563d62babdda5d1d68a0f606afd04ce4b3c880d Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Tue, 4 Sep 2018 00:16:53 +0200 Subject: [PATCH 171/474] add svg failing tests --- Readme.md | 4 ++-- test/public/tests.js | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/Readme.md b/Readme.md index 8957c2f60..bc599c402 100644 --- a/Readme.md +++ b/Readme.md @@ -43,7 +43,7 @@ You can quickly install the dependencies by using the command for your OS: OS | Command ----- | ----- -OS X | Using [Homebrew](https://brew.sh/):
`brew install pkg-config cairo pango libpng jpeg giflib`

Using [MacPorts](https://www.macports.org/):
`port install pkgconfig cairo pango libpng jpeg giflib` +OS X | Using [Homebrew](https://brew.sh/):
`brew install pkg-config cairo pango libpng jpeg giflib`

Using [MacPorts](https://www.macports.org/):
`port install pkgconfig cairo pango libpng jpeg giflib libsrvg` Ubuntu | `sudo apt-get install libcairo2-dev libjpeg-dev libpango1.0-dev libgif-dev build-essential g++` Fedora | `sudo yum install cairo cairo-devel cairomm-devel libjpeg-turbo-devel pango pango-devel pangomm pangomm-devel giflib-devel` Solaris | `pkgin install cairo pango pkg-config xproto renderproto kbproto xextproto` @@ -178,7 +178,7 @@ image contained in the canvas. the background palette index (indexed PNGs only) and/or the resolution (ppi): `{compressionLevel: 6, filters: canvas.PNG_ALL_FILTERS, palette: undefined, backgroundIndex: 0, resolution: undefined}`. All properties are optional. - + Note that the PNG format encodes the resolution in pixels per meter, so if you specify `96`, the file will encode 3780 ppm (~96.01 ppi). The resolution is undefined by default to match common browser behavior. diff --git a/test/public/tests.js b/test/public/tests.js index 9441f0f3a..7f6325de2 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -1687,6 +1687,31 @@ tests['drawImage(img) svg'] = function (ctx, done) { img.src = imageSrc('tree.svg') } +tests['drawImage(img) svg with scaling from drawImage'] = function (ctx, done) { + var img = new Image() + img.onload = function () { + ctx.drawImage(img, -800, -800, 1000, 1000) + done(null) + } + img.onerror = function () { + done(new Error('Failed to load image')) + } + img.src = imageSrc('tree.svg') +} + +tests['drawImage(img) svg with scaling from ctx'] = function (ctx, done) { + var img = new Image() + img.onload = function () { + ctx.scale(100, 100) + ctx.drawImage(img, -8, -8, 10, 10) + done(null) + } + img.onerror = function () { + done(new Error('Failed to load image')) + } + img.src = imageSrc('tree.svg') +} + tests['drawImage(img,x,y)'] = function (ctx, done) { var img = new Image() img.onload = function () { From 8104357f9eb43e99455fdc1a2c90e21e7afaad57 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Tue, 4 Sep 2018 11:01:03 -0400 Subject: [PATCH 172/474] sticky headers for tests --- test/public/style.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/public/style.css b/test/public/style.css index 6e4d276b4..f8a1c3abb 100644 --- a/test/public/style.css +++ b/test/public/style.css @@ -37,3 +37,10 @@ table tr td:nth-child(3) { table tr td p { margin: 5px 0; } + +table th { + background: white; + position: -webkit-sticky; + position: sticky; + top: 0; +} From ae49b69813925f863436b673c1f9ac2f60cd1495 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Mon, 3 Sep 2018 13:07:52 -0700 Subject: [PATCH 173/474] Throw/emit nice errors if built without needed image format `canvas.jpegStream` without JPEG support -> throw errors `img.src = xxx` without support for that format -> emit error Fixes #770 --- CHANGELOG.md | 3 +++ lib/jpegstream.js | 4 ++++ src/Image.cc | 59 ++++++++++++++++++++++++++++++++++++----------- 3 files changed, 52 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f741b3a6f..189d88d00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -112,6 +112,9 @@ canvas.createJPEGStream() // new * Support for `img.src = ` to match browsers * Support reading data URL on `img.src` * Readme: add dependencies command for OpenBSD + * Throw error if calling jpegStream when canvas was not built with JPEG support + * Emit error if trying to load GIF, SVG or JPEG image when canvas was not built + with support for that format 1.6.x (unreleased) ================== diff --git a/lib/jpegstream.js b/lib/jpegstream.js index 3698c1dc9..e44ed4c65 100644 --- a/lib/jpegstream.js +++ b/lib/jpegstream.js @@ -34,6 +34,10 @@ var JPEGStream = module.exports = function JPEGStream(canvas, options) { throw new TypeError("Class constructors cannot be invoked without 'new'"); } + if (canvas.streamJPEGSync === undefined) { + throw new Error("node-canvas was built without JPEG support."); + } + Readable.call(this); this.options = options; diff --git a/src/Image.cc b/src/Image.cc index b6175bd0a..997ff081f 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -277,14 +277,21 @@ Image::loadFromBuffer(uint8_t *buf, unsigned len) { memcpy(data, buf, (len < 4 ? len : 4) * sizeof(uint8_t)); if (isPNG(data)) return loadPNGFromBuffer(buf); + + if (isGIF(data)) { #ifdef HAVE_GIF - if (isGIF(data)) return loadGIFFromBuffer(buf, len); + return loadGIFFromBuffer(buf, len); +#else + this->errorInfo.set("node-canvas was built without GIF support"); + return CAIRO_STATUS_READ_ERROR; #endif + } + + if (isJPEG(data)) { #ifdef HAVE_JPEG #if CAIRO_VERSION_MINOR < 10 - if (isJPEG(data)) return loadJPEGFromBuffer(buf, len); + return loadJPEGFromBuffer(buf, len); #else - if (isJPEG(data)) { if (DATA_IMAGE == data_mode) return loadJPEGFromBuffer(buf, len); if (DATA_MIME == data_mode) return decodeJPEGBufferIntoMimeSurface(buf, len); if ((DATA_IMAGE | DATA_MIME) == data_mode) { @@ -293,15 +300,26 @@ Image::loadFromBuffer(uint8_t *buf, unsigned len) { if (status) return status; return assignDataAsMime(buf, len, CAIRO_MIME_TYPE_JPEG); } - } -#endif +#endif // CAIRO_VERSION_MINOR < 10 +#else // HAVE_JPEG + this->errorInfo.set("node-canvas was built without JPEG support"); + return CAIRO_STATUS_READ_ERROR; #endif -#ifdef HAVE_RSVG + } + // confirm svg using first 1000 chars // if a very long comment precedes the root tag, isSVG returns false unsigned head_len = (len < 1000 ? len : 1000); - if (isSVG(buf, head_len)) return loadSVGFromBuffer(buf, len); + if (isSVG(buf, head_len)) { +#ifdef HAVE_RSVG + return loadSVGFromBuffer(buf, len); +#else + this->errorInfo.set("node-canvas was built without SVG support"); + return CAIRO_STATUS_READ_ERROR; #endif + } + + this->errorInfo.set("Unsupported image type"); return CAIRO_STATUS_READ_ERROR; } @@ -436,18 +454,25 @@ Image::loadSurface() { return loadPNG(); } - // gif + + if (isGIF(buf)) { #ifdef HAVE_GIF - if (isGIF(buf)) return loadGIF(stream); + return loadGIF(stream); +#else + this->errorInfo.set("node-canvas was built without GIF support"); + return CAIRO_STATUS_READ_ERROR; #endif + } - // jpeg + if (isJPEG(buf)) { #ifdef HAVE_JPEG - if (isJPEG(buf)) return loadJPEG(stream); + return loadJPEG(stream); +#else + this->errorInfo.set("node-canvas was built without JPEG support"); + return CAIRO_STATUS_READ_ERROR; #endif + } -// svg -#ifdef HAVE_RSVG // confirm svg using first 1000 chars // if a very long comment precedes the root tag, isSVG returns false uint8_t head[1000] = {0}; @@ -461,8 +486,14 @@ Image::loadSurface() { return CAIRO_STATUS_READ_ERROR; } rewind(stream); - if (isSVG(head, head_len)) return loadSVG(stream); + if (isSVG(head, head_len)) { +#ifdef HAVE_RSVG + return loadSVG(stream); +#else + this->errorInfo.set("node-canvas was built without SVG support"); + return CAIRO_STATUS_READ_ERROR; #endif + } fclose(stream); From 9be2f80806150a8513dd9decc5be8dfe67711477 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Tue, 4 Sep 2018 17:57:59 +0200 Subject: [PATCH 174/474] Fix the global composite operation usage with drawImage (#1229) * add-visual-comparision * add-visual-comparision * fix * fix2 * fix global * better diffs * fixed pixel match * fixed lint * extra command * extra command * push * some better * better css * wow fixed? * more * ok good fix! * fix another bug * lint * another test * another test * another test * removed extra files * ok!!!! * remove save restore --- src/CanvasRenderingContext2d.cc | 73 ++++++++++++++------------------- test/public/tests.js | 2 +- 2 files changed, 31 insertions(+), 44 deletions(-) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 99ffe3b47..911efeca1 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -1111,7 +1111,9 @@ NAN_METHOD(Context2d::DrawImage) { , dx = 0 , dy = 0 , dw = 0 - , dh = 0; + , dh = 0 + , fw = 0 + , fh = 0; cairo_surface_t *surface; @@ -1123,15 +1125,15 @@ NAN_METHOD(Context2d::DrawImage) { if (!img->isComplete()) { return Nan::ThrowError("Image given has not completed loading"); } - sw = img->width; - sh = img->height; + fw = sw = img->width; + fh = sh = img->height; surface = img->surface(); // Canvas } else if (Nan::New(Canvas::constructor)->HasInstance(obj)) { Canvas *canvas = Nan::ObjectWrap::Unwrap(obj); - sw = canvas->getWidth(); - sh = canvas->getHeight(); + fw = sw = canvas->getWidth(); + fh = sh = canvas->getHeight(); surface = canvas->surface(); // Invalid @@ -1180,13 +1182,23 @@ NAN_METHOD(Context2d::DrawImage) { // Scale src float fx = (float) dw / sw; float fy = (float) dh / sh; + bool needScale = dw != sw || dh != sh; + bool needCut = sw != fw || sh != fh; - if (dw != sw || dh != sh) { - cairo_scale(ctx, fx, fy); - dx /= fx; - dy /= fy; - dw /= fx; - dh /= fy; + bool sameCanvas = surface == context->canvas()->surface(); + bool needsExtraSurface = sameCanvas || needCut || needScale; + cairo_surface_t *surfTemp = NULL; + cairo_t *ctxTemp = NULL; + + if (needsExtraSurface) { + surfTemp = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, dw, dh); + ctxTemp = cairo_create(surfTemp); + cairo_scale(ctxTemp, fx, fy); + cairo_set_source_surface(ctxTemp, surface, -sx, -sy); + cairo_pattern_set_filter(cairo_get_source(ctxTemp), context->state->imageSmoothingEnabled ? context->state->patternQuality : CAIRO_FILTER_NEAREST); + cairo_pattern_set_extend(cairo_get_source(ctxTemp), CAIRO_EXTEND_REFLECT); + cairo_paint_with_alpha(ctxTemp, 1); + surface = surfTemp; } // apply shadow if there is one @@ -1208,54 +1220,29 @@ NAN_METHOD(Context2d::DrawImage) { // implementation, and its not immediately clear why an offset is necessary, but without it, the result // in chrome is different. cairo_set_source_surface(ctx, shadow_surface, - dx - sx + (context->state->shadowOffsetX / fx) - pad + 1.4, - dy - sy + (context->state->shadowOffsetY / fy) - pad + 1.4); + dx + context->state->shadowOffsetX - pad + 1.4, + dy + context->state->shadowOffsetY - pad + 1.4); cairo_paint(ctx); - // cleanup cairo_destroy(shadow_context); cairo_surface_destroy(shadow_surface); } else { context->setSourceRGBA(context->state->shadow); cairo_mask_surface(ctx, surface, - dx - sx + (context->state->shadowOffsetX / fx), - dy - sy + (context->state->shadowOffsetY / fy)); + dx + (context->state->shadowOffsetX), + dy + (context->state->shadowOffsetY)); } } - bool sameCanvas = surface == context->canvas()->surface(); - cairo_surface_t *surfTemp = NULL; - cairo_t *ctxTemp = NULL; - - if (sameCanvas) { - int width = context->canvas()->getWidth(); - int height = context->canvas()->getHeight(); - - surfTemp = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); - ctxTemp = cairo_create(surfTemp); - - cairo_set_source_surface(ctxTemp, surface, 0, 0); - cairo_pattern_set_filter(cairo_get_source(ctxTemp), context->state->imageSmoothingEnabled ? context->state->patternQuality : CAIRO_FILTER_NEAREST); - cairo_pattern_set_extend(cairo_get_source(ctxTemp), CAIRO_EXTEND_REFLECT); - cairo_paint_with_alpha(ctxTemp, 1); - - surface = surfTemp; - } - - context->savePath(); - cairo_rectangle(ctx, dx, dy, dw, dh); - cairo_clip(ctx); - context->restorePath(); - // Paint - cairo_set_source_surface(ctx, surface, dx - sx, dy - sy); + cairo_set_source_surface(ctx, surface, dx, dy); cairo_pattern_set_filter(cairo_get_source(ctx), context->state->imageSmoothingEnabled ? context->state->patternQuality : CAIRO_FILTER_NEAREST); - cairo_pattern_set_extend(cairo_get_source(ctx), CAIRO_EXTEND_REFLECT); + cairo_pattern_set_extend(cairo_get_source(ctx), CAIRO_EXTEND_NONE); cairo_paint_with_alpha(ctx, context->state->globalAlpha); cairo_restore(ctx); - if (sameCanvas) { + if (needsExtraSurface) { cairo_destroy(ctxTemp); cairo_surface_destroy(surfTemp); } diff --git a/test/public/tests.js b/test/public/tests.js index 7f6325de2..9120e293e 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -1220,7 +1220,7 @@ tests['known bug #416'] = function (ctx, done) { ctx.save() ctx.translate(img2.width / 2, img1.height / 2) ctx.rotate(Math.PI / 4) - ctx.scale(0.5) + ctx.scale(0.5, 0.5) ctx.translate(-img2.width / 2, -img1.height / 2) ctx.drawImage(img2, 0, 0) ctx.restore() From 5a382dd9f713b7b059576ba4d02c33ac96cc6859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Sat, 8 Sep 2018 14:17:19 +0200 Subject: [PATCH 175/474] 2.0.0-alpha.14 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 54b9388f0..f8ab9c416 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.0.0-alpha.13", + "version": "2.0.0-alpha.14", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 2019b08ca752dd0b5c797179b449fffaf257ed6b Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sun, 9 Sep 2018 11:46:41 -0700 Subject: [PATCH 176/474] tests: pass error to done callback --- test/public/tests.js | 76 +++++++++++--------------------------------- 1 file changed, 19 insertions(+), 57 deletions(-) diff --git a/test/public/tests.js b/test/public/tests.js index 9120e293e..0fc068a33 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -1573,9 +1573,7 @@ tests['shadow image'] = function (ctx, done) { ctx.drawImage(img, 0, 0) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('star.png') } @@ -1589,9 +1587,7 @@ tests['scaled shadow image'] = function (ctx, done) { ctx.drawImage(img, 10, 10, 80, 80) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('star.png') } @@ -1657,9 +1653,7 @@ tests['drawImage(img,0,0)'] = function (ctx, done) { ctx.drawImage(img, 0, 0) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } @@ -1669,9 +1663,7 @@ tests['drawImage(img) jpeg'] = function (ctx, done) { ctx.drawImage(img, 0, 0, 100, 100) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('face.jpeg') } @@ -1681,9 +1673,7 @@ tests['drawImage(img) svg'] = function (ctx, done) { ctx.drawImage(img, 0, 0, 100, 100) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('tree.svg') } @@ -1693,9 +1683,7 @@ tests['drawImage(img) svg with scaling from drawImage'] = function (ctx, done) { ctx.drawImage(img, -800, -800, 1000, 1000) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('tree.svg') } @@ -1706,9 +1694,7 @@ tests['drawImage(img) svg with scaling from ctx'] = function (ctx, done) { ctx.drawImage(img, -8, -8, 10, 10) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('tree.svg') } @@ -1718,9 +1704,7 @@ tests['drawImage(img,x,y)'] = function (ctx, done) { ctx.drawImage(img, 5, 25) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } @@ -1730,9 +1714,7 @@ tests['drawImage(img,x,y,w,h) scale down'] = function (ctx, done) { ctx.drawImage(img, 25, 25, 10, 10) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } @@ -1742,9 +1724,7 @@ tests['drawImage(img,x,y,w,h) scale up'] = function (ctx, done) { ctx.drawImage(img, 0, 0, 200, 200) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } @@ -1754,9 +1734,7 @@ tests['drawImage(img,x,y,w,h) scale vertical'] = function (ctx, done) { ctx.drawImage(img, 0, 0, img.width, 200) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } @@ -1766,9 +1744,7 @@ tests['drawImage(img,sx,sy,sw,sh,x,y,w,h)'] = function (ctx, done) { ctx.drawImage(img, 13, 13, 45, 45, 25, 25, img.width / 2, img.height / 2) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } @@ -1780,9 +1756,7 @@ tests['drawImage(img,0,0) globalAlpha'] = function (ctx, done) { ctx.drawImage(img, 0, 0) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } @@ -1797,9 +1771,7 @@ tests['drawImage(img,0,0) clip'] = function (ctx, done) { ctx.drawImage(img, 0, 0) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } @@ -1986,9 +1958,7 @@ tests['putImageData() png data'] = function (ctx, done) { done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } @@ -2009,9 +1979,7 @@ tests['putImageData() png data 2'] = function (ctx, done) { done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } @@ -2033,9 +2001,7 @@ tests['putImageData() png data 3'] = function (ctx, done) { ctx.putImageData(imageData, 50, 50) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } @@ -2253,9 +2219,7 @@ tests['image sampling (#1084)'] = function (ctx, done) { if (loaded2) done() } - img1.onerror = function () { - done(new Error('Failed to load image')) - } + img1.onerror = done img2.onload = () => { loaded2 = true @@ -2263,9 +2227,7 @@ tests['image sampling (#1084)'] = function (ctx, done) { if (loaded1) done() } - img2.onerror = function () { - done(new Error('Failed to load image')) - } + img2.onerror = done img1.src = imageSrc('halved-1.jpeg') img2.src = imageSrc('halved-2.jpeg') From 10a82ec68cc8485d40f340afe9a8373096834af6 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sun, 9 Sep 2018 11:55:03 -0700 Subject: [PATCH 177/474] support reading CMYK, YCCK JPEGs Fixes #425 --- CHANGELOG.md | 1 + src/Image.cc | 78 +++++++++++++++++++++++------------- src/Image.h | 4 +- test/fixtures/grayscale.jpg | Bin 0 -> 17946 bytes test/fixtures/ycck.jpg | Bin 0 -> 117827 bytes test/public/tests.js | 21 ++++++++++ 6 files changed, 75 insertions(+), 29 deletions(-) create mode 100644 test/fixtures/grayscale.jpg create mode 100644 test/fixtures/ycck.jpg diff --git a/CHANGELOG.md b/CHANGELOG.md index 189d88d00..16a7779a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,7 @@ canvas.createJPEGStream() // new * Provide better, Node.js core-style coded errors for failed sys calls. (For example, provide an error with code 'ENOENT' if setting `img.src` to a path that does not exist.) + * Support reading CMYK, YCCK JPEGs. ### Added * Prebuilds (#992) with different libc versions to the prebuilt binary (#1140) diff --git a/src/Image.cc b/src/Image.cc index 997ff081f..5a84dbed1 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -777,6 +777,18 @@ static void jpeg_mem_src (j_decompress_ptr cinfo, void* buffer, long nbytes) { #endif +void Image::jpegToARGB(jpeg_decompress_struct* args, uint8_t* data, uint8_t* src, JPEGDecodeL decode) { + int stride = naturalWidth * 4; + for (int y = 0; y < naturalHeight; ++y) { + jpeg_read_scanlines(args, &src, 1); + uint32_t *row = (uint32_t*)(data + stride * y); + for (int x = 0; x < naturalWidth; ++x) { + int bx = args->output_components * x; + row[x] = decode(src + bx); + } + } +} + /* * Takes an initialised jpeg_decompress_struct and decodes the * data into _surface. @@ -784,9 +796,8 @@ static void jpeg_mem_src (j_decompress_ptr cinfo, void* buffer, long nbytes) { cairo_status_t Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args) { - int stride = naturalWidth * 4; - cairo_status_t status; - + cairo_status_t status = CAIRO_STATUS_SUCCESS; + uint8_t *data = (uint8_t *) malloc(naturalWidth * naturalHeight * 4); if (!data) { jpeg_abort_decompress(args); @@ -804,33 +815,44 @@ Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args) { return CAIRO_STATUS_NO_MEMORY; } - for (int y = 0; y < naturalHeight; ++y) { - jpeg_read_scanlines(args, &src, 1); - uint32_t *row = (uint32_t *)(data + stride * y); - for (int x = 0; x < naturalWidth; ++x) { - if (args->jpeg_color_space == 1) { - uint32_t *pixel = row + x; - *pixel = 255 << 24 - | src[x] << 16 - | src[x] << 8 - | src[x]; - } else { - int bx = 3 * x; - uint32_t *pixel = row + x; - *pixel = 255 << 24 - | src[bx + 0] << 16 - | src[bx + 1] << 8 - | src[bx + 2]; - } - } + // These are the three main cases to handle. libjpeg converts YCCK to CMYK + // and YCbCr to RGB by default. + switch (args->out_color_space) { + case JCS_CMYK: + jpegToARGB(args, data, src, [](uint8_t const* src) { + uint16_t k = static_cast(src[3]); + uint8_t r = k * src[0] / 255; + uint8_t g = k * src[1] / 255; + uint8_t b = k * src[2] / 255; + return 255 << 24 | r << 16 | g << 8 | b; + }); + break; + case JCS_RGB: + jpegToARGB(args, data, src, [](uint8_t const* src) { + uint8_t r = src[0], g = src[1], b = src[2]; + return 255 << 24 | r << 16 | g << 8 | b; + }); + break; + case JCS_GRAYSCALE: + jpegToARGB(args, data, src, [](uint8_t const* src) { + uint8_t v = src[0]; + return 255 << 24 | v << 16 | v << 8 | v; + }); + break; + default: + this->errorInfo.set("Unsupported JPEG encoding"); + status = CAIRO_STATUS_READ_ERROR; + break; } - _surface = cairo_image_surface_create_for_data( - data - , CAIRO_FORMAT_ARGB32 - , naturalWidth - , naturalHeight - , cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, naturalWidth)); + if (!status) { + _surface = cairo_image_surface_create_for_data( + data + , CAIRO_FORMAT_ARGB32 + , naturalWidth + , naturalHeight + , cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, naturalWidth)); + } jpeg_finish_decompress(args); jpeg_destroy_decompress(args); diff --git a/src/Image.h b/src/Image.h index 2dcbd4759..10783809b 100644 --- a/src/Image.h +++ b/src/Image.h @@ -10,6 +10,7 @@ #include "Canvas.h" #include "CanvasError.h" +#include #ifdef HAVE_JPEG #include @@ -34,7 +35,7 @@ #endif #endif - +using JPEGDecodeL = std::function; class Image: public Nan::ObjectWrap { public: @@ -81,6 +82,7 @@ class Image: public Nan::ObjectWrap { #ifdef HAVE_JPEG cairo_status_t loadJPEGFromBuffer(uint8_t *buf, unsigned len); cairo_status_t loadJPEG(FILE *stream); + void jpegToARGB(jpeg_decompress_struct* args, uint8_t* data, uint8_t* src, JPEGDecodeL decode); cairo_status_t decodeJPEGIntoSurface(jpeg_decompress_struct *info); #if CAIRO_VERSION_MINOR >= 10 cairo_status_t decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len); diff --git a/test/fixtures/grayscale.jpg b/test/fixtures/grayscale.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4988e9e55a99217293ad3a947fd6809eaca5fe78 GIT binary patch literal 17946 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 bM)mA%oT3Y?2hM*>| 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 LqlJHqqxStT5(#7d literal 0 HcmV?d00001 diff --git a/test/fixtures/ycck.jpg b/test/fixtures/ycck.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0fbcd3b0c11ed55e888f13d059e0a3665bb09935 GIT binary patch literal 117827 zcmeFa1z43!*D!q3oq_=p(%m5-EiKI^r8aDG(~U|A##T~72?goy5)cCIC}V&WKPN7(GG$%H6bntg76_? z3<$ykJ`5~%@WS}!$20(b%p*TGNV|`L`z;>A0r8aI{U9R-^>;sj!k|H?LLe5(!Jz%= zFTvpW4uc621tIW;=)qHiA#~)Y0{+44ju5&Ybl&&i!wA~-?H}P@2n4Z{L*Kvhwr+3) zqmF}{x0{!Pn>(YTz9^%nwT+`IIt&R32#QDwh)N0xG71Vy3J6OIOF$5I3X6)QP2@j`hQj?)j0!#lZUBZd}l!pMAq{{jF~pzQ6AqCdQ94FoH0D zmQ8H&y*yR$MfdOMdpLTy0udke#!^ue!aQ7sNRL90Z3r6fZ~_X4i1G393Gj&t2#6_& z2#F}D$%u)`s81d{Mt$tqNebd4_xDJd2DMN0K83&r0l z;P44Vfe(2==~x&{5GDl%76r!P6zC-3VGD*VXeAEDkpm4zJ1jsk*f_X&_ymMR7=KR0 zfH1LsB$7iI*ce!t*qAtYxcE3&#KItv0t@>Lr67*HuJth{k5D08s@RNj1!fj%y{B+t z5znwbMLgCx{ZDV#Y`jE|KdiXQcK(imtj!2Vkk4#y#4tfM+9AX#Y3AWn^ylO;|~U5zD9FHVgmKI`8_Xdm|e4qA|rfDM{U z2u&K{sT8d9Po8$YeV}w@)o*=zr@5{re))7~Q*h(q+NUIi%V^vbCU+! zndFZB;kN#Z5lyC(w`897iNc zmi6xER%PkoG2FJ`dz-;CTxwdG=Dd?{Oy;b~R{S?;pURjXLW3o=#fQ*szDzrPcbU?e znv1Fu0kY>+0zcqQR?t1zYLJ;#);6svgwpCFUF3q<-3d6ij?dfFtsEEk3l#{O$yt9h z=IqBn5HX+NBU|F1n0nc@!t-JuTT$mdD;bM2`huz5h690;$Il~1%9Z&PN&Bt{Ev2o0 zb~%Kkz7lXQnGFkGvC*v5czU&2^}e`vpYo~d+tHO1%VqmmW79*h-9$Qb4RRATQbAF( zi*5{qetmxHi@_VW5?(Bo=4|xu<`auLn?JNJzU#8Z@HKj;o+?-ZpzRAw{Aay1uhBHS z@eR7j1eqpDlf6f;daHst3)4wSDF>KaShD-SkD2UY-_POEmfgxM+>bFrJ^xsB;w;$| zL&|=2_k@f8Xzg^;YLF&;GCxLBi@%hXB6DJSNnAg;u=Zf1tRi?-)8XD{se&suUhLza zYmz(OV6I#}DD-^!fs{^R@?zkon_h>{8xp+ihUK%A(^I86a2u0Di1tbc=c3HR%|e&- zt)eln^^2B@7a1SC{Hk>IAcA<8dbsMKLU0)O>ZUivj$x0qXrGezAk(AqlTYWcafllY zZ3m54FHyzsB%0gM77l;Lw}~2CnY%qcWof`i!e;*FvUCB%EFP`cZEH=8iRU57Pq*kw z`$*mPw>)w)t~hr(lx-elT+;6I%e4s)X5Y}$AJ>_hlHWe7+&d$`uDEz|rK9uF=#<$_ znol-|kkCbsMN5;ilcswYiww^>bxDUDLh!cWev7MHw8yyFbB$oncpR=bjZ5M!H1Ak; zs%e{S1(e!}^&=XiQ7?9P`fGH0Dx-OWghLw~(#UED8McleLJLHNFWOwFuZb@tYEs+I zPDJ}p!HRgwAM?78&&7WdofBt2#=uW8fS^v{ko}~|Tgp!T{9w_(TT+ZszCX ziMSe%{D#Gb1YR`ue)(8EVSf;&eudyW59=THejjA>Qo0+UQ^~>)bVg?THipeE!{E2Gk%}@eh7AQ=-9&eic|?m zl!tXJk1`*W(ygdz=NW$5#;d(fK&ZifOx$f~d+us6l6heSAY$6kNNaa0DlkQ`f(PRs z{czoVx}AK^_g<1$P6akh*t#5th(bPAdaoQ*%zGU|t+GRnUo}T8?@p-2EsWD0LbcSd zBMw9Yn{^iNo~?N!Yx6mA^zwb_YqBM+XJ&!wTnSG7z6R}{ z;ed-@{aCT@hD&%;+iTCM`j1ivH%DumTR-YIOD%nz5I&7H`FT`L68FmSjlJa0S-Ax| z=7T99lv5VYyHTi!GXEbZ1Z)^?#r-}dzWrTVySH3a8kegezrIv~zM_Pc50!F)+s*-I{W z_vv?u6VL5N%A8jW+vt8(I5wq7I#Kp{;$GN0>8ct(6YT++q>pp6hmeGs?6aYz%AHr5 zfz<&gzjS|P)5T2@$vJ3Y_~f?Vb+WSX{l{lZirlo*O4ksNLgdx7lNpLnZ1p6ls4j(1 zT~W%U6$rX?(od{0=Mc)an`(AEga{58Hk)(I`HlD8mJXp&iEC9DZyOeQWs4r@r>9{qYxn{$!mGCD zjMphk(+ZDAQZl99X+wi&?nyv&+Y~FVV!|?aoTfIH4?I5~LQjFD?n5@Svwo>75|{K{ zu}jDL+QXpaw8_7ANkz+R_rCpNd2Ud3See6IIuzj-;1rO zcNL`(@$;J6y#H}N$SX;4VC|YT6y#OS$yo*F=WM{|oQH!y$wA{R9wr%%*S>wOBnu@C z=|Y{8X>dSu$NqNl=Qvzcbz=1jPvs&@E~;5voGvP7c(?j|ePfKQ=<*??+Iq`vfy=0nP`CEmm;X}{<0tsKU&xWnA>Hu4a9rB$!f1*Vqh z@+_NP=M7Bl*Yh-oP|L#kg9n9Wq%)7K4xuze$sv?y>i=NoPUZk;>=r&e6_SEa(^XN-kMiSACE3<{EM*8O~Dzp?-yyYc?)0O_8&;e4N1P3`*|dTQFT z=2Irq8bcv)MJ)L}&hgs=cYVs7XKiYO`wF-ZA<6j)(}DH`EDb_+ui82}KB2QD!!qPz z!e6tg*}L(&Iy&>ZIC>w!vL3;b zucbH6!ng3`jFYINC#MLX9HmJfrPYHsIe5@I4FnOndjcg)+sE78#~TEXs?zcD)`z>f zA&(%`T)hzpS05KNFxrp)Dk446VX~vV`i}l+2n9!PbRKl_w=xl)TGlQI10`d$V#Wj+ zLkP$da)bOKM#u z*?ar`3@EuE&;JYn-27Iwf{nAiHr&Yu)PICT2+~tgPy`{g!Vc-#GBVn_`M~Ae-k^g= zPQ}ypU*Z&y{|cwzX=?y;^;TxqL!!s)r&B=MGX7P(o)^;lC|<`ODW~@noX{6x>e%ae{TOMq9}Nt?=w?F~5O&{+`BSw9K$rikYV~c9-{N&VZRAYQC43t_ ztd%1#`8RDEknuO|8uMsm*^bgkkKRWw*^uvJPI44P6DR!<$Q^)z<4zd}HH{uKGwWp@|LY=4$_=95hk>KCIyF=JGB4tr>du z4&T#{x`*q9bhJTu86q#BNgLx2_23+(foQbMz&i@4o&R0gc=n!dKJI^r!gcd>w0Cs< z&N0MF`smWNkK#ZXB-TFOZYl^@gr~JP!WKk81_ACr1S#P+U~~*RMa{*Y5q$pJ{m1h0 zM1B{?-$xCN{6EHMdfERF+23Grt&!da*7kpZBDFz)dy@gKL8Ur zxOoQ3BOUF3V2bQp=T(11gP!BrBJ8YvjszNkFT&INFTf3dME?<-2yUfad z$6r>*UslIoR>xmf$6r>*UslIoR>xmf$6r>*UslIoR>yzD>UgAuoB?_V1i`=q^b}y) zgXAGwU|*g2jKSxkJph87&@S1{{ey#SoidSH4m-U9M$m6;(&K#c>r}BzNJiY z@b-3>5cIOx56X1u=$@;rn+c+b<8Q}<^XG=4EY-na;bhMRb zG7{Ak&~#TsI5?^XdLr}#wG3|$ z7Zl%+e=O@hP=4sC_C;|4XKp8?p zya0mNE5Oy;+Mn0ei}^)T&Kd!co3em#Vj zn~$dr0)R9BsLIP*$rp6ze;VySW>)eA&Hb6({dZXXL(L!obV>MUBW*mr{<;<$V2Jj# z=JmAp;{}?(x1*~)8W#dTGrIpcm-N@!e)n6G4VYQ0@z#;(oEZk{gIU??12tnCr}PVNZ%qv|v@CG`-lwvK4S zxgyfq3!u^6)6LCJ5@7fTzz>q>iVX zt&a`D^T$Bx0ER_6!ac1$0~iGb_yh!*7y;=>$|J#;+6KtGyCWSDK+G6;`XE37KWIbp z$esiS28^BvnsWg@y&b)gi0@)R9_jtvhjc{qm?YBL)gIuA_q%b6N#^&PHRt%zjE-~! zS&n?)a`XSLOn_uCF6R-Rj=l(6WluL3#v>+_bo?u13jPV1U?9#43JZt}iz*0-D}aIc zme=;L2Jvs^{bug`KOTs04emYfjRT+rG)3_Wj?FpooHv%ow?9iicjiyT* zYjoH7-QB#rzfBp8-fn+L@pZNRKAAfHCXW6;SM7fux&J6xzM=N#$o)HE`rjY9|Mz|Q z7fJV5efbM%|3CEQw_W2uoUHx99Dj!b|8qb7qs0Ed-H#*5`AtCZ|5f&U+2;5S1PP)?+o{D2whch`F4fCUTl z=voivPlpA3SlHkVuAyL~9S$zek;BEs!^6kJ!zCafBm~!g{^&5Uu&{8jaR_j62}lSD z2uaA$*Lujnl(%z|EeAprv@f zzt{s|<6vRpVnE+-_JHR8J{9aI|L$fFCTJblW5>nA0xwK23`{HvYzPNj?UC2TJ!TEA z_D~7MX5dkOyIJ#A_&EF%i)YvxtD=a0+`|eRuV<0RF55gxR;oA+Le{;d-tg5kt|BbtOb?u|?zi>*ZnK%aAyq90! zF}AqFC8=)e6o^VLc+@$*wEOKQ5Ec%=C@wZW7M9D={y6$Fl01$sXfduwXsl31`6sHU zeQ%i+sP*8UYr-rdiebm&^jRO;R6N7;>KC0kfAxgH9dPgIwUcej#$e<|q?ZT=sc3@#6mVt?Dl2gg5Tp*?Vxw{*IDw6GFh@)Fe` z#7a&{+RLK_%zv}d|3*PAro_|3(|Y`sc(mZ&eO9X+IRav5^Sl(XeX)-z z)QTkP?KcT0Uz~Sojb_B-yXZiWE_-~e9A^6{bZ#^J>Y(Vu`+J%s=e*zrZ?5)URn%-$ zM#36L3vNG7KRr8HYOqE-oOaGRg<2K&F{Q#C4rMmf?X#g}`xTdro{Mos*9^PxLFa~i zrxNOUofW8hjaGMs;ra2NZ5%dKw;1(XQ85kbzlsu5JpomDkR?ngVVtegXyI8LRFo4&8X_cXd=U zbj~%c`qH!?k{iR+D2=bcSaolZJUM1ih^yb^SX=i+3kh3H?8CQKeUgo%9u<<-&H!Ry zQ_Om+PbvKD*h1`Dje|1ZxzM+3KYPo1{OyGb&(nBZI)+USPTVWAU|jeve`<>_qvxPG zZ{~|*o~2a4RgO(lmaDk?q1=c)b<8%kagmtVC}J=*4d-lfY={4PKGj5C&hsm%oZ%ox zJCawkUp2dKrsqDxGmR1Z{BZ^hUlF}+b-)$z(iY=F{YWZNNt!!ld5k)!eJN@!7dO2X z2_}XJRL6lDX0s||WUCiEZ}Pjnq)L@eQy5+lRK+NDO8>T9ocI|fnRhivSg^=Bhvzd> zejnCfqVjnMi6wYKP?Uko7|@;dlAUr`zc#VlDHh|WuGBX2?W?#466J=GEm&Sc7_ief zsS2;eS_ZD9P{W9e`$9+QqHTH?TFw4ooygNZ9jDwy6-i6YNFiTO>!uI{>^Jy>rLM>` z@AA3vnSJ>BNuBUnxG@RnzPgIB-77bwKXAIoesezV{A~{9$W-?&x_S?iPnT70WE-tX zlp8$$@Y4NZqrOl@ga@Y~0|C(*o@TyfIO^;6OH}%?m-AG+1Iotjj;CxrPoeS%o#ZpD zwJXGFeP5z-X@b*MDOGp0PhYOTL)o4!uc*#&%fq7KibT0l=KCPmfh{LRp4f@0&CTc6 z3`MB9rq7KQ^1Vcr%L**M6w#}~$Bt8WRNuQ$HIr+j?lWpMfgCM-;VP=BsP1Q$-aU=& z@Nyt75oQ5PXUes5&^9C*fagy|r@30!Q;LLy5VjcSYgg)8-V7qwjyi+{Q>X)b?v#!> z#Ydjz*^}Sw8CuKTwrkb@ZyL4^|7*HLv4LU2G!QngkuWkpKQ{z+lDJ-xzzeRPv2vqV z7Cw7D%J|IxplDARD)2xdWATS zQ~GqIPH8tG7T}&hvc%0X+e?miK{}oh*bz=8Euz>?BxGDs26WtVXH!QD-%kN@b)8yWOrC{{YX zAmyI%5QSkGQ4a+>Q1lK)e$K6FweUXh#PkrU)vww(VPY1|#bMoo1J%*ei>&C6c$LF| zDROko?vZm+?7WgLgw1tK0XBZKLrK-uWkzgTj4N4jA^+ZxP2@!>uS(NLacj)<#Non; zdWzl*Aumx)+MUTVO@&1=ky*pr#f*{mv6B(_1cnCi&caWTg|Dx>3&Tsj`x>)da+zrF znXV#=13YG%eA9Jx73BegAqr4oH!_r*T0G!f%54k6@I0IRJb zx~r#)d#UDeW$ft-ba<;zr|_i=`mcqM%P6E!BLnUz#Za58_r<>CD#|IEKv6Rix?Wn* zn)2l;8t-}FbNcq9aKXmMS&ocJ0}PEgdH9pPAA1La-F><4s$7m^W)VkzSQ0qL^7@Qv zR78kWT1Yl$%~-iXw-ev%)X;(3D6?omEm`M+7$@hN={7O00v|%DRVJ0uKHrd_L{8kHBb3fFaI zDlRX0Dc^?pe0BADN*Y1pvo)g|H#-CqvN@_t>8IUaq8>YK;BPP_<|a?WssvAl^}i{9 zqeh(du%1<33U@raq5nF(h|p5}q<%z8YDg??FQPD9!CXL zzml6UM?&cS1cm$)MjOMt@iZk>xT7X2$CN!a=b8$q4->^1AGk5~yfQ?06T9}<+V+PO z>MO9hS5l@ss41p|XXYGV)C!U^GQJpNl3f@r>=lL=nmpSotzF8#tmy?WzKWiH-TrrL zsDE9D9R7E7{sP?XU2c)nH-tK>RJHX-z5qfow%B*PC~&$r6ownmoWBzT763sRd8Uhr z73Ndz%u#~oyLa9I=8bC>I)p^*>+OzvGMk3rR5>`DOwz&NIlCtpI$XUBXe`-waJBkt zt(o#HRZn}LDq-X8=dsvoip_NIl^oO<_Y~o!_UkowT!_-6bokg}Z4}bO_0>gTLFAL1 z<)Z0ypoYx7@|rB}Mei>c7NNEWK4ZW7^-SK1j2opF*wC+)6_CYS<=Teu+C!Ss^Q3{?pwP zx5%RJ7UKksc!Kt+t*a#L^)1{g4(@PDu<;EJ0FG0e> zI_4S9O=~hoDlNacpD|iLRUpU0PDn4c%&ohf4^}SG>xFtvwsBVik|i{m7ze|iT@C$m z$;JGoB*ip5@5Uj-vC`Ygw`Ed)CicrZ)oq$}D>*!?w%pfp6Y#wGy@&l*pIB_GB{ES8 zRbkC!V1*_3>gc<^mMDis@2FV4I!SYT&`|$WAXjC%2xFb;gBuVYSa2y=c6LySXBVHf zO)u1mlY5=*IGiT;-V2^j*EV08g0v0YTrtTon6uDXzP(ENhFhHeJZkV`ZCOdHSsYn+ zG3)$W&(Cw`B?!)TQ29&^)~ol2)2D<2O+u^a-28%r32AMvB&*3?Y%BE;O>N3&%_oAg zXncV<$aS!=uS|Pf>fzKU)crUs4zqk+PL<2tpym*=&nvwKgz2V#b`;{>vFnx;Dbxl( z@LmbeH!OYV7`ResvXtoYT5jlJdq!OxeqOO(29S)>du@kMs^4I--4(Zv5jNwuOln2= zJgZp8XX}{~;J$R4mpRLg+8?Dj(XdVr`4nEdb^K<#4L3J6eA7lDBSh5oJR#xsN;&M) zl8_Tl|C~WP#+{p7Z~Ct$3j`uVs72_O)Lx=;0{xPGqIpI1S)AQ@`}Q?d+|E<$Q*sN; zbU(i>w~iV10I=g0`~3BH29)h(9p0%u)B3St^$J0B1xlJ?8Pa50VA>z>KUb2Fo(EiWq9K#K23U`U;a8*j;F~6n7@S z=rH2OTIH$vwZ9$Jx)5^l{xSg>ANZ*Z~D)LPQ`kZ$-W9fvUnDY&>b6?cgjI^r)1Q|mI@)>FTmvLJy)pn%1?x?tl zWf!DZjn((GP+4epsIOV;%N5p|USWq9`E77-+|pVE#U~*}fm?Ia-y?l1=gO8wI-Bb>n1*`X-*>J)Gazt` zx2o{uN-Pzp4^}KzHsGFfzwvspP+=iA2f|8`lNy^o{q|_Vw3hT^oKYP@-&5s=;zb=e z_f#Bhs+2Nusq_u4_0`E$o;L;he?h`RrguJf243_i-fp%h%p<;SN2x!+m{IJcN+mPp zgoTh6rQk%RP%oj2hI#r=ntz0Ze%F!}P{|hD6f<2jtZY?~MlI zAMG)w)e{bqvXAONM#ARAgEuzF&d7DR?s{GHvdVk=o(y;NlcCed4N|#V>v1g{)bO!{`?FA_ zEe88QIM%;s*hoBt5+u8kv_^^JFdP;kk8?znoR;CSdJc{XxI5)WMWaEU2Nq5qUDUPr ziUv@b&3SJFL`iT=^0+mDs$;0;wm`@#?#c!{ulrAj97<2^ z_3h=ZC*4P~#%*`-R!f#<=PQx(l#q}&Kl8=O?_JhH*k;i>|eF8=sj>WDSyZ8JVH=1B=c>h^tw)T9am*m*2d&c#?;? zsOV&jnv&e?4X27~C+ClE%Z(;(Co?RtC6DaFTOK7V$6b_bbm|;Je5s3ZZq~epgsp!} z-K$8HjA+g3eV~4J$;y;FKTe;WI^WbFuNY5p5Dcn+_Da^S$abaav%ZO1HqX?jY@}RO zlq>WtG=s;q3P5))*0s|T@v4o@vsDf7bBJFk1fAjt-(-r^)mIRE4}|m9@XdFcvfaau zE+Ua``Om3M38=37w^JdkP4Bc7B%^v)HO06RmXb1JJn!OTU>e^gC+Za%x`*1v_K+fX z3Ixz zs2k*8FuP=Cz|V7Aj*3CjuGNy~!(-%w?$0Co&HT#Bmo91O_=n$Gj!}~ufg6*JV*#6I zuuFl-gxqIgX3?B(+Zj~4^6`RJZUwo~mGVz7QH}bqER*jzVW)6nnJ7Kc>FaCu*Hf^N z4Gp8Aj~~yXzYZ_OyYHfMv0jb*mj9@fgi{DzXQAPBJ3F|8b1$J0Kpc19xyNThS@l?( zV)l+5=aHb^ACK2L>1FDANxu}#i_?t{=G&Q!hDG!3YRk%9Jz5uIPLgxB%9XZosrZzl zZjTnWHA@G-WA+Z_8dI~k7l=EbU|m*K6H|)I)p18HbIC zuZz0|L76;4luOTNSk@5+#bo3G7&Wi4;Q3;e??#K+t}ts=QK%p}so+@=h+Lt(Q@0YS z3(A+o^(5?CO-ANNVth)wr42*o=&2#>ZnRplQ+=ABt5^}Vz8pFvkcn}*s+-- zA?nbPXBjT1lb3DKi4-)7$!%?ih;Xs@BWp6Krs1Uzy;sbQ&+zC+7~0O2n!=7*$xwV6 z>~97_I5D3EQ;E#ObYSsFzdzbhC?Nf;Q$AqjoTji!dzOl_zx(Y8*M>M@c)5A!v6at0eOK=avM#>>{m)^!v>GQo+dNqe^?8kQ@&U|7BFMl+x5E%0;Wu2!t z;$AxpA%tF)Oi@{P8VXgP@FYstBS>d2*5rOXR&I39;^2~#xHU_%TXDyyf{rux9*cR5 zm|9PgjTU#sxSor;$xbb?(sQgL5>G7jpgIXi)vKW|>etdmMMlZ4pc+5J8irjL5)z4O zmDo6Pyxk{tTuIc{7AM5G+U~#7BQDvl#Yi1Gndy3^2AKFVS43U(wQ-|`;+lylpM2iW zP}ByKtWd>X??_RW3FZ1*I;Ue-sdLw3<4}Xb_b&o3*|%TLC8+7 zQXQF3-oAC;T@j%=J5M3EhB&UB+%n8R;(`+MK zVS{1#+hD_?`MbVcAbihI$XaKe62k5eNj}!Q33P8bP2^mUIh&3lCyhd5KrbZ6*>cQ+ z-F41I4(tR3u-~&8xZLLKl^YO--F(e%b5<_(Jsk-Fah4}f7K(6lJEzq$g&|Ege!Mh( zb+vkh)KOFuhlMpWtl}JHC1xoNrIRWl{wTvj2IDep!#p}Q89zBdV*JL?^PUQ3GMI`O zCC-A%n1ul>gEr0)Sm++#E1U?Zd2MF4XgXqAaT=K{W;uN>G~7DvY;uBYP6GRjY9Jp!qk|F-P^hj(6!DIu0*jC#;W@XOJu7MJ#!S(2s0Z}0l##b~W1bBj|F z3DU_eaaVH%f=!kUO|dO&)#DsDjfWNeW2IyoceO3rT{OMs>ElsnQpw#)aN@t>2dS@TH-_=RBTbe#>R@5fAY{=KqG<1G6vxaQ zf}=?Ub1Wadst&e49t-y_-^JZ%USOTj43;=XM3nQzPzdwEl#bzTDv}Ucxsp~3lXt-{ zX+f`+NNgO1CQJq+g*t`ym?w}AlKIVy2Im}ZEqND#D|jGZcM=g6h|Fp%3? zZdC9-DUM~WrQ2V!F&cm7&2iSHz;Y>ynzmpJPzit1W?|QT%10u{yF+ik+J0b#8TnEU z%jL282E0@Wb%ruxuIExia{1)_+-LRWu#2MwSh_q^rz>d^>Gsi>+ed_zqIJY^yc=1W)-=4?w4E#6&tjMJu&9B^cx?Tx?u=B1PM3}j& z)fh*TG0wV&#v`@K#YOV1yx^r>&D{*Ir1qWG-|!BOQ0LcSz2SNh8Y$17pPv8WGaahY zr+Qj=C!}A@s#3M5miR?3s#xQgX@pJ?>?NvzKYoidpKj`dRI)JB9wTM?8cXDalU|zh z>-uto_33tkPs4b1u_1&f%P05Dm2hX>1Ia;!M(ZOn_Qp((RZpgk^D(tG>2yzv>o3N$ zg_Gx(W6=*UMv4?(*cs7n8Rjopsza-)!!3qqwC9iGi#4 z+5`2KpN`3he$nKR=}-0(1^ZHVwHvtcmeIwFFVuW*ms?(cTY7CWUk_-4CiF==w+crS zx7cGO&fP}6Y)=ct3rZoaM$f3bU)P-vJdJeLSEj1Uo%(rVI81D2GQMyavK76l)|4wG zL_+VuSdYlIA3{6bAHLpodHbkQxV5q3MXT9oyX!MrSKsEdUfPn8q7I7NVc&@EqIZir zc~Dm47suhsyG?SL_RFSp6cYBKpmXVzu~1n-*(5L}9a(-0)QTKpoNCt7-3lj#VoxvO znTAPVk|-G%iE({u7G3lU)Zp>+wp~~cOu5nNF_A5b>iRSB?$ToQX?fcm#7Kq9Gd{HjL@{EN0lRnx2O@z?Le9 zt0Yp4VF5V?~--@f-W-Hd=wvm&LAIa!dYv&woMJ4*C}4tjI(`Q_Fu zUDxHTm{YotSVR*Qe%0KhIW4Mp&=m&)wvnA?^ZL5=sSS^S1^-ko0=l%eJTc-AO;mM; z`P5%RvH|hDf33FnyuL3R#TKkLbo$m)9X)Gq554k?{4Xz_2qi}z3?pG5#U-mw#F$=G zfKyylT_MZMs4JzkJyUNWz%pC~QIsJ^3uqNaK8nx<(M}NMC+nY)s^h>6FA%Z9#fYaN zm!$c4akOyt0pEwj$MF8*YHs~16!w~*8gx2-uP}vL`=2^8Ao~xWiYcYr8{K%e6ZCu& z7N6m^nv~WZ8nT&@!PYRGh3Q-J1Z<{V)(!|N_Tm{~U7f9qeHbNlt0h#6L)ACuy<8qO zv6JsPU#@4Dr*=YJvd7e%NoKjBXO4FXxws0~-h_J@@)&sMLEdv?><2stQa^ux_sb&xL^8Z8_-gm}H%99(Sg?jzpuQFAq(h|e&WVMTCP zJAPUzJ5_EpCZo2#$!V4RIA`s-H8Rr2Lu9h9*tcLe>UjbN6d# zD!&-O-5J4BN6w>X%Bh0gsmf7O^!(n8U&ia|yb>gA%`*OS!`Wjyvpsx#C9CH9n0xQD z)cIEMd70-ic7Iubyb?cLkU zY#8jOhYuG|83-bZ|!mOSxstZwDt^}g~3L^m3fA) zE={hpZlX#jjNV{ku!LM1Zb-gkN(o4N@eR066yklV92VC_MNw6$S`LorEIZ)!1DOx) zJ=y!xyH|B*%u!g$BOl{ammmfX0f|zcjXL_w%iiYRFj%}?8!EL}xD=@vdyQcFmH}qS zaj?~)Z_9io+T{@vwo|&3veO(;&>?U_Z=#JmF$!@G9ywa}#YWocVOWHdoVh3jhkNTovJDe zd|_9pnk}KL%}VQmO)5oE_Yzfc>m{CcP1NdJmsw*gq9r9S)3zn3x*%jyo;~UvP`eBX z`~qwGMNk!2EQe2@y-X^fdxG&EP2?i|Mc>ka6l&YoMhUwmGflyUXxGRYR#(p~5Qij?= zxfLcFFIXy*zfiuOT+sfsAUm2BD;@HsW_1M1kP{akCw^F3^t`2_ETr;6*uXMOERXxuhSG`>_zm^2N&WlN^?wkx|0!tpTS~{;XEtvR=PF&n8AE*{d zT#`OlDCl=A^WhpkmVhq(d{~U!$7XjxLF>_J&Fm#e`-VGe_Gvp;ow3skdqOVjDML`0 zYPPtnQOqVHp;Y|m(9)rCi)QHa2s3?T{iYgExUpsR-Iy5;GmV81gcCdddF zWR)!@^{yaf-{k@4tu(eZHZDgI(bKW{ zrVK#$>o}6G=^u-DtMO@w?mh>9pvbP(QsBTk$Sg(uHE+Sxcx$K1y@IDym?r+h7XB`JDfzmQ`u=a$^4em z>3HNRQj}Q2gBz3XPSyB}1JUNF#{ug}sgVhj7m=5lX5xh$wZArOHSM@(pGtZ1-s%(JSDfmuss%hzb@Lf)k zS7Ir&pPItUxQL=G=k8dXGr;=PAO8~7tRyPZ-}vF7;yXIrxS}PJsmgZsC!5yoIku{)1!s2n>iYnybI?m>r&OSop6i> zu@h88zT9-GHQez4Izv6bT!adrT?i^x-LM+k20|-F^Yq$HX75<-6=qNUYF}$G(#8M# zeH!o=TK=iC+o6viX*l!>z!6rNQ4@ZzAEx+$>4U9#16$G}Ht)j;;z;T;c!p(yt)Bh> zX2NL6yNo3<&{3)lzq{{-w#*v3Ci-hjXx<*@y_*MrD4_nay&zOU65?!`ziiFF7NWht&O4BaC^%I?1-0Hf?BCJzRv>1IoF7IJQ z*~1J&Y;ZV=3Y8kuugPi#Vi0UjD>MJT&s^dWIB) zyN~f+jB87&zk6->ngaXHv2gBFag0QtFf0n?9PrrK3Y5rzp%8n6IF?(rlg5!b194~Y zOcoSQ(~EvNtfrP@y}Q;9#>J<5uZyPV=gf@&Yv}Xt^&5|= zA2D%Kl(5uTP&Nqn-D{Q4;I7LgH%^#5J`0Z2J%-;`+PY$rdFk%Gy2&EAf-P?GSD2i} z;EJV{*%egQI3t5ebieVqB9F(7r-TG&+C&T?zE1J6LWP&+FMu?hZ@ItznmfQ$G|tAr zM3ejkg%JPAZC@+nggNIj9S5**6B(@Xt*xy0ceT089bboFp?pFP&EU_rYD$pH^iBf9 z&nHU>hxQ9y=h#GVbCqHvFv9d;WKXXiQ;=tl8d_8WR2DF_smh+X&0g~`u#sK>x9yIA zwQ|P8k@RdSaKa?O zft>*HL^s@~ha!3okMgbkZQd!t^K5UdrgIXCGt$2Vn0*;47nAT~!FOlg`uPDoDjG?0tIViTtfKNLR zyDSVZQtV7_>GHirpUG1ZcD}4CF6_2JSSKu2rLA3OEAJH81q{+1k<~P2?s8vy>DbiO zgLyN*1@@Q~>#j}!rV<_lHIP|pl1r0~_#;B5Oyv#t zPfmp;h3z`>_7MLa52uwYawmFL5%7^W5%?O|W}=(%#TCOzE<7 zC9ouYrBxi%zYwii74asy^E`>`dcN__i>HQ0j`F^k)nCtF`!E9*uchm^I>u|73(Q7U z2KyF z6X*bf@7lHM_>IIGo2hvTa}~jJV`n(T*>az_0WlG|lTZw{4KZ4vCY< z&E5Y>w|H^1aL!W7yff9sbV+E0fk?GoHZbcxA&Hn3mVV5++V@&u{OOx{9_1FF$XQx4 z&Z4}0dY1QD^o!;SvKBEq@nWF(aJg}tjOU#=zH%vm2UFHmTSw)j0(FMnf1nE3RDwcLQ~Vs*n`~I{D{R zKHHLv5`Msm|r5}M28-&dz2Fj_dsS3FM38v7<@Vp5qYlJ9KWF>a>7 z@CKaOwzn9=DJ0ikG*J70OksXYvGsaJ?FPA~bQzyIk8VAIoNnmp$C4|P9>69YKsU(a z%5pzz7x`jFT)x#mE+-}iA~JxlQ4#RJs8V|SY@I+~F;0-W1c+54k zIROsapEr4UGvlWE;(UEOYkcdc_SLhV*+y3Q8H;PKXRCpL*9e1`IWlcA1yHlt5zn77 z%#e9NxF2PlULV8XQB*|A)2jjA}CKvJO=Q=>()oF9B4LBGN&+^n@y) zfK)+1snUx==)DPqCM6(*j({K?1nIqZsY(EqB7T>5X4X41Ykti7-ucH;jCr1W?>YPI zv-i19F*6uAZRU+&8yy@vVNS7~54oI{a)LLXHcHvc;mijui@|=8-)X0yX1~|OpNlJs zI5E~h*zFOKvVKDuwt>Zqm(Desy~l}boXbZe_<6!?=vVYeJ|)IdE!X4~55Op)r;Y4} z4dP_CGbeaWBx9TzICh}CsI!0tjLE^Z_V6mW;74KF;}$Rwu_N-U)(* z2ndSk>tj5#kAgD21jV(Yj8JRZOP$u?4MmYJT#b5y&SyTp_DVLEs!S1k4hFR#oVTp? zi&si{>=ySUd!xIuxsRN0kE<~x*dX4fYcJ&O;zO*ea9EH5`2FKn=CNc2|E?iL@2VYniC_^!`NP3> z9fwwCn8Djho+2}33w5uIL!!2;)F8v0o7W(Q;5ykLQ+{SuM&5TV#o`LuLJ(zc*zuLv zS8F@EVS1h)Owtuba1!5~F>+hgkG@8w_Eez*tkTK%i?z3#{h8hYf)^_;mQB$qB$9m% zJm+NuPX=y^5@2(u6321>A^2qo?)b+|LHTFDjB;Q!c| zJFHl`?AgMj1Oe-AoQThcB_y4+&ct+nyIPrGM{S?FZl0Il)2Yu|_|DcuN*P3C%oMMr zFkX7`NDlAm;Z;#~;}&A`I=9C07(naPdq zE_(WUn>APspRQPg#w(kxkFuebT&OX%{JPau*9pC3Tz8T?UVhN(E*bIEUw(LeGD4wC zu27SFR@(DD;GG!6a#;$Wk`5@B!g1~88?HG8egn8(A4J^he?>_n`+sC_!B~hn-pYHm z3diH@PAVs&*_Cfxa{6QoBEsgad#7J6B=dGmLK8Zj8yYKZRhCCvZ$*dQEQ!h(rZ%qy zRw=`-G4DK$Q?sXyb~hic9r%{Dy34%d?38?tLC+>u>h!aSAZ&AS<^$)1jj@3$ zELKxzjo)kPBJS?(MwI_C*4Wh?EqX`7JHnjN=>eWA zYg{sn#Es;~gh^OE>J;Sf)0{0QM^nI*7GG?9tTb!@cc#vmp?B`ww0uXAqd?`A9XO2H zlYs#lmd~{<^0b=otfwYReKL<}E6qA6>#~hJpazyp*Keo?d#*RVPFWCJx-azg4w&8C8u1MhQXU`js?p)yUd@>awV7O}dqw z@nWMsb*0|O%Fptf&&T9tUA_HAD2k^dAXOfG>C1npVzW-a{1nEjm|2w&GO`&gkrvHrFhNf_l|TD(|aq( zv?&pxHOYmr$N zs1xhE;hIQ}s%~5M(H4keVDBe_C` z>kgjcDQbt8xM^!ngT=15za+M`Qy<`Kxz_-W56U0qoz$i$j4i(dOReZkw2;U$N{M ztw)~?&|l*4Lom>7kj|Pde>_%ke@WwE_w+k4A+MEQD0yhUOCDaUcJ?U-!SA)pRbwbq z9f8oKG+viV!zU&wloz=ci;c~EnnvK{i=xg%YXweEl3az5^b2ZM`er~Y= z6Qd`5j97e$d|~Pa7(Bx-TlPwu`a@GWBtAx4b2%)IHeHo}N186;x83}&#oVO7kTMV0 z7ZHBfc7k}*;3hJ*3@xakKyrSv&u9~l+1GIijN!1kX?4Q9(zgpY`Wd!*^J{BKGehl;oA`?0NVEk4qvWuIF&a1J7qP>StK=I1;UD{jCe<3A|*fOO@u= zFHm5|{6le^==f2(uD)pKnM+Uy8o%5pUTX$pxL$FwZm~h|E6JVmb%7~*kfjk7fHENBG5P})jG#Vfz#An_dW zGbiz4&i=G;9cX$zQ*}(zzPHH9C9v0)cnW-j&L|nZI{d9Vjb8me_(FihF>`Xcst^D9 zA*+_s{e=18wBO13Iyu>ZLul?$YHA#N8ftc5Ey>3`Q(1*lxP;3h4pPh_>8u3;il5-d zE%6TpB}I?TXBpGlkOQTd7n){~p-jBQXgCTFRs0f^kMX3hlx`T>jL*(=Fo@jEjg8PD zIx#CIwHunKUrH#4dBDyBu&rMejfa;s8XE6C-p16VV7tS#bJ?s>(0GdvXMehzhn7m^+Hi&;;%^d71S45X zxbr&!?$z{HJtaTR&bLcVIsBuyXpJi|HAePzK7{IV;8s8=X)!#96j>bb^_l-Gm4e7S zec!p~eaAC>jpdNGkH_>Ah3a+anZg> z9KPnH{93-htxUgQFTx|dpPBJ31V1ay84p5Kg5m@P)!aHWxxtjXr0V|hUx-lw<}))I zt;Rlwxkb)hjFXj73f(o~`=-@p(IR^kxfBO7b6pT&Yd}#KU79FwXQa}tkIH7ju`|Zx`2J70g2LI93Wk1sQi4{Pa5v`jO78gW`F}*%8Nx!0-ZB(sK?(ooJ z>$$8m=F=LEn7k!LdByS8Wng&n7{kF&PIWGqnO}AXg`$WQ8ijOl2i^u&Ff_UGIE){F za+#JyVm*euM1k|%9)GA)^yUcB`;{a#!*65=I4sx8O-1A$_Yv3}&$M_e8NnbI?LCUa z1nV^=XYwF?e7`r>*RIMO6?}i=#0wV-jUzrw$Xy*6kl&svDVn{C>_VRfYueqTw5699 zj0>b5z1?q=BO+C1HF)O(Xn012hUoRIiWSG@80`F9ut7Z0xJqA_HWh6RO@*FHJ9y~@ z&Dp6;;J$}K+2-q#Jv*FiQ@=`!NjVJ-ON*Jmkb%tb^E@&$GD~*1Ud%?wcDt&#%-Br( z&Qf>SSt;OXwDaHx##N{u&n$bAqaNBn25UJzMr(}c@;*2DkW5`CV}azGd4K;56Sfb@FB z>JB1V;(%u=@2}K*KP;3uqu6fHig5)!dTQPa*M}<-?DlM!h%r zxYA8m%CB>jJ06c&mUr2eic3iO+ju1$PySePL0t%?Z<@~T*+hm*o zEE=$A(&tA-?B@qkx((g6j-DaDiI@*A`&uCC@x*bQ6r?=JI6l?Fq_V5yOA)v09Iuvp z6>5^!gUiqgJ?=aeSJ3ip;vVf8o!*|_#?B-_u-rdGzw}`Z|0ol_3H&d!W(=cf3`62 zp{l^iSwkJ`CeZ22DrAzqFg={(14Q*pNzU0qMX_afKbHFZEa8yqS}{x4Q1v{K5XB8T z^a8JGKS!YFS-HKKp=wb6M=z^5f+v)ltHBNp#T)8}^ZG@zCDWyb8~gqieM?faGxKJe zJgMI~joCMtg2dMoi>@vB)8W{WLO995^9%zeCW}eccujoCOps``%2zz5=T_Xxu1Yvw zLG-(bc`{HXTaY`LSboE5GQAKik1JEcp^RtG=%qOP(5a959qCNqh(R!O3>bb?>J2F6 zkoTd*0~;}bxa!N7HA9-kC`fKG8pLGt&9k=!k$k}tw0lY}sRM%!@#&=8R5kEbHGejh z>7qL|d_Xqk8@bu%Tf1rM7;itc)7t1imnq(e`)M5A(5WY?>_z!t z?`5+*KeFm6|ASylxo=xfNfI7E(9FqUwjD@(Ni;ItNmZd7I7eY2ug)KNNGL*kN@F0sqLU~X|)TcEr`Cf@|{NaNzJ{GBxzW@ z{fGjl+DWdbKs+8|GNQwflOGRI{AtnPWIa{EcY#<*pDu$%?&UTr24%8&wLiH(f-oeC z01%x{4UDMK+u@SDp5ZWBWN^JjRlL>*Y(%Og7gttR7B;Hx-c=*K8VmRdg&I9CMElV= zPXC+cSkP|IbsLTpPynI?L5B(V9+hSvAthQP#F%l_h5E`9s9C2d?W0-XMzW1KF)Gun~ zwaj#fa1@#T{b)H$UI-(@@I~7+y5D&A{$;tV6Ag*0p$Clt?B?BaQYG<1`3iL~azKgG zRgH>%t2XRW4hiXm(I`?*a{PrZ<`>vgW!^T2&LNMbo8O4B)YLUMb__=A1>z|TO#H4p zmou#EG7iPYD%D(q+rY_W_?(|2%sP`;@z@W;{{m3MXwD0Vq>DQb!Go(INK*PT-WGGFnuNjDTz z&6{W{fIOtY#89Gro>U`LWr<72r^54J`~)J$>cP}s!~9xP{H(E2sgy!Qp9`(Jxm>&n zl(h&VAPfXoe2Yf7xw-O>&T8rY^xOEhA$&GBVl4G?`zx=l(9Esk0bbn?x7(0*l*3Q& zW>+&Mhf5)D2zy`O8o_mE0aaPsRm5x)6g*Apt4$=_%FVdX$b5(6-bl_{-0N@}ShFf8 zL&_7e-yX1^1;Jd}dUtQHz+L;`Dt$@&J9jNf4GOUHWHsDxElb>HT%wl=V<6^R}#ULcrrZ931f>~f+jm|$I?07^DgT+TjW7#=P4uw!GDN?PWi;!_=zY9r(W4U_XUa#}sq8cvxu>||Ak5?iY%w>VIBvZV=v!}5*vyDY zX)gadU}mrObOCL0yDN-ZsEP zt|T*dNr7MqqqE{VGP>AeXbkB+)z>OkVI0@i%ek-ga)$ zXhJxj#1z40*igv#>lL8d6#u2kC;u0o&8< zfJ?b8yT$JN#o0Dey^)5}s>hn%H!xo$*`5i|!#c-Po(W9`XU3`M0_ox41@fZFPvK!p#)Hle2D1pk zLt5t2wm3WrpM|~Zg_jzX=Qc0qu zcWln{(?PuSLK1w836Pq}_`~N<>jzD4=Q!CUYQE&uU#K1lgrodfVh=t&V2+mCXqT#-t!l&E#YOMasL8=fp{;Cu-#G^tNM0?gFn}pF0S66D`AHsaWUa>?;KGC)O zZKJnksS-$Km@k=;pI#ivM0*4~daZm69po&FF)5}GcxtBMc782ZQF{{*k3R-=HIu&= zy4+Y+Jxo)iZIJ&n9aBci8!2ZJiUcodq&fFPH*0nM3B$zxeC;Tq^6%sjot}CV{nR_N zyK;meL$@ZKWNM&|1eL6m5rljaP^0M>n%@d>y#o$y$g%iu9hN1YnNC}zjCleMg*Q_m zW#QlCrItL1iz4v%M_T}dCTtD=?Cs9fj2|yVN&|z^!-IaX^mOVwT^eH$omHJ?h4I#} z)JPtcwh`iJQ#Xo=luJH$@BnI+R{o_vCnlZrS%NSFzT7DOa>bXzI#{S)hmU(PGpPsc zm^OWqp5iD*l!{zGo+FV#C*_HZj9jpqWcK*;X>^y=m-}xnA#XGiF4R%}a(%V*INUiz zzu}3KHA{X-%*b6MO(x|k${RV&u$zH0x7i%zb{xohdVHtReAx2}Nl&#+gFg|v!KHnj z1nXpbs6XX(jJ|LhFbM4r>3*4$r66bF`fqx#ZgRS3d0{&8^OdXZE%pu2Y4m{ac6sf< zdqlJhrdwZh&291i24p1v_S-u!`lOb~vcX%Xp5tDZU*!!>ImSo^ z+Hyx-U6c6nXk_WBIZb+ksZ#j-T-y2@)9?x7-`TgX>o~};dOAFpyb>oNv4d4gLUv~5 zCRE4@CRdlUPU{Cj98|ntOJ$(%+iPjw0|}qEp>|ZyncBXdQD`Sj;K38~nHe zzEwt7b6HC8ic^kh)L7s3-RSaDj)KDDE);{5MM^mDR+xZZp^l>irWaZoFMynqMhfV#T|4A`m2Yi_cM3TqL~ zu=O}GD0_r7w$mVXXQpeNVFV{w!H%+YE;yP)xvPL60=4egojjX7O<7qe{S(&v@}G<+ z(~)_|wX|c&ec@;8HW4k%ryJjrNCPxWa$8(K05rx(SxOv2`$2$R@Hz+n9`|gS!+)vLm1fNRbqR4V1ZBl!W@9^@&3;f zMhtHtAxHmF((L^+nEvm7l>L{TwWaSyiSiQkM@0o!z~1j% z!XRpJSP$hneX*^HY-IzilvIZp20gzh-AdxklaQB?thy$pzsg=JN1#dI^E(5KKpA5C zl*p^1SB$1`XV-gEAxCGW8BMArSpwkD*Zne^j71_7Zo+l0j)kNf@GlI6x5f&eT8AX-yEreesI7=#u0gz z`!y1tl;iY+r?mQX)?zHLLb7NC`f4T4zb^*AbcMz=E))x6zKu*9G>%|to1FgAf z%BxYEcXU*X^9cieL)raJo~ToBB+G%bRP!`=(e}t36=&vJJG}Br;ls^quLS&Yen$IK zz=s*&IWyp3*bdAV&MkGgw$&Z5zVlJ}10y z*WLoDJ?QLCG-()>m7{7y_3l%^&c_;mfe!~ICD=Uw;80X+TcmIA7fI1zluI#erYlRy z;)Mi!0OfhnWr%p2Yb1ofhekx2)I7ElP_udFMw~TUKp=AtzM*08X{`Es>K@~`fmA{+ zC$~tH@NjTuBTBGI2coSEkhaVw^o^o(aqPG1yjcvPm{7SlL)?U9))2K~>+MXa9#L~V zeRy~mhjK46ua&}mwdZL)jIt4b$^nd+^=K+gsm!?l{>kkb2~GMabOvwSTY|^%pxy-K z&K$xkE?Rh6iH3?J(e}pRcFv=`YASR20jxVDBTgZ@O9J)}$=_-IqlV*+2BN+l2}jcj zf%3zg=rBr{Hm+7XS_{FNJdMU1z4-j%y=dw-Zro?QyV2_}BA=+)Oea9_y`0nmt=l52 zc(Fb9>eQT{-c;9az|JaaMDyBvIl@xj`;%{yQP(;w%ToSAj#&Oe^y|gN&)&(?tNxjT zp_JvU*T1$*?5LUzg7n!OX?ih%bXT&nu8+BLvA!K^-QpE^)n~khJ^m$=uRD-H@Irf~ z^bJ%x96vkfO_wefV6n%KBdA`JGR;1s5s12AR>Sunyw)Md`v3_6VKy}Ov=<#=*R;)5` zZyhf%T2z0c{FMG0KQK0U!z_f2ppgKQQQxJj`O)wta9`CgzHSKQ&Xj=xxO1Cx@84kjb&xU@^QF$G+Yz} zFUYG+t`5{TIc#|6Z>K)%^Y3-(GQE<)k=FXhjo%5UmQf zj|O%Huwk#g=|ThfA?ims+Y5F0e2ioWR^%SyyT9=mM?I3W{_W0D&f8JkAdRVIOJ2r^ z`2)f~J~x1=Mym1ItplfA!xb9c8hw73QVi;wVXWY8Eng+DfX#}m^E=WzOJ%e^(N(?S zRY}9F1VFa#f1J^O<2l;`j?yiX49*V1WYhM_CKeIwhb2Fa;m530&CM!4i9qam=?P35 zAJ3dF3-SncojC5i*LT^_X8%fdp+LQ29!NZ9S$0e1-JBWSI`6G8B3n5c9TwdZ{P;0T zvh^UaMxWL%!jHNRl0VqrE8ASu$fr1-jG)8U!>j5+EXdRF)RK*Zbr@SZ%Jf%Yd3I4f z1}tMDyxvRSo{2J+I2ki$l;FZ;sW3YE=^}VfS3uvu;$p=%+wx-OqR@|h^?)SW;)|dN z*>^U9I#2FmRe`)x-Enb;5HP6PBKy)@tf5h?r1weR2n zoUYqzlIOY(y*(3FwFy*V*M&X zduo^V5%O8rglbP(J34|DgMYhAKL_TJuQ^e6@LUn^={BR}ff6Lzaa)#}F z+f98CEoHg9-!smUO2R|RJB0>0TOETN(0mAij=htjH=U$)u=5r?Ob(WDI1+D+Gl)k!J16wyj+wFPPZ*#?3U z56=Y2@TsYEZ$?kFG#mmmDTOBxO?#H}H3HSERLc#aIa71fIj)vA)eNZW8kDSA{)C*) zX(u#6rE(V3q|qQjooqx(wkS{jDd%N3Iw(Nc=2{kh7X2NF+E!7HP8YSS(YpJC|xVJ|svD{Q7Jf8a41`XnFHm zPpzVU(Y`?w)c32j#_s#G5^s}bwuG<7DehZ4`0B&K9f>;ntBXy*obsvc;tf`1>ZC>3 zP>8-T@fSkQ8{ch+&mD0t@F2r4W0(qr)<5Kz^4@y<%w&Qd8oGE=76xPaYhX?zuMM#p zM=FI3;6ly-bM>EqSdoCeh!bP+Gv`uIMrRnP%N|ULBnv>(x_*%Bc920>xub}n6|l*W zEr9*rcxNizqS4Z58H2j(GYV5Ir{=Cz;=_rfzsY}ij#xgMe9L6enVGh~{pWb_u;8K# z8VM)k$x*BnDYEo{Vf&0`o4S~r?g!%#^sd&dw&GCTb->fn^u0+vnvQ|XN%@h`y|Tf7 zX(-DWx>(@$B6KwK7zz$}3>f~C!kw~dciK~#A%A$mmWHvU!14~{M^wmzP33#*v}cn< z6JLWgkE?4hJFkDOSs zSb0unt@Hz~n7%(el;MB$r^z>PljaivbS*rKO&1+BbllhS9wyMGbfX}k(V=ky3@zs_ zFcbc#*Ee&c$OS%3X;#OM+U(MA;TbUb6v>;>{?o|V4o>*nf!qaj>&$bUPJ4>y+XO|? z?hWC%w00vJWEOT+8P_-s8%{y*=xN6HjDqI$0!)VLX+Ia|L1zry)~boZXzY*?RN~SX zn4``*Lt#^gvS;;;1(y0vqVyW$*y26q@xvdIQ2!)<(?PQGr;bwkY--CmTNPxm!ER9- z&OmYwJta*U3OluF^iC_!_EW1EBi2_ji%RZzAxSUlj@O&Rb5Ww#R;RBhGJH0fAfN9@Ppdx!%Qmf#~F^Q45%L*ZQOWg0Y%2eCL0 zN|tihJWILD{QJhjy|HWK78+x^+f}l^LZ{LR?A7tKZ=s0y5SjnIrDo;tG>kf82`aXK z`H)Mv+k`-*q8OE&E6smZ{JDYRPgcNLs%TTtU~N=ZkE)uhAIUyp@r#R86{nc@nr5^Y zIEL9%cso_#(|%1yY^r&-BZ<8`V>BhSK!{&OBv`}XX^#s5PPsSUWQY^872Yb;AZ0_h_BUp{)Q7IyQ4NUy$xi>`8Kcb>2Q1(tR<-=k1e%rVkjCgpYG!Ll-%tOYqsrii! zXQMZ*!>2WFO^&MOM0F&lT6Sje+@B_$pK`}Z13BA%bon^n)GI+o$Uw zLWZ4h{%RYg`YWGWYBKg)m}g0-xzhN*`=HGwhsY{G6Oop43NFR`LEqtm<&cOmEo{JjTPLsmy`o%VefpWQw*_Ex{nNjOS_ zyl<%XTJl`FGGY(bbX)4jo%ani z{zCK^ourW!C3K}VT*&c^H@b&pa{9F(51m)%ZM)cgG{`A5H5{o7V$<`VbI7qFR1k?L(W0#K`bAldwP2oQI<2 z-f(>|G7H`Jh`_+!D{ZmLsk54cfk2YirjV#YEBhSk+jW|?b;o4XPqm)N@7J$N0#OPJPih)VT#0X24tYh{L6p;bLaPA4qge@%_o?z+Vy4A z=qI>j%Q|p8vtM8oP}!Hh*6;R35P7q89YdcWbNM$X9IjVTD){n|qbI}jp9Y7I${lxI zaN3AYqOAx5xgtbAEb#xxo&iJ+UCIfm7`AuItQPbXhn+5 zrTi!C$bki8?#o9ij1lU(%}&*W*|A?to_Yvl{eC>XIHpAyv|9P;{Chg!Syhc z!r;oax%kq3kj~#g+>Iiv08nOd|~S_BqFXIr`Nrn|a103x7M{ zqVm-!DKD}~yfiyVo72IWR~hVo=?y;(Z7?%7W7v1Z5x3drAenha=wt6<*|xFWo}HQ{ zuj5)J2<^m2+}S^UHYS=F`fd!2{IFD2{1MokY$ZzN3@fs+()HuCmno5(cgMaVuOsOvLz8pE4vGar`hJo;IwY%83tr=xi@!G2s1 zP?YrOCYDIRyc7m-oiy%!C<^*uW2#YXbvBc&WFRm?6(&O(q~r0sq`qD26l1v4vwkhj zDpk+X>VA||Ox&DE*sg)|BMH?B4xc!&l52Zlug|3aJ6H3+3hV#ncWwVgH|GKPuiL>y z%xr6t8wl#M;>d>o7MdG$47jyX6{Fp{#bB^eA8k;|+>fTt4KS|zZGKHa# zs&8*=8Lr;oLok!7!@*>$aA$h6M{Z^+*Ii%uL{<0f+`AX@*(+t^N646Sa~z2D1`)3| ziTwCet5jz_GDHM0^%eYT^l(^3eL+*zz6mcLfYNucSb9-uXM8X7RVAE`7`bc2?TX#W zB!&)nKO1o_CP22NJ9;@FmXnlo6qD_8gijf!$)jLp^Og4f0Ya#&&%P&B0?+kYfCG4^ z>q8gkB>i{9icZr{F@DUpo{pLiR>oG=Lwo11;~Nweu}m-n73}?s^CS6*i@{>OOU4@I z<9t=U4?8d#H_oyBR}~J!fF$!3-)zy%&t4T<{m$e_qZNLPv$H?6in1)@^~!MoJfzMi zgD6La+Nw^Zv9VC6eaXY6PXluDNk?ifpOu{}QC0`ol00GD*ESPb|vj!^fw3Wen(wP;uDw_Hw7 z?yi`hUzxh-QsSRdpw$GvQ&5jrz2sOX)p01(i2fzAW)8OC^s87wMObIGDPfqkxjL0!f*0&X^cr>56CymJzys}r$6hnO@}OwVrb8x^JvkboOHBV=c_BVu$ALu!u$oUL5yfi=7W|RfT*(dp47g; z6on~gg)8Sh<&&6)N+ev`5~e{;v%y8HrHNqfcE(wP@%#cwXv|NN{znBBg?HWEk&vg1%B??&e^e?}#f z(J>9j9;n4K{T)AM^Y^YM71!ELm@K}~?I|vPR}fD9^E#X2?Y|H^;utwF(B|il$DdG7 z5=T?Yj?}1ARwSMt-iz-PnjXjE7P!uXqOEWv4Hc{uGqSRZv~ElapJeA?iu*9|U8w}I z+oIBS2;L+j;H&gDW^jvylXmAwr2W~f+yQ62!l4H%2HY2Buni2VN6T$7>~3 znwwu>jJc(H?t~uTA)S69@jy3M#`oQvMu~>ITN}j(#0n+Kg|>>KlW;?CULA>~>Hr<7 zqXFu(UsIHHDsILPKRo_w#*d* zO{hXOg+fZnJoB##&2zAR9G6eGg_TpIaITB97pgmLXLBRB+~gfblh#0=03_+O2POiB zx3T_vR53pU0xO_eTDVn53)==z++@LF;PVyVoP39zqO)jxmEs_>SSUkJWB3+V4ozrP zDLwQUgzRYTFAwT+{WvFoc5>i$H}U3{z{I8O+P*L51K9{dFyOXtm;1Jl$Nu@KL6}x2 zd^F257o;ZbM6B)Lhc1F@Nf`FoL!O9|9ZdoVv^)UJ6REohzvvX=?y_$LG5py#~?{w^ru`l0keLbM%CboV>`QiMz8b?JdaF?F`g|OXm3b|#j_Ic|g zdp+|daadB;PsSi^U!b-YU#~sZ6IFw~3wTv_U23uvr(}LCAV(j>YS^8QF)&>J&Nu(fPzKE*OplTWqXMbl$8FC?aaV2J)YfUvVj^DRT zD8&kC2@$`+PyNm5R;xqAN!_s1o)0EEEMc*O-W(a_%}DbnUj_}RuIF+&>^1Mm&tsiy zpkOvgEbFP4;x{&m;5L!$L`uf%ybcs0Rp2f%xIqg<66z;j@Qpa{x=kp;ii;&L?|qF^ zU&*!lRxL1yr<*r{Yc-HK0|Oz>UI$N5@;`1$po8}vIgLW^c*j0;<1kaWPD>9BPEG?2 zIRTAZK2GX@ylTyfJ?$gE-Bq+ZH5=WmLGs8%Uxg~6cbRA^f;DjG8Z5EpH9&jV^5o9; zGyM9rz8$EkI;$RTn1F&Sz9{5LC(e{VY7cR|wDG>`GS6f|l|AuSsH*8WLQ1XN0Ch)q zr1bg^@S?UaR7Czj$D4Yj#4l2W+vKJeoNX23^O?E=BJewGbn6K^1y5|Cc ze0S!Gfvfx;m;zPn?(KANS;5XVOS#?qO~dD%Mk{?IKdx7{mYmF=?k;nyVwD#n9it7e zxX~x7=^-wN*wJKmg$Y|OuY^uf$Js{UT37sNRI%Dv=jLoYm%R2Sc7h1jtoMKi7ar7> zBHe_8G5mIGeX&LGQw6>Sd08rNB@|9*4+fECS2-pMMPa7ma6erzy*(cJ)=177KOl zMHp$qt#$CWdSZv9b<|3Bgf)oLQ*@ju@&!2g+1op-lAKxND@&GKz_`hL z_n`WF{u~$s@4_?3lL$*n8mV_a6Y)?8-&B_Bla9KRZC&7qeiiWb_V%c$qq5u9cy*i{ zo7o>{LZi=?i0GSdS)UUqcYxHqF?4$RwcNGh^r&A(m;1GsU^yz>6tiifF{4xUgA{Yz z%RJPfpeh0E!;=QC-~K`pO=MC@N{IxYAUWg1jXKaNL?*duR0ePN$u1cIsqOEukj~v0 z_F=$&H|>%8pWQG6TZ$uAR6r<1j+oaT+*V~FxFFQj=9QfUHm;8ep?)j8+&DNG z=Trv_YdOorYClSQJk#a&B>FZ7E+0`;2~|^=J?H#rYxD!=Bg=}pfG_+;rU>k25OLBQ zN`^t_BwjDWIIBwBa9|7ZXMvxaLj7tWI*@M!gjZMM{?E{(l;L37Ozua@TK_xq5L-G9dG_Hs-?L7Eh52EcaWMhUt;Oj`AvL zSdj#~=7F@p5gHC~C;iD>@p0Pzddkgd>2(UYq@=I9LceUhDl5yfKHwhsZza(F#1dpv zvz_?yTOzNqd&vqPd@VP(LK^_OtfV{j)W0s6Cwn5ovXzd3SL?YL86}*$;o9^Vn5@}C z*yc^5V6;B@q8#N+XjOy~PElG{YK!B007NdTiyb+L(LSi3_$>U# zx<}O7Xd*ZdR?#)gfFunp1h)kr{)G%p{Dtsb6pxPt2$WT1uYu!rwD>GDyc zIODIL{<7)P<7?fL0Uz$WQ?SEgURm$PpjS>lk!6zGrrRGl*;FwPGNhi`U&uXKUf(&% zdHak|kLMR9E@4~<19Sd9wL8Pao+Yd?AijUDl_Y(zmzgE07qjWA;t_&$lL{u@SI_EM z(jk3mSw`iDd$}EI;L4b87@C;@yDKVJdSOR|qJi!zf+;5=^YUA_ zh52cjpyHyd+Wn{)U@!1*O%7gWeEE#ihT@yF=+nVf{0ZjtQsZ4YXw&a^(`fK^?no7J zr+PBC^za=(#|!6JGwu?|6D?8W8K7BE0EP4e_3zM^7JPI&oaTf<0yoZV23alYP?V5C zjnMIl1}Vd@aea`OW1BYoO)!IC*L6K+kNja;N%(g_DOH#uh)N@W!XZ}^oBmrM`Y-Y8 z$aX7=+QiJrfg*$CcUU#A)-z;-Br?$FL}V2P_N2c6;9#}4>-s)8XkIJae(RVv-b*6f z8`WL5qL48hrv_yl+sguu=ocI*vj(9e@w1%YzNmlf(NRafwO&^;nnKNcg8WhAZ@T!1 zJFI-V2E&nW)^<_wgjC*KWkr%gd%^r1>R)@Q`k3F2#d0h`j`o)pDHGBxz#Cx`?a)e2 z9$62YTA57;4CG(PI{p@R?($`kaebcY?2>n4X5gdnC*!?14&$tElXUhEz(G7q1=w4o z=(C^3o5WGC+?ebj%Z^mmr~x}(iC{_6-8Jw)mOI1vZ?HrLM3zckf7}Hit2i*S^#kAq ziUZDzzy-+;5jt@}@yTZ}&7-mif#5q~fp!U1d^&;If%-oi4K0f`q2^{<_95lt7?g0$ z2ji%D8vc79--@~r>8d+3KFbFwu4lu&Q+IK8h-)Ab%g0o=L^Xa$OjFM#lOX}k-9Z7X z3C3`5Y6G#-j7<)*oX?e~MiW!$mA)n`2(aG;je3LsvOY8a>s0fv+NeIa-u=L^^dmPY%VyZ8;wgOtBL${JaLnp-M%CKb%6Xv_KK!sm!k4K-1lMKoGF`( zTn%zr)QO1=T5DT0eSyr>!g}+{1o@xQ!++Tnk={8fX3FhWH1dTs0@qQi1=Y85-nK4* z6k@#fo%iwL(xORPUc3zxF~M~4MlW?`F=2?ygJB?mD)6ybPcZq`l`%>4vRyf>URMx@ zs5=WPC+W_td;)Yqi8s4Gdnu0UX>7un0&0kF>WAi~3vJ#fJ`Q8KiTlp`@jxTN;K2 zC6o{ll$0)E2!|mgq-*Gw4wVi?7!UzLNkPCbspR)8_Bs3P^Zw#J?>YMqugeRVnKhra z))V)0Klk&I@zI9R;@|_!wrEx4Rbp^42QEMUg-u-9(GLg)&}%kx&9!F)AEwk5_`+sN zQ`^i`7e+rSm!`JVAZzNeF+mL zNFJs7)P=s458a3xqGn=nkH)@~Qu)o%^$N-qh2&eg1gUfxd$|+ikbB`3_ZRUKDZ|SF z)Yl1i-pUn`;Ofd4&uFLfA}0nvyx^IkY<7M*2%#z;{U;{^n2GqSU`vLstm7kYr)gow zRCI*{Es7i$dkKEE^73LWO4Xaw<_^#7%A6{!K2=2KwEl^1vS1(M%XznxHGtWlR@B7Z zZ*Pm5aV=TjRu)8dMIxz{hE_$4)k#YA7l5Q~P9TMDd2f?e+9HGJoxfhDCDA^qx6}8# ze^-2Xl7FcO0AF)nBlU;@ZsJ-`!N_v{d2Ku*PpCG0)X$DR+f$YT*xt}k*M*Vwy3^5a zf$=w8#_s7Y9byN>Wx9{J2W&`F-aG0{gDkM>#Qm^waM|p@Gq;M5J%?J5>(g3w-7Q&f zD-`^#x<6Y8P=?I>e4p@nmDPOelbwe22kpYWUGnZasF`xlYLNDsr@Apd?cOXGgf~X> zd{`%qhZYF`z>PZ5;SdZ!krTZK`Bv7CH#fKZ;{(a=f|uu!+4FqPuHWP@o~A0 zkrOt@_}S6VeAzS9QWPsN5_F)r%y3b zTs7eK_uf(zt83df{!r@d>%YOc9%6?06B*A?wAf^ba&d2P1T>-zl4y(*$<{~uie#m_ zQB!%PAcJ1Q@7;upVUGZ8S;H#qUF;sX|#>l8W#b~UYF_ssom*<}jjX3DZj*o~U2lx1E9iMbU z$+sHO^hn&bX9QI$aDen5GJW{XYXgYBC~5QCunmFp&KTlK7^lq0BC2FhKxg{BFBh0| zLDnr8yu5NzW6TOIjlr8^&=euG)^16wb8PvAu|7{e2l)x32}dc*k)NG_3e|hC28Ua> zWRGY1XO;DmQsUVojdci#NgyfM!mmNOtz^V=Rlmcoyk8ht1lLt=)2?}Wa+$qKaf{YT zK`eUL@b7|v;q8oXA1xQr)_)0`#`CSA-Jks;oN>vEf%(Rp_ql? zvuatB_PiDf$(NVhUDb0hoi}0$h>v`!lM`i3pa)rY^S}brAsG1Lpz?2s5h~=_SRmQ% z;4B`4%c84q5BC)!bcFMXS2*%BM-UV3&A8~G+|EtuBmj}s z1L(U^!cqF8c{eAb8;rHFi!Z-Yw;}nSy|M7UC5?p2n`D?Dg28c9ohrucf@42+R5!-7 zpiK7W0QMM<0l~wk>?2dm?$D)ao20*RJqzVXGmT?L;-!XMg1DeL1^p+c@91z*)Te-K z^OKNE@KlQ=yaZh4Coth}8Bpw`yH-_v5)V}57@vFTgKZzZC|x)E6c?*&MBj+IwA1S_ z!4$>(bxlS-0_O5RLAo&?<^6uZ(qi;=O5-6EYJ5MhRY`6fGQSO5%MGQ%;rUb=Q^H2c zcO^O;QUGC&F9+}T*RLxd#Gu8|dElg0zG*v3BKgJ;uO#`n5M-?+<42IGKS-@#TU}*0 z>2kE?a=w0nUK8^+=JFoU)^LloxG-hW{)6FfF#R=p7Cb90^|Z8tx{x!d8o5{Ml9nMM z*oRgE(c0u=;0Hh3k61^_mwNf#X`QhWy-kaIdmPOwsY;5PL^m@*VZTOX-S(LO^;Ecp9bE^87p|kL2e1IJiXL>AA`K#LxzMpHao!n$C z*(~+SL3?9^E9Q%~vuHXh*i@J_N-xdq4I9OPA_*~N2KS+ckp-#CvyszY zNY17AVZhNZTd^`7epk#Q)|h~Rk2E=L8N@Eq$_<@o;|K5E?okIlvHYsW5-B#!hOBpH z))lRx6`pYm)>(i)A4)Bulow;Ts;uIr;gw)sT|nY%Ft_cSnBog#<){H_*SemUHy`#& zQa8tf=F4h*S_dIFSz`4Xp)u~~BwZ8l-{i<%UiR3Hz^dn`%ZRhy6t9{UFTIj*mD)x9i(5#!s%ezaKA|Gr-;i~n(7i5_Qz}y7 zM)tsB3D==5H`q)@#)xl?{CO}J1`7P(s`>D)uh?KuyL}>WS%9}-HGbM6a<(8hhFl5` zU}N_y@A&L_1y2>t!U@hGC_Y@=!^&IIif-Y=RR;ym{zBb|kowJoU%EJsBnkuACL2Bk zqF|MskjI$vor}`hH4ioTVB(_HJ&<|F&iWzglsSqkHKv$G8v~i@GzZdj2+-C3|C<~D z(qe4_XQOPa2CcIKF&G`NHvO2pT+Y;?D5ByzM=+G-$r$)jz3OnhP4te^u!o@mk*_-C z^mV70x5>*eSmPYj7aXS~h7;&_9vHui-e-|sSZ@cmCP~ZT<#}5Q+$|tnTBgD5pO&XZ zc{Z{KG_f>>+|+6>uPa;7Qs%Z8Y{CEh#I+R^#|?PV%_Q19*cXDRq{*5bCCeEI z?{$|CekpkwCve^}jEJHc#Z70nz+%lS>Od~(5Nhu-Oow9Ub1A?LW%MrOZILsi0AI^w z&Eu|=UrctRX@-yne)iQcPAhPiWme`H4%0A8x)3zqDs|C5RLSkD7PLkRd@loo-u4M( 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

J!EtesOU?z4ciLvIYJd4E)X33sg9UymoR0YP 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

RZura`~#ok(P3z^Q|8i)L%XN8$tp+8)8Jd|7b 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 Date: Sun, 9 Sep 2018 14:40:23 -0700 Subject: [PATCH 178/474] malloc/free -> new/delete[] --- src/Image.cc | 25 ++++++++++++------------- src/Image.h | 2 +- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/Image.cc b/src/Image.cc index 5a84dbed1..12880642e 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -200,8 +200,8 @@ Image::clearData() { _surface = NULL; } - free(_data); - _data = NULL; + delete[] _data; + _data = nullptr; free(filename); filename = NULL; @@ -356,7 +356,7 @@ Image::readPNG(void *c, uint8_t *data, unsigned int len) { Image::Image() { filename = NULL; - _data = NULL; + _data = nullptr; _data_len = 0; _surface = NULL; width = height = 0; @@ -613,7 +613,7 @@ Image::loadGIFFromBuffer(uint8_t *buf, unsigned len) { return CAIRO_STATUS_INVALID_SIZE; } - uint8_t *data = (uint8_t *) malloc(naturalWidth * naturalHeight * 4); + uint8_t *data = new uint8_t[naturalWidth * naturalHeight * 4]; if (!data) { GIF_CLOSE_FILE(gif); this->errorInfo.set(NULL, "malloc", errno); @@ -719,7 +719,7 @@ Image::loadGIFFromBuffer(uint8_t *buf, unsigned len) { cairo_status_t status = cairo_surface_status(_surface); if (status) { - free(data); + delete[] data; return status; } @@ -798,7 +798,7 @@ cairo_status_t Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args) { cairo_status_t status = CAIRO_STATUS_SUCCESS; - uint8_t *data = (uint8_t *) malloc(naturalWidth * naturalHeight * 4); + uint8_t *data = new uint8_t[naturalWidth * naturalHeight * 4]; if (!data) { jpeg_abort_decompress(args); jpeg_destroy_decompress(args); @@ -806,7 +806,7 @@ Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args) { return CAIRO_STATUS_NO_MEMORY; } - uint8_t *src = (uint8_t *) malloc(naturalWidth * args->output_components); + uint8_t *src = new uint8_t[naturalWidth * args->output_components]; if (!src) { free(data); jpeg_abort_decompress(args); @@ -858,14 +858,13 @@ Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args) { jpeg_destroy_decompress(args); status = cairo_surface_status(_surface); + delete[] src; + if (status) { - free(data); - free(src); + delete[] data; return status; } - free(src); - _data = data; return CAIRO_STATUS_SUCCESS; @@ -930,7 +929,7 @@ Image::decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len) { // Data alloc // 8 pixels per byte using Alpha Channel format to reduce memory requirement. int buf_size = naturalHeight * cairo_format_stride_for_width(CAIRO_FORMAT_A1, naturalWidth); - uint8_t *data = (uint8_t *) malloc(buf_size); + uint8_t *data = new uint8_t[buf_size]; if (!data) { this->errorInfo.set(NULL, "malloc", errno); return CAIRO_STATUS_NO_MEMORY; @@ -950,7 +949,7 @@ Image::decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len) { cairo_status_t status = cairo_surface_status(_surface); if (status) { - free(data); + delete[] data; return status; } diff --git a/src/Image.h b/src/Image.h index 10783809b..585067c48 100644 --- a/src/Image.h +++ b/src/Image.h @@ -117,7 +117,7 @@ class Image: public Nan::ObjectWrap { private: cairo_surface_t *_surface; - uint8_t *_data; + uint8_t *_data = nullptr; int _data_len; #ifdef HAVE_RSVG RsvgHandle *_rsvg; From 5301420f0cd7f65ee60be33a1ec4d9d9fe557cd2 Mon Sep 17 00:00:00 2001 From: Abhishek khanna Date: Sun, 16 Sep 2018 20:45:01 +0530 Subject: [PATCH 179/474] Update Readme with Canvas API link (#1213) * Updated Readme with Canvas API link * Tweak readme documentation text --- Readme.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Readme.md b/Readme.md index bc599c402..7e73a5419 100644 --- a/Readme.md +++ b/Readme.md @@ -519,6 +519,10 @@ Notes and caveats: Examples are placed in _./examples_, be sure to check them out! most produce a png image of the same name, and others such as _live-clock.js_ launch an http server to be viewed in the browser. +## Documentation + +This project is an implementation of the Web Canvas API. For API documentation, please visit: [Mozilla Web Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) + ## Testing If you have not previously, init git submodules: From 1046abfc7ced2a32bdbcee70d87f52f56b20277a Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 19 Sep 2018 16:16:39 +0200 Subject: [PATCH 180/474] Hide Image.prototype.source (#1247) * Hide Image.prototype.source * Add NAN_METHOD --- CHANGELOG.md | 1 + lib/image.js | 30 +++++++++++++++++++++--------- src/Image.cc | 10 +++++++--- src/Image.h | 4 ++-- test/image.test.js | 7 +++++++ 5 files changed, 38 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16a7779a7..a5c80b688 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,7 @@ canvas.createJPEGStream() // new example, provide an error with code 'ENOENT' if setting `img.src` to a path that does not exist.) * Support reading CMYK, YCCK JPEGs. + * Hide `Image.prototype.source` ### Added * Prebuilds (#992) with different libc versions to the prebuilt binary (#1140) diff --git a/lib/image.js b/lib/image.js index f373d849c..33b3d768b 100644 --- a/lib/image.js +++ b/lib/image.js @@ -15,6 +15,13 @@ const Image = module.exports = bindings.Image const http = require("http") const https = require("https") +const proto = Image.prototype; +const _getSource = proto.getSource; +const _setSource = proto.setSource; + +delete proto.getSource; +delete proto.setSource; + Object.defineProperty(Image.prototype, 'src', { /** * src setter. Valid values: @@ -33,8 +40,7 @@ Object.defineProperty(Image.prototype, 'src', { // 'base64' must come before the comma const isBase64 = val.lastIndexOf('base64', commaI) const content = val.slice(commaI + 1) - this.source = Buffer.from(content, isBase64 ? 'base64' : 'utf8') - this._originalSource = val + setSource(this, Buffer.from(content, isBase64 ? 'base64' : 'utf8'), val); } else if (/^\s*https?:\/\//.test(val)) { // remote URL const onerror = err => { if (typeof this.onerror === 'function') { @@ -52,23 +58,20 @@ Object.defineProperty(Image.prototype, 'src', { const buffers = [] res.on('data', buffer => buffers.push(buffer)) res.on('end', () => { - this.source = Buffer.concat(buffers) - this._originalSource = undefined + setSource(this, Buffer.concat(buffers)); }) }).on('error', onerror) } else { // local file path assumed - this.source = val - this._originalSource = undefined + setSource(this, val); } } else if (Buffer.isBuffer(val)) { - this.source = val - this._originalSource = undefined + setSource(this, val); } }, get() { // TODO https://github.com/Automattic/node-canvas/issues/118 - return this._originalSource || this.source; + return getSource(this); }, configurable: true @@ -90,3 +93,12 @@ Image.prototype.inspect = function(){ + (this.complete ? ' complete' : '') + ']'; }; + +function getSource(img){ + return img._originalSource || _getSource.call(img); +} + +function setSource(img, src, origSrc){ + _setSource.call(img, src); + img._originalSource = origSrc; +} diff --git a/src/Image.cc b/src/Image.cc index 12880642e..a5d30cc56 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -56,12 +56,14 @@ Image::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { // Prototype Local proto = ctor->PrototypeTemplate(); - SetProtoAccessor(proto, Nan::New("source").ToLocalChecked(), GetSource, SetSource, ctor); SetProtoAccessor(proto, Nan::New("complete").ToLocalChecked(), GetComplete, NULL, ctor); SetProtoAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, SetWidth, ctor); SetProtoAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, SetHeight, ctor); SetProtoAccessor(proto, Nan::New("naturalWidth").ToLocalChecked(), GetNaturalWidth, NULL, ctor); SetProtoAccessor(proto, Nan::New("naturalHeight").ToLocalChecked(), GetNaturalHeight, NULL, ctor); + + Nan::SetMethod(proto, "getSource", GetSource); + Nan::SetMethod(proto, "setSource", SetSource); #if CAIRO_VERSION_MINOR >= 10 SetProtoAccessor(proto, Nan::New("dataMode").ToLocalChecked(), GetDataMode, SetDataMode, ctor); ctor->Set(Nan::New("MODE_IMAGE").ToLocalChecked(), Nan::New(DATA_IMAGE)); @@ -182,7 +184,7 @@ NAN_SETTER(Image::SetHeight) { * Get src path. */ -NAN_GETTER(Image::GetSource) { +NAN_METHOD(Image::GetSource){ Image *img = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(img->filename ? img->filename : "").ToLocalChecked()); } @@ -222,10 +224,12 @@ Image::clearData() { * Set src path. */ -NAN_SETTER(Image::SetSource) { +NAN_METHOD(Image::SetSource){ Image *img = Nan::ObjectWrap::Unwrap(info.This()); cairo_status_t status = CAIRO_STATUS_READ_ERROR; + Local value = info[0]; + img->clearData(); // Clear errno in case some unrelated previous syscall failed errno = 0; diff --git a/src/Image.h b/src/Image.h index 585067c48..6c0dd49c9 100644 --- a/src/Image.h +++ b/src/Image.h @@ -45,17 +45,17 @@ class Image: public Nan::ObjectWrap { static Nan::Persistent constructor; static void Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target); static NAN_METHOD(New); - static NAN_GETTER(GetSource); static NAN_GETTER(GetComplete); static NAN_GETTER(GetWidth); static NAN_GETTER(GetHeight); static NAN_GETTER(GetNaturalWidth); static NAN_GETTER(GetNaturalHeight); static NAN_GETTER(GetDataMode); - static NAN_SETTER(SetSource); static NAN_SETTER(SetDataMode); static NAN_SETTER(SetWidth); static NAN_SETTER(SetHeight); + static NAN_METHOD(GetSource); + static NAN_METHOD(SetSource); inline uint8_t *data(){ return cairo_image_surface_get_data(_surface); } inline int stride(){ return cairo_image_surface_get_stride(_surface); } static int isPNG(uint8_t *data); diff --git a/test/image.test.js b/test/image.test.js index bac0d6605..138b1c70f 100644 --- a/test/image.test.js +++ b/test/image.test.js @@ -268,4 +268,11 @@ describe('Image', function () { return Promise.all(corruptSources.map(src => loadImage(src).catch(() => null))) }) + + it('does not contain `source` property', function () { + var keys = Reflect.ownKeys(Image.prototype); + assert.ok(!keys.includes('source')); + assert.ok(!keys.includes('getSource')); + assert.ok(!keys.includes('setSource')); + }); }) From af9a503333349ef4e5a208176acdd60974320f2d Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Tue, 25 Sep 2018 12:51:48 -0400 Subject: [PATCH 181/474] update text layout before calculating the extent (#1088, #1239) * update the layout before calculating the extent * update the layout before calculating the extent * forgot the ; * removed ; from tests * remove code duplication * cleanup * Update CanvasRenderingContext2d.cc * fixed header Fixes #1088 --- src/CanvasRenderingContext2d.cc | 24 ++++++++++++++---------- src/CanvasRenderingContext2d.h | 2 +- test/public/tests.js | 16 ++++++++++++++++ 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 911efeca1..14553f01c 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -2045,8 +2045,8 @@ NAN_METHOD(Context2d::Stroke) { */ double -get_text_scale(Context2d *context, char *str, double maxWidth) { - PangoLayout *layout = context->layout(); +get_text_scale(PangoLayout *layout, double maxWidth) { + PangoRectangle logical_rect; pango_layout_get_pixel_extents(layout, NULL, &logical_rect); @@ -2070,9 +2070,13 @@ paintText(const Nan::FunctionCallbackInfo &info, bool stroke) { double scaled_by = 1; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); + PangoLayout *layout = context->layout(); + + pango_layout_set_text(layout, *str, -1); + pango_cairo_update_layout(context->context(), layout); if (argsNum == 3) { - scaled_by = get_text_scale(context, *str, args[2]); + scaled_by = get_text_scale(layout, args[2]); cairo_save(context->context()); cairo_scale(context->context(), scaled_by, 1); } @@ -2080,9 +2084,9 @@ paintText(const Nan::FunctionCallbackInfo &info, bool stroke) { context->savePath(); if (context->state->textDrawingMode == TEXT_DRAW_GLYPHS) { if (stroke == true) { context->stroke(); } else { context->fill(); } - context->setTextPath(*str, x, y); + context->setTextPath(x, y); } else if (context->state->textDrawingMode == TEXT_DRAW_PATHS) { - context->setTextPath(*str, x, y); + context->setTextPath(x, y); if (stroke == true) { context->stroke(); } else { context->fill(); } } context->restorePath(); @@ -2131,16 +2135,16 @@ inline double getBaselineAdjustment(PangoLayout* layout, short baseline) { } /* - * Set text path for the given string at (x, y). + * Set text path for the string in the layout at (x, y). + * This function is called by paintText and won't behave correctly + * if is not called from there. + * it needs pango_layout_set_text and pango_cairo_update_layout to be called before */ void -Context2d::setTextPath(const char *str, double x, double y) { +Context2d::setTextPath(double x, double y) { PangoRectangle logical_rect; - pango_layout_set_text(_layout, str, -1); - pango_cairo_update_layout(_context, _layout); - switch (state->textAlignment) { // center case 0: diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index adb9661ad..539cf4329 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -156,7 +156,7 @@ class Context2d: public Nan::ObjectWrap { inline bool hasShadow(); void inline setSourceRGBA(rgba_t color); void inline setSourceRGBA(cairo_t *ctx, rgba_t color); - void setTextPath(const char *str, double x, double y); + void setTextPath(double x, double y); void blur(cairo_surface_t *surface, int radius); void shadow(void (fn)(cairo_t *cr)); void shadowStart(); diff --git a/test/public/tests.js b/test/public/tests.js index bed0dde55..6f05d0c87 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -859,6 +859,22 @@ tests['fillText() maxWidth argument'] = function (ctx) { ctx.fillText('Drawing text can be fun!', 0, 20 * 7) } +tests['maxWidth bug first usage path'] = function (ctx, done) { + ctx.textDrawingMode = 'path' + ctx.fillText('Drawing text can be fun!', 0, 20, 50) + ctx.fillText('Drawing text can be fun!', 0, 40, 50) + ctx.fillText('Drawing text can be fun changing text bug!', 0, 60, 50) + done() +} + +tests['maxWidth bug first usage glyph'] = function (ctx, done) { + ctx.textDrawingMode = 'glyph' + ctx.fillText('Drawing text can be fun!', 0, 20, 50) + ctx.fillText('Drawing text can be fun!', 0, 40, 50) + ctx.fillText('Drawing text can be fun changing text bug!', 0, 60, 50) + done() +} + tests['strokeText()'] = function (ctx) { ctx.strokeStyle = '#666' ctx.strokeRect(0, 0, 200, 200) From 9da6ed4ff637c24863d2073788bb25b6d4b01590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Tue, 25 Sep 2018 19:04:08 +0100 Subject: [PATCH 182/474] 2.0.0-alpha.15 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f8ab9c416..de3008dca 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.0.0-alpha.14", + "version": "2.0.0-alpha.15", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 66dbdb9f5161ec6bb71e937451883b1a6f2eaab6 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Tue, 25 Sep 2018 11:56:33 -0700 Subject: [PATCH 183/474] fix text alignment when maxWidth is limiting Fixes #1253 --- CHANGELOG.md | 2 ++ src/CanvasRenderingContext2d.cc | 4 ++-- test/public/tests.js | 24 ++++++++++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5c80b688..67b20faf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -89,6 +89,8 @@ canvas.createJPEGStream() // new that does not exist.) * Support reading CMYK, YCCK JPEGs. * Hide `Image.prototype.source` + * Fix behavior of maxWidth (#1088) + * Fix behavior of textAlignment with maxWidth (#1253) ### Added * Prebuilds (#992) with different libc versions to the prebuilt binary (#1140) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 14553f01c..945e55775 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -2084,9 +2084,9 @@ paintText(const Nan::FunctionCallbackInfo &info, bool stroke) { context->savePath(); if (context->state->textDrawingMode == TEXT_DRAW_GLYPHS) { if (stroke == true) { context->stroke(); } else { context->fill(); } - context->setTextPath(x, y); + context->setTextPath(x / scaled_by, y); } else if (context->state->textDrawingMode == TEXT_DRAW_PATHS) { - context->setTextPath(x, y); + context->setTextPath(x / scaled_by, y); if (stroke == true) { context->stroke(); } else { context->fill(); } } context->restorePath(); diff --git a/test/public/tests.js b/test/public/tests.js index 6f05d0c87..356427028 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -875,6 +875,30 @@ tests['maxWidth bug first usage glyph'] = function (ctx, done) { done() } +tests['fillText() maxWidth argument + textAlign center (#1253)'] = function (ctx) { + ctx.font = 'Helvetica, sans' + ctx.textAlign = 'center' + ctx.fillText('Drawing text can be fun!', 100, 20) + + for (var i = 1; i < 6; i++) { + ctx.fillText('Drawing text can be fun!', 100, 20 * (7 - i), i * 20) + } + + ctx.fillText('Drawing text can be fun!', 100, 20 * 7) +} + +tests['fillText() maxWidth argument + textAlign right'] = function (ctx) { + ctx.font = 'Helvetica, sans' + ctx.textAlign = 'right' + ctx.fillText('Drawing text can be fun!', 200, 20) + + for (var i = 1; i < 6; i++) { + ctx.fillText('Drawing text can be fun!', 200, 20 * (7 - i), i * 20) + } + + ctx.fillText('Drawing text can be fun!', 200, 20 * 7) +} + tests['strokeText()'] = function (ctx) { ctx.strokeStyle = '#666' ctx.strokeRect(0, 0, 200, 200) From 661c5d2e95803c7c432499bbf76e79bd7e333e8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Tue, 25 Sep 2018 21:48:06 +0100 Subject: [PATCH 184/474] 2.0.0-alpha.16 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index de3008dca..356995da2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.0.0-alpha.15", + "version": "2.0.0-alpha.16", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 66595b8a8a2fa86a85b0e48a0e764ab83126d710 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Sat, 29 Sep 2018 10:33:25 +0100 Subject: [PATCH 185/474] Bump all dependencies --- package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 356995da2..92f3d99f4 100644 --- a/package.json +++ b/package.json @@ -39,14 +39,14 @@ "package_name": "{module_name}-v{version}-{node_abi}-{platform}-{libc}-{arch}.tar.gz" }, "dependencies": { - "node-pre-gyp": "^0.9.0", - "nan": "^2.9.2" + "nan": "^2.11.1", + "node-pre-gyp": "^0.11.0" }, "devDependencies": { - "assert-rejects": "^0.1.1", - "express": "^4.14.0", - "mocha": "^3.1.2", - "standard": "^8.5.0" + "assert-rejects": "^1.0.0", + "express": "^4.16.3", + "mocha": "^5.2.0", + "standard": "^12.0.1" }, "engines": { "node": ">=6" From 7973d613e203e28214c66a8ef4a6e12dd7b86ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Sat, 29 Sep 2018 10:41:59 +0100 Subject: [PATCH 186/474] Fix coding style issues --- examples/font.js | 8 ++++---- examples/image-caption-overlay.js | 6 +++--- examples/indexed-png-alpha.js | 6 +++--- examples/indexed-png-image-data.js | 6 +++--- examples/state.js | 20 ++++++++++---------- test/public/app.js | 2 +- test/public/tests.js | 22 +++++++++++----------- test/server.js | 2 +- util/has_lib.js | 4 ++-- 9 files changed, 38 insertions(+), 38 deletions(-) diff --git a/examples/font.js b/examples/font.js index 8c4bdfb21..2a370f45e 100644 --- a/examples/font.js +++ b/examples/font.js @@ -10,10 +10,10 @@ function fontFile (name) { // `registerFont`. When you set `ctx.font`, refer to the styles and the family // name as it is embedded in the TTF. If you aren't sure, open the font in // FontForge and visit Element -> Font Information and copy the Family Name -Canvas.registerFont(fontFile('Pfennig.ttf'), {family: 'pfennigFont'}) -Canvas.registerFont(fontFile('PfennigBold.ttf'), {family: 'pfennigFont', weight: 'bold'}) -Canvas.registerFont(fontFile('PfennigItalic.ttf'), {family: 'pfennigFont', style: 'italic'}) -Canvas.registerFont(fontFile('PfennigBoldItalic.ttf'), {family: 'pfennigFont', weight: 'bold', style: 'italic'}) +Canvas.registerFont(fontFile('Pfennig.ttf'), { family: 'pfennigFont' }) +Canvas.registerFont(fontFile('PfennigBold.ttf'), { family: 'pfennigFont', weight: 'bold' }) +Canvas.registerFont(fontFile('PfennigItalic.ttf'), { family: 'pfennigFont', style: 'italic' }) +Canvas.registerFont(fontFile('PfennigBoldItalic.ttf'), { family: 'pfennigFont', weight: 'bold', style: 'italic' }) var canvas = Canvas.createCanvas(320, 320) var ctx = canvas.getContext('2d') diff --git a/examples/image-caption-overlay.js b/examples/image-caption-overlay.js index 9e765703e..e156b42b8 100644 --- a/examples/image-caption-overlay.js +++ b/examples/image-caption-overlay.js @@ -1,7 +1,7 @@ -import {createWriteStream} from 'fs' +import { createWriteStream } from 'fs' import pify from 'pify' import imageSizeOf from 'image-size' -import {createCanvas, loadImage, Image} from 'canvas' +import { createCanvas, loadImage, Image } from 'canvas' const imageSizeOfP = pify(imageSizeOf) @@ -62,7 +62,7 @@ function createCaptionOverlay ({ (async () => { try { const source = 'images/lime-cat.jpg' - const {width, height} = await imageSizeOfP(source) + const { width, height } = await imageSizeOfP(source) const canvas = createCanvas(width, height) const ctx = canvas.getContext('2d') diff --git a/examples/indexed-png-alpha.js b/examples/indexed-png-alpha.js index 63b062d81..7303aa845 100644 --- a/examples/indexed-png-alpha.js +++ b/examples/indexed-png-alpha.js @@ -2,7 +2,7 @@ var Canvas = require('..') var fs = require('fs') var path = require('path') var canvas = Canvas.createCanvas(200, 200) -var ctx = canvas.getContext('2d', {pixelFormat: 'A8'}) +var ctx = canvas.getContext('2d', { pixelFormat: 'A8' }) // Matches the "fillStyle" browser test, made by using alpha fillStyle value var palette = new Uint8ClampedArray(37 * 4) @@ -30,5 +30,5 @@ for (i = 0; i < 6; i++) { } } -canvas.createPNGStream({palette: palette}) - .pipe(fs.createWriteStream(path.join(__dirname, 'indexed2.png'))) +canvas.createPNGStream({ palette: palette }) + .pipe(fs.createWriteStream(path.join(__dirname, 'indexed2.png'))) diff --git a/examples/indexed-png-image-data.js b/examples/indexed-png-image-data.js index 3481ac99d..12c25c2de 100644 --- a/examples/indexed-png-image-data.js +++ b/examples/indexed-png-image-data.js @@ -2,7 +2,7 @@ var Canvas = require('..') var fs = require('fs') var path = require('path') var canvas = Canvas.createCanvas(200, 200) -var ctx = canvas.getContext('2d', {pixelFormat: 'A8'}) +var ctx = canvas.getContext('2d', { pixelFormat: 'A8' }) // Matches the "fillStyle" browser test, made by manipulating imageData var palette = new Uint8ClampedArray(37 * 4) @@ -35,5 +35,5 @@ for (i = 0; i < 6; i++) { } ctx.putImageData(idata, 0, 0) -canvas.createPNGStream({palette: palette}) - .pipe(fs.createWriteStream(path.join(__dirname, 'indexed.png'))) +canvas.createPNGStream({ palette: palette }) + .pipe(fs.createWriteStream(path.join(__dirname, 'indexed.png'))) diff --git a/examples/state.js b/examples/state.js index c3302f4cf..3349a1a8b 100644 --- a/examples/state.js +++ b/examples/state.js @@ -5,21 +5,21 @@ var Canvas = require('..') var canvas = Canvas.createCanvas(150, 150) var ctx = canvas.getContext('2d') -ctx.fillRect(0, 0, 150, 150) // Draw a rectangle with default settings -ctx.save() // Save the default state +ctx.fillRect(0, 0, 150, 150) // Draw a rectangle with default settings +ctx.save() // Save the default state -ctx.fillStyle = '#09F' // Make changes to the settings +ctx.fillStyle = '#09F' // Make changes to the settings ctx.fillRect(15, 15, 120, 120) // Draw a rectangle with new settings -ctx.save() // Save the current state -ctx.fillStyle = '#FFF' // Make changes to the settings +ctx.save() // Save the current state +ctx.fillStyle = '#FFF' // Make changes to the settings ctx.globalAlpha = 0.5 -ctx.fillRect(30, 30, 90, 90) // Draw a rectangle with new settings +ctx.fillRect(30, 30, 90, 90) // Draw a rectangle with new settings -ctx.restore() // Restore previous state -ctx.fillRect(45, 45, 60, 60) // Draw a rectangle with restored settings +ctx.restore() // Restore previous state +ctx.fillRect(45, 45, 60, 60) // Draw a rectangle with restored settings -ctx.restore() // Restore original state -ctx.fillRect(60, 60, 30, 30) // Draw a rectangle with restored settings +ctx.restore() // Restore original state +ctx.fillRect(60, 60, 30, 30) // Draw a rectangle with restored settings canvas.createPNGStream().pipe(fs.createWriteStream(path.join(__dirname, 'state.png'))) diff --git a/test/public/app.js b/test/public/app.js index e706f2c72..db3b66e48 100644 --- a/test/public/app.js +++ b/test/public/app.js @@ -21,7 +21,7 @@ function pdfLink (name) { function localRendering (name) { var canvas = create('canvas', { width: 200, height: 200, title: name }) - var ctx = canvas.getContext('2d', {alpha: true}) + var ctx = canvas.getContext('2d', { alpha: true }) var initialFillStyle = ctx.fillStyle ctx.fillStyle = 'white' ctx.fillRect(0, 0, 200, 200) diff --git a/test/public/tests.js b/test/public/tests.js index 356427028..33c644cd4 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -113,11 +113,11 @@ tests['arc()'] = function (ctx) { ctx.beginPath() ctx.arc(75, 75, 50, 0, Math.PI * 2, true) // Outer circle ctx.moveTo(110, 75) - ctx.arc(75, 75, 35, 0, Math.PI, false) // Mouth + ctx.arc(75, 75, 35, 0, Math.PI, false) // Mouth ctx.moveTo(65, 65) - ctx.arc(60, 65, 5, 0, Math.PI * 2, true) // Left eye + ctx.arc(60, 65, 5, 0, Math.PI * 2, true) // Left eye ctx.moveTo(95, 65) - ctx.arc(90, 65, 5, 0, Math.PI * 2, true) // Right eye + ctx.arc(90, 65, 5, 0, Math.PI * 2, true) // Right eye ctx.stroke() } @@ -125,10 +125,10 @@ tests['arc() 2'] = function (ctx) { for (var i = 0; i < 4; i++) { for (var j = 0; j < 3; j++) { ctx.beginPath() - var x = 25 + j * 50 // x coordinate - var y = 25 + i * 50 // y coordinate - var radius = 20 // Arc radius - var startAngle = 0 // Starting point on circle + var x = 25 + j * 50 // x coordinate + var y = 25 + i * 50 // y coordinate + var radius = 20 // Arc radius + var startAngle = 0 // Starting point on circle var endAngle = Math.PI + (Math.PI * j) / 2 // End point on circle var anticlockwise = (i % 2) === 1 // clockwise or anticlockwise @@ -311,7 +311,7 @@ tests['scale()'] = function (ctx) { // Uniform scaling ctx.save() ctx.translate(50, 50) - drawSpirograph(ctx, 22, 6, 5) // no scaling + drawSpirograph(ctx, 22, 6, 5) // no scaling ctx.translate(100, 0) ctx.scale(0.75, 0.75) @@ -408,8 +408,7 @@ tests['clip() 2'] = function (ctx) { for (var j = 1; j < 50; j++) { ctx.save() ctx.fillStyle = '#fff' - ctx.translate(75 - Math.floor(Math.random() * 150), - 75 - Math.floor(Math.random() * 150)) + ctx.translate(75 - Math.floor(Math.random() * 150), 75 - Math.floor(Math.random() * 150)) drawStar(ctx, Math.floor(Math.random() * 4) + 2) ctx.restore() } @@ -497,8 +496,9 @@ tests['createLinearGradient()'] = function (ctx) { ctx.fillRect(10, 10, 130, 130) ctx.strokeRect(50, 50, 50, 50) + // Specifically test that setting the fillStyle to the current fillStyle works ctx.fillStyle = '#13b575' - ctx.fillStyle = ctx.fillStyle + ctx.fillStyle = ctx.fillStyle // eslint-disable-line no-self-assign ctx.fillRect(65, 65, 20, 20) var lingrad3 = ctx.createLinearGradient(0, 0, 200, 0) diff --git a/test/server.js b/test/server.js index d373ea9ca..0660491d1 100644 --- a/test/server.js +++ b/test/server.js @@ -12,7 +12,7 @@ function renderTest (canvas, name, cb) { throw new Error('Unknown test: ' + name) } - var ctx = canvas.getContext('2d', {pixelFormat: 'RGBA32'}) + var ctx = canvas.getContext('2d', { pixelFormat: 'RGBA32' }) var initialFillStyle = ctx.fillStyle ctx.fillStyle = 'white' ctx.fillRect(0, 0, 200, 200) diff --git a/util/has_lib.js b/util/has_lib.js index c8aa5a417..b4ea9e835 100644 --- a/util/has_lib.js +++ b/util/has_lib.js @@ -24,7 +24,7 @@ function hasSystemLib (lib) { var libName = 'lib' + lib + '.+(so|dylib)' var libNameRegex = new RegExp(libName) - // Try using ldconfig on linux systems + // Try using ldconfig on linux systems if (hasLdconfig()) { try { if (childProcess.execSync('ldconfig -p 2>/dev/null | grep -E "' + libName + '"').length) { @@ -35,7 +35,7 @@ function hasSystemLib (lib) { } } - // Try checking common library locations + // Try checking common library locations return SYSTEM_PATHS.some(function (systemPath) { try { var dirListing = fs.readdirSync(systemPath) From 5df91bc172bc55e73efbab8954bd05049a78d3ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Sat, 29 Sep 2018 10:54:27 +0100 Subject: [PATCH 187/474] 2.0.0-alpha.17 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 92f3d99f4..c8736307f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.0.0-alpha.16", + "version": "2.0.0-alpha.17", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From ddda5d383b737edbafed3e84586c2858be02dbe3 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Wed, 3 Oct 2018 10:54:08 -0400 Subject: [PATCH 188/474] make isBase64 boolean (#1267) --- lib/image.js | 2 +- test/image.test.js | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/image.js b/lib/image.js index 33b3d768b..ff84f2b24 100644 --- a/lib/image.js +++ b/lib/image.js @@ -38,7 +38,7 @@ Object.defineProperty(Image.prototype, 'src', { if (/^\s*data:/.test(val)) { // data: URI const commaI = val.indexOf(',') // 'base64' must come before the comma - const isBase64 = val.lastIndexOf('base64', commaI) + const isBase64 = val.lastIndexOf('base64', commaI) !== -1 const content = val.slice(commaI + 1) setSource(this, Buffer.from(content, isBase64 ? 'base64' : 'utf8'), val); } else if (/^\s*https?:\/\//.test(val)) { // remote URL diff --git a/test/image.test.js b/test/image.test.js index 138b1c70f..1162f3cac 100644 --- a/test/image.test.js +++ b/test/image.test.js @@ -17,6 +17,7 @@ const png_checkers = `${__dirname}/fixtures/checkers.png` const png_clock = `${__dirname}/fixtures/clock.png` const jpg_chrome = `${__dirname}/fixtures/chrome.jpg` const jpg_face = `${__dirname}/fixtures/face.jpeg` +const svg_tree = `${__dirname}/fixtures/tree.svg` describe('Image', function () { it('Prototype and ctor are well-shaped, don\'t hit asserts on accessors (GH-803)', function () { @@ -81,6 +82,30 @@ describe('Image', function () { }) }) + it('loads SVG data URL base64', function () { + const base64Enc = fs.readFileSync(svg_tree, 'base64') + const dataURL = `data:image/svg+xml;base64,${base64Enc}` + return loadImage(dataURL).then((img) => { + assert.strictEqual(img.onerror, null) + assert.strictEqual(img.onload, null) + assert.strictEqual(img.width, 200) + assert.strictEqual(img.height, 200) + assert.strictEqual(img.complete, true) + }) + }) + + it('loads SVG data URL utf8', function () { + const utf8Encoded = fs.readFileSync(svg_tree, 'utf8') + const dataURL = `data:image/svg+xml;utf8,${utf8Encoded}` + return loadImage(dataURL).then((img) => { + assert.strictEqual(img.onerror, null) + assert.strictEqual(img.onload, null) + assert.strictEqual(img.width, 200) + assert.strictEqual(img.height, 200) + assert.strictEqual(img.complete, true) + }) + }) + it('calls Image#onload multiple times', function () { return loadImage(png_clock).then((img) => { let onloadCalled = 0 From aaf7137ee9529a9dc09e2edff715766c1e2edafc Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Tue, 9 Oct 2018 20:47:36 +0200 Subject: [PATCH 189/474] Fix drawImage regression with particular use case of 9 arguments. (#1251) * ok! * ok! * added-test * fix lint * reworked a name --- src/CanvasRenderingContext2d.cc | 29 +++++++++++++++++++------- test/public/tests.js | 37 +++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 8 deletions(-) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 945e55775..1a4abb4ef 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -1112,8 +1112,8 @@ NAN_METHOD(Context2d::DrawImage) { , dy = 0 , dw = 0 , dh = 0 - , fw = 0 - , fh = 0; + , source_w = 0 + , source_h = 0; cairo_surface_t *surface; @@ -1125,15 +1125,15 @@ NAN_METHOD(Context2d::DrawImage) { if (!img->isComplete()) { return Nan::ThrowError("Image given has not completed loading"); } - fw = sw = img->width; - fh = sh = img->height; + source_w = sw = img->width; + source_h = sh = img->height; surface = img->surface(); // Canvas } else if (Nan::New(Canvas::constructor)->HasInstance(obj)) { Canvas *canvas = Nan::ObjectWrap::Unwrap(obj); - fw = sw = canvas->getWidth(); - fh = sh = canvas->getHeight(); + source_w = sw = canvas->getWidth(); + source_h = sh = canvas->getHeight(); surface = canvas->surface(); // Invalid @@ -1183,10 +1183,11 @@ NAN_METHOD(Context2d::DrawImage) { float fx = (float) dw / sw; float fy = (float) dh / sh; bool needScale = dw != sw || dh != sh; - bool needCut = sw != fw || sh != fh; + bool needCut = sw != source_w || sh != source_h || sx < 0 || sy < 0; + bool needCairoClip = sx < 0 || sy < 0 || sw > source_w || sh > source_h; bool sameCanvas = surface == context->canvas()->surface(); - bool needsExtraSurface = sameCanvas || needCut || needScale; + bool needsExtraSurface = sameCanvas || needCut || needScale || needCairoClip; cairo_surface_t *surfTemp = NULL; cairo_t *ctxTemp = NULL; @@ -1194,6 +1195,18 @@ NAN_METHOD(Context2d::DrawImage) { surfTemp = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, dw, dh); ctxTemp = cairo_create(surfTemp); cairo_scale(ctxTemp, fx, fy); + if (needCairoClip) { + float clip_w = (std::min)(sw, source_w); + float clip_h = (std::min)(sh, source_h); + if (sx > 0) { + clip_w -= sx; + } + if (sy > 0) { + clip_h -= sy; + } + cairo_rectangle(ctxTemp, -sx , -sy , clip_w, clip_h); + cairo_clip(ctxTemp); + } cairo_set_source_surface(ctxTemp, surface, -sx, -sy); cairo_pattern_set_filter(cairo_get_source(ctxTemp), context->state->imageSmoothingEnabled ? context->state->patternQuality : CAIRO_FILTER_NEAREST); cairo_pattern_set_extend(cairo_get_source(ctxTemp), CAIRO_EXTEND_REFLECT); diff --git a/test/public/tests.js b/test/public/tests.js index 33c644cd4..899986158 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -1239,6 +1239,7 @@ gco.forEach(op => { var img2 = new Image() img1.onload = function () { img2.onload = function () { + ctx.globalAlpha = 0.7 ctx.drawImage(img1, 0, 0) ctx.globalCompositeOperation = op ctx.drawImage(img2, 0, 0) @@ -1250,6 +1251,42 @@ gco.forEach(op => { } }) +gco.forEach(op => { + tests['9 args, transform, globalCompositeOperator ' + op] = function (ctx, done) { + var img1 = new Image() + var img2 = new Image() + img1.onload = function () { + img2.onload = function () { + ctx.globalAlpha = 0.7 + ctx.drawImage(img1, 0, 0) + ctx.globalCompositeOperation = op + ctx.rotate(0.1) + ctx.scale(0.8, 1.2) + ctx.translate(5, -5) + ctx.drawImage(img2, -80, -50, 400, 400, 10, 10, 180, 180) + done() + } + img2.src = imageSrc('newcontent.png') + } + img1.src = imageSrc('existing.png') + } +}) + +tests['drawImage issue #1249'] = function (ctx, done) { + var img1 = new Image() + var img2 = new Image() + img1.onload = function () { + img2.onload = function () { + ctx.drawImage(img1, 0, 0, 200, 200) + ctx.drawImage(img2, -8, -8, 18, 18, 0, 0, 200, 200) + ctx.restore() + done() + } + img2.src = imageSrc('checkers.png') + } + img1.src = imageSrc('chrome.jpg') +} + tests['known bug #416'] = function (ctx, done) { var img1 = new Image() var img2 = new Image() From 586b395afb4a7bd5515b28d255118debceb2d139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Wed, 10 Oct 2018 08:27:37 +0200 Subject: [PATCH 190/474] 2.0.0-alpha.18 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c8736307f..ddb7e012a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.0.0-alpha.17", + "version": "2.0.0-alpha.18", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 8064216a41cb995b4463bc6a6439b61b2743e47b Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 13 Oct 2018 15:41:39 -0700 Subject: [PATCH 191/474] Revamp readme (#1272) --- CHANGELOG.md | 34 +++- Readme.md | 563 ++++++++++++++++++++++++--------------------------- 2 files changed, 295 insertions(+), 302 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67b20faf0..ad96dd2bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,24 +10,40 @@ project adheres to [Semantic Versioning](http://semver.org/). **Upgrading from 1.x** ```js -// (1) The quality argument for canvas.createJPEGStream/canvas.jpegStream now +// (1) The Canvas constructor is no longer the default export from the module. +/* old: */ +const Canvas = require('canvas') +const mycanvas = new Canvas(width, height) +/* new: */ +const { createCanvas, Canvas } = require('canvas') +const mycanvas = createCanvas(width, height) +mycanvas instanceof Canvas // true + +/* old: */ +const Canvas = require('canvas') +const myimg = new Canvas.Image() +/* new: */ +const { Image } = require('canvas') +const myimg = new Image() + +// (2) The quality argument for canvas.createJPEGStream/canvas.jpegStream now // goes from 0 to 1 instead of from 0 to 100: -canvas.createJPEGStream({quality: 50}) // old -canvas.createJPEGStream({quality: 0.5}) // new +canvas.createJPEGStream({ quality: 50 }) // old +canvas.createJPEGStream({ quality: 0.5 }) // new -// (2) The ZLIB compression level and PNG filter options for canvas.toBuffer are +// (3) The ZLIB compression level and PNG filter options for canvas.toBuffer are // now named instead of positional arguments: canvas.toBuffer(undefined, 3, canvas.PNG_FILTER_NONE) // old -canvas.toBuffer(undefined, {compressionLevel: 3, filters: canvas.PNG_FILTER_NONE}) // new +canvas.toBuffer(undefined, { compressionLevel: 3, filters: canvas.PNG_FILTER_NONE }) // new // or specify the mime type explicitly: -canvas.toBuffer("image/png", {compressionLevel: 3, filters: canvas.PNG_FILTER_NONE}) // new +canvas.toBuffer('image/png', { compressionLevel: 3, filters: canvas.PNG_FILTER_NONE }) // new -// (3) #2 also applies for canvas.pngStream, although these arguments were not +// (4) #2 also applies for canvas.pngStream, although these arguments were not // documented: canvas.pngStream(3, canvas.PNG_FILTER_NONE) // old -canvas.pngStream({compressionLevel: 3, filters: canvas.PNG_FILTER_NONE}) // new +canvas.pngStream({ compressionLevel: 3, filters: canvas.PNG_FILTER_NONE }) // new -// (4) canvas.syncPNGStream() and canvas.syncJPEGStream() have been removed: +// (5) canvas.syncPNGStream() and canvas.syncJPEGStream() have been removed: canvas.syncPNGStream() // old canvas.createSyncPNGStream() // old canvas.createPNGStream() // new diff --git a/Readme.md b/Readme.md index 7e73a5419..b99715cee 100644 --- a/Readme.md +++ b/Readme.md @@ -1,29 +1,18 @@ # node-canvas ------ - ## This is the documentation for version 2.0.0-alpha Alpha versions of 2.0 can be installed using `npm install canvas@next`. -See the [changelog](https://github.com/Automattic/node-canvas/blob/master/CHANGELOG.md) -for a guide to upgrading from 1.x to 2.x. +See the [changelog](https://github.com/Automattic/node-canvas/blob/master/CHANGELOG.md) for a guide to upgrading from 1.x to 2.x. **For version 1.x documentation, see [the v1.x branch](https://github.com/Automattic/node-canvas/tree/v1.x)** ----- -### Canvas graphics API backed by Cairo [![Build Status](https://travis-ci.org/Automattic/node-canvas.svg?branch=master)](https://travis-ci.org/Automattic/node-canvas) [![NPM version](https://badge.fury.io/js/canvas.svg)](http://badge.fury.io/js/canvas) - node-canvas is a [Cairo](http://cairographics.org/) backed Canvas implementation for [NodeJS](http://nodejs.org). - -## Authors - - - TJ Holowaychuk ([tj](http://github.com/tj)) - - Nathan Rajlich ([TooTallNate](http://github.com/TooTallNate)) - - Rod Vagg ([rvagg](http://github.com/rvagg)) - - Juriy Zaytsev ([kangax](http://github.com/kangax)) +node-canvas is a [Cairo](http://cairographics.org/)-backed Canvas implementation for [Node.js](http://nodejs.org). ## Installation @@ -31,32 +20,29 @@ for a guide to upgrading from 1.x to 2.x. $ npm install canvas ``` -By default, binaries for macOS, Linux and Windows will be downloaded. If you want to build from source, use `npm install --build-from-source`. +By default, binaries for macOS, Linux and Windows will be downloaded. If you want to build from source, use `npm install --build-from-source` and see the **Compiling** section below. -Currently the minimum version of node required is __6.0.0__ +The minimum version of Node.js required is **6.0.0**. ### Compiling -If you don't have a supported OS or processor architecture, or you use `--build-from-source`, the module will be compiled on your system. Unless previously installed you'll _need_ __Cairo__ and __Pango__. For system-specific installation view the [Wiki](https://github.com/Automattic/node-canvas/wiki/_pages). +If you don't have a supported OS or processor architecture, or you use `--build-from-source`, the module will be compiled on your system. This requires several dependencies, including Cairo and Pango. -You can quickly install the dependencies by using the command for your OS: +For detailed installation information, see the [wiki](https://github.com/Automattic/node-canvas/wiki/_pages). One-line installation instructions for common OSes are below. Note that libgif/giflib, librsvg and libjpeg are optional and only required if you need GIF, SVG and JPEG support, respectively. OS | Command ----- | ----- -OS X | Using [Homebrew](https://brew.sh/):
`brew install pkg-config cairo pango libpng jpeg giflib`

Using [MacPorts](https://www.macports.org/):
`port install pkgconfig cairo pango libpng jpeg giflib libsrvg` -Ubuntu | `sudo apt-get install libcairo2-dev libjpeg-dev libpango1.0-dev libgif-dev build-essential g++` -Fedora | `sudo yum install cairo cairo-devel cairomm-devel libjpeg-turbo-devel pango pango-devel pangomm pangomm-devel giflib-devel` +OS X | Using [Homebrew](https://brew.sh/):
`brew install pkg-config cairo pango libpng jpeg giflib librsvg` +Ubuntu | `sudo apt-get install build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev` +Fedora | `sudo yum install gcc-c++ cairo-devel pango-devel libjpeg-turbo-devel giflib-devel` Solaris | `pkgin install cairo pango pkg-config xproto renderproto kbproto xextproto` OpenBSD | `doas pkg_add cairo pango png jpeg giflib` -Windows | [Instructions on our wiki](https://github.com/Automattic/node-canvas/wiki/Installation:-Windows) +Windows | See the [wiki](https://github.com/Automattic/node-canvas/wiki/Installation:-Windows) +Others | See the [wiki](https://github.com/Automattic/node-canvas/wiki) **Mac OS X v10.11+:** If you have recently updated to Mac OS X v10.11+ and are experiencing trouble when compiling, run the following command: `xcode-select --install`. Read more about the problem [on Stack Overflow](http://stackoverflow.com/a/32929012/148072). -## Screencasts - - - [Introduction](http://screenr.com/CTk) - -## Example +## Quick Example ```javascript const { createCanvas, loadImage } = require('canvas') @@ -84,27 +70,118 @@ loadImage('examples/images/lime-cat.jpg').then((image) => { }) ``` -## Know issues +## Documentation + +This project is an implementation of the Web Canvas API and implements that API as closely as possible. For API documentation, please visit [Mozilla Web Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API). (See [Compatibility Status](https://github.com/Automattic/node-canvas/wiki/Compatibility-Status) for the current API compliance.) All utility methods and non-standard APIs are documented below. + +### Utility methods + +* [createCanvas()](#createcanvas) +* [createImageData()](#createimagedata) +* [loadImage()](#loadimage) +* [registerFont()](#registerfont) + +### Non-standard APIs + +* [Image#src](#imagesrc) +* [Image#dataMode](#imagedatamode) +* [Canvas#toBuffer()](#canvastobuffer) +* [Canvas#createPNGStream()](#canvascreatepngstream) +* [Canvas#createJPEGStream()](#canvascreatejpegstream) +* [Canvas#createPDFStream()](#canvascreatepdfstream) +* [Canvas#toDataURL()](#canvastodataurl) +* [CanvasRenderingContext2D#patternQuality](#canvasrenderingcontext2dpatternquality) +* [CanvasRenderingContext2D#filter](#canvasrenderingcontext2dfilter) +* [CanvasRenderingContext2D#textDrawingMode](#canvasrenderingcontext2dtextdrawingmode) +* [CanvasRenderingContext2D#globalCompositeOperator = 'saturate'](#canvasrenderingcontext2dglobalcompositeoperator--saturate) +* [CanvasRenderingContext2D#antialias](#canvasrenderingcontext2dantialias) + +### createCanvas() + +> ```ts +> createCanvas(width: number, height: number, type?: 'PDF'|'SVG') => Canvas +> ``` + +Creates a Canvas instance. This method works in both Node.js and Web browsers, where there is no Canvas constructor. (See `browser.js` for the implementation that runs in browsers.) + +```js +const { createCanvas } = require('canvas') +const mycanvas = createCanvas(200, 200) +const myPDFcanvas = createCanvas(600, 800, 'pdf') // see "PDF Support" section +``` + +### createImageData() + +> ```ts +> createImageData(width: number, height: number) => ImageData +> createImageData(data: Uint8ClampedArray, width: number, height?: number) => ImageData +> // for alternative pixel formats: +> createImageData(data: Uint16Array, width: number, height?: number) => ImageData +> ``` + +Creates an ImageData instance. This method works in both Node.js and Web browsers. + +```js +const { createImageData } = require('canvas') +const width = 20, height = 20 +const arraySize = width * height * 4 +const mydata = createImageData(new Uint8ClampedArray(arraySize), width) +``` + +### loadImage() + +> ```ts +> loadImage() => Promise +> ``` + +Convenience method for loading images. This method works in both Node.js and Web browsers. + +```js +const { loadImage } = require('canvas') +const myimg = loadImage('http://server.com/image.png') + +myimg.then(() => { + // do something with image +}).catch(err => { + console.log('oh no!', err) +}) + +// or with async/await: +const myimg = await loadImage('http://server.com/image.png') +// do something with image +``` + +### registerFont() -- CMYK images are not supported ([#1183](https://github.com/Automattic/node-canvas/issues/1183), [#425](https://github.com/Automattic/node-canvas/issues/425)) -- `ctx.fillText` `maxWidth` is inconsistent ([#1088](https://github.com/Automattic/node-canvas/issues/1183), [#1088](https://github.com/Automattic/node-canvas/issues/425)) -- Async `canvas.toBuffer` for PDF is not working ([#821](https://github.com/Automattic/node-canvas/issues/821)) +> ```ts +> registerFont(path: string, { family: string, weight?: string, style?: string }) => void +> ``` -[See all list of bugs](https://github.com/Automattic/node-canvas/issues?q=is%3Aissue+is%3Aopen+label%3ABug). +To use a font file that is not installed as a system font, use `registerFont()` to register the font with Canvas. *This must be done before the Canvas is created.* -## Non-Standard APIs +```js +const { registerFont, createCanvas } = require('canvas') +registerFont('comicsans.ttf', { family: 'Comic Sans' }) -node-canvas implements the [HTML Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) as closely as possible. -(See [Compatibility Status](https://github.com/Automattic/node-canvas/wiki/Compatibility-Status) -for the current API compliance.) All non-standard APIs are documented below. +const canvas = createCanvas(500, 500) +const ctx = canvas.getContext('2d') + +ctx.font = '12px "Comic Sans"' +ctx.fillText('Everyone hates this font :(', 250, 10) +``` + +The second argument is an object with properties that resemble the CSS properties that are specified in `@font-face` rules. You must specify at least `family`. `weight`, and `style` are optional and default to `'normal'`. ### Image#src -As in browsers, `img.src` can be set to a `data:` URI or a remote URL. In addition, -node-canvas allows setting `src` to a local file path or to a `Buffer` instance. +> ```ts +> img.src: string|Buffer +> ``` + +As in browsers, `img.src` can be set to a `data:` URI or a remote URL. In addition, node-canvas allows setting `src` to a local file path or `Buffer` instance. ```javascript -const { Image } = require('canvas'); +const { Image } = require('canvas') // From a buffer: fs.readFile('images/squid.png', (err, squid) => { @@ -130,90 +207,78 @@ img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAA // ... as above ``` -*Note: In some cases, `img.src=` is currently synchronous. However, you should -always use `img.onload` and `img.onerror`, as we intend to make `img.src=` always -asynchronous as it is in browsers. See https://github.com/Automattic/node-canvas/issues/1007.* +*Note: In some cases, `img.src=` is currently synchronous. However, you should always use `img.onload` and `img.onerror`, as we intend to make `img.src=` always asynchronous as it is in browsers. See https://github.com/Automattic/node-canvas/issues/1007.* ### Image#dataMode -node-canvas adds `Image#dataMode` support, which can be used to opt-in to mime data tracking of images (currently only JPEGs). +> ```ts +> img.dataMode: number +> ``` -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. +Applies to JPEG images drawn to PDF canvases only. + +Setting `img.dataMode = Image.MODE_MIME` or `Image.MODE_MIME|Image.MODE_IMAGE` enables MIME data tracking of images. When MIME data is tracked, PDF canvases can embed JPEGs directly into the output, rather than re-encoding into PNG. This can drastically reduce filesize and speed up rendering. ```javascript -const { Image } = require('canvas'); -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 +const { Image, createCanvas } = require('canvas') +const canvas = createCanvas(w, h, 'pdf') +const 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. +If working with a non-PDF canvas, image data *must* be tracked; otherwise the output will be junk. + +Enabling mime data tracking has no benefits (only a slow down) unless you are generating a PDF. ### Canvas#toBuffer() -Creates a [`Buffer`](https://nodejs.org/api/buffer.html) object representing the -image contained in the canvas. - -> `canvas.toBuffer((err: Error|null, result: Buffer) => void[, mimeType[, config]]) => void` -> `canvas.toBuffer([mimeType[, config]]) => Buffer` - -* **callback** If provided, the buffer will be provided in the callback instead - of being returned by the function. Invoked with an error as the first argument - if encoding failed, or the resulting buffer as the second argument if it - succeeded. Not supported for mimeType `raw` or for PDF or SVG canvases (there - is no async work to do in those cases). -* **mimeType** A string indicating the image format. Valid options are `image/png`, - `image/jpeg` (if node-canvas was built with JPEG support) and `raw` (unencoded - ARGB32 data in native-endian byte order, top-to-bottom). Defaults to - `image/png`. If the canvas is a PDF or SVG canvas, this argument is ignored - and a PDF or SVG is returned always. +> ```ts +> canvas.toBuffer((err: Error|null, result: Buffer) => void, mimeType?: string, config?: any) => void +> canvas.toBuffer(mimeType?: string, config?: any) => Buffer +> ``` + +Creates a [`Buffer`](https://nodejs.org/api/buffer.html) object representing the image contained in the canvas. + +* **callback** If provided, the buffer will be provided in the callback instead of being returned by the function. Invoked with an error as the first argument if encoding failed, or the resulting buffer as the second argument if it succeeded. Not supported for mimeType `raw` or for PDF or SVG canvases. +* **mimeType** A string indicating the image format. Valid options are `image/png`, `image/jpeg` (if node-canvas was built with JPEG support) and `raw` (unencoded ARGB32 data in native-endian byte order, top-to-bottom). Defaults to `image/png`. If the canvas is a PDF or SVG canvas, this argument is ignored and a PDF or SVG is returned always. * **config** - * For `image/jpeg` an object specifying the quality (0 to 1), if progressive - compression should be used and/or if chroma subsampling should be used: - `{quality: 0.75, progressive: false, chromaSubsampling: true}`. All - properties are optional. - * For `image/png`, an object specifying the ZLIB compression level (between 0 - and 9), the compression filter(s), the palette (indexed PNGs only), the - the background palette index (indexed PNGs only) and/or the resolution (ppi): - `{compressionLevel: 6, filters: canvas.PNG_ALL_FILTERS, palette: undefined, backgroundIndex: 0, resolution: undefined}`. - All properties are optional. - - Note that the PNG format encodes the resolution in pixels per meter, so if - you specify `96`, the file will encode 3780 ppm (~96.01 ppi). The resolution - is undefined by default to match common browser behavior. + * For `image/jpeg` an object specifying the quality (0 to 1), if progressive compression should be used and/or if chroma subsampling should be used: `{quality: 0.75, progressive: false, chromaSubsampling: true}`. All properties are optional. + * For `image/png`, an object specifying the ZLIB compression level (between 0 and 9), the compression filter(s), the palette (indexed PNGs only), the the background palette index (indexed PNGs only) and/or the resolution (ppi): `{compressionLevel: 6, filters: canvas.PNG_ALL_FILTERS, palette: undefined, backgroundIndex: 0, resolution: undefined}`. All properties are optional. + + Note that the PNG format encodes the resolution in pixels per meter, so if you specify `96`, the file will encode 3780 ppm (~96.01 ppi). The resolution is undefined by default to match common browser behavior. **Return value** -If no callback is provided, a [`Buffer`](https://nodejs.org/api/buffer.html). -If a callback is provided, none. +If no callback is provided, a [`Buffer`](https://nodejs.org/api/buffer.html). If a callback is provided, none. #### Examples -```javascript +```js // Default: buf contains a PNG-encoded image const buf = canvas.toBuffer() // PNG-encoded, zlib compression level 3 for faster compression but bigger files, no filtering -const buf2 = canvas.toBuffer('image/png', {compressionLevel: 3, filters: canvas.PNG_FILTER_NONE}) +const buf2 = canvas.toBuffer('image/png', { compressionLevel: 3, filters: canvas.PNG_FILTER_NONE }) // JPEG-encoded, 50% quality -const buf3 = canvas.toBuffer('image/jpeg', {quality: 0.5}) +const buf3 = canvas.toBuffer('image/jpeg', { quality: 0.5 }) // Asynchronous PNG canvas.toBuffer((err, buf) => { - if (err) throw err; // encoding failed + if (err) throw err // encoding failed // buf is PNG-encoded image }) canvas.toBuffer((err, buf) => { - if (err) throw err; // encoding failed + if (err) throw err // encoding failed // buf is JPEG-encoded image at 95% quality -}, 'image/jpeg', {quality: 0.95}) +}, 'image/jpeg', { quality: 0.95 }) // ARGB32 pixel values, native-endian const buf4 = canvas.toBuffer('raw') -const {stride, width} = canvas +const { stride, width } = canvas // In memory, this is `canvas.height * canvas.stride` bytes long. // The top row of pixels, in ARGB order, left-to-right, is: const topPixelsARGBLeftToRight = buf4.slice(0, width * 4) @@ -225,18 +290,15 @@ const myCanvas = createCanvas(w, h, 'pdf') myCanvas.toBuffer() // returns a buffer containing a PDF-encoded canvas ``` -### Canvas#createPNGStream(options) +### Canvas#createPNGStream() -Creates a [`ReadableStream`](https://nodejs.org/api/stream.html#stream_class_stream_readable) -that emits PNG-encoded data. +> ```ts +> canvas.createPNGStream(config?: any) => ReadableStream +> ``` -> `canvas.createPNGStream([config]) => ReadableStream` +Creates a [`ReadableStream`](https://nodejs.org/api/stream.html#stream_class_stream_readable) that emits PNG-encoded data. -* `config` An object specifying the ZLIB compression level (between 0 and 9), - the compression filter(s), the palette (indexed PNGs only) and/or the - background palette index (indexed PNGs only): - `{compressionLevel: 6, filters: canvas.PNG_ALL_FILTERS, palette: undefined, backgroundIndex: 0, resolution: undefined}`. - All properties are optional. +* `config` An object specifying the ZLIB compression level (between 0 and 9), the compression filter(s), the palette (indexed PNGs only) and/or the background palette index (indexed PNGs only): `{compressionLevel: 6, filters: canvas.PNG_ALL_FILTERS, palette: undefined, backgroundIndex: 0, resolution: undefined}`. All properties are optional. #### Examples @@ -266,18 +328,15 @@ canvas.createPNGStream({ ### Canvas#createJPEGStream() -Creates a [`createJPEGStream`](https://nodejs.org/api/stream.html#stream_class_stream_readable) -that emits JPEG-encoded data. +> ```ts +> canvas.createJPEGStream(config?: any) => ReadableStream +> ``` -_Note: At the moment, `createJPEGStream()` is synchronous under the hood. That is, it -runs in the main thread, not in the libuv threadpool._ +Creates a [`ReadableStream`](https://nodejs.org/api/stream.html#stream_class_stream_readable) that emits JPEG-encoded data. -> `canvas.createJPEGStream([config]) => ReadableStream` +*Note: At the moment, `createJPEGStream()` is synchronous under the hood. That is, it runs in the main thread, not in the libuv threadpool.* -* `config` an object specifying the quality (0 to 1), if progressive compression - should be used and/or if chroma subsampling should be used: - `{quality: 0.75, progressive: false, chromaSubsampling: true}`. All properties - are optional. +* `config` an object specifying the quality (0 to 1), if progressive compression should be used and/or if chroma subsampling should be used: `{quality: 0.75, progressive: false, chromaSubsampling: true}`. All properties are optional. #### Examples @@ -295,140 +354,118 @@ const stream = canvas.createJPEGStream({ }) ``` -### Canvas#toDataURL() sync and async +### Canvas#createPDFStream() -The following syntax patterns are supported: +> ```ts +> canvas.createPDFStream(config?: any) => ReadableStream +> ``` -```javascript -var dataUrl = canvas.toDataURL(); // defaults to PNG -var dataUrl = canvas.toDataURL('image/png'); -canvas.toDataURL(function(err, png){ }); // defaults to PNG -canvas.toDataURL('image/png', function(err, png){ }); -canvas.toDataURL('image/jpeg', function(err, jpeg){ }); // sync JPEG is not supported -canvas.toDataURL('image/jpeg', {opts...}, function(err, jpeg){ }); // see Canvas#createJPEGStream for valid options -canvas.toDataURL('image/jpeg', quality, function(err, jpeg){ }); // spec-following; quality from 0 to 1 -``` - -### `registerFont` for bundled fonts +Applies to PDF canvases only. Creates a [`ReadableStream`](https://nodejs.org/api/stream.html#stream_class_stream_readable) that emits the encoded PDF. `canvas.toBuffer()` also produces an encoded PDF, but `createPDFStream()` can be used to reduce memory usage. -It can be useful to use a custom font file if you are distributing code that uses node-canvas and a specific font. Or perhaps you are using it to do automated tests and you want the renderings to be the same across operating systems regardless of what fonts are installed. +### Canvas#toDataURL() -To do that, you should use `registerFont()`. - -**You need to call it before the Canvas is created** - -```javascript -const { registerFont, createCanvas } = require('canvas'); -registerFont('comicsans.ttf', {family: 'Comic Sans'}); +This is a standard API, but several non-standard calls are supported. The full list of supported calls is: -var canvas = createCanvas(500, 500), - ctx = canvas.getContext('2d'); - -ctx.font = '12px "Comic Sans"'; -ctx.fillText('Everyone hates this font :(', 250, 10); +```js +dataUrl = canvas.toDataURL() // defaults to PNG +dataUrl = canvas.toDataURL('image/png') +canvas.toDataURL((err, png) => { }) // defaults to PNG +canvas.toDataURL('image/png', (err, png) => { }) +canvas.toDataURL('image/jpeg', (err, jpeg) => { }) // sync JPEG is not supported +canvas.toDataURL('image/jpeg', {...opts}, (err, jpeg) => { }) // see Canvas#createJPEGStream for valid options +canvas.toDataURL('image/jpeg', quality, (err, jpeg) => { }) // spec-following; quality from 0 to 1 ``` -The second argument is an object with properties that resemble the CSS properties that are specified in `@font-face` rules. You must specify at least `family`. `weight`, and `style` are optional (and default to "normal"). - ### CanvasRenderingContext2D#patternQuality -Given one of the values below will alter pattern (gradients, images, etc) render quality, defaults to _good_. +> ```ts +> context.patternQuality: 'fast'|'good'|'best'|'nearest'|'bilinear' +> ``` - - fast - - good - - best - - nearest - - bilinear +Defaults to `'good'`. Affects pattern (gradient, image, etc.) rendering quality. -### CanvasRenderingContext2D#textDrawingMode +### CanvasRenderingContext2D#filter -Can be either `path` or `glyph`. Using `glyph` is much faster than `path` for drawing, and when using a PDF context will embed the text natively, so will be selectable and lower filesize. The downside is that cairo does not have any subpixel precision for `glyph`, so this will be noticeably lower quality for text positioning in cases such as rotated text. Also, strokeText in `glyph` will act the same as fillText, except using the stroke style for the fill. +> ```ts +> context.filter: 'fast'|'good'|'best'|'nearest'|'bilinear' +> ``` -Defaults to _path_. +Defaults to `'good'`. Like `patternQuality`, but applies to transformations affecting more than just patterns. -This property is tracked as part of the canvas state in save/restore. +### CanvasRenderingContext2D#textDrawingMode -### CanvasRenderingContext2D#filter +> ```ts +> context.textDrawingMode: 'path'|'glyph' +> ``` -Like `patternQuality`, but applies to transformations effecting more than just patterns. Defaults to _good_. +Defaults to `'path'`. The effect depends on the canvas type: - - fast - - good - - best - - nearest - - bilinear +* **Standard (image)** `glyph` and `path` both result in rasterized text. Glyph mode is faster than `path`, but may result in lower-quality text, especially when rotated or translated. -### Global Composite Operations +* **PDF** `glyph` will embed text instead of paths into the PDF. This is faster to encode, faster to open with PDF viewers, yields a smaller file size and makes the text selectable. The subset of the font needed to render the glyphs will be embedded in the PDF. This is usually the mode you want to use with PDF canvases. -In addition to those specified and commonly implemented by browsers, the following have been added: +* **SVG** `glyph` does *not* cause `` elements to be produced as one might expect ([cairo bug](https://gitlab.freedesktop.org/cairo/cairo/issues/253)). Rather, `glyph` will create a `` section with a `` for each glyph, then those glyphs be reused via `` elements. `path` mode creates a `` element for each text string. `glyph` mode is faster and yields a smaller file size. - - multiply - - screen - - overlay - - hard-light - - soft-light - - hsl-hue - - hsl-saturation - - hsl-color - - hsl-luminosity +In `glyph` mode, `ctx.strokeText()` and `ctx.fillText()` behave the same (aside from using the stroke and fill style, respectively). -## Anti-Aliasing +This property is tracked as part of the canvas state in save/restore. - Set anti-aliasing mode +### CanvasRenderingContext2D#globalCompositeOperator = 'saturate' - - default - - none - - gray - - subpixel +In addition to all of the standard global composite operators defined by the Canvas specification, the ['saturate'](https://www.cairographics.org/operators/#saturate) operator is also available. - For example: +### CanvasRenderingContext2D#antialias -```javascript -ctx.antialias = 'none'; -``` +> ```ts +> context.antialias: 'default'|'none'|'gray'|'subpixel' +> ``` -## PDF Support +Sets the anti-aliasing mode. - Basic PDF support was added in 0.11.0. If you are building cairo from source, be sure to use `--enable-pdf=yes` for the PDF backend. - node-canvas must know that it is creating a PDF on initialization, using the "pdf" string: +## PDF Output Support + +node-canvas can create PDF documents instead of images. The canvas type must be set when creating the canvas as follows: ```js -var canvas = createCanvas(200, 500, 'pdf'); +const canvas = createCanvas(200, 500, 'pdf') ``` - An additional method `.addPage()` is then available to create - multiple page PDFs: +An additional method `.addPage()` is then available to create multiple page PDFs: ```js -ctx.font = '22px Helvetica'; -ctx.fillText('Hello World', 50, 80); -ctx.addPage(); +// On first page +ctx.font = '22px Helvetica' +ctx.fillText('Hello World', 50, 80) -ctx.font = '22px Helvetica'; -ctx.fillText('Hello World 2', 50, 80); -ctx.addPage(); +ctx.addPage() +// Now on second page +ctx.font = '22px Helvetica' +ctx.fillText('Hello World 2', 50, 80) -ctx.font = '22px Helvetica'; -ctx.fillText('Hello World 3', 50, 80); -ctx.addPage(); +canvas.toBuffer() // returns a PDF file +canvas.createPDFStream() // returns a ReadableStream that emits a PDF ``` -## SVG Support +See also: + +* [Image#dataMode](#imagedatamode) for embedding JPEGs in PDFs +* [Canvas#createPDFStream()](#canvascreatepdfstream) for creating PDF streams +* [CanvasRenderingContext2D#textDrawingMode](#canvasrenderingcontext2dtextdrawingmode) + for embedding text instead of paths + +## SVG Output Support - Just like PDF support, make sure to install cairo with `--enable-svg=yes`. - You also need to tell node-canvas that it is working on SVG upon its initialization: +node-canvas can create SVG documents instead of images. The canva type must be set when creating the canvas as follows: ```js -var canvas = createCanvas(200, 500, 'svg'); +const canvas = createCanvas(200, 500, 'svg') // Use the normal primitives. -fs.writeFileSync('out.svg', canvas.toBuffer()); +fs.writeFileSync('out.svg', canvas.toBuffer()) ``` ## SVG Image Support -If librsvg is available when node-canvas is installed, node-canvas can render -SVG images to your canvas context. This currently works by rasterizing the SVG -image (i.e. drawing an SVG image to an SVG canvas will not preserve the SVG data). +If librsvg is available when node-canvas is installed, node-canvas can render SVG images to your canvas context. This currently works by rasterizing the SVG image (i.e. drawing an SVG image to an SVG canvas will not preserve the SVG data). ```js const img = new Image() @@ -439,111 +476,53 @@ img.src = './example.svg' ## Image pixel formats (experimental) -node-canvas has experimental support for additional pixel formats, roughly -following the [Canvas color space proposal](https://github.com/WICG/canvas-color-space/blob/master/CanvasColorSpaceProposal.md). +node-canvas has experimental support for additional pixel formats, roughly following the [Canvas color space proposal](https://github.com/WICG/canvas-color-space/blob/master/CanvasColorSpaceProposal.md). ```js -var canvas = createCanvas(200, 200); -var ctx = canvas.getContext('2d', {pixelFormat: 'A8'}); +const canvas = createCanvas(200, 200) +const ctx = canvas.getContext('2d', { pixelFormat: 'A8' }) ``` -By default, canvases are created in the `RGBA32` format, which corresponds to -the native HTML Canvas behavior. Each pixel is 32 bits. The JavaScript APIs -that involve pixel data (`getImageData`, `putImageData`) store the colors in -the order {red, green, blue, alpha} without alpha pre-multiplication. (The C++ -API stores the colors in the order {alpha, red, green, blue} in native-[endian](https://en.wikipedia.org/wiki/Endianness) -ordering, with alpha pre-multiplication.) +By default, canvases are created in the `RGBA32` format, which corresponds to the native HTML Canvas behavior. Each pixel is 32 bits. The JavaScript APIs that involve pixel data (`getImageData`, `putImageData`) store the colors in the order {red, green, blue, alpha} without alpha pre-multiplication. (The C++ API stores the colors in the order {alpha, red, green, blue} in native-[endian](https://en.wikipedia.org/wiki/Endianness) ordering, with alpha pre-multiplication.) These additional pixel formats have experimental support: -* `RGB24` Like `RGBA32`, but the 8 alpha bits are always opaque. This format is - always used if the `alpha` context attribute is set to false (i.e. - `canvas.getContext('2d', {alpha: false})`). This format can be faster than - `RGBA32` because transparency does not need to be calculated. -* `A8` Each pixel is 8 bits. This format can either be used for creating - grayscale images (treating each byte as an alpha value), or for creating - indexed PNGs (treating each byte as a palette index) (see [the example using - alpha values with `fillStyle`](examples/indexed-png-alpha.js) and [the - example using `imageData`](examples/indexed-png-image-data.js)). -* `RGB16_565` Each pixel is 16 bits, with red in the upper 5 bits, green in the - middle 6 bits, and blue in the lower 5 bits, in native platform endianness. - Some hardware devices and frame buffers use this format. Note that PNG does - not support this format; when creating a PNG, the image will be converted to - 24-bit RGB. This format is thus suboptimal for generating PNGs. - `ImageData` instances for this mode use a `Uint16Array` instead of a `Uint8ClampedArray`. -* `A1` Each pixel is 1 bit, and pixels are packed together into 32-bit - quantities. The ordering of the bits matches the endianness of the - platform: on a little-endian machine, the first pixel is the least- - significant bit. This format can be used for creating single-color images. - *Support for this format is incomplete, see note below.* -* `RGB30` Each pixel is 30 bits, with red in the upper 10, green - in the middle 10, and blue in the lower 10. (Requires Cairo 1.12 or later.) - *Support for this format is incomplete, see note below.* +* `RGB24` Like `RGBA32`, but the 8 alpha bits are always opaque. This format is always used if the `alpha` context attribute is set to false (i.e. `canvas.getContext('2d', {alpha: false})`). This format can be faster than `RGBA32` because transparency does not need to be calculated. +* `A8` Each pixel is 8 bits. This format can either be used for creating grayscale images (treating each byte as an alpha value), or for creating indexed PNGs (treating each byte as a palette index) (see [the example using alpha values with `fillStyle`](examples/indexed-png-alpha.js) and [the example using `imageData`](examples/indexed-png-image-data.js)). +* `RGB16_565` Each pixel is 16 bits, with red in the upper 5 bits, green in the middle 6 bits, and blue in the lower 5 bits, in native platform endianness. Some hardware devices and frame buffers use this format. Note that PNG does not support this format; when creating a PNG, the image will be converted to 24-bit RGB. This format is thus suboptimal for generating PNGs. `ImageData` instances for this mode use a `Uint16Array` instead of a `Uint8ClampedArray`. +* `A1` Each pixel is 1 bit, and pixels are packed together into 32-bit quantities. The ordering of the bits matches the endianness of the + platform: on a little-endian machine, the first pixel is the least-significant bit. This format can be used for creating single-color images. *Support for this format is incomplete, see note below.* +* `RGB30` Each pixel is 30 bits, with red in the upper 10, green in the middle 10, and blue in the lower 10. (Requires Cairo 1.12 or later.) *Support for this format is incomplete, see note below.* Notes and caveats: -* Using a non-default format can affect the behavior of APIs that involve pixel - data: +* Using a non-default format can affect the behavior of APIs that involve pixel data: - * `context2d.createImageData` The size of the array returned depends on the - number of bit per pixel for the underlying image data format, per the above - descriptions. - * `context2d.getImageData` The format of the array returned depends on the - underlying image mode, per the above descriptions. Be aware of platform - endianness, which can be determined using node.js's [`os.endianness()`](https://nodejs.org/api/os.html#os_os_endianness) + * `context2d.createImageData` The size of the array returned depends on the number of bit per pixel for the underlying image data format, per the above descriptions. + * `context2d.getImageData` The format of the array returned depends on the underlying image mode, per the above descriptions. Be aware of platform endianness, which can be determined using node.js's [`os.endianness()`](https://nodejs.org/api/os.html#os_os_endianness) function. * `context2d.putImageData` As above. -* `A1` and `RGB30` do not yet support `getImageData` or `putImageData`. Have a - use case and/or opinion on working with these formats? Open an issue and let - us know! (See #935.) +* `A1` and `RGB30` do not yet support `getImageData` or `putImageData`. Have a use case and/or opinion on working with these formats? Open an issue and let us know! (See #935.) -* `A1`, `A8`, `RGB30` and `RGB16_565` with shadow blurs may crash or not render - properly. +* `A1`, `A8`, `RGB30` and `RGB16_565` with shadow blurs may crash or not render properly. -* The `ImageData(width, height)` and `ImageData(Uint8ClampedArray, width)` - constructors assume 4 bytes per pixel. To create an `ImageData` instance with - a different number of bytes per pixel, use - `new ImageData(new Uint8ClampedArray(size), width, height)` or - `new ImageData(new Uint16ClampedArray(size), width, height)`. +* The `ImageData(width, height)` and `ImageData(Uint8ClampedArray, width)` constructors assume 4 bytes per pixel. To create an `ImageData` instance with a different number of bytes per pixel, use `new ImageData(new Uint8ClampedArray(size), width, height)` or `new ImageData(new Uint16ClampedArray(size), width, height)`. ## Benchmarks - Although node-canvas is extremely new, and we have not even begun optimization yet it is already quite fast. For benchmarks vs other node canvas implementations view this [gist](https://gist.github.com/664922), or update the submodules and run `$ make benchmark` yourself. - -## Contribute - - Want to contribute to node-canvas? patches for features, bug fixes, documentation, examples and others are certainly welcome. Take a look at the [issue queue](https://github.com/Automattic/node-canvas/issues) for existing issues. +Benchmarks live in the `benchmarks` directory. ## Examples - Examples are placed in _./examples_, be sure to check them out! most produce a png image of the same name, and others such as _live-clock.js_ launch an http server to be viewed in the browser. - -## Documentation - -This project is an implementation of the Web Canvas API. For API documentation, please visit: [Mozilla Web Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) - -## Testing - -If you have not previously, init git submodules: +Examples line in the `examples` directory. Most produce a png image of the same name, and others such as *live-clock.js* launch an HTTP server to be viewed in the browser. - $ git submodule update --init +## Original Authors -Install the node modules: - - $ npm install - -Build node-canvas: - - $ node-gyp rebuild - -Unit tests: - - $ make test - -Visual tests: - - $ make test-server + - TJ Holowaychuk ([tj](http://github.com/tj)) + - Nathan Rajlich ([TooTallNate](http://github.com/TooTallNate)) + - Rod Vagg ([rvagg](http://github.com/rvagg)) + - Juriy Zaytsev ([kangax](http://github.com/kangax)) ## License @@ -553,21 +532,19 @@ Copyright (c) 2010 LearnBoost, and contributors <dev@learnboost.com> Copyright (c) 2014 Automattic, Inc and contributors <dev@automattic.com> -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the 'Software'), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. From 4bc2b049ca681cf03c204fba594098c125f804b5 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 13 Oct 2018 19:22:09 -0700 Subject: [PATCH 192/474] Ship 2.0.0 --- CHANGELOG.md | 8 +++++++- Readme.md | 15 ++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad96dd2bd..b9b4184fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -2.0.0 (unreleased -- encompasses all alpha versions) +(Unreleased) +================== +### Changed +### Added +### Fixed + +2.0.0 ================== **Upgrading from 1.x** diff --git a/Readme.md b/Readme.md index b99715cee..3c3d76637 100644 --- a/Readme.md +++ b/Readme.md @@ -1,14 +1,5 @@ # node-canvas -## This is the documentation for version 2.0.0-alpha -Alpha versions of 2.0 can be installed using `npm install canvas@next`. - -See the [changelog](https://github.com/Automattic/node-canvas/blob/master/CHANGELOG.md) for a guide to upgrading from 1.x to 2.x. - -**For version 1.x documentation, see [the v1.x branch](https://github.com/Automattic/node-canvas/tree/v1.x)** - ------ - [![Build Status](https://travis-ci.org/Automattic/node-canvas.svg?branch=master)](https://travis-ci.org/Automattic/node-canvas) [![NPM version](https://badge.fury.io/js/canvas.svg)](http://badge.fury.io/js/canvas) @@ -70,6 +61,12 @@ loadImage('examples/images/lime-cat.jpg').then((image) => { }) ``` +## Upgrading from 2.x + +See the [changelog](https://github.com/Automattic/node-canvas/blob/master/CHANGELOG.md) for a guide to upgrading from 1.x to 2.x. + +For version 1.x documentation, see [the v1.x branch](https://github.com/Automattic/node-canvas/tree/v1.x). + ## Documentation This project is an implementation of the Web Canvas API and implements that API as closely as possible. For API documentation, please visit [Mozilla Web Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API). (See [Compatibility Status](https://github.com/Automattic/node-canvas/wiki/Compatibility-Status) for the current API compliance.) All utility methods and non-standard APIs are documented below. From f154f186e8de99a4cd0935945ea78c98e18de2ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Sun, 14 Oct 2018 15:00:39 +0200 Subject: [PATCH 193/474] 2.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ddb7e012a..8e7d913b9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.0.0-alpha.18", + "version": "2.0.0", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 14cde41766d4f69d0f41e9fc20832af031785d6a Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sun, 14 Oct 2018 13:34:32 -0700 Subject: [PATCH 194/474] Rename 'filter' to 'quality' Ref #1063 --- CHANGELOG.md | 8 ++++++++ Readme.md | 6 +++--- src/CanvasRenderingContext2d.cc | 6 +++--- src/CanvasRenderingContext2d.h | 4 ++-- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9b4184fe..091950aa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,11 @@ canvas.createPNGStream() // new canvas.syncJPEGStream() // old canvas.createSyncJPEGStream() // old canvas.createJPEGStream() // new + +// (6) Context2d.filter has been renamed to context2d.quality to avoid a +// conflict with the new standard 'filter' property. +context.filter = 'best' // old +context.quality = 'best' // new ``` ### Breaking @@ -72,6 +77,9 @@ canvas.createJPEGStream() // new * See also: *Correct some of the `globalCompositeOperator` types* under **Fixed**. These changes were bug-fixes, but will break existing code relying on the incorrect types. + * Rename `context2d.filter` to `context2d.quality` to avoid a conflict with the + new standard 'filter' property. Note that the standard 'filter' property is + not yet implemented. ### Fixed * Fix build with SVG support enabled (#1123) diff --git a/Readme.md b/Readme.md index 3c3d76637..90d7c62cc 100644 --- a/Readme.md +++ b/Readme.md @@ -88,7 +88,7 @@ This project is an implementation of the Web Canvas API and implements that API * [Canvas#createPDFStream()](#canvascreatepdfstream) * [Canvas#toDataURL()](#canvastodataurl) * [CanvasRenderingContext2D#patternQuality](#canvasrenderingcontext2dpatternquality) -* [CanvasRenderingContext2D#filter](#canvasrenderingcontext2dfilter) +* [CanvasRenderingContext2D#quality](#canvasrenderingcontext2dquality) * [CanvasRenderingContext2D#textDrawingMode](#canvasrenderingcontext2dtextdrawingmode) * [CanvasRenderingContext2D#globalCompositeOperator = 'saturate'](#canvasrenderingcontext2dglobalcompositeoperator--saturate) * [CanvasRenderingContext2D#antialias](#canvasrenderingcontext2dantialias) @@ -381,10 +381,10 @@ canvas.toDataURL('image/jpeg', quality, (err, jpeg) => { }) // spec-following; q Defaults to `'good'`. Affects pattern (gradient, image, etc.) rendering quality. -### CanvasRenderingContext2D#filter +### CanvasRenderingContext2D#quality > ```ts -> context.filter: 'fast'|'good'|'best'|'nearest'|'bilinear' +> context.quality: 'fast'|'good'|'best'|'nearest'|'bilinear' > ``` Defaults to `'good'`. Like `patternQuality`, but applies to transformations affecting more than just patterns. diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 1a4abb4ef..3eadc30ea 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -171,7 +171,7 @@ Context2d::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { SetProtoAccessor(proto, Nan::New("shadowBlur").ToLocalChecked(), GetShadowBlur, SetShadowBlur, ctor); SetProtoAccessor(proto, Nan::New("antialias").ToLocalChecked(), GetAntiAlias, SetAntiAlias, ctor); SetProtoAccessor(proto, Nan::New("textDrawingMode").ToLocalChecked(), GetTextDrawingMode, SetTextDrawingMode, ctor); - SetProtoAccessor(proto, Nan::New("filter").ToLocalChecked(), GetFilter, SetFilter, ctor); + SetProtoAccessor(proto, Nan::New("quality").ToLocalChecked(), GetQuality, SetQuality, ctor); Nan::Set(target, Nan::New("CanvasRenderingContext2d").ToLocalChecked(), ctor->GetFunction()); } @@ -1569,7 +1569,7 @@ NAN_SETTER(Context2d::SetTextDrawingMode) { * Get filter. */ -NAN_GETTER(Context2d::GetFilter) { +NAN_GETTER(Context2d::GetQuality) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *filter; switch (cairo_pattern_get_filter(cairo_get_source(context->context()))) { @@ -1586,7 +1586,7 @@ NAN_GETTER(Context2d::GetFilter) { * Set filter. */ -NAN_SETTER(Context2d::SetFilter) { +NAN_SETTER(Context2d::SetQuality) { Nan::Utf8String str(value->ToString()); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_filter_t filter; diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index 539cf4329..f1044995d 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -133,7 +133,7 @@ class Context2d: public Nan::ObjectWrap { static NAN_GETTER(GetShadowBlur); static NAN_GETTER(GetAntiAlias); static NAN_GETTER(GetTextDrawingMode); - static NAN_GETTER(GetFilter); + static NAN_GETTER(GetQuality); static NAN_SETTER(SetPatternQuality); static NAN_SETTER(SetImageSmoothingEnabled); static NAN_SETTER(SetGlobalCompositeOperation); @@ -149,7 +149,7 @@ class Context2d: public Nan::ObjectWrap { static NAN_SETTER(SetShadowBlur); static NAN_SETTER(SetAntiAlias); static NAN_SETTER(SetTextDrawingMode); - static NAN_SETTER(SetFilter); + static NAN_SETTER(SetQuality); inline void setContext(cairo_t *ctx) { _context = ctx; } inline cairo_t *context(){ return _context; } inline Canvas *canvas(){ return _canvas; } From e88ccb6e1b623d8485e3079ca60512b6f87e25c2 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Mon, 15 Oct 2018 11:03:52 +0200 Subject: [PATCH 195/474] Add visual comparision to tests (#1228) * add-visual-comparision * add-visual-comparision * fix * fix2 * better diffs * fixed pixel match * fixed lint * extra command * better css * more * new tests * more tests * removed the double empty line * updated test description * add pixelmatch as a package * add pixelmatch as a package * fixed? * fixed standard --- package.json | 1 + test/public/app.html | 1 + test/public/app.js | 41 ++++++++++++--- test/public/style.css | 5 +- test/public/tests.js | 118 ++++++++++++++++++++++++++++++++++++++++++ test/server.js | 4 ++ 6 files changed, 162 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 8e7d913b9..4a60b1154 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "assert-rejects": "^1.0.0", "express": "^4.16.3", "mocha": "^5.2.0", + "pixelmatch": "^4.0.2", "standard": "^12.0.1" }, "engines": { diff --git a/test/public/app.html b/test/public/app.html index 8948b7884..6a9dacd45 100644 --- a/test/public/app.html +++ b/test/public/app.html @@ -12,6 +12,7 @@

node-canvas

The tests below assert visual and api integrity by running the exact same code utilizing the client canvas api, as well as node-canvas.

+ diff --git a/test/public/app.js b/test/public/app.js index db3b66e48..0a2eafd07 100644 --- a/test/public/app.js +++ b/test/public/app.js @@ -18,19 +18,38 @@ function pdfLink (name) { }) } -function localRendering (name) { +function localRendering (name, callback) { var canvas = create('canvas', { width: 200, height: 200, title: name }) - + var tests = window.tests var ctx = canvas.getContext('2d', { alpha: true }) var initialFillStyle = ctx.fillStyle ctx.fillStyle = 'white' ctx.fillRect(0, 0, 200, 200) ctx.fillStyle = initialFillStyle - window.tests[name](ctx, function () {}) - + if (tests[name].length === 2) { + tests[name](ctx, callback) + } else { + tests[name](ctx) + callback(null) + } return canvas } +function getDifference (canvas, image, outputCanvas) { + var imgCanvas = create('canvas', { width: 200, height: 200 }) + var ctx = imgCanvas.getContext('2d', { alpha: true }) + var output = outputCanvas.getContext('2d', { alpha: true }).getImageData(0, 0, 200, 200) + ctx.drawImage(image, 0, 0, 200, 200) + var imageDataCanvas = ctx.getImageData(0, 0, 200, 200).data + var imageDataGolden = canvas.getContext('2d', { alpha: true }).getImageData(0, 0, 200, 200).data + window.pixelmatch(imageDataCanvas, imageDataGolden, output.data, 200, 200, { + includeAA: false, + threshold: 0.15 + }) + outputCanvas.getContext('2d', { alpha: true }).putImageData(output, 0, 0) + return outputCanvas +} + function clearTests () { var table = document.getElementById('tests') if (table) document.body.removeChild(table) @@ -45,12 +64,22 @@ function runTests () { create('thead', {}, [ create('th', { textContent: 'node-canvas' }), create('th', { textContent: 'browser canvas' }), + create('th', { textContent: 'visual diffs' }), create('th', { textContent: '' }) ]), create('tbody', {}, testNames.map(function (name) { + var img = create('img') + var canvasOuput = create('canvas', { width: 200, height: 200, title: name }) + var canvas = localRendering(name, function () { + img.onload = function () { + getDifference(canvas, img, canvasOuput) + } + img.src = '/render?name=' + encodeURIComponent(name) + }) return create('tr', {}, [ - create('td', {}, [create('img', { src: '/render?name=' + encodeURIComponent(name) })]), - create('td', {}, [localRendering(name)]), + create('td', {}, [img]), + create('td', {}, [canvas]), + create('td', {}, [canvasOuput]), create('td', {}, [create('h3', { textContent: name }), pdfLink(name)]) ]) })) diff --git a/test/public/style.css b/test/public/style.css index f8a1c3abb..75116758e 100644 --- a/test/public/style.css +++ b/test/public/style.css @@ -26,11 +26,12 @@ p.msg { } table tr td:nth-child(1), -table tr td:nth-child(2) { +table tr td:nth-child(2), +table tr td:nth-child(3) { width: 200px; } -table tr td:nth-child(3) { +table tr td:nth-child(4) { padding: 0 45px; } diff --git a/test/public/tests.js b/test/public/tests.js index 899986158..d26a922bf 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -1654,6 +1654,51 @@ tests['shadow image'] = function (ctx, done) { img.src = imageSrc('star.png') } +tests['shadow image with crop'] = function (ctx, done) { + var img = new Image() + img.onload = function () { + ctx.shadowColor = '#000' + ctx.shadowBlur = 4 + ctx.shadowOffsetX = 4 + ctx.shadowOffsetY = 4 + + // cropped + ctx.drawImage(img, 100, 100, 150, 150, 25, 25, 150, 150) + done(null) + } + img.onerror = function () { + done(new Error('Failed to load image')) + } + img.src = imageSrc('face.jpeg') +} + +tests['shadow image with crop and zoom'] = function (ctx, done) { + var img = new Image() + img.onload = function () { + ctx.shadowColor = '#000' + ctx.shadowBlur = 4 + ctx.shadowOffsetX = 4 + ctx.shadowOffsetY = 4 + + // cropped + ctx.drawImage(img, 100, 100, 40, 40, 25, 25, 150, 150) + done(null) + } + img.onerror = function () { + done(new Error('Failed to load image')) + } + img.src = imageSrc('face.jpeg') +} + +tests['drawImage canvas over canvas'] = function (ctx) { + // Drawing canvas to itself + ctx.fillStyle = 'white' + ctx.fillRect(0, 0, 200, 200) + ctx.fillStyle = 'black' + ctx.fillRect(5, 5, 10, 10) + ctx.drawImage(ctx.canvas, 20, 20) +} + tests['scaled shadow image'] = function (ctx, done) { var img = new Image() img.onload = function () { @@ -1668,6 +1713,79 @@ tests['scaled shadow image'] = function (ctx, done) { img.src = imageSrc('star.png') } +tests['smoothing disabled image'] = function (ctx, done) { + var img = new Image() + img.onload = function () { + ctx.imageSmoothingEnabled = false + ctx.patternQuality = 'good' + // cropped + ctx.drawImage(img, 0, 0, 10, 10, 0, 0, 200, 200) + done(null) + } + img.onerror = function () { + done(new Error('Failed to load image')) + } + img.src = imageSrc('face.jpeg') +} + +tests['createPattern() with globalAlpha and smoothing off scaling down'] = function (ctx, done) { + var img = new Image() + img.onload = function () { + ctx.imageSmoothingEnabled = false + ctx.patternQuality = 'good' + var pattern = ctx.createPattern(img, 'repeat') + ctx.scale(0.1, 0.1) + ctx.globalAlpha = 0.95 + ctx.fillStyle = pattern + ctx.fillRect(100, 100, 800, 800) + ctx.globalAlpha = 1 + ctx.strokeStyle = pattern + ctx.lineWidth = 800 + ctx.strokeRect(1400, 1100, 1, 800) + done() + } + img.src = imageSrc('face.jpeg') +} + +tests['createPattern() with globalAlpha and smoothing off scaling up'] = function (ctx, done) { + var img = new Image() + img.onload = function () { + ctx.imageSmoothingEnabled = false + ctx.patternQuality = 'good' + var pattern = ctx.createPattern(img, 'repeat') + ctx.scale(20, 20) + ctx.globalAlpha = 0.95 + ctx.fillStyle = pattern + ctx.fillRect(1, 1, 8, 3) + ctx.globalAlpha = 1 + ctx.strokeStyle = pattern + ctx.lineWidth = 2 + ctx.strokeRect(2, 6, 6, 1) + done() + } + img.src = imageSrc('face.jpeg') +} + +tests['smoothing and gradients (gradients are not influenced by patternQuality)'] = function (ctx) { + var grad1 = ctx.createLinearGradient(0, 0, 10, 10) + grad1.addColorStop(0, 'yellow') + grad1.addColorStop(0.25, 'red') + grad1.addColorStop(0.75, 'blue') + grad1.addColorStop(1, 'limegreen') + ctx.imageSmoothingEnabled = false + ctx.patternQuality = 'nearest' + ctx.globalAlpha = 0.9 + // linear grad box + ctx.fillStyle = grad1 + ctx.moveTo(0, 0) + ctx.lineTo(200, 0) + ctx.lineTo(200, 200) + ctx.lineTo(0, 200) + ctx.lineTo(0, 0) + ctx.scale(20, 20) + ctx.fill() +} + tests['shadow integration'] = function (ctx) { ctx.shadowBlur = 5 ctx.shadowOffsetX = 10 diff --git a/test/server.js b/test/server.js index 0660491d1..d583a7748 100644 --- a/test/server.js +++ b/test/server.js @@ -32,6 +32,10 @@ app.get('/', function (req, res) { res.sendFile(path.join(__dirname, 'public', 'app.html')) }) +app.get('/pixelmatch.js', function (req, res) { + res.sendFile(path.join(__dirname, '../node_modules/pixelmatch/', 'index.js')) +}) + app.get('/render', function (req, res, next) { var canvas = Canvas.createCanvas(200, 200) From fd1713578fe5e1163ed455a1c432ed24f90bdd84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Tue, 16 Oct 2018 08:43:39 +0200 Subject: [PATCH 196/474] 2.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4a60b1154..8a6876043 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.0.0", + "version": "2.0.1", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 1d77bb042adc6924e078bf3c487d504bef26bb17 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sun, 21 Oct 2018 18:05:32 +0200 Subject: [PATCH 197/474] Update Readme.md --- Readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Readme.md b/Readme.md index 90d7c62cc..193dc97c2 100644 --- a/Readme.md +++ b/Readme.md @@ -32,6 +32,7 @@ Windows | See the [wiki](https://github.com/Automattic/node-canvas/wiki/Installa Others | See the [wiki](https://github.com/Automattic/node-canvas/wiki) **Mac OS X v10.11+:** If you have recently updated to Mac OS X v10.11+ and are experiencing trouble when compiling, run the following command: `xcode-select --install`. Read more about the problem [on Stack Overflow](http://stackoverflow.com/a/32929012/148072). +If you have xcode 10.0 or higher installed, in order to build from source you need NPM 6.4.1 or higher. ## Quick Example From 3c884454de84a2c9fe3731b5bf674f19df69cb1f Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 25 Oct 2018 09:44:13 -0700 Subject: [PATCH 198/474] Emit more useful error if libjpeg is old (#1289) Ref #1279 --- src/JPEGStream.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/JPEGStream.h b/src/JPEGStream.h index 473be72fc..0f5f9dfe0 100644 --- a/src/JPEGStream.h +++ b/src/JPEGStream.h @@ -11,6 +11,11 @@ #include #include +#if JPEG_LIB_VERSION < 80 && !defined(MEM_SRCDST_SUPPORTED) +// jpeg_mem_dest: +#error("libjpeg-turbo v1.3 or later, or libjpeg v8 or later is required") +#endif + /* * Expanded data destination object for closure output, * inspired by IJG's jdatadst.c From 186b474a9549a80ad4f36d445a7aee78e676f215 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 27 Oct 2018 19:22:28 +0200 Subject: [PATCH 199/474] Adapt to V8 7.0 (#1288) --- src/Canvas.cc | 32 +++++----- src/CanvasGradient.cc | 22 +++---- src/CanvasPattern.cc | 2 +- src/CanvasRenderingContext2d.cc | 110 ++++++++++++++++---------------- src/Image.cc | 10 +-- src/ImageData.cc | 12 ++-- src/backend/Backend.cc | 11 ++++ src/backend/Backend.h | 3 + src/backend/ImageBackend.cc | 17 ++--- src/backend/ImageBackend.h | 1 + src/backend/PdfBackend.cc | 13 ++-- src/backend/PdfBackend.h | 1 + src/backend/SvgBackend.cc | 13 ++-- src/backend/SvgBackend.h | 1 + 14 files changed, 125 insertions(+), 123 deletions(-) diff --git a/src/Canvas.cc b/src/Canvas.cc index b87fc3b31..1ffed6b3d 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -88,9 +88,9 @@ NAN_METHOD(Canvas::New) { Backend* backend = NULL; if (info[0]->IsNumber()) { - int width = info[0]->Uint32Value(), height = 0; + int width = Nan::To(info[0]).FromMaybe(0), height = 0; - if (info[1]->IsNumber()) height = info[1]->Uint32Value(); + if (info[1]->IsNumber()) height = Nan::To(info[1]).FromMaybe(0); if (info[2]->IsString()) { if (0 == strcmp("pdf", *Nan::Utf8String(info[2]))) @@ -107,7 +107,7 @@ NAN_METHOD(Canvas::New) { if (Nan::New(ImageBackend::constructor)->HasInstance(info[0]) || Nan::New(PdfBackend::constructor)->HasInstance(info[0]) || Nan::New(SvgBackend::constructor)->HasInstance(info[0])) { - backend = Nan::ObjectWrap::Unwrap(info[0]->ToObject()); + backend = Nan::ObjectWrap::Unwrap(Nan::To(info[0]).ToLocalChecked()); }else{ return Nan::ThrowTypeError("Invalid arguments"); } @@ -162,7 +162,7 @@ NAN_GETTER(Canvas::GetWidth) { NAN_SETTER(Canvas::SetWidth) { if (value->IsNumber()) { Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - canvas->backend()->setWidth(value->Uint32Value()); + canvas->backend()->setWidth(Nan::To(value).FromMaybe(0)); canvas->resurface(info.This()); } } @@ -183,7 +183,7 @@ NAN_GETTER(Canvas::GetHeight) { NAN_SETTER(Canvas::SetHeight) { if (value->IsNumber()) { Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - canvas->backend()->setHeight(value->Uint32Value()); + canvas->backend()->setHeight(Nan::To(value).FromMaybe(0)); canvas->resurface(info.This()); } } @@ -238,17 +238,17 @@ Canvas::ToBufferAsyncAfter(uv_work_t *req) { static void parsePNGArgs(Local arg, PngClosure& pngargs) { if (arg->IsObject()) { - Local obj = arg->ToObject(); + Local obj = Nan::To(arg).ToLocalChecked(); Local cLevel = obj->Get(Nan::New("compressionLevel").ToLocalChecked()); if (cLevel->IsUint32()) { - uint32_t val = cLevel->Uint32Value(); + uint32_t val = Nan::To(cLevel).FromMaybe(0); // See quote below from spec section 4.12.5.5. if (val <= 9) pngargs.compressionLevel = val; } Local filters = obj->Get(Nan::New("filters").ToLocalChecked()); - if (filters->IsUint32()) pngargs.filters = filters->Uint32Value(); + if (filters->IsUint32()) pngargs.filters = Nan::To(filters).FromMaybe(0); Local palette = obj->Get(Nan::New("palette").ToLocalChecked()); if (palette->IsUint8ClampedArray()) { @@ -263,7 +263,7 @@ static void parsePNGArgs(Local arg, PngClosure& pngargs) { // Optional background color index: Local backgroundIndexVal = obj->Get(Nan::New("backgroundIndex").ToLocalChecked()); if (backgroundIndexVal->IsUint32()) { - pngargs.backgroundIndex = static_cast(backgroundIndexVal->Uint32Value()); + pngargs.backgroundIndex = static_cast(Nan::To(backgroundIndexVal).FromMaybe(0)); } } } @@ -274,11 +274,11 @@ static void parseJPEGArgs(Local arg, JpegClosure& jpegargs) { // user agent must use its default quality value, as if the quality argument // had not been given." - 4.12.5.5 if (arg->IsObject()) { - Local obj = arg->ToObject(); + Local obj = Nan::To(arg).ToLocalChecked(); Local qual = obj->Get(Nan::New("quality").ToLocalChecked()); if (qual->IsNumber()) { - double quality = qual->NumberValue(); + double quality = Nan::To(qual).FromMaybe(0); if (quality >= 0.0 && quality <= 1.0) { jpegargs.quality = static_cast(100.0 * quality); } @@ -286,15 +286,15 @@ static void parseJPEGArgs(Local arg, JpegClosure& jpegargs) { Local chroma = obj->Get(Nan::New("chromaSubsampling").ToLocalChecked()); if (chroma->IsBoolean()) { - bool subsample = chroma->BooleanValue(); + bool subsample = Nan::To(chroma).FromMaybe(0); jpegargs.chromaSubsampling = subsample ? 2 : 1; } else if (chroma->IsNumber()) { - jpegargs.chromaSubsampling = chroma->Uint32Value(); + jpegargs.chromaSubsampling = Nan::To(chroma).FromMaybe(0); } Local progressive = obj->Get(Nan::New("progressive").ToLocalChecked()); if (!progressive->IsUndefined()) { - jpegargs.progressive = progressive->BooleanValue(); + jpegargs.progressive = Nan::To(progressive).FromMaybe(0); } } } @@ -663,7 +663,7 @@ NAN_METHOD(Canvas::RegisterFont) { PangoFontDescription *user_desc = pango_font_description_new(); // now check the attrs, there are many ways to be wrong - Local js_user_desc = info[1]->ToObject(); + Local js_user_desc = Nan::To(info[1]).ToLocalChecked(); Local family_prop = Nan::New("family").ToLocalChecked(); Local weight_prop = Nan::New("weight").ToLocalChecked(); Local style_prop = Nan::New("style").ToLocalChecked(); @@ -857,7 +857,7 @@ Canvas::resurface(Local canvas) { // Reset context context = canvas->Get(Nan::New("context").ToLocalChecked()); if (!context->IsUndefined()) { - Context2d *context2d = ObjectWrap::Unwrap(context->ToObject()); + Context2d *context2d = ObjectWrap::Unwrap(Nan::To(context).ToLocalChecked()); cairo_t *prev = context2d->context(); context2d->setContext(createCairoContext()); cairo_destroy(prev); diff --git a/src/CanvasGradient.cc b/src/CanvasGradient.cc index 74e3284ad..1f493af78 100644 --- a/src/CanvasGradient.cc +++ b/src/CanvasGradient.cc @@ -42,10 +42,10 @@ NAN_METHOD(Gradient::New) { // Linear if (4 == info.Length()) { Gradient *grad = new Gradient( - info[0]->NumberValue() - , info[1]->NumberValue() - , info[2]->NumberValue() - , info[3]->NumberValue()); + Nan::To(info[0]).FromMaybe(0) + , Nan::To(info[1]).FromMaybe(0) + , Nan::To(info[2]).FromMaybe(0) + , Nan::To(info[3]).FromMaybe(0)); grad->Wrap(info.This()); info.GetReturnValue().Set(info.This()); return; @@ -54,12 +54,12 @@ NAN_METHOD(Gradient::New) { // Radial if (6 == info.Length()) { Gradient *grad = new Gradient( - info[0]->NumberValue() - , info[1]->NumberValue() - , info[2]->NumberValue() - , info[3]->NumberValue() - , info[4]->NumberValue() - , info[5]->NumberValue()); + Nan::To(info[0]).FromMaybe(0) + , Nan::To(info[1]).FromMaybe(0) + , Nan::To(info[2]).FromMaybe(0) + , Nan::To(info[3]).FromMaybe(0) + , Nan::To(info[4]).FromMaybe(0) + , Nan::To(info[5]).FromMaybe(0)); grad->Wrap(info.This()); info.GetReturnValue().Set(info.This()); return; @@ -87,7 +87,7 @@ NAN_METHOD(Gradient::AddColorStop) { rgba_t color = rgba_create(rgba); cairo_pattern_add_color_stop_rgba( grad->pattern() - , info[0]->NumberValue() + , Nan::To(info[0]).FromMaybe(0) , color.r , color.g , color.b diff --git a/src/CanvasPattern.cc b/src/CanvasPattern.cc index 69ab75a97..f37a0028c 100644 --- a/src/CanvasPattern.cc +++ b/src/CanvasPattern.cc @@ -45,7 +45,7 @@ NAN_METHOD(Pattern::New) { cairo_surface_t *surface; - Local obj = info[0]->ToObject(); + Local obj = Nan::To(info[0]).ToLocalChecked(); // Image if (Nan::New(Image::constructor)->HasInstance(obj)) { diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 3eadc30ea..d403024cc 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -76,7 +76,7 @@ inline static bool checkArgs(const Nan::FunctionCallbackInfo &info, doubl bool areArgsValid = true; for (int i = offset; i < argsEnd; i++) { - double val = info[i]->NumberValue(); + double val = Nan::To(info[i]).FromMaybe(0); if (areArgsValid) { if (val != val || @@ -644,7 +644,7 @@ NAN_METHOD(Context2d::New) { if (!info[0]->IsObject()) return Nan::ThrowTypeError("Canvas expected"); - Local obj = info[0]->ToObject(); + Local obj = Nan::To(info[0]).ToLocalChecked(); if (!Nan::New(Canvas::constructor)->HasInstance(obj)) return Nan::ThrowTypeError("Canvas expected"); Canvas *canvas = Nan::ObjectWrap::Unwrap(obj); @@ -653,7 +653,7 @@ NAN_METHOD(Context2d::New) { if (isImageBackend) { cairo_format_t format = ImageBackend::DEFAULT_FORMAT; if (info[1]->IsObject()) { - Local ctxAttributes = info[1]->ToObject(); + Local ctxAttributes = Nan::To(info[1]).ToLocalChecked(); Local pixelFormat = ctxAttributes->Get(Nan::New("pixelFormat").ToLocalChecked()); if (pixelFormat->IsString()) { @@ -670,7 +670,7 @@ NAN_METHOD(Context2d::New) { // alpha: false forces use of RGB24 Local alpha = ctxAttributes->Get(Nan::New("alpha").ToLocalChecked()); - if (alpha->IsBoolean() && !alpha->BooleanValue()) { + if (alpha->IsBoolean() && !Nan::To(alpha).FromMaybe(0)) { format = CAIRO_FORMAT_RGB24; } } @@ -728,7 +728,7 @@ NAN_METHOD(Context2d::AddPage) { NAN_METHOD(Context2d::PutImageData) { if (!info[0]->IsObject()) return Nan::ThrowTypeError("ImageData expected"); - Local obj = info[0]->ToObject(); + Local obj = Nan::To(info[0]).ToLocalChecked(); if (!Nan::New(ImageData::constructor)->HasInstance(obj)) return Nan::ThrowTypeError("ImageData expected"); @@ -746,8 +746,8 @@ NAN_METHOD(Context2d::PutImageData) { , sy = 0 , sw = 0 , sh = 0 - , dx = info[1]->Int32Value() - , dy = info[2]->Int32Value() + , dx = Nan::To(info[1]).FromMaybe(0) + , dy = Nan::To(info[2]).FromMaybe(0) , rows , cols; @@ -759,10 +759,10 @@ NAN_METHOD(Context2d::PutImageData) { break; // imageData, dx, dy, sx, sy, sw, sh case 7: - sx = info[3]->Int32Value(); - sy = info[4]->Int32Value(); - sw = info[5]->Int32Value(); - sh = info[6]->Int32Value(); + sx = Nan::To(info[3]).FromMaybe(0); + sy = Nan::To(info[4]).FromMaybe(0); + sw = Nan::To(info[5]).FromMaybe(0); + sh = Nan::To(info[6]).FromMaybe(0); // fix up negative height, width if (sw < 0) sx += sw, sw = -sw; if (sh < 0) sy += sh, sh = -sh; @@ -917,10 +917,10 @@ NAN_METHOD(Context2d::GetImageData) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Canvas *canvas = context->canvas(); - int sx = info[0]->Int32Value(); - int sy = info[1]->Int32Value(); - int sw = info[2]->Int32Value(); - int sh = info[3]->Int32Value(); + int sx = Nan::To(info[0]).FromMaybe(0); + int sy = Nan::To(info[1]).FromMaybe(0); + int sw = Nan::To(info[2]).FromMaybe(0); + int sh = Nan::To(info[3]).FromMaybe(0); if (!sw) return Nan::ThrowError("IndexSizeError: The source width is 0."); @@ -1117,7 +1117,7 @@ NAN_METHOD(Context2d::DrawImage) { cairo_surface_t *surface; - Local obj = info[0]->ToObject(); + Local obj = Nan::To(info[0]).ToLocalChecked(); // Image if (Nan::New(Image::constructor)->HasInstance(obj)) { @@ -1275,7 +1275,7 @@ NAN_GETTER(Context2d::GetGlobalAlpha) { */ NAN_SETTER(Context2d::SetGlobalAlpha) { - double n = value->NumberValue(); + double n = Nan::To(value).FromMaybe(0); if (n >= 0 && n <= 1) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); context->state->globalAlpha = n; @@ -1340,7 +1340,7 @@ NAN_GETTER(Context2d::GetGlobalCompositeOperation) { NAN_SETTER(Context2d::SetPatternQuality) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Nan::Utf8String quality(value->ToString()); + Nan::Utf8String quality(Nan::To(value).ToLocalChecked()); if (0 == strcmp("fast", *quality)) { context->state->patternQuality = CAIRO_FILTER_FAST; } else if (0 == strcmp("good", *quality)) { @@ -1377,7 +1377,7 @@ NAN_GETTER(Context2d::GetPatternQuality) { NAN_SETTER(Context2d::SetImageSmoothingEnabled) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->imageSmoothingEnabled = value->BooleanValue(); + context->state->imageSmoothingEnabled = Nan::To(value).FromMaybe(0); } /* @@ -1396,7 +1396,7 @@ NAN_GETTER(Context2d::GetImageSmoothingEnabled) { NAN_SETTER(Context2d::SetGlobalCompositeOperation) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); - Nan::Utf8String opStr(value->ToString()); // Unlike CSS colors, this *is* case-sensitive + Nan::Utf8String opStr(Nan::To(value).ToLocalChecked()); // Unlike CSS colors, this *is* case-sensitive const std::map blendmodes = { // composite modes: {"clear", CAIRO_OPERATOR_CLEAR}, @@ -1453,7 +1453,7 @@ NAN_GETTER(Context2d::GetShadowOffsetX) { NAN_SETTER(Context2d::SetShadowOffsetX) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->shadowOffsetX = value->NumberValue(); + context->state->shadowOffsetX = Nan::To(value).FromMaybe(0); } /* @@ -1471,7 +1471,7 @@ NAN_GETTER(Context2d::GetShadowOffsetY) { NAN_SETTER(Context2d::SetShadowOffsetY) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->shadowOffsetY = value->NumberValue(); + context->state->shadowOffsetY = Nan::To(value).FromMaybe(0); } /* @@ -1488,7 +1488,7 @@ NAN_GETTER(Context2d::GetShadowBlur) { */ NAN_SETTER(Context2d::SetShadowBlur) { - int n = value->NumberValue(); + int n = Nan::To(value).FromMaybe(0); if (n >= 0) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); context->state->shadowBlur = n; @@ -1516,7 +1516,7 @@ NAN_GETTER(Context2d::GetAntiAlias) { */ NAN_SETTER(Context2d::SetAntiAlias) { - Nan::Utf8String str(value->ToString()); + Nan::Utf8String str(Nan::To(value).ToLocalChecked()); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); cairo_antialias_t a; @@ -1556,7 +1556,7 @@ NAN_GETTER(Context2d::GetTextDrawingMode) { */ NAN_SETTER(Context2d::SetTextDrawingMode) { - Nan::Utf8String str(value->ToString()); + Nan::Utf8String str(Nan::To(value).ToLocalChecked()); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); if (0 == strcmp("path", *str)) { context->state->textDrawingMode = TEXT_DRAW_PATHS; @@ -1587,7 +1587,7 @@ NAN_GETTER(Context2d::GetQuality) { */ NAN_SETTER(Context2d::SetQuality) { - Nan::Utf8String str(value->ToString()); + Nan::Utf8String str(Nan::To(value).ToLocalChecked()); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_filter_t filter; if (0 == strcmp("fast", *str)) { @@ -1618,7 +1618,7 @@ NAN_GETTER(Context2d::GetMiterLimit) { */ NAN_SETTER(Context2d::SetMiterLimit) { - double n = value->NumberValue(); + double n = Nan::To(value).FromMaybe(0); if (n > 0) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_set_miter_limit(context->context(), n); @@ -1639,7 +1639,7 @@ NAN_GETTER(Context2d::GetLineWidth) { */ NAN_SETTER(Context2d::SetLineWidth) { - double n = value->NumberValue(); + double n = Nan::To(value).FromMaybe(0); if (n > 0 && n != std::numeric_limits::infinity()) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_set_line_width(context->context(), n); @@ -1668,7 +1668,7 @@ NAN_GETTER(Context2d::GetLineJoin) { NAN_SETTER(Context2d::SetLineJoin) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); - Nan::Utf8String type(value->ToString()); + Nan::Utf8String type(Nan::To(value).ToLocalChecked()); if (0 == strcmp("round", *type)) { cairo_set_line_join(ctx, CAIRO_LINE_JOIN_ROUND); } else if (0 == strcmp("bevel", *type)) { @@ -1700,7 +1700,7 @@ NAN_GETTER(Context2d::GetLineCap) { NAN_SETTER(Context2d::SetLineCap) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); - Nan::Utf8String type(value->ToString()); + Nan::Utf8String type(Nan::To(value).ToLocalChecked()); if (0 == strcmp("round", *type)) { cairo_set_line_cap(ctx, CAIRO_LINE_CAP_ROUND); } else if (0 == strcmp("square", *type)) { @@ -1718,8 +1718,8 @@ NAN_METHOD(Context2d::IsPointInPath) { if (info[0]->IsNumber() && info[1]->IsNumber()) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); - double x = info[0]->NumberValue() - , y = info[1]->NumberValue(); + double x = Nan::To(info[0]).FromMaybe(0) + , y = Nan::To(info[1]).FromMaybe(0); context->setFillRule(info[2]); info.GetReturnValue().Set(Nan::New(cairo_in_fill(ctx, x, y) || cairo_in_stroke(ctx, x, y))); return; @@ -1734,7 +1734,7 @@ NAN_METHOD(Context2d::IsPointInPath) { NAN_METHOD(Context2d::SetFillPattern) { if (!info[0]->IsObject()) return Nan::ThrowTypeError("Gradient or Pattern expected"); - Local obj = info[0]->ToObject(); + Local obj = Nan::To(info[0]).ToLocalChecked(); if (Nan::New(Gradient::constructor)->HasInstance(obj)){ Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Gradient *grad = Nan::ObjectWrap::Unwrap(obj); @@ -1755,7 +1755,7 @@ NAN_METHOD(Context2d::SetFillPattern) { NAN_METHOD(Context2d::SetStrokePattern) { if (!info[0]->IsObject()) return Nan::ThrowTypeError("Gradient or Pattern expected"); - Local obj = info[0]->ToObject(); + Local obj = Nan::To(info[0]).ToLocalChecked(); if (Nan::New(Gradient::constructor)->HasInstance(obj)){ Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Gradient *grad = Nan::ObjectWrap::Unwrap(obj); @@ -1775,7 +1775,7 @@ NAN_METHOD(Context2d::SetStrokePattern) { NAN_SETTER(Context2d::SetShadowColor) { short ok; - Nan::Utf8String str(value->ToString()); + Nan::Utf8String str(Nan::To(value).ToLocalChecked()); uint32_t rgba = rgba_from_string(*str, &ok); if (ok) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -2077,7 +2077,7 @@ paintText(const Nan::FunctionCallbackInfo &info, bool stroke) { if(!checkArgs(info, args, argsNum, 1)) return; - Nan::Utf8String str(info[0]->ToString()); + Nan::Utf8String str(Nan::To(info[0]).ToLocalChecked()); double x = args[0]; double y = args[1]; double scaled_by = 1; @@ -2226,7 +2226,7 @@ NAN_METHOD(Context2d::SetFont) { Nan::Utf8String weight(info[0]); Nan::Utf8String style(info[1]); - double size = info[2]->NumberValue(); + double size = Nan::To(info[2]).FromMaybe(0); Nan::Utf8String unit(info[3]); Nan::Utf8String family(info[4]); @@ -2261,7 +2261,7 @@ NAN_METHOD(Context2d::MeasureText) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); - Nan::Utf8String str(info[0]->ToString()); + Nan::Utf8String str(Nan::To(info[0]).ToLocalChecked()); Local obj = Nan::New(); PangoRectangle _ink_rect, _logical_rect; @@ -2336,7 +2336,7 @@ NAN_METHOD(Context2d::MeasureText) { NAN_METHOD(Context2d::SetTextBaseline) { if (!info[0]->IsInt32()) return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->textBaseline = info[0]->Int32Value(); + context->state->textBaseline = Nan::To(info[0]).FromMaybe(0); } /* @@ -2346,7 +2346,7 @@ NAN_METHOD(Context2d::SetTextBaseline) { NAN_METHOD(Context2d::SetTextAlignment) { if (!info[0]->IsInt32()) return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->textAlignment = info[0]->Int32Value(); + context->state->textAlignment = Nan::To(info[0]).FromMaybe(0); } /* @@ -2362,7 +2362,7 @@ NAN_METHOD(Context2d::SetLineDash) { for (uint32_t i=0; i d = dash->Get(i % dash->Length()); if (!d->IsNumber()) return; - a[i] = d->NumberValue(); + a[i] = Nan::To(d).FromMaybe(0); if (a[i] == 0) zero_dashes++; if (a[i] < 0 || isnan(a[i]) || isinf(a[i])) return; } @@ -2402,7 +2402,7 @@ NAN_METHOD(Context2d::GetLineDash) { * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash */ NAN_SETTER(Context2d::SetLineDashOffset) { - double offset = value->NumberValue(); + double offset = Nan::To(value).FromMaybe(0); if (isnan(offset) || isinf(offset)) return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -2505,25 +2505,25 @@ NAN_METHOD(Context2d::Arc) { || !info[3]->IsNumber() || !info[4]->IsNumber()) return; - bool anticlockwise = info[5]->BooleanValue(); + bool anticlockwise = Nan::To(info[5]).FromMaybe(0); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); - if (anticlockwise && M_PI * 2 != info[4]->NumberValue()) { + if (anticlockwise && M_PI * 2 != Nan::To(info[4]).FromMaybe(0)) { cairo_arc_negative(ctx - , info[0]->NumberValue() - , info[1]->NumberValue() - , info[2]->NumberValue() - , info[3]->NumberValue() - , info[4]->NumberValue()); + , Nan::To(info[0]).FromMaybe(0) + , Nan::To(info[1]).FromMaybe(0) + , Nan::To(info[2]).FromMaybe(0) + , Nan::To(info[3]).FromMaybe(0) + , Nan::To(info[4]).FromMaybe(0)); } else { cairo_arc(ctx - , info[0]->NumberValue() - , info[1]->NumberValue() - , info[2]->NumberValue() - , info[3]->NumberValue() - , info[4]->NumberValue()); + , Nan::To(info[0]).FromMaybe(0) + , Nan::To(info[1]).FromMaybe(0) + , Nan::To(info[2]).FromMaybe(0) + , Nan::To(info[3]).FromMaybe(0) + , Nan::To(info[4]).FromMaybe(0)); } } @@ -2653,7 +2653,7 @@ NAN_METHOD(Context2d::Ellipse) { double rotation = args[4]; double startAngle = args[5]; double endAngle = args[6]; - bool anticlockwise = info[7]->BooleanValue(); + bool anticlockwise = Nan::To(info[7]).FromMaybe(0); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); diff --git a/src/Image.cc b/src/Image.cc index a5d30cc56..fee9694f8 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -116,7 +116,7 @@ NAN_GETTER(Image::GetDataMode) { NAN_SETTER(Image::SetDataMode) { if (value->IsNumber()) { Image *img = Nan::ObjectWrap::Unwrap(info.This()); - int mode = value->Uint32Value(); + int mode = Nan::To(value).FromMaybe(0); img->data_mode = (data_mode_t) mode; } } @@ -148,7 +148,7 @@ NAN_GETTER(Image::GetWidth) { NAN_SETTER(Image::SetWidth) { if (value->IsNumber()) { Image *img = Nan::ObjectWrap::Unwrap(info.This()); - img->width = value->Uint32Value(); + img->width = Nan::To(value).FromMaybe(0); } } @@ -176,7 +176,7 @@ NAN_GETTER(Image::GetHeight) { NAN_SETTER(Image::SetHeight) { if (value->IsNumber()) { Image *img = Nan::ObjectWrap::Unwrap(info.This()); - img->height = value->Uint32Value(); + img->height = Nan::To(value).FromMaybe(0); } } @@ -242,8 +242,8 @@ NAN_METHOD(Image::SetSource){ status = img->load(); // Buffer } else if (Buffer::HasInstance(value)) { - uint8_t *buf = (uint8_t *) Buffer::Data(value->ToObject()); - unsigned len = Buffer::Length(value->ToObject()); + uint8_t *buf = (uint8_t *) Buffer::Data(Nan::To(value).ToLocalChecked()); + unsigned len = Buffer::Length(Nan::To(value).ToLocalChecked()); status = img->loadFromBuffer(buf, len); } diff --git a/src/ImageData.cc b/src/ImageData.cc index e8e030ab5..751edfea1 100644 --- a/src/ImageData.cc +++ b/src/ImageData.cc @@ -46,12 +46,12 @@ NAN_METHOD(ImageData::New) { int length; if (info[0]->IsUint32() && info[1]->IsUint32()) { - width = info[0]->Uint32Value(); + width = Nan::To(info[0]).FromMaybe(0); if (width == 0) { Nan::ThrowRangeError("The source width is zero."); return; } - height = info[1]->Uint32Value(); + height = Nan::To(info[1]).FromMaybe(0); if (height == 0) { Nan::ThrowRangeError("The source height is zero."); return; @@ -72,7 +72,7 @@ NAN_METHOD(ImageData::New) { // Don't assert that the ImageData length is a multiple of four because some // data formats are not 4 BPP. - width = info[1]->Uint32Value(); + width = Nan::To(info[1]).FromMaybe(0); if (width == 0) { Nan::ThrowRangeError("The source width is zero."); return; @@ -81,7 +81,7 @@ NAN_METHOD(ImageData::New) { // Don't assert that the byte length is a multiple of 4 * width, ditto. if (info[2]->IsUint32()) { // Explicit height given - height = info[2]->Uint32Value(); + height = Nan::To(info[2]).FromMaybe(0); } else { // Calculate height assuming 4 BPP int size = length / 4; height = size / width; @@ -96,14 +96,14 @@ NAN_METHOD(ImageData::New) { return; } - width = info[1]->Uint32Value(); + width = Nan::To(info[1]).FromMaybe(0); if (width == 0) { Nan::ThrowRangeError("The source width is zero."); return; } if (info[2]->IsUint32()) { // Explicit height given - height = info[2]->Uint32Value(); + height = Nan::To(info[2]).FromMaybe(0); } else { // Calculate height assuming 2 BPP int size = length / 2; height = size / width; diff --git a/src/backend/Backend.cc b/src/backend/Backend.cc index 4a2e0c212..34ef541f5 100644 --- a/src/backend/Backend.cc +++ b/src/backend/Backend.cc @@ -14,6 +14,17 @@ Backend::~Backend() this->destroySurface(); } +void Backend::init(const Nan::FunctionCallbackInfo &info) { + int width = 0; + int height = 0; + if (info[0]->IsNumber()) width = Nan::To(info[0]).FromMaybe(0); + if (info[1]->IsNumber()) height = Nan::To(info[1]).FromMaybe(0); + + Backend *backend = construct(width, height); + + backend->Wrap(info.This()); + info.GetReturnValue().Set(info.This()); +} void Backend::setCanvas(Canvas* _canvas) { diff --git a/src/backend/Backend.h b/src/backend/Backend.h index 40d2d328d..0682c7ae4 100644 --- a/src/backend/Backend.h +++ b/src/backend/Backend.h @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -28,6 +29,8 @@ class Backend : public Nan::ObjectWrap Canvas* canvas; Backend(string name, int width, int height); + static void init(const Nan::FunctionCallbackInfo &info); + static Backend *construct(int width, int height){ return nullptr; } public: virtual ~Backend(); diff --git a/src/backend/ImageBackend.cc b/src/backend/ImageBackend.cc index f63f7d7c4..b9d169af8 100644 --- a/src/backend/ImageBackend.cc +++ b/src/backend/ImageBackend.cc @@ -14,6 +14,10 @@ ImageBackend::~ImageBackend() } } +Backend *ImageBackend::construct(int width, int height){ + return new ImageBackend(width, height); +} + // This returns an approximate value only, suitable for Nan::AdjustExternalMemory. // The formats that don't map to intrinsic types (RGB30, A1) round up. int32_t ImageBackend::approxBytesPerPixel() { @@ -79,15 +83,6 @@ void ImageBackend::Initialize(Handle target) target->Set(Nan::New("ImageBackend").ToLocalChecked(), ctor->GetFunction()); } -NAN_METHOD(ImageBackend::New) -{ - int width = 0; - int height = 0; - if (info[0]->IsNumber()) width = info[0]->Uint32Value(); - if (info[1]->IsNumber()) height = info[1]->Uint32Value(); - - ImageBackend* backend = new ImageBackend(width, height); - - backend->Wrap(info.This()); - info.GetReturnValue().Set(info.This()); +NAN_METHOD(ImageBackend::New) { + init(info); } diff --git a/src/backend/ImageBackend.h b/src/backend/ImageBackend.h index cfd1285e3..c245565e0 100644 --- a/src/backend/ImageBackend.h +++ b/src/backend/ImageBackend.h @@ -17,6 +17,7 @@ class ImageBackend : public Backend public: ImageBackend(int width, int height); ~ImageBackend(); + static Backend *construct(int width, int height); cairo_format_t getFormat(); void setFormat(cairo_format_t format); diff --git a/src/backend/PdfBackend.cc b/src/backend/PdfBackend.cc index 03b70e38e..df409799a 100644 --- a/src/backend/PdfBackend.cc +++ b/src/backend/PdfBackend.cc @@ -21,6 +21,9 @@ PdfBackend::~PdfBackend() { destroySurface(); } +Backend *PdfBackend::construct(int width, int height){ + return new PdfBackend(width, height); +} cairo_surface_t* PdfBackend::createSurface() { if (!_closure) _closure = new PdfSvgClosure(canvas); @@ -48,13 +51,5 @@ void PdfBackend::Initialize(Handle target) { } NAN_METHOD(PdfBackend::New) { - int width = 0; - int height = 0; - if (info[0]->IsNumber()) width = info[0]->Uint32Value(); - if (info[1]->IsNumber()) height = info[1]->Uint32Value(); - - PdfBackend* backend = new PdfBackend(width, height); - - backend->Wrap(info.This()); - info.GetReturnValue().Set(info.This()); + init(info); } diff --git a/src/backend/PdfBackend.h b/src/backend/PdfBackend.h index 0e41157d9..2c597a703 100644 --- a/src/backend/PdfBackend.h +++ b/src/backend/PdfBackend.h @@ -20,6 +20,7 @@ class PdfBackend : public Backend PdfBackend(int width, int height); ~PdfBackend(); + static Backend *construct(int width, int height); static Nan::Persistent constructor; static void Initialize(v8::Handle target); diff --git a/src/backend/SvgBackend.cc b/src/backend/SvgBackend.cc index 26f5df360..eaec46e83 100644 --- a/src/backend/SvgBackend.cc +++ b/src/backend/SvgBackend.cc @@ -21,6 +21,9 @@ SvgBackend::~SvgBackend() { destroySurface(); } +Backend *SvgBackend::construct(int width, int height){ + return new SvgBackend(width, height); +} cairo_surface_t* SvgBackend::createSurface() { if (!_closure) _closure = new PdfSvgClosure(canvas); @@ -50,13 +53,5 @@ void SvgBackend::Initialize(Handle target) { } NAN_METHOD(SvgBackend::New) { - int width = 0; - int height = 0; - if (info[0]->IsNumber()) width = info[0]->Uint32Value(); - if (info[1]->IsNumber()) height = info[1]->Uint32Value(); - - SvgBackend* backend = new SvgBackend(width, height); - - backend->Wrap(info.This()); - info.GetReturnValue().Set(info.This()); + init(info); } diff --git a/src/backend/SvgBackend.h b/src/backend/SvgBackend.h index 47391ba89..b703a3b94 100644 --- a/src/backend/SvgBackend.h +++ b/src/backend/SvgBackend.h @@ -20,6 +20,7 @@ class SvgBackend : public Backend SvgBackend(int width, int height); ~SvgBackend(); + static Backend *construct(int width, int height); static Nan::Persistent constructor; static void Initialize(v8::Handle target); From 53879efe1a8bf98d63484d46c89f3ed695ba5a0d Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 27 Oct 2018 10:22:43 -0700 Subject: [PATCH 200/474] Require cairo 1.10.0+ explicitly (#1291) Cairo 1.10.0 was released in 2010 and is the oldest version that will work AFAICT Ref #1290 and most of the [CentOS issues](https://github.com/Automattic/node-canvas/issues?q=is%3Aissue+centos+label%3A%22Installation+help%22). --- CHANGELOG.md | 2 ++ Readme.md | 2 +- src/CanvasRenderingContext2d.cc | 4 ---- src/Image.cc | 18 ------------------ src/Image.h | 2 -- src/PNG.h | 4 ---- src/backend/Backend.h | 5 ----- src/init.cc | 11 +++++++++++ 8 files changed, 14 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 091950aa1..3d7e0dd68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ project adheres to [Semantic Versioning](http://semver.org/). ================== ### Changed ### Added +* Warn when building with old, unsupported versions of cairo or libjpeg. + ### Fixed 2.0.0 diff --git a/Readme.md b/Readme.md index 193dc97c2..ed55420ca 100644 --- a/Readme.md +++ b/Readme.md @@ -19,7 +19,7 @@ The minimum version of Node.js required is **6.0.0**. If you don't have a supported OS or processor architecture, or you use `--build-from-source`, the module will be compiled on your system. This requires several dependencies, including Cairo and Pango. -For detailed installation information, see the [wiki](https://github.com/Automattic/node-canvas/wiki/_pages). One-line installation instructions for common OSes are below. Note that libgif/giflib, librsvg and libjpeg are optional and only required if you need GIF, SVG and JPEG support, respectively. +For detailed installation information, see the [wiki](https://github.com/Automattic/node-canvas/wiki/_pages). One-line installation instructions for common OSes are below. Note that libgif/giflib, librsvg and libjpeg are optional and only required if you need GIF, SVG and JPEG support, respectively. Cairo v1.10.0 or later is required. OS | Command ----- | ----- diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index d403024cc..daaaa3fc8 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -1310,7 +1310,6 @@ NAN_GETTER(Context2d::GetGlobalCompositeOperation) { // Note: "source-over" and "normal" are synonyms. Chrome and FF both report // "source-over" after setting gCO to "normal". // case CAIRO_OPERATOR_OVER: op = "normal"; -#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 10, 0) case CAIRO_OPERATOR_MULTIPLY: op = "multiply"; break; case CAIRO_OPERATOR_SCREEN: op = "screen"; break; case CAIRO_OPERATOR_OVERLAY: op = "overlay"; break; @@ -1326,7 +1325,6 @@ NAN_GETTER(Context2d::GetGlobalCompositeOperation) { case CAIRO_OPERATOR_HSL_SATURATION: op = "saturation"; break; case CAIRO_OPERATOR_HSL_COLOR: op = "color"; break; case CAIRO_OPERATOR_HSL_LUMINOSITY: op = "luminosity"; break; -#endif // non-standard: case CAIRO_OPERATOR_SATURATE: op = "saturate"; break; } @@ -1414,7 +1412,6 @@ NAN_SETTER(Context2d::SetGlobalCompositeOperation) { {"lighter", CAIRO_OPERATOR_ADD}, // blend modes: {"normal", CAIRO_OPERATOR_OVER}, -#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 10, 0) {"multiply", CAIRO_OPERATOR_MULTIPLY}, {"screen", CAIRO_OPERATOR_SCREEN}, {"overlay", CAIRO_OPERATOR_OVERLAY}, @@ -1430,7 +1427,6 @@ NAN_SETTER(Context2d::SetGlobalCompositeOperation) { {"saturation", CAIRO_OPERATOR_HSL_SATURATION}, {"color", CAIRO_OPERATOR_HSL_COLOR}, {"luminosity", CAIRO_OPERATOR_HSL_LUMINOSITY}, -#endif // non-standard: {"saturate", CAIRO_OPERATOR_SATURATE} }; diff --git a/src/Image.cc b/src/Image.cc index fee9694f8..0da8eaf0d 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -64,11 +64,9 @@ Image::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { Nan::SetMethod(proto, "getSource", GetSource); Nan::SetMethod(proto, "setSource", SetSource); -#if CAIRO_VERSION_MINOR >= 10 SetProtoAccessor(proto, Nan::New("dataMode").ToLocalChecked(), GetDataMode, SetDataMode, ctor); ctor->Set(Nan::New("MODE_IMAGE").ToLocalChecked(), Nan::New(DATA_IMAGE)); ctor->Set(Nan::New("MODE_MIME").ToLocalChecked(), Nan::New(DATA_MIME)); -#endif Nan::Set(target, Nan::New("Image").ToLocalChecked(), ctor->GetFunction()); } @@ -98,8 +96,6 @@ NAN_GETTER(Image::GetComplete) { info.GetReturnValue().Set(Nan::New(Image::COMPLETE == img->state)); } -#if CAIRO_VERSION_MINOR >= 10 - /* * Get dataMode. */ @@ -121,8 +117,6 @@ NAN_SETTER(Image::SetDataMode) { } } -#endif - /* * Get natural width */ @@ -293,9 +287,6 @@ Image::loadFromBuffer(uint8_t *buf, unsigned len) { if (isJPEG(data)) { #ifdef HAVE_JPEG -#if CAIRO_VERSION_MINOR < 10 - return loadJPEGFromBuffer(buf, len); -#else if (DATA_IMAGE == data_mode) return loadJPEGFromBuffer(buf, len); if (DATA_MIME == data_mode) return decodeJPEGBufferIntoMimeSurface(buf, len); if ((DATA_IMAGE | DATA_MIME) == data_mode) { @@ -304,7 +295,6 @@ Image::loadFromBuffer(uint8_t *buf, unsigned len) { if (status) return status; return assignDataAsMime(buf, len, CAIRO_MIME_TYPE_JPEG); } -#endif // CAIRO_VERSION_MINOR < 10 #else // HAVE_JPEG this->errorInfo.set("node-canvas was built without JPEG support"); return CAIRO_STATUS_READ_ERROR; @@ -894,8 +884,6 @@ static void canvas_jpeg_output_message(j_common_ptr cinfo) { cjerr->image->errorInfo.set(buff); } -#if CAIRO_VERSION_MINOR >= 10 - /* * Takes a jpeg data buffer and assigns it as mime data to a * dummy surface @@ -1010,8 +998,6 @@ Image::assignDataAsMime(uint8_t *data, int len, const char *mime_type) { , mime_closure); } -#endif - /* * Load jpeg from buffer. */ @@ -1090,7 +1076,6 @@ Image::loadJPEG(FILE *stream) { status = decodeJPEGIntoSurface(&args); fclose(stream); } else { // We'll need the actual source jpeg data, so read fully. -#if CAIRO_VERSION_MINOR >= 10 uint8_t *buf; unsigned len; @@ -1123,9 +1108,6 @@ Image::loadJPEG(FILE *stream) { fclose(stream); free(buf); -#else - status = CAIRO_STATUS_READ_ERROR; -#endif } return status; diff --git a/src/Image.h b/src/Image.h index 6c0dd49c9..a1f1ff1bc 100644 --- a/src/Image.h +++ b/src/Image.h @@ -84,10 +84,8 @@ class Image: public Nan::ObjectWrap { cairo_status_t loadJPEG(FILE *stream); void jpegToARGB(jpeg_decompress_struct* args, uint8_t* data, uint8_t* src, JPEGDecodeL decode); 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 CanvasError errorInfo; void loaded(); diff --git a/src/PNG.h b/src/PNG.h index e0bd86a66..f5daca8c4 100644 --- a/src/PNG.h +++ b/src/PNG.h @@ -17,10 +17,6 @@ #define unlikely(expr) (expr) #endif -#ifndef CAIRO_FORMAT_INVALID -#define CAIRO_FORMAT_INVALID -1 -#endif - static void canvas_png_flush(png_structp png_ptr) { /* Do nothing; fflush() is said to be just a waste of energy. */ (void) png_ptr; /* Stifle compiler warning */ diff --git a/src/backend/Backend.h b/src/backend/Backend.h index 0682c7ae4..75ae37aad 100644 --- a/src/backend/Backend.h +++ b/src/backend/Backend.h @@ -53,12 +53,7 @@ class Backend : public Nan::ObjectWrap // Overridden by ImageBackend. SVG and PDF thus always return INVALID. virtual cairo_format_t getFormat() { -#ifndef CAIRO_FORMAT_INVALID - // For old Cairo (CentOS) support - return static_cast(-1); -#else return CAIRO_FORMAT_INVALID; -#endif } bool isSurfaceValid(); diff --git a/src/init.cc b/src/init.cc index 1e3a2f3cf..3c2f721cf 100644 --- a/src/init.cc +++ b/src/init.cc @@ -9,6 +9,17 @@ #include #include +#include +#if CAIRO_VERSION < CAIRO_VERSION_ENCODE(1, 10, 0) +// CAIRO_FORMAT_RGB16_565: undeprecated in v1.10.0 +// CAIRO_STATUS_INVALID_SIZE: v1.10.0 +// CAIRO_FORMAT_INVALID: v1.10.0 +// Lots of the compositing operators: v1.10.0 +// JPEG MIME tracking: v1.10.0 +// Note: CAIRO_FORMAT_RGB30 is v1.12.0 and still optional +#error("cairo v1.10.0 or later is required") +#endif + #include "Backends.h" #include "Canvas.h" #include "CanvasGradient.h" From 03af3dde7db50b199766a2549ef9888a4eb93b61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Wed, 7 Nov 2018 18:28:47 +0000 Subject: [PATCH 201/474] history: 2.1.0 --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d7e0dd68..f0dd17b2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,13 @@ project adheres to [Semantic Versioning](http://semver.org/). ================== ### Changed ### Added -* Warn when building with old, unsupported versions of cairo or libjpeg. - ### Fixed +2.1.0 +================== +### Added +* Warn when building with old, unsupported versions of cairo or libjpeg. + 2.0.0 ================== From a5921f6185049cdab9f2ee92c4a2700d00e9fcd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Wed, 7 Nov 2018 18:28:52 +0000 Subject: [PATCH 202/474] 2.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8a6876043..88f85bae3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.0.1", + "version": "2.1.0", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From cc6c173bfd1d77ef1fd3552d655c0a4545218a2a Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 12 Nov 2018 19:56:42 +0100 Subject: [PATCH 203/474] Add BMP support (#1295) * Add BMP support * Add support for BITMAPCOREHEADER * Improve error handling * Fix several things --- CHANGELOG.md | 1 + Readme.md | 6 + binding.gyp | 1 + src/Image.cc | 93 +++++++ src/Image.h | 3 + src/bmp/BMPParser.cc | 383 ++++++++++++++++++++++++++ src/bmp/BMPParser.h | 60 ++++ src/bmp/LICENSE.md | 24 ++ test/fixtures/bmp/1-bit.bmp | Bin 0 -> 1214 bytes test/fixtures/bmp/24-bit.bmp | Bin 0 -> 70 bytes test/fixtures/bmp/32-bit.bmp | Bin 0 -> 154 bytes test/fixtures/bmp/bomb.bmp | Bin 0 -> 30 bytes test/fixtures/bmp/min.bmp | Bin 0 -> 30 bytes test/fixtures/bmp/negative-height.bmp | Bin 0 -> 62 bytes test/image.test.js | 139 +++++++++- 15 files changed, 709 insertions(+), 1 deletion(-) create mode 100644 src/bmp/BMPParser.cc create mode 100644 src/bmp/BMPParser.h create mode 100644 src/bmp/LICENSE.md create mode 100644 test/fixtures/bmp/1-bit.bmp create mode 100644 test/fixtures/bmp/24-bit.bmp create mode 100644 test/fixtures/bmp/32-bit.bmp create mode 100644 test/fixtures/bmp/bomb.bmp create mode 100644 test/fixtures/bmp/min.bmp create mode 100644 test/fixtures/bmp/negative-height.bmp diff --git a/CHANGELOG.md b/CHANGELOG.md index f0dd17b2e..bf024b4b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ================== ### Added * Warn when building with old, unsupported versions of cairo or libjpeg. +* BMP support 2.0.0 ================== diff --git a/Readme.md b/Readme.md index ed55420ca..6f779535f 100644 --- a/Readme.md +++ b/Readme.md @@ -524,6 +524,8 @@ Examples line in the `examples` directory. Most produce a png image of the same ## License +### node-canvas + (The MIT License) Copyright (c) 2010 LearnBoost, and contributors <dev@learnboost.com> @@ -546,3 +548,7 @@ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +### BMP parser + +See [license](src/bmp/LICENSE.md) diff --git a/binding.gyp b/binding.gyp index c0f39f6d2..e770b86c7 100644 --- a/binding.gyp +++ b/binding.gyp @@ -63,6 +63,7 @@ 'src/backend/ImageBackend.cc', 'src/backend/PdfBackend.cc', 'src/backend/SvgBackend.cc', + 'src/bmp/BMPParser.cc', 'src/Backends.cc', 'src/Canvas.cc', 'src/CanvasGradient.cc', diff --git a/src/Image.cc b/src/Image.cc index 0da8eaf0d..a6f69d95a 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -12,6 +12,7 @@ #include "Util.h" #include "Canvas.h" #include "Image.h" +#include "bmp/BMPParser.h" #ifdef HAVE_GIF typedef struct { @@ -313,6 +314,9 @@ Image::loadFromBuffer(uint8_t *buf, unsigned len) { #endif } + if (isBMP(buf, len)) + return loadBMPFromBuffer(buf, len); + this->errorInfo.set("Unsupported image type"); return CAIRO_STATUS_READ_ERROR; } @@ -489,6 +493,9 @@ Image::loadSurface() { #endif } + if (isBMP(buf, 2)) + return loadBMP(stream); + fclose(stream); this->errorInfo.set("Unsupported image type"); @@ -1223,6 +1230,77 @@ Image::loadSVG(FILE *stream) { #endif /* HAVE_RSVG */ +/* + * Load BMP from buffer. + */ + +cairo_status_t Image::loadBMPFromBuffer(uint8_t *buf, unsigned len){ + BMPParser::Parser parser; + + // Reversed ARGB32 with pre-multiplied alpha + uint8_t pixFmt[5] = {2, 1, 0, 3, 1}; + parser.parse(buf, len, pixFmt); + + if (parser.getStatus() != BMPParser::Status::OK) { + errorInfo.reset(); + errorInfo.message = parser.getErrMsg(); + return CAIRO_STATUS_READ_ERROR; + } + + width = naturalWidth = parser.getWidth(); + height = naturalHeight = parser.getHeight(); + uint8_t *data = parser.getImgd(); + + _surface = cairo_image_surface_create_for_data( + data, + CAIRO_FORMAT_ARGB32, + width, + height, + cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width) + ); + + // No need to delete the data + cairo_status_t status = cairo_surface_status(_surface); + if (status) return status; + + _data = data; + parser.clearImgd(); + + return CAIRO_STATUS_SUCCESS; +} + +/* + * Load BMP. + */ + +cairo_status_t Image::loadBMP(FILE *stream){ + struct stat s; + int fd = fileno(stream); + + // Stat + if (fstat(fd, &s) < 0) { + fclose(stream); + return CAIRO_STATUS_READ_ERROR; + } + + uint8_t *buf = new uint8_t[s.st_size]; + + if (!buf) { + fclose(stream); + errorInfo.set(NULL, "malloc", errno); + return CAIRO_STATUS_NO_MEMORY; + } + + size_t read = fread(buf, s.st_size, 1, stream); + fclose(stream); + + cairo_status_t result = CAIRO_STATUS_READ_ERROR; + if (read == 1) result = loadBMPFromBuffer(buf, s.st_size); + delete[] buf; + + return result; +} + /* * Return UNKNOWN, SVG, GIF, JPEG, or PNG based on the filename. */ @@ -1286,3 +1364,18 @@ Image::isSVG(uint8_t *data, unsigned len) { } return false; } + +/* + * Check for valid BMP signatures + */ + +int Image::isBMP(uint8_t *data, unsigned len) { + if(len < 2) return false; + string sig = string(1, (char)data[0]) + (char)data[1]; + return sig == "BM" || + sig == "BA" || + sig == "CI" || + sig == "CP" || + sig == "IC" || + sig == "PT"; +} diff --git a/src/Image.h b/src/Image.h index a1f1ff1bc..8d00b5659 100644 --- a/src/Image.h +++ b/src/Image.h @@ -62,6 +62,7 @@ class Image: public Nan::ObjectWrap { static int isJPEG(uint8_t *data); static int isGIF(uint8_t *data); static int isSVG(uint8_t *data, unsigned len); + static int isBMP(uint8_t *data, unsigned len); static cairo_status_t readPNG(void *closure, unsigned char *data, unsigned len); inline int isComplete(){ return COMPLETE == state; } cairo_surface_t *surface(); @@ -87,6 +88,8 @@ class Image: public Nan::ObjectWrap { cairo_status_t decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len); cairo_status_t assignDataAsMime(uint8_t *data, int len, const char *mime_type); #endif + cairo_status_t loadBMPFromBuffer(uint8_t *buf, unsigned len); + cairo_status_t loadBMP(FILE *stream); CanvasError errorInfo; void loaded(); cairo_status_t load(); diff --git a/src/bmp/BMPParser.cc b/src/bmp/BMPParser.cc new file mode 100644 index 000000000..d0ae1be1d --- /dev/null +++ b/src/bmp/BMPParser.cc @@ -0,0 +1,383 @@ +#include + +#include "BMPParser.h" + +using namespace std; +using namespace BMPParser; + +#define MAX_IMG_SIZE 10000 + +#define E(cond, msg) if(cond) return setErr(msg) +#define EU(cond, msg) if(cond) return setErrUnsupported(msg) +#define EX(cond, msg) if(cond) return setErrUnknown(msg) + +#define I1() get() +#define U1() get() +#define I2() get() +#define U2() get() +#define I4() get() +#define U4() get() + +#define I1UC() get() +#define U1UC() get() +#define I2UC() get() +#define U2UC() get() +#define I4UC() get() +#define U4UC() get() + +#define CALC_MASK(col) \ + col##Mask = U4(); \ + if(col##Mask == 0xffu) col##Mask = 0; \ + else if(col##Mask == 0xff00u) col##Mask = 8; \ + else if(col##Mask == 0xff0000u) col##Mask = 16; \ + else if(col##Mask == 0xff000000u) col##Mask = 24; \ + else EU(1, #col " mask"); + +#define CHECK_OVERRUN(size, type) \ + if(ptr + (size) - data > len){ \ + setErr("unexpected end of file"); \ + return type(); \ + } + +Parser::~Parser(){ + data = nullptr; + ptr = nullptr; + + if(imgd){ + delete[] imgd; + imgd = nullptr; + } +} + +void Parser::parse(uint8_t *buf, int bufSize, uint8_t *format){ + assert(status == Status::EMPTY); + + data = ptr = buf; + len = bufSize; + + // Start parsing file header + setOp("file header"); + + // File header signature + string fhSig = getStr(2); + string temp = "file header signature"; + EU(fhSig == "BA", temp + " \"BA\""); + EU(fhSig == "CI", temp + " \"CI\""); + EU(fhSig == "CP", temp + " \"CP\""); + EU(fhSig == "IC", temp + " \"IC\""); + EU(fhSig == "PT", temp + " \"PT\""); + EX(fhSig != "BM", temp); // BM + + // Length of the file should be equal to `len` + E(U4() != static_cast(len), "inconsistent file size"); + + // Skip unused values + skip(4); + + // Offset where the pixel array (bitmap data) can be found + auto imgdOffset = U4(); + + // Start parsing DIB header + setOp("DIB header"); + + // Prepare some variables in case they are needed + uint32_t compr = 0; + uint32_t redMask = 0, greenMask = 0, blueMask = 0, alphaMask = 0; + + /** + * Type of the DIB (device-independent bitmap) header + * is determined by its size. Most BMP files use BITMAPINFOHEADER. + */ + auto dibSize = U4(); + temp = "DIB header"; + EU(dibSize == 64, temp + " \"OS22XBITMAPHEADER\""); + EU(dibSize == 16, temp + " \"OS22XBITMAPHEADER\""); + EU(dibSize == 52, temp + " \"BITMAPV2INFOHEADER\""); + EU(dibSize == 56, temp + " \"BITMAPV3INFOHEADER\""); + EU(dibSize == 124, temp + " \"BITMAPV5HEADER\""); + + // BITMAPCOREHEADER, BITMAPINFOHEADER, BITMAPV4HEADER + auto isDibValid = dibSize == 12 || dibSize == 40 || dibSize == 108; + EX(!isDibValid, temp); + + // Image width + w = dibSize == 12 ? U2() : I4(); + E(!w, "image width is 0"); + E(w < 0, "negative image width"); + E(w > MAX_IMG_SIZE, "too large image width"); + + // Image height (specification allows negative values) + h = dibSize == 12 ? U2() : I4(); + E(!h, "image height is 0"); + E(h > MAX_IMG_SIZE, "too large image height"); + + bool isHeightNegative = h < 0; + if(isHeightNegative) h = -h; + + // Number of color planes (must be 1) + E(U2() != 1, "number of color planes must be 1"); + + // Bits per pixel (color depth) + auto bpp = U2(); + auto isBppValid = bpp == 1 || bpp == 24 || bpp == 32; + EU(!isBppValid, "color depth"); + + // Calculate image data size and padding + uint32_t expectedImgdSize = (((w * bpp + 31) >> 5) << 2) * h; + uint32_t rowPadding = (-w * bpp & 31) >> 3; + uint32_t imgdSize = 0; + + if(dibSize == 40 || dibSize == 108){ + // Compression type + compr = U4(); + temp = "compression type"; + EU(compr == 1, temp + " \"BI_RLE8\""); + EU(compr == 2, temp + " \"BI_RLE4\""); + EU(compr == 4, temp + " \"BI_JPEG\""); + EU(compr == 5, temp + " \"BI_PNG\""); + EU(compr == 6, temp + " \"BI_ALPHABITFIELDS\""); + EU(compr == 11, temp + " \"BI_CMYK\""); + EU(compr == 12, temp + " \"BI_CMYKRLE8\""); + EU(compr == 13, temp + " \"BI_CMYKRLE4\""); + + // BI_RGB and BI_BITFIELDS + auto isComprValid = compr == 0 || compr == 3; + EX(!isComprValid, temp); + + // Also ensure that BI_BITFIELDS appears only with BITMAPV4HEADER and 32-bit colors + if(compr == 3){ + E(dibSize != 108, "compression BI_BITFIELDS can be used only with BITMAPV4HEADER"); + E(bpp != 32, "compression BI_BITFIELDS can be used only with 32-bit color depth"); + } + + // Size of the image data + imgdSize = U4(); + + // Horizontal and vertical resolution (ignored) + skip(8); + + // Number of colors in the palette or 0 if no palette is present + auto palColNum = U4(); + EU(palColNum, "non-empty color palette"); + + // Number of important colors used or 0 if all colors are important + auto impCols = U4(); + EU(impCols, "non-zero important colors"); + + // BITMAPV4HEADER has additional properties + if(dibSize == 108){ + // If BI_BITFIELDS are used, calculate masks, otherwise ignore them + if(compr == 3){ + // Convert each mask to bit offset for faster shifting + CALC_MASK(red); + CALC_MASK(green); + CALC_MASK(blue); + CALC_MASK(alpha); + }else{ + skip(16); + } + + // Encure that the color space is LCS_WINDOWS_COLOR_SPACE + string colSpace = getStr(4, 1); + EU(colSpace != "Win ", "color space \"" + colSpace + "\""); + + // The rest 48 bytes are ignored for LCS_WINDOWS_COLOR_SPACE + skip(48); + } + } + + /** + * Skip to the image data. There may be other chunks between, + * but they are optional. + */ + E(ptr - data > imgdOffset, "image data overlaps with another structure"); + ptr = data + imgdOffset; + + // Start parsing image data + setOp("image data"); + + if(!imgdSize){ + // Value 0 is allowed only for BI_RGB compression type + E(compr != 0, "missing image data size"); + imgdSize = expectedImgdSize; + }else{ + E(imgdSize != expectedImgdSize, "inconsistent image data size"); + } + + // Ensure that all image data is present + E(ptr - data + imgdSize > len, "not enough image data"); + + // Direction of reading rows + int yStart = h - 1; + int yEnd = -1; + int dy = isHeightNegative ? 1 : -1; + + // In case of negative height, read rows backward + if(isHeightNegative){ + yStart = 0; + yEnd = h; + } + + // Allocate output image data array + int buffLen = w * h << 2; + imgd = new (nothrow) uint8_t[buffLen]; + E(!imgd, "unable to allocate memory"); + + // Prepare color valus + uint8_t color[4] = {0}; + uint8_t &red = color[0]; + uint8_t &green = color[1]; + uint8_t &blue = color[2]; + uint8_t &alpha = color[3]; + + // Check if pre-multiplied alpha is used + bool premul = format ? format[4] : 0; + + // Main loop + for(int y = yStart; y != yEnd; y += dy){ + // Use in-byte offset for bpp < 8 + uint8_t colOffset = 0; + uint8_t cval = 0; + + for(int x = 0; x != w; x++){ + // Index in the output image data + int i = (x + y * w) << 2; + + switch(compr){ + case 0: // BI_RGB + switch(bpp){ + case 1: + if(colOffset) ptr--; + cval = (U1UC() >> (7 - colOffset)) & 1; + red = green = blue = cval ? 255 : 0; + alpha = 255; + colOffset = (colOffset + 1) & 7; + break; + + case 24: + blue = U1UC(); + green = U1UC(); + red = U1UC(); + alpha = 255; + break; + + case 32: + blue = U1UC(); + green = U1UC(); + red = U1UC(); + alpha = U1UC(); + break; + } + break; + + case 3: // BI_BITFIELDS + auto col = U4UC(); + red = col >> redMask; + green = col >> greenMask; + blue = col >> blueMask; + alpha = col >> alphaMask; + break; + } + + /** + * Pixel format: + * red, + * green, + * blue, + * alpha, + * is alpha pre-multiplied + * Default is [0, 1, 2, 3, 0] + */ + + if(premul && alpha != 255){ + double a = alpha / 255.; + red = static_cast(red * a + .5); + green = static_cast(green * a + .5); + blue = static_cast(blue * a + .5); + } + + if(format){ + imgd[i] = color[format[0]]; + imgd[i + 1] = color[format[1]]; + imgd[i + 2] = color[format[2]]; + imgd[i + 3] = color[format[3]]; + }else{ + imgd[i] = red; + imgd[i + 1] = green; + imgd[i + 2] = blue; + imgd[i + 3] = alpha; + } + } + + // Skip unused bytes in the current row + skip(rowPadding); + } + + if(status == Status::ERROR) + return; + + E(ptr - data != len, "extra data found at the end of file"); + status = Status::OK; +}; + +void Parser::clearImgd(){ imgd = nullptr; } +int32_t Parser::getWidth() const{ return w; } +int32_t Parser::getHeight() const{ return h; } +uint8_t *Parser::getImgd() const{ return imgd; } +Status Parser::getStatus() const{ return status; } + +string Parser::getErrMsg() const{ + return "Error while processing " + getOp() + " - " + err; +} + +template T Parser::get(){ + if(check) + CHECK_OVERRUN(sizeof(T), T); + T val = *(T*)ptr; + ptr += sizeof(T); + return val; +} + +string Parser::getStr(int size, bool reverse){ + CHECK_OVERRUN(size, string); + string val = ""; + + while(size--){ + if(reverse) val = string(1, static_cast(*ptr++)) + val; + else val += static_cast(*ptr++); + } + + return val; +} + +void Parser::skip(int size){ + CHECK_OVERRUN(size, void); + ptr += size; +} + +void Parser::setOp(string val){ + if(status != Status::EMPTY) return; + op = val; +} + +string Parser::getOp() const{ + return op; +} + +void Parser::setErrUnsupported(string msg){ + setErr("unsupported " + msg); +} + +void Parser::setErrUnknown(string msg){ + setErr("unknown " + msg); +} + +void Parser::setErr(string msg){ + if(status != Status::EMPTY) return; + err = msg; + status = Status::ERROR; +} + +string Parser::getErr() const{ + return err; +} diff --git a/src/bmp/BMPParser.h b/src/bmp/BMPParser.h new file mode 100644 index 000000000..86d1465ed --- /dev/null +++ b/src/bmp/BMPParser.h @@ -0,0 +1,60 @@ +#ifndef __NODE_BMP_PARSER_H__ +#define __NODE_BMP_PARSER_H__ + +#ifdef ERROR +#define ERROR_ ERROR +#undef ERROR +#endif + +#include + +namespace BMPParser{ + enum Status{ + EMPTY, + OK, + ERROR, + }; + + class Parser{ + public: + Parser()=default; + ~Parser(); + void parse(uint8_t *buf, int bufSize, uint8_t *format=nullptr); + void clearImgd(); + int32_t getWidth() const; + int32_t getHeight() const; + uint8_t *getImgd() const; + Status getStatus() const; + std::string getErrMsg() const; + + private: + Status status = Status::EMPTY; + uint8_t *data = nullptr; + uint8_t *ptr = nullptr; + int len = 0; + int32_t w = 0; + int32_t h = 0; + uint8_t *imgd = nullptr; + std::string err = ""; + std::string op = ""; + + template T get(); + std::string getStr(int len, bool reverse=false); + void skip(int len); + + void setOp(std::string val); + std::string getOp() const; + + void setErrUnsupported(std::string msg); + void setErrUnknown(std::string msg); + void setErr(std::string msg); + std::string getErr() const; + }; +} + +#ifdef ERROR_ +#define ERROR ERROR_ +#undef ERROR_ +#endif + +#endif diff --git a/src/bmp/LICENSE.md b/src/bmp/LICENSE.md new file mode 100644 index 000000000..ea89a5dfe --- /dev/null +++ b/src/bmp/LICENSE.md @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to \ No newline at end of file diff --git a/test/fixtures/bmp/1-bit.bmp b/test/fixtures/bmp/1-bit.bmp new file mode 100644 index 0000000000000000000000000000000000000000..04c5f892e0337f385009af4a7a9303b7f1ebaf8b GIT binary patch literal 1214 zcmd^-F>b>!3`H5FfJbM{)O76VQA3abox1ca&FCShb+W=o(GfIiFrt2b+msNXMaKeV zAVV)-rano(ynQ|hIG7&f z+T`)0=JqF!a&2>%?Ec8y-*aLslbz=dp4NJj-khWH#L6w4Vpas}A$}c45Nr_zO3qzuN!+ literal 0 HcmV?d00001 diff --git a/test/fixtures/bmp/24-bit.bmp b/test/fixtures/bmp/24-bit.bmp new file mode 100644 index 0000000000000000000000000000000000000000..4f1f51086b81444df055b171409d8c9fc9a14bad GIT binary patch literal 70 qcmZ?rbz^`4Ga#h_#7t1k$RGih5CD?G+z<>F02BX#fPn#sz!(6U>j#$r literal 0 HcmV?d00001 diff --git a/test/fixtures/bmp/32-bit.bmp b/test/fixtures/bmp/32-bit.bmp new file mode 100644 index 0000000000000000000000000000000000000000..edc32b449c7d6f8fb15a9acd1eba337d5654e2b5 GIT binary patch literal 154 zcmZ?roy7nFRX{2Sh*^M`35XdP6d0I+v;q(db3-tY2?hTd7$6A5|F4jj84i;nmIi9C W2bxh2)L#GpKM;dV1VIKM4+sEA;u*{U literal 0 HcmV?d00001 diff --git a/test/fixtures/bmp/bomb.bmp b/test/fixtures/bmp/bomb.bmp new file mode 100644 index 0000000000000000000000000000000000000000..60703159a094a433fd62c5ec31ef6102979bc673 GIT binary patch literal 30 ccmZ?rm1BSaDImoI#Oea-0*nj_K-PZ-03HJZVgLXD literal 0 HcmV?d00001 diff --git a/test/fixtures/bmp/min.bmp b/test/fixtures/bmp/min.bmp new file mode 100644 index 0000000000000000000000000000000000000000..688af7180a991c7144b071a7108baac2bffd461c GIT binary patch literal 30 ZcmZ?rm1BSaDImoI#Ef7l0c8DW000?J0qy_* literal 0 HcmV?d00001 diff --git a/test/fixtures/bmp/negative-height.bmp b/test/fixtures/bmp/negative-height.bmp new file mode 100644 index 0000000000000000000000000000000000000000..5a0ab3ae3623ddbac43d2d7b60504338e5d1d9a6 GIT binary patch literal 62 kcmZ?rwPSz)Ga#h_#Ed}v@Bjb*j0_SG39RIQ1_mGk0O?2vKL7v# literal 0 HcmV?d00001 diff --git a/test/image.test.js b/test/image.test.js index 1162f3cac..8e79cae2f 100644 --- a/test/image.test.js +++ b/test/image.test.js @@ -6,18 +6,20 @@ * Module dependencies. */ -const loadImage = require('../').loadImage +const {createCanvas, loadImage} = require('../'); const Image = require('../').Image const assert = require('assert') const assertRejects = require('assert-rejects') const fs = require('fs') +const path = require('path') const png_checkers = `${__dirname}/fixtures/checkers.png` const png_clock = `${__dirname}/fixtures/clock.png` const jpg_chrome = `${__dirname}/fixtures/chrome.jpg` const jpg_face = `${__dirname}/fixtures/face.jpeg` const svg_tree = `${__dirname}/fixtures/tree.svg` +const bmp_dir = `${__dirname}/fixtures/bmp` describe('Image', function () { it('Prototype and ctor are well-shaped, don\'t hit asserts on accessors (GH-803)', function () { @@ -300,4 +302,139 @@ describe('Image', function () { assert.ok(!keys.includes('getSource')); assert.ok(!keys.includes('setSource')); }); + + describe('supports BMP', function () { + it('parses 1-bit image', function (done) { + let img = new Image(); + + img.onload = () => { + assert.strictEqual(img.width, 111); + assert.strictEqual(img.height, 72); + done(); + }; + + img.onerror = err => { throw err; }; + img.src = path.join(bmp_dir, '1-bit.bmp'); + }); + + it('parses 24-bit image', function (done) { + let img = new Image(); + + img.onload = () => { + assert.strictEqual(img.width, 2); + assert.strictEqual(img.height, 2); + + testImgd(img, [ + 0, 0, 255, 255, + 0, 255, 0, 255, + 255, 0, 0, 255, + 255, 255, 255, 255, + ]); + + done(); + }; + + img.onerror = err => { throw err; }; + img.src = path.join(bmp_dir, '24-bit.bmp'); + }); + + it('parses 32-bit image', function (done) { + let img = new Image(); + + img.onload = () => { + assert.strictEqual(img.width, 4); + assert.strictEqual(img.height, 2); + + testImgd(img, [ + 0, 0, 255, 255, + 0, 255, 0, 255, + 255, 0, 0, 255, + 255, 255, 255, 255, + 0, 0, 255, 127, + 0, 255, 0, 127, + 255, 0, 0, 127, + 255, 255, 255, 127, + ]); + + done(); + }; + + img.onerror = err => { throw err; }; + img.src = fs.readFileSync(path.join(bmp_dir, '32-bit.bmp')); // Also tests loading from buffer + }); + + it('parses minimal BMP', function (done) { + let img = new Image(); + + img.onload = () => { + assert.strictEqual(img.width, 1); + assert.strictEqual(img.height, 1); + + testImgd(img, [ + 255, 0, 0, 255, + ]); + + done(); + }; + + img.onerror = err => { throw err; }; + img.src = path.join(bmp_dir, 'min.bmp'); + }); + + it('properly handles negative height', function (done) { + let img = new Image(); + + img.onload = () => { + assert.strictEqual(img.width, 1); + assert.strictEqual(img.height, 2); + + testImgd(img, [ + 255, 0, 0, 255, + 0, 255, 0, 255, + ]); + + done(); + }; + + img.onerror = err => { throw err; }; + img.src = path.join(bmp_dir, 'negative-height.bmp'); + }); + + it('catches BMP errors', function (done) { + let img = new Image(); + + img.onload = () => { + throw new Error('Invalid image should not be loaded properly'); + }; + + img.onerror = err => { + let msg = 'Error while processing file header - unexpected end of file'; + assert.strictEqual(err.message, msg); + done(); + }; + + img.src = Buffer.from('BM'); + }); + + it('BMP bomb', function (done) { + let img = new Image(); + + img.onload = () => { + throw new Error('Invalid image should not be loaded properly'); + }; + + img.onerror = err => { + done(); + }; + + img.src = path.join(bmp_dir, 'bomb.bmp'); + }); + + function testImgd(img, data){ + let ctx = createCanvas(img.width, img.height).getContext('2d'); + ctx.drawImage(img, 0, 0); + var actualData = ctx.getImageData(0, 0, img.width, img.height).data; + assert.strictEqual(String(actualData), String(data)); + } + }); }) From 96bdd4f9d883b5ff11d0409d9a24a6879db579d2 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 27 Oct 2018 16:05:15 +0200 Subject: [PATCH 204/474] Reset context on resurface --- CHANGELOG.md | 4 +- lib/context2d.js | 267 +-------------- package.json | 2 +- src/Canvas.cc | 1 + src/CanvasRenderingContext2d.cc | 577 ++++++++++++++++++++++++-------- src/CanvasRenderingContext2d.h | 39 ++- test/canvas.test.js | 59 +++- 7 files changed, 521 insertions(+), 428 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf024b4b3..4ebf5841c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,13 +9,15 @@ project adheres to [Semantic Versioning](http://semver.org/). ================== ### Changed ### Added +* BMP support + ### Fixed +* Reset context on resurface (#1292) 2.1.0 ================== ### Added * Warn when building with old, unsupported versions of cairo or libjpeg. -* BMP support 2.0.0 ================== diff --git a/lib/context2d.js b/lib/context2d.js index b9c28e6ea..75f50635b 100644 --- a/lib/context2d.js +++ b/lib/context2d.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict' /*! * Canvas - Context2d @@ -6,268 +6,9 @@ * MIT Licensed */ -/** - * Module dependencies. - */ - const bindings = require('./bindings') const parseFont = require('./parse-font') -const Context2d = module.exports = bindings.CanvasRenderingContext2d -const CanvasGradient = bindings.CanvasGradient -const CanvasPattern = bindings.CanvasPattern -const ImageData = bindings.ImageData -const DOMMatrix = require('./DOMMatrix').DOMMatrix - -/** - * Text baselines. - */ - -var baselines = ['alphabetic', 'top', 'bottom', 'middle', 'ideographic', 'hanging']; - -/** - * Create a pattern from `Image` or `Canvas`. - * - * @param {Image|Canvas} image - * @param {String} repetition - * @return {CanvasPattern} - * @api public - */ - -Context2d.prototype.createPattern = function(image, repetition){ - return new CanvasPattern(image, repetition || 'repeat'); -}; - -/** - * Create a linear gradient at the given point `(x0, y0)` and `(x1, y1)`. - * - * @param {Number} x0 - * @param {Number} y0 - * @param {Number} x1 - * @param {Number} y1 - * @return {CanvasGradient} - * @api public - */ - -Context2d.prototype.createLinearGradient = function(x0, y0, x1, y1){ - return new CanvasGradient(x0, y0, x1, y1); -}; - -/** - * Create a radial gradient at the given point `(x0, y0)` and `(x1, y1)` - * and radius `r0` and `r1`. - * - * @param {Number} x0 - * @param {Number} y0 - * @param {Number} r0 - * @param {Number} x1 - * @param {Number} y1 - * @param {Number} r1 - * @return {CanvasGradient} - * @api public - */ - -Context2d.prototype.createRadialGradient = function(x0, y0, r0, x1, y1, r1){ - return new CanvasGradient(x0, y0, r0, x1, y1, r1); -}; - -/** - * Reset transform matrix to identity, then apply the given args. - * - * @param {...} - * @api public - */ - -Context2d.prototype.setTransform = function(){ - this.resetTransform(); - this.transform.apply(this, arguments); -}; - -Object.defineProperty(Context2d.prototype, 'currentTransform', { - get: function () { - var values = new Float64Array(6) - this._getMatrix(values) - return new DOMMatrix(values) - }, - set: function (m) { - if (!(m instanceof DOMMatrix)) { - throw new TypeError('Expected DOMMatrix') - } - this.setTransform(m.a, m.b, m.c, m.d, m.e, m.f) - }, - configurable: true -}) - -/** - * Set the fill style with the given css color string. - * - * @api public - */ - -Context2d.prototype.__defineSetter__('fillStyle', function(val){ - if (val instanceof CanvasGradient - || val instanceof CanvasPattern) { - this.lastFillStyle = val; - this._setFillPattern(val); - } else { - this.lastFillStyle = undefined; - this._setFillColor(String(val)); - } -}); - -/** - * Get previous fill style. - * - * @return {CanvasGradient|String} - * @api public - */ - -Context2d.prototype.__defineGetter__('fillStyle', function(){ - return this.lastFillStyle || this.fillColor; -}); - -/** - * Set the stroke style with the given css color string. - * - * @api public - */ - -Context2d.prototype.__defineSetter__('strokeStyle', function(val){ - if (val instanceof CanvasGradient - || val instanceof CanvasPattern) { - this.lastStrokeStyle = val; - this._setStrokePattern(val); - } else { - this._setStrokeColor(String(val)); - } -}); - -/** - * Get previous stroke style. - * - * @return {CanvasGradient|String} - * @api public - */ - -Context2d.prototype.__defineGetter__('strokeStyle', function(){ - return this.lastStrokeStyle || this.strokeColor; -}); - -/** - * Set font. - * - * @see exports.parseFont() - * @api public - */ - -Context2d.prototype.__defineSetter__('font', function(val){ - if (!val) return; - if ('string' == typeof val) { - var font; - if (font = parseFont(val)) { - this.lastFontString = val; - this._setFont( - font.weight - , font.style - , font.size - , font.unit - , font.family); - } - } -}); - -/** - * Get the current font. - * - * @api public - */ - -Context2d.prototype.__defineGetter__('font', function(){ - return this.lastFontString || '10px sans-serif'; -}); - -/** - * Set text baseline. - * - * @api public - */ - -Context2d.prototype.__defineSetter__('textBaseline', function(val){ - if (!val) return; - var n = baselines.indexOf(val); - if (~n) { - this.lastBaseline = val; - this._setTextBaseline(n); - } -}); - -/** - * Get the current baseline setting. - * - * @api public - */ - -Context2d.prototype.__defineGetter__('textBaseline', function(){ - return this.lastBaseline || 'alphabetic'; -}); - -/** - * Set text alignment. - * - * @api public - */ - -Context2d.prototype.__defineSetter__('textAlign', function(val){ - switch (val) { - case 'center': - this._setTextAlignment(0); - this.lastTextAlignment = val; - break; - case 'left': - case 'start': - this._setTextAlignment(-1); - this.lastTextAlignment = val; - break; - case 'right': - case 'end': - this._setTextAlignment(1); - this.lastTextAlignment = val; - break; - } -}); - -/** - * Get the current font. - * - * @see exports.parseFont() - * @api public - */ - -Context2d.prototype.__defineGetter__('textAlign', function(){ - return this.lastTextAlignment || 'start'; -}); - -/** - * Create `ImageData` with the given dimensions or - * `ImageData` instance for dimensions. - * - * @param {Number|ImageData} width - * @param {Number} height - * @return {ImageData} - * @api public - */ +const { DOMMatrix } = require('./DOMMatrix') -Context2d.prototype.createImageData = function (width, height) { - if (typeof width === 'object') { - height = width.height - width = width.width - } - var Bpp = this.canvas.stride / this.canvas.width; - var nBytes = Bpp * width * height - var arr; - if (this.pixelFormat === "RGB16_565") { - arr = new Uint16Array(nBytes / 2); - } else { - arr = new Uint8ClampedArray(nBytes); - } - return new ImageData(arr, width, height); -} +bindings.CanvasRenderingContext2dInit(DOMMatrix, parseFont) +module.exports = bindings.CanvasRenderingContext2d diff --git a/package.json b/package.json index 88f85bae3..fecbf592a 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "scripts": { "prebenchmark": "node-gyp build", "benchmark": "node benchmarks/run.js", - "pretest": "standard examples/*.js test/server.js test/public/*.js benchmark/run.js util/has_lib.js browser.js index.js && node-gyp build", + "pretest": "standard examples/*.js test/server.js test/public/*.js benchmark/run.js lib/context2d.js util/has_lib.js browser.js index.js && node-gyp build", "test": "mocha test/*.test.js", "pretest-server": "node-gyp build", "test-server": "node test/server.js", diff --git a/src/Canvas.cc b/src/Canvas.cc index 1ffed6b3d..8b2825a43 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -860,6 +860,7 @@ Canvas::resurface(Local canvas) { Context2d *context2d = ObjectWrap::Unwrap(Nan::To(context).ToLocalChecked()); cairo_t *prev = context2d->context(); context2d->setContext(createCairoContext()); + context2d->resetState(); cairo_destroy(prev); } } diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index daaaa3fc8..a4e32522a 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -96,6 +96,9 @@ inline static bool checkArgs(const Nan::FunctionCallbackInfo &info, doubl return areArgsValid; } +Nan::Persistent Context2d::_DOMMatrix; +Nan::Persistent Context2d::_parseFont; + /* * Initialize Context2d. */ @@ -115,6 +118,7 @@ Context2d::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { Nan::SetPrototypeMethod(ctor, "drawImage", DrawImage); Nan::SetPrototypeMethod(ctor, "putImageData", PutImageData); Nan::SetPrototypeMethod(ctor, "getImageData", GetImageData); + Nan::SetPrototypeMethod(ctor, "createImageData", CreateImageData); Nan::SetPrototypeMethod(ctor, "addPage", AddPage); Nan::SetPrototypeMethod(ctor, "save", Save); Nan::SetPrototypeMethod(ctor, "restore", Restore); @@ -122,6 +126,7 @@ Context2d::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { Nan::SetPrototypeMethod(ctor, "translate", Translate); Nan::SetPrototypeMethod(ctor, "transform", Transform); Nan::SetPrototypeMethod(ctor, "resetTransform", ResetTransform); + Nan::SetPrototypeMethod(ctor, "setTransform", SetTransform); Nan::SetPrototypeMethod(ctor, "isPointInPath", IsPointInPath); Nan::SetPrototypeMethod(ctor, "scale", Scale); Nan::SetPrototypeMethod(ctor, "clip", Clip); @@ -145,22 +150,15 @@ Context2d::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { Nan::SetPrototypeMethod(ctor, "ellipse", Ellipse); Nan::SetPrototypeMethod(ctor, "setLineDash", SetLineDash); Nan::SetPrototypeMethod(ctor, "getLineDash", GetLineDash); - Nan::SetPrototypeMethod(ctor, "_setFont", SetFont); - Nan::SetPrototypeMethod(ctor, "_setFillColor", SetFillColor); - Nan::SetPrototypeMethod(ctor, "_setStrokeColor", SetStrokeColor); - Nan::SetPrototypeMethod(ctor, "_setFillPattern", SetFillPattern); - Nan::SetPrototypeMethod(ctor, "_setStrokePattern", SetStrokePattern); - Nan::SetPrototypeMethod(ctor, "_setTextBaseline", SetTextBaseline); - Nan::SetPrototypeMethod(ctor, "_setTextAlignment", SetTextAlignment); - Nan::SetPrototypeMethod(ctor, "_getMatrix", GetMatrix); + Nan::SetPrototypeMethod(ctor, "createPattern", CreatePattern); + Nan::SetPrototypeMethod(ctor, "createLinearGradient", CreateLinearGradient); + Nan::SetPrototypeMethod(ctor, "createRadialGradient", CreateRadialGradient); SetProtoAccessor(proto, Nan::New("pixelFormat").ToLocalChecked(), GetFormat, NULL, ctor); SetProtoAccessor(proto, Nan::New("patternQuality").ToLocalChecked(), GetPatternQuality, SetPatternQuality, ctor); SetProtoAccessor(proto, Nan::New("imageSmoothingEnabled").ToLocalChecked(), GetImageSmoothingEnabled, SetImageSmoothingEnabled, ctor); SetProtoAccessor(proto, Nan::New("globalCompositeOperation").ToLocalChecked(), GetGlobalCompositeOperation, SetGlobalCompositeOperation, ctor); SetProtoAccessor(proto, Nan::New("globalAlpha").ToLocalChecked(), GetGlobalAlpha, SetGlobalAlpha, ctor); SetProtoAccessor(proto, Nan::New("shadowColor").ToLocalChecked(), GetShadowColor, SetShadowColor, ctor); - SetProtoAccessor(proto, Nan::New("fillColor").ToLocalChecked(), GetFillColor, NULL, ctor); - SetProtoAccessor(proto, Nan::New("strokeColor").ToLocalChecked(), GetStrokeColor, NULL, ctor); SetProtoAccessor(proto, Nan::New("miterLimit").ToLocalChecked(), GetMiterLimit, SetMiterLimit, ctor); SetProtoAccessor(proto, Nan::New("lineWidth").ToLocalChecked(), GetLineWidth, SetLineWidth, ctor); SetProtoAccessor(proto, Nan::New("lineCap").ToLocalChecked(), GetLineCap, SetLineCap, ctor); @@ -172,7 +170,14 @@ Context2d::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { SetProtoAccessor(proto, Nan::New("antialias").ToLocalChecked(), GetAntiAlias, SetAntiAlias, ctor); SetProtoAccessor(proto, Nan::New("textDrawingMode").ToLocalChecked(), GetTextDrawingMode, SetTextDrawingMode, ctor); SetProtoAccessor(proto, Nan::New("quality").ToLocalChecked(), GetQuality, SetQuality, ctor); + SetProtoAccessor(proto, Nan::New("currentTransform").ToLocalChecked(), GetCurrentTransform, SetCurrentTransform, ctor); + SetProtoAccessor(proto, Nan::New("fillStyle").ToLocalChecked(), GetFillStyle, SetFillStyle, ctor); + SetProtoAccessor(proto, Nan::New("strokeStyle").ToLocalChecked(), GetStrokeStyle, SetStrokeStyle, ctor); + SetProtoAccessor(proto, Nan::New("font").ToLocalChecked(), GetFont, SetFont, ctor); + SetProtoAccessor(proto, Nan::New("textBaseline").ToLocalChecked(), GetTextBaseline, SetTextBaseline, ctor); + SetProtoAccessor(proto, Nan::New("textAlign").ToLocalChecked(), GetTextAlign, SetTextAlign, ctor); Nan::Set(target, Nan::New("CanvasRenderingContext2d").ToLocalChecked(), ctor->GetFunction()); + Nan::Set(target, Nan::New("CanvasRenderingContext2dInit").ToLocalChecked(), Nan::New(SaveExternalModules)); } /* @@ -184,6 +189,37 @@ Context2d::Context2d(Canvas *canvas) { _context = canvas->createCairoContext(); _layout = pango_cairo_create_layout(_context); state = states[stateno = 0] = (canvas_state_t *) malloc(sizeof(canvas_state_t)); + + resetState(true); +} + +/* + * Destroy cairo context. + */ + +Context2d::~Context2d() { + while(stateno >= 0) { + pango_font_description_free(states[stateno]->fontDescription); + free(states[stateno--]); + } + g_object_unref(_layout); + cairo_destroy(_context); + _resetPersistentHandles(); +} + +/* + * Reset canvas state. + */ + +void Context2d::resetState(bool init) { + if (!init) { + free(state->fillPattern); + free(state->strokePattern); + free(state->fillGradient); + free(state->strokeGradient); + pango_font_description_free(state->fontDescription); + } + state->shadowBlur = 0; state->shadowOffsetX = state->shadowOffsetY = 0; state->globalAlpha = 1; @@ -191,8 +227,8 @@ Context2d::Context2d(Canvas *canvas) { state->fillPattern = state->strokePattern = NULL; state->fillGradient = state->strokeGradient = NULL; state->textBaseline = TEXT_BASELINE_ALPHABETIC; - rgba_t transparent = { 0,0,0,1 }; - rgba_t transparent_black = { 0,0,0,0 }; + rgba_t transparent = { 0, 0, 0, 1 }; + rgba_t transparent_black = { 0, 0, 0, 0 }; state->fill = transparent; state->stroke = transparent; state->shadow = transparent_black; @@ -202,19 +238,16 @@ Context2d::Context2d(Canvas *canvas) { state->fontDescription = pango_font_description_from_string("sans serif"); pango_font_description_set_absolute_size(state->fontDescription, 10 * PANGO_SCALE); pango_layout_set_font_description(_layout, state->fontDescription); -} -/* - * Destroy cairo context. - */ + _resetPersistentHandles(); +} -Context2d::~Context2d() { - while(stateno >= 0) { - pango_font_description_free(states[stateno]->fontDescription); - free(states[stateno--]); - } - g_object_unref(_layout); - cairo_destroy(_context); +void Context2d::_resetPersistentHandles() { + _fillStyle.Reset(); + _strokeStyle.Reset(); + _font.Reset(); + _textBaseline.Reset(); + _textAlign.Reset(); } /* @@ -670,7 +703,7 @@ NAN_METHOD(Context2d::New) { // alpha: false forces use of RGB24 Local alpha = ctxAttributes->Get(Nan::New("alpha").ToLocalChecked()); - if (alpha->IsBoolean() && !Nan::To(alpha).FromMaybe(0)) { + if (alpha->IsBoolean() && !Nan::To(alpha).FromMaybe(false)) { format = CAIRO_FORMAT_RGB24; } } @@ -683,6 +716,15 @@ NAN_METHOD(Context2d::New) { info.GetReturnValue().Set(info.This()); } +/* + * Save some external modules as private references. + */ + +NAN_METHOD(Context2d::SaveExternalModules) { + _DOMMatrix.Reset(Nan::To(info[0]).ToLocalChecked()); + _parseFont.Reset(Nan::To(info[1]).ToLocalChecked()); +} + /* * Get format (string). */ @@ -1083,6 +1125,48 @@ NAN_METHOD(Context2d::GetImageData) { info.GetReturnValue().Set(instance); } +/** + * Create `ImageData` with the given dimensions or + * `ImageData` instance for dimensions. + */ + +NAN_METHOD(Context2d::CreateImageData){ + Isolate *iso = Isolate::GetCurrent(); + Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); + Canvas *canvas = context->canvas(); + int32_t width, height; + + if (info[0]->IsObject()) { + Local ctx = Nan::GetCurrentContext(); + Local obj = Nan::To(info[0]).ToLocalChecked(); + width = Nan::To(obj->Get(ctx, Nan::New("width").ToLocalChecked()).ToLocalChecked()).FromMaybe(0); + height = Nan::To(obj->Get(ctx, Nan::New("height").ToLocalChecked()).ToLocalChecked()).FromMaybe(0); + } else { + width = Nan::To(info[0]).FromMaybe(0); + height = Nan::To(info[1]).FromMaybe(0); + } + + int stride = canvas->stride(); + double Bpp = static_cast(stride) / canvas->getWidth(); + int nBytes = static_cast(Bpp * width * height + .5); + + Local ab = ArrayBuffer::New(iso, nBytes); + Local arr; + + if (canvas->backend()->getFormat() == CAIRO_FORMAT_RGB16_565) + arr = Uint16Array::New(ab, 0, nBytes / 2); + else + arr = Uint8ClampedArray::New(ab, 0, nBytes); + + const int argc = 3; + Local argv[argc] = { arr, Nan::New(width), Nan::New(height) }; + + Local ctor = Nan::GetFunction(Nan::New(ImageData::constructor)).ToLocalChecked(); + Local instance = Nan::NewInstance(ctor, argc, argv).ToLocalChecked(); + + info.GetReturnValue().Set(instance); +} + /* * Draw image src image to the destination (context). * @@ -1375,7 +1459,7 @@ NAN_GETTER(Context2d::GetPatternQuality) { NAN_SETTER(Context2d::SetImageSmoothingEnabled) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->imageSmoothingEnabled = Nan::To(value).FromMaybe(0); + context->state->imageSmoothingEnabled = Nan::To(value).FromMaybe(false); } /* @@ -1600,6 +1684,155 @@ NAN_SETTER(Context2d::SetQuality) { cairo_pattern_set_filter(cairo_get_source(context->context()), filter); } +/* + * Get current transform. + */ + +NAN_GETTER(Context2d::GetCurrentTransform) { + Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); + Isolate *iso = Isolate::GetCurrent(); + + Local arr = Float64Array::New(ArrayBuffer::New(iso, 48), 0, 6); + Nan::TypedArrayContents dest(arr); + cairo_matrix_t matrix; + cairo_get_matrix(context->context(), &matrix); + (*dest)[0] = matrix.xx; + (*dest)[1] = matrix.yx; + (*dest)[2] = matrix.xy; + (*dest)[3] = matrix.yy; + (*dest)[4] = matrix.x0; + (*dest)[5] = matrix.y0; + + const int argc = 1; + Local argv[argc] = { arr }; + Local instance = Nan::NewInstance(_DOMMatrix.Get(iso), argc, argv).ToLocalChecked(); + + info.GetReturnValue().Set(instance); +} + +/* + * Set current transform. + */ + +NAN_SETTER(Context2d::SetCurrentTransform) { + Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); + Local ctx = Nan::GetCurrentContext(); + +#if NODE_MAJOR_VERSION > 6 + if (!Nan::To(value).ToLocalChecked()->InstanceOf(ctx, _DOMMatrix.Get(Isolate::GetCurrent())).ToChecked()) + return Nan::ThrowTypeError("Expected DOMMatrix"); +#endif + Local mat = Nan::To(value).ToLocalChecked(); + + cairo_matrix_t matrix; + cairo_matrix_init(&matrix, + Nan::To(mat->Get(ctx, Nan::New("a").ToLocalChecked()).ToLocalChecked()).FromMaybe(0), + Nan::To(mat->Get(ctx, Nan::New("b").ToLocalChecked()).ToLocalChecked()).FromMaybe(0), + Nan::To(mat->Get(ctx, Nan::New("c").ToLocalChecked()).ToLocalChecked()).FromMaybe(0), + Nan::To(mat->Get(ctx, Nan::New("d").ToLocalChecked()).ToLocalChecked()).FromMaybe(0), + Nan::To(mat->Get(ctx, Nan::New("e").ToLocalChecked()).ToLocalChecked()).FromMaybe(0), + Nan::To(mat->Get(ctx, Nan::New("f").ToLocalChecked()).ToLocalChecked()).FromMaybe(0) + ); + + cairo_transform(context->context(), &matrix); +} + +/* + * Get current fill style. + */ + +NAN_GETTER(Context2d::GetFillStyle) { + Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); + Isolate *iso = Isolate::GetCurrent(); + Local style; + + if (context->_fillStyle.IsEmpty()) + style = context->_getFillColor(); + else + style = context->_fillStyle.Get(iso); + + info.GetReturnValue().Set(style); +} + +/* + * Set current fill style. + */ + +NAN_SETTER(Context2d::SetFillStyle) { + Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); + Local ctx = Nan::GetCurrentContext(); + + if (Nan::New(Gradient::constructor)->HasInstance(value) || + Nan::New(Pattern::constructor)->HasInstance(value)) { + context->_fillStyle.Reset(value); + + Local obj = Nan::To(value).ToLocalChecked(); + if (Nan::New(Gradient::constructor)->HasInstance(obj)){ + Gradient *grad = Nan::ObjectWrap::Unwrap(obj); + context->state->fillGradient = grad->pattern(); + } else if(Nan::New(Pattern::constructor)->HasInstance(obj)){ + Pattern *pattern = Nan::ObjectWrap::Unwrap(obj); + context->state->fillPattern = pattern->pattern(); + } else { + return Nan::ThrowTypeError("Gradient or Pattern expected"); + } + } else { + MaybeLocal mstr = Nan::To(value); + if (mstr.IsEmpty()) return; + Local str = mstr.ToLocalChecked(); + context->_fillStyle.Reset(); + context->_setFillColor(str); + } +} + +/* + * Get current stroke style. + */ + +NAN_GETTER(Context2d::GetStrokeStyle) { + Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); + Local style; + + if (context->_strokeStyle.IsEmpty()) + style = context->_getStrokeColor(); + else + style = context->_strokeStyle.Get(Isolate::GetCurrent()); + + info.GetReturnValue().Set(style); +} + +/* + * Set current stroke style. + */ + +NAN_SETTER(Context2d::SetStrokeStyle) { + Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); + Isolate *iso = Isolate::GetCurrent(); + Local ctx = Nan::GetCurrentContext(); + + if (Nan::New(Gradient::constructor)->HasInstance(value) || + Nan::New(Pattern::constructor)->HasInstance(value)) { + context->_strokeStyle.Reset(value); + + Local obj = Nan::To(value).ToLocalChecked(); + if (Nan::New(Gradient::constructor)->HasInstance(obj)){ + Gradient *grad = Nan::ObjectWrap::Unwrap(obj); + context->state->strokeGradient = grad->pattern(); + } else if(Nan::New(Pattern::constructor)->HasInstance(obj)){ + Pattern *pattern = Nan::ObjectWrap::Unwrap(obj); + context->state->strokePattern = pattern->pattern(); + } else { + return Nan::ThrowTypeError("Gradient or Pattern expected"); + } + } else { + MaybeLocal mstr = Nan::To(value); + if (mstr.IsEmpty()) return; + Local str = mstr.ToLocalChecked(); + context->_strokeStyle.Reset(); + context->_setStrokeColor(str); + } +} + /* * Get miter limit. */ @@ -1723,48 +1956,6 @@ NAN_METHOD(Context2d::IsPointInPath) { info.GetReturnValue().Set(Nan::False()); } -/* - * Set fill pattern, useV internally for fillStyle= - */ - -NAN_METHOD(Context2d::SetFillPattern) { - if (!info[0]->IsObject()) - return Nan::ThrowTypeError("Gradient or Pattern expected"); - Local obj = Nan::To(info[0]).ToLocalChecked(); - if (Nan::New(Gradient::constructor)->HasInstance(obj)){ - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Gradient *grad = Nan::ObjectWrap::Unwrap(obj); - context->state->fillGradient = grad->pattern(); - } else if(Nan::New(Pattern::constructor)->HasInstance(obj)){ - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Pattern *pattern = Nan::ObjectWrap::Unwrap(obj); - context->state->fillPattern = pattern->pattern(); - } else { - return Nan::ThrowTypeError("Gradient or Pattern expected"); - } -} - -/* - * Set stroke pattern, used internally for strokeStyle= - */ - -NAN_METHOD(Context2d::SetStrokePattern) { - if (!info[0]->IsObject()) - return Nan::ThrowTypeError("Gradient or Pattern expected"); - Local obj = Nan::To(info[0]).ToLocalChecked(); - if (Nan::New(Gradient::constructor)->HasInstance(obj)){ - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Gradient *grad = Nan::ObjectWrap::Unwrap(obj); - context->state->strokeGradient = grad->pattern(); - } else if(Nan::New(Pattern::constructor)->HasInstance(obj)){ - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Pattern *pattern = Nan::ObjectWrap::Unwrap(obj); - context->state->strokePattern = pattern->pattern(); - } else { - return Nan::ThrowTypeError("Gradient or Pattern expected"); - } -} - /* * Set shadow color. */ @@ -1794,56 +1985,82 @@ NAN_GETTER(Context2d::GetShadowColor) { * Set fill color, used internally for fillStyle= */ -NAN_METHOD(Context2d::SetFillColor) { +void Context2d::_setFillColor(Local arg) { short ok; - - if (!info[0]->IsString()) return; - Nan::Utf8String str(info[0]); - + Nan::Utf8String str(arg); uint32_t rgba = rgba_from_string(*str, &ok); if (!ok) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->fillPattern = context->state->fillGradient = NULL; - context->state->fill = rgba_create(rgba); + state->fillPattern = state->fillGradient = NULL; + state->fill = rgba_create(rgba); } /* * Get fill color. */ -NAN_GETTER(Context2d::GetFillColor) { +Local Context2d::_getFillColor() { char buf[64]; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - rgba_to_string(context->state->fill, buf, sizeof(buf)); - info.GetReturnValue().Set(Nan::New(buf).ToLocalChecked()); + rgba_to_string(state->fill, buf, sizeof(buf)); + return Nan::New(buf).ToLocalChecked(); } /* * Set stroke color, used internally for strokeStyle= */ -NAN_METHOD(Context2d::SetStrokeColor) { +void Context2d::_setStrokeColor(Local arg) { short ok; - - if (!info[0]->IsString()) return; - Nan::Utf8String str(info[0]); - + Nan::Utf8String str(arg); uint32_t rgba = rgba_from_string(*str, &ok); if (!ok) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->strokePattern = context->state->strokeGradient = NULL; - context->state->stroke = rgba_create(rgba); + state->strokePattern = state->strokeGradient = NULL; + state->stroke = rgba_create(rgba); } /* * Get stroke color. */ -NAN_GETTER(Context2d::GetStrokeColor) { +Local Context2d::_getStrokeColor() { char buf[64]; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - rgba_to_string(context->state->stroke, buf, sizeof(buf)); - info.GetReturnValue().Set(Nan::New(buf).ToLocalChecked()); + rgba_to_string(state->stroke, buf, sizeof(buf)); + return Nan::New(buf).ToLocalChecked(); +} + +NAN_METHOD(Context2d::CreatePattern) { + Local image = info[0]; + Local repetition = info[1]; + + if (!Nan::To(repetition).FromMaybe(false)) + repetition = Nan::New("repeat").ToLocalChecked(); + + const int argc = 2; + Local argv[argc] = { image, repetition }; + + Local ctor = Nan::GetFunction(Nan::New(Pattern::constructor)).ToLocalChecked(); + Local instance = Nan::NewInstance(ctor, argc, argv).ToLocalChecked(); + + info.GetReturnValue().Set(instance); +} + +NAN_METHOD(Context2d::CreateLinearGradient) { + const int argc = 4; + Local argv[argc] = { info[0], info[1], info[2], info[3] }; + + Local ctor = Nan::GetFunction(Nan::New(Gradient::constructor)).ToLocalChecked(); + Local instance = Nan::NewInstance(ctor, argc, argv).ToLocalChecked(); + + info.GetReturnValue().Set(instance); +} + +NAN_METHOD(Context2d::CreateRadialGradient) { + const int argc = 6; + Local argv[argc] = { info[0], info[1], info[2], info[3], info[4], info[5] }; + + Local ctor = Nan::GetFunction(Nan::New(Gradient::constructor)).ToLocalChecked(); + Local instance = Nan::NewInstance(ctor, argc, argv).ToLocalChecked(); + + info.GetReturnValue().Set(instance); } /* @@ -1977,20 +2194,14 @@ NAN_METHOD(Context2d::ResetTransform) { cairo_identity_matrix(context->context()); } -NAN_METHOD(Context2d::GetMatrix) { +/* + * Reset transform matrix to identity, then apply the given args. + */ + +NAN_METHOD(Context2d::SetTransform) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - // Accept a receiver array to avoid an alloc (this small array will be - // a view of a larger, shared ArrayBuffer). - Local destTa = info[0].As(); - Nan::TypedArrayContents dest(destTa); - cairo_matrix_t matrix; - cairo_get_matrix(context->context(), &matrix); - (*dest)[0] = matrix.xx; - (*dest)[1] = matrix.yx; - (*dest)[2] = matrix.xy; - (*dest)[3] = matrix.yy; - (*dest)[4] = matrix.x0; - (*dest)[5] = matrix.y0; + cairo_identity_matrix(context->context()); + Context2d::Transform(info); } /* @@ -2203,6 +2414,23 @@ NAN_METHOD(Context2d::MoveTo) { cairo_move_to(context->context(), args[0], args[1]); } +/* + * Get font. + */ + +NAN_GETTER(Context2d::GetFont) { + Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); + Isolate *iso = Isolate::GetCurrent(); + Local font; + + if (context->_font.IsEmpty()) + font = Nan::New("10px sans-serif").ToLocalChecked(); + else + font = context->_font.Get(iso); + + info.GetReturnValue().Set(font); +} + /* * Set font: * - weight @@ -2210,21 +2438,27 @@ NAN_METHOD(Context2d::MoveTo) { * - size * - unit * - family - */ + */ -NAN_METHOD(Context2d::SetFont) { - // Ignore invalid args - if (!info[0]->IsString() - || !info[1]->IsString() - || !info[2]->IsNumber() - || !info[3]->IsString() - || !info[4]->IsString()) return; +NAN_SETTER(Context2d::SetFont) { + if (!value->IsString()) return; + + Isolate *iso = Isolate::GetCurrent(); + Local ctx = Nan::GetCurrentContext(); + + Local str = Nan::To(value).ToLocalChecked(); + if (!str->Length()) return; + + const int argc = 1; + Local argv[argc] = { value }; + Local parsed = _parseFont.Get(iso)->Call(ctx, ctx->Global(), argc, argv).ToLocalChecked(); + Local font = Nan::To(parsed).ToLocalChecked(); - Nan::Utf8String weight(info[0]); - Nan::Utf8String style(info[1]); - double size = Nan::To(info[2]).FromMaybe(0); - Nan::Utf8String unit(info[3]); - Nan::Utf8String family(info[4]); + Nan::Utf8String weight(font->Get(ctx, Nan::New("weight").ToLocalChecked()).ToLocalChecked()); + Nan::Utf8String style(font->Get(ctx, Nan::New("style").ToLocalChecked()).ToLocalChecked()); + double size = Nan::To(font->Get(ctx, Nan::New("size").ToLocalChecked()).ToLocalChecked()).FromMaybe(0); + Nan::Utf8String unit(font->Get(ctx, Nan::New("unit").ToLocalChecked()).ToLocalChecked()); + Nan::Utf8String family(font->Get(ctx, Nan::New("family").ToLocalChecked()).ToLocalChecked()); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -2242,8 +2476,90 @@ NAN_METHOD(Context2d::SetFont) { if (size > 0) pango_font_description_set_absolute_size(sys_desc, size * PANGO_SCALE); context->state->fontDescription = sys_desc; - pango_layout_set_font_description(context->_layout, sys_desc); + + context->_font.Reset(value); +} + +/* + * Get text baseline. + */ + +NAN_GETTER(Context2d::GetTextBaseline) { + Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); + Isolate *iso = Isolate::GetCurrent(); + Local font; + + if (context->_textBaseline.IsEmpty()) + font = Nan::New("alphabetic").ToLocalChecked(); + else + font = context->_textBaseline.Get(iso); + + info.GetReturnValue().Set(font); +} + +/* + * Set text baseline. + */ + +NAN_SETTER(Context2d::SetTextBaseline) { + if (!value->IsString()) return; + + Nan::Utf8String opStr(Nan::To(value).ToLocalChecked()); + const std::map modes = { + {"alphabetic", 0}, + {"top", 1}, + {"bottom", 2}, + {"middle", 3}, + {"ideographic", 4}, + {"hanging", 5} + }; + auto op = modes.find(*opStr); + if (op == modes.end()) return; + + Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); + context->state->textBaseline = op->second; + context->_textBaseline.Reset(value); +} + +/* + * Get text align. + */ + +NAN_GETTER(Context2d::GetTextAlign) { + Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); + Isolate *iso = Isolate::GetCurrent(); + Local font; + + if (context->_textAlign.IsEmpty()) + font = Nan::New("start").ToLocalChecked(); + else + font = context->_textAlign.Get(iso); + + info.GetReturnValue().Set(font); +} + +/* + * Set text align. + */ + +NAN_SETTER(Context2d::SetTextAlign) { + if (!value->IsString()) return; + + Nan::Utf8String opStr(Nan::To(value).ToLocalChecked()); + const std::map modes = { + {"center", 0}, + {"left", -1}, + {"start", -1}, + {"right", 1}, + {"end", 1} + }; + auto op = modes.find(*opStr); + if (op == modes.end()) return; + + Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); + context->state->textAlignment = op->second; + context->_textAlign.Reset(value); } /* @@ -2325,30 +2641,11 @@ NAN_METHOD(Context2d::MeasureText) { info.GetReturnValue().Set(obj); } -/* - * Set text baseline. - */ - -NAN_METHOD(Context2d::SetTextBaseline) { - if (!info[0]->IsInt32()) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->textBaseline = Nan::To(info[0]).FromMaybe(0); -} - -/* - * Set text alignment. -1 0 1 - */ - -NAN_METHOD(Context2d::SetTextAlignment) { - if (!info[0]->IsInt32()) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->textAlignment = Nan::To(info[0]).FromMaybe(0); -} - /* * Set line dash * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash */ + NAN_METHOD(Context2d::SetLineDash) { if (!info[0]->IsArray()) return; Local dash = Local::Cast(info[0]); @@ -2501,7 +2798,7 @@ NAN_METHOD(Context2d::Arc) { || !info[3]->IsNumber() || !info[4]->IsNumber()) return; - bool anticlockwise = Nan::To(info[5]).FromMaybe(0); + bool anticlockwise = Nan::To(info[5]).FromMaybe(false); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); @@ -2649,7 +2946,7 @@ NAN_METHOD(Context2d::Ellipse) { double rotation = args[4]; double startAngle = args[5]; double endAngle = args[6]; - bool anticlockwise = Nan::To(info[7]).FromMaybe(0); + bool anticlockwise = Nan::To(info[7]).FromMaybe(false); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index f1044995d..421928da6 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -71,9 +71,12 @@ class Context2d: public Nan::ObjectWrap { canvas_state_t *states[CANVAS_MAX_STATES]; canvas_state_t *state; Context2d(Canvas *canvas); + static Nan::Persistent _DOMMatrix; + static Nan::Persistent _parseFont; static Nan::Persistent constructor; static void Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target); static NAN_METHOD(New); + static NAN_METHOD(SaveExternalModules); static NAN_METHOD(DrawImage); static NAN_METHOD(PutImageData); static NAN_METHOD(Save); @@ -83,6 +86,7 @@ class Context2d: public Nan::ObjectWrap { static NAN_METHOD(Scale); static NAN_METHOD(Transform); static NAN_METHOD(ResetTransform); + static NAN_METHOD(SetTransform); static NAN_METHOD(IsPointInPath); static NAN_METHOD(BeginPath); static NAN_METHOD(ClosePath); @@ -95,9 +99,7 @@ class Context2d: public Nan::ObjectWrap { static NAN_METHOD(SetFont); static NAN_METHOD(SetFillColor); static NAN_METHOD(SetStrokeColor); - static NAN_METHOD(SetFillPattern); static NAN_METHOD(SetStrokePattern); - static NAN_METHOD(SetTextBaseline); static NAN_METHOD(SetTextAlignment); static NAN_METHOD(SetLineDash); static NAN_METHOD(GetLineDash); @@ -114,15 +116,17 @@ class Context2d: public Nan::ObjectWrap { static NAN_METHOD(ArcTo); static NAN_METHOD(Ellipse); static NAN_METHOD(GetImageData); - static NAN_METHOD(GetMatrix); + static NAN_METHOD(CreateImageData); + static NAN_METHOD(GetStrokeColor); + static NAN_METHOD(CreatePattern); + static NAN_METHOD(CreateLinearGradient); + static NAN_METHOD(CreateRadialGradient); static NAN_GETTER(GetFormat); static NAN_GETTER(GetPatternQuality); static NAN_GETTER(GetImageSmoothingEnabled); static NAN_GETTER(GetGlobalCompositeOperation); static NAN_GETTER(GetGlobalAlpha); static NAN_GETTER(GetShadowColor); - static NAN_GETTER(GetFillColor); - static NAN_GETTER(GetStrokeColor); static NAN_GETTER(GetMiterLimit); static NAN_GETTER(GetLineCap); static NAN_GETTER(GetLineJoin); @@ -134,6 +138,12 @@ class Context2d: public Nan::ObjectWrap { static NAN_GETTER(GetAntiAlias); static NAN_GETTER(GetTextDrawingMode); static NAN_GETTER(GetQuality); + static NAN_GETTER(GetCurrentTransform); + static NAN_GETTER(GetFillStyle); + static NAN_GETTER(GetStrokeStyle); + static NAN_GETTER(GetFont); + static NAN_GETTER(GetTextBaseline); + static NAN_GETTER(GetTextAlign); static NAN_SETTER(SetPatternQuality); static NAN_SETTER(SetImageSmoothingEnabled); static NAN_SETTER(SetGlobalCompositeOperation); @@ -150,6 +160,12 @@ class Context2d: public Nan::ObjectWrap { static NAN_SETTER(SetAntiAlias); static NAN_SETTER(SetTextDrawingMode); static NAN_SETTER(SetQuality); + static NAN_SETTER(SetCurrentTransform); + static NAN_SETTER(SetFillStyle); + static NAN_SETTER(SetStrokeStyle); + static NAN_SETTER(SetFont); + static NAN_SETTER(SetTextBaseline); + static NAN_SETTER(SetTextAlign); inline void setContext(cairo_t *ctx) { _context = ctx; } inline cairo_t *context(){ return _context; } inline Canvas *canvas(){ return _canvas; } @@ -171,10 +187,23 @@ class Context2d: public Nan::ObjectWrap { void save(); void restore(); void setFontFromState(); + void resetState(bool init = false); inline PangoLayout *layout(){ return _layout; } private: ~Context2d(); + void _resetPersistentHandles(); + Local _getFillColor(); + Local _getStrokeColor(); + void _setFillColor(Local arg); + void _setFillPattern(Local arg); + void _setStrokeColor(Local arg); + void _setStrokePattern(Local arg); + Nan::Persistent _fillStyle; + Nan::Persistent _strokeStyle; + Nan::Persistent _font; + Nan::Persistent _textBaseline; + Nan::Persistent _textAlign; Canvas *_canvas; cairo_t *_context; cairo_path_t *_path; diff --git a/test/canvas.test.js b/test/canvas.test.js index 77e012648..41e35eeb2 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -348,21 +348,33 @@ describe('Canvas', function () { }); it('Canvas#{width,height}=', function () { - var context, canvas = createCanvas(100, 200); + const canvas = createCanvas(100, 200); + const context = canvas.getContext('2d'); - assert.equal(100, canvas.width); - assert.equal(200, canvas.height); + assert.equal(canvas.width, 100); + assert.equal(canvas.height, 200); - canvas = createCanvas(); - context = canvas.getContext("2d"); - assert.equal(0, canvas.width); - assert.equal(0, canvas.height); + context.globalAlpha = .5; + context.fillStyle = '#0f0'; + context.strokeStyle = '#0f0'; + context.font = '20px arial'; + context.fillRect(0, 0, 1, 1); canvas.width = 50; - canvas.height = 50; - assert.equal(50, canvas.width); - assert.equal(50, canvas.height); - assert.equal(1, context.lineWidth); // #1095 + canvas.height = 70; + assert.equal(canvas.width, 50); + assert.equal(canvas.height, 70); + + context.font = '20px arial'; + assert.equal(context.font, '20px arial'); + canvas.width |= 0; + + assert.equal(context.lineWidth, 1); // #1095 + assert.equal(context.globalAlpha, 1); // #1292 + assert.equal(context.fillStyle, '#000000'); + assert.equal(context.strokeStyle, '#000000'); + assert.equal(context.font, '10px sans-serif'); + assert.strictEqual(context.getImageData(0, 0, 1, 1).data.join(','), '0,0,0,0'); }); it('Canvas#stride', function() { @@ -902,6 +914,7 @@ describe('Canvas', function () { ctx.textBaseline = "bottom" metrics = ctx.measureText("Alphabet") + assert.strictEqual(ctx.textBaseline, "bottom") assert.ok(metrics.alphabeticBaseline > 0) // ~4-5 assert.ok(metrics.actualBoundingBoxAscent > 0) // On the baseline or slightly above @@ -914,13 +927,23 @@ describe('Canvas', function () { var ctx = canvas.getContext('2d'); ctx.scale(0.1, 0.3); - var actual = ctx.currentTransform; - assert.equal(actual.a, 0.1); - assert.equal(actual.b, 0); - assert.equal(actual.c, 0); - assert.equal(actual.d, 0.3); - assert.equal(actual.e, 0); - assert.equal(actual.f, 0); + var mat1 = ctx.currentTransform; + assert.equal(mat1.a, 0.1); + assert.equal(mat1.b, 0); + assert.equal(mat1.c, 0); + assert.equal(mat1.d, 0.3); + assert.equal(mat1.e, 0); + assert.equal(mat1.f, 0); + + ctx.resetTransform(); + var mat2 = ctx.currentTransform; + assert.equal(mat2.a, 1); + assert.equal(mat2.d, 1); + + ctx.currentTransform = mat1; + var mat3 = ctx.currentTransform; + assert.equal(mat3.a, 0.1); + assert.equal(mat3.d, 0.3); }); it('Context2d#createImageData(ImageData)', function () { From 99bda6dc6060172122097bd39b8f0326f522988a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Tue, 20 Nov 2018 09:55:21 +0000 Subject: [PATCH 205/474] Fix typo in standard file list --- benchmarks/run.js | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/benchmarks/run.js b/benchmarks/run.js index e088e0692..4914ea97b 100644 --- a/benchmarks/run.js +++ b/benchmarks/run.js @@ -65,8 +65,8 @@ function done (benchmark, times, start, isAsync) { // node-canvas bm('fillStyle= name', function () { - ctx.fillStyle = "transparent"; -}); + ctx.fillStyle = 'transparent' +}) bm('lineTo()', function () { ctx.lineTo(0, 50) @@ -138,11 +138,11 @@ bm('moveTo() / arc() / stroke()', function () { ctx.beginPath() ctx.arc(75, 75, 50, 0, Math.PI * 2, true) // Outer circle ctx.moveTo(110, 75) - ctx.arc(75, 75, 35, 0, Math.PI, false) // Mouth + ctx.arc(75, 75, 35, 0, Math.PI, false) // Mouth ctx.moveTo(65, 65) - ctx.arc(60, 65, 5, 0, Math.PI * 2, true) // Left eye + ctx.arc(60, 65, 5, 0, Math.PI * 2, true) // Left eye ctx.moveTo(95, 65) - ctx.arc(90, 65, 5, 0, Math.PI * 2, true) // Right eye + ctx.arc(90, 65, 5, 0, Math.PI * 2, true) // Right eye ctx.stroke() }) diff --git a/package.json b/package.json index fecbf592a..5198c14b6 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "scripts": { "prebenchmark": "node-gyp build", "benchmark": "node benchmarks/run.js", - "pretest": "standard examples/*.js test/server.js test/public/*.js benchmark/run.js lib/context2d.js util/has_lib.js browser.js index.js && node-gyp build", + "pretest": "standard examples/*.js test/server.js test/public/*.js benchmarks/run.js lib/context2d.js util/has_lib.js browser.js index.js && node-gyp build", "test": "mocha test/*.test.js", "pretest-server": "node-gyp build", "test-server": "node test/server.js", From 62b48ae5b6a6b7998ced6634cf9ea51b7176ccdc Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 29 Nov 2018 11:14:57 -0800 Subject: [PATCH 206/474] Support Jest test framework Jest re-evaluates modules, whereas `require` is only supposed to evaluate them once. Fix: make reloading lib/image.js safe. Fixes #1310 Fixes #1294 Fixes #1250 --- CHANGELOG.md | 1 + lib/image.js | 11 +++-------- src/Image.cc | 9 ++++++--- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ebf5841c..fc2ae0f0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Fixed * Reset context on resurface (#1292) +* Support Jest test framework (#1311) 2.1.0 ================== diff --git a/lib/image.js b/lib/image.js index ff84f2b24..9313b997d 100644 --- a/lib/image.js +++ b/lib/image.js @@ -15,12 +15,7 @@ const Image = module.exports = bindings.Image const http = require("http") const https = require("https") -const proto = Image.prototype; -const _getSource = proto.getSource; -const _setSource = proto.setSource; - -delete proto.getSource; -delete proto.setSource; +const {GetSource, SetSource} = bindings; Object.defineProperty(Image.prototype, 'src', { /** @@ -95,10 +90,10 @@ Image.prototype.inspect = function(){ }; function getSource(img){ - return img._originalSource || _getSource.call(img); + return img._originalSource || GetSource.call(img); } function setSource(img, src, origSrc){ - _setSource.call(img, src); + SetSource.call(img, src); img._originalSource = origSrc; } diff --git a/src/Image.cc b/src/Image.cc index a6f69d95a..bf7de79de 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -62,13 +62,16 @@ Image::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { SetProtoAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, SetHeight, ctor); SetProtoAccessor(proto, Nan::New("naturalWidth").ToLocalChecked(), GetNaturalWidth, NULL, ctor); SetProtoAccessor(proto, Nan::New("naturalHeight").ToLocalChecked(), GetNaturalHeight, NULL, ctor); - - Nan::SetMethod(proto, "getSource", GetSource); - Nan::SetMethod(proto, "setSource", SetSource); SetProtoAccessor(proto, Nan::New("dataMode").ToLocalChecked(), GetDataMode, SetDataMode, ctor); + ctor->Set(Nan::New("MODE_IMAGE").ToLocalChecked(), Nan::New(DATA_IMAGE)); ctor->Set(Nan::New("MODE_MIME").ToLocalChecked(), Nan::New(DATA_MIME)); + Nan::Set(target, Nan::New("Image").ToLocalChecked(), ctor->GetFunction()); + + // Used internally in lib/image.js + NAN_EXPORT(target, GetSource); + NAN_EXPORT(target, SetSource); } /* From 3a0ab0eb6ce69d7c7fbb452c94528f991d707873 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 29 Nov 2018 11:18:33 -0800 Subject: [PATCH 207/474] Friendlier issue template --- .github/ISSUE_TEMPLATE.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 83f9fa52f..cc2799598 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,4 +1,3 @@ - - - - ## Issue or Feature +- [ ] If this is an issue with installation, I have read the [troubleshooting guide](https://github.com/Automattic/node-canvas/issues/1511). + ## Steps to Reproduce From 198080580a0e3938c48daae357b88a1638a9ddcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Sun, 17 Oct 2021 15:36:39 +0200 Subject: [PATCH 342/474] Fix building on M1 macOS --- binding.gyp | 9 ++++++++- util/has_lib.js | 3 ++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/binding.gyp b/binding.gyp index 777032c0b..57f14ab8c 100644 --- a/binding.gyp +++ b/binding.gyp @@ -164,8 +164,11 @@ '-l<(jpeg_root)/lib/jpeg.lib', ] }, { + 'include_dirs': [ + ' Date: Wed, 7 Jul 2021 14:23:55 +0200 Subject: [PATCH 343/474] use classes/const/let etc --- CHANGELOG.md | 1 + lib/DOMMatrix.js | 1011 +++++++++++++++++++++++---------------------- lib/canvas.js | 12 +- lib/jpegstream.js | 58 ++- lib/parse-font.js | 13 +- lib/pdfstream.js | 54 ++- lib/pngstream.js | 62 ++- 7 files changed, 605 insertions(+), 606 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4aa501f04..29a7d2136 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ project adheres to [Semantic Versioning](http://semver.org/). (Unreleased) ================== ### Changed +* Refactor functions to classes. * Changed `DOMPoint()` constructor to check for parameter nullability. * Changed `DOMMatrix.js` to use string literals for non-special cases. * Remove semicolons from Dommatrix.js. diff --git a/lib/DOMMatrix.js b/lib/DOMMatrix.js index c76c04f96..31178b939 100644 --- a/lib/DOMMatrix.js +++ b/lib/DOMMatrix.js @@ -4,21 +4,19 @@ const util = require('util') // DOMMatrix per https://drafts.fxtf.org/geometry/#DOMMatrix -function DOMPoint(x, y, z, w) { - if (!(this instanceof DOMPoint)) { - throw new TypeError("Class constructors cannot be invoked without 'new'") - } - - if (typeof x === 'object' && x !== null) { - w = x.w - z = x.z - y = x.y - x = x.x +class DOMPoint { + constructor (x, y, z, w) { + if (typeof x === 'object' && x !== null) { + w = x.w + z = x.z + y = x.y + x = x.x + } + this.x = typeof x === 'number' ? x : 0 + this.y = typeof y === 'number' ? y : 0 + this.z = typeof z === 'number' ? z : 0 + this.w = typeof w === 'number' ? w : 1 } - this.x = typeof x === 'number' ? x : 0 - this.y = typeof y === 'number' ? y : 0 - this.z = typeof z === 'number' ? z : 0 - this.w = typeof w === 'number' ? w : 1 } // Constants to index into _values (col-major) @@ -31,7 +29,7 @@ const DEGREE_PER_RAD = 180 / Math.PI const RAD_PER_DEGREE = Math.PI / 180 function parseMatrix(init) { - var parsed = init.replace('matrix(', '') + let parsed = init.replace('matrix(', ''); parsed = parsed.split(',', 7) // 6 + 1 to handle too many params if (parsed.length !== 6) throw new Error(`Failed to parse ${init}`) parsed = parsed.map(parseFloat) @@ -44,14 +42,14 @@ function parseMatrix(init) { } function parseMatrix3d(init) { - var parsed = init.replace('matrix3d(', '') + let parsed = init.replace('matrix3d(', ''); parsed = parsed.split(',', 17) // 16 + 1 to handle too many params if (parsed.length !== 16) throw new Error(`Failed to parse ${init}`) return parsed.map(parseFloat) } function parseTransform(tform) { - var type = tform.split('(', 1)[0] + const type = tform.split('(', 1)[0]; switch (type) { case 'matrix': return parseMatrix(tform) @@ -63,107 +61,475 @@ function parseTransform(tform) { } } -function DOMMatrix (init) { - if (!(this instanceof DOMMatrix)) { - throw new TypeError("Class constructors cannot be invoked without 'new'") +class DOMMatrix { + constructor(init) { + this._is2D = true + this._values = new Float64Array([ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ]) + + let i; + + if (typeof init === 'string') { // parse CSS transformList + if (init === '') return // default identity matrix + const tforms = init.split(/\)\s+/, 20).map(parseTransform); + if (tforms.length === 0) return + init = tforms[0] + for (i = 1; i < tforms.length; i++) init = multiply(tforms[i], init) + } + + i = 0 + if (init && init.length === 6) { + setNumber2D(this, M11, init[i++]) + setNumber2D(this, M12, init[i++]) + setNumber2D(this, M21, init[i++]) + setNumber2D(this, M22, init[i++]) + setNumber2D(this, M41, init[i++]) + setNumber2D(this, M42, init[i++]) + } else if (init && init.length === 16) { + setNumber2D(this, M11, init[i++]) + setNumber2D(this, M12, init[i++]) + setNumber3D(this, M13, init[i++]) + setNumber3D(this, M14, init[i++]) + setNumber2D(this, M21, init[i++]) + setNumber2D(this, M22, init[i++]) + setNumber3D(this, M23, init[i++]) + setNumber3D(this, M24, init[i++]) + setNumber3D(this, M31, init[i++]) + setNumber3D(this, M32, init[i++]) + setNumber3D(this, M33, init[i++]) + setNumber3D(this, M34, init[i++]) + setNumber2D(this, M41, init[i++]) + setNumber2D(this, M42, init[i++]) + setNumber3D(this, M43, init[i++]) + setNumber3D(this, M44, init[i]) + } else if (init !== undefined) { + throw new TypeError('Expected string or array.') + } } - this._is2D = true - this._values = new Float64Array([ - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1 - ]) - - var i - - if (typeof init === 'string') { // parse CSS transformList - if (init === '') return // default identity matrix - var tforms = init.split(/\)\s+/, 20).map(parseTransform) - if (tforms.length === 0) return - init = tforms[0] - for (i = 1; i < tforms.length; i++) init = multiply(tforms[i], init) - } - - i = 0 - if (init && init.length === 6) { - setNumber2D(this, M11, init[i++]) - setNumber2D(this, M12, init[i++]) - setNumber2D(this, M21, init[i++]) - setNumber2D(this, M22, init[i++]) - setNumber2D(this, M41, init[i++]) - setNumber2D(this, M42, init[i++]) - } else if (init && init.length === 16) { - setNumber2D(this, M11, init[i++]) - setNumber2D(this, M12, init[i++]) - setNumber3D(this, M13, init[i++]) - setNumber3D(this, M14, init[i++]) - setNumber2D(this, M21, init[i++]) - setNumber2D(this, M22, init[i++]) - setNumber3D(this, M23, init[i++]) - setNumber3D(this, M24, init[i++]) - setNumber3D(this, M31, init[i++]) - setNumber3D(this, M32, init[i++]) - setNumber3D(this, M33, init[i++]) - setNumber3D(this, M34, init[i++]) - setNumber2D(this, M41, init[i++]) - setNumber2D(this, M42, init[i++]) - setNumber3D(this, M43, init[i++]) - setNumber3D(this, M44, init[i]) - } else if (init !== undefined) { - throw new TypeError('Expected string or array.') + toString() { + return this.is2D ? + `matrix(${this.a}, ${this.b}, ${this.c}, ${this.d}, ${this.e}, ${this.f})` : + `matrix3d(${this._values.join(', ')})` } -} -DOMMatrix.fromMatrix = function (init) { - if (!(init instanceof DOMMatrix)) throw new TypeError('Expected DOMMatrix') - return new DOMMatrix(init._values) -} -DOMMatrix.fromFloat32Array = function (init) { - if (!(init instanceof Float32Array)) throw new TypeError('Expected Float32Array') - return new DOMMatrix(init) -} -DOMMatrix.fromFloat64Array = function (init) { - if (!(init instanceof Float64Array)) throw new TypeError('Expected Float64Array') - return new DOMMatrix(init) -} + multiply(other) { + return newInstance(this._values).multiplySelf(other) + } -// TODO || is for Node.js pre-v6.6.0 -DOMMatrix.prototype[util.inspect.custom || 'inspect'] = function (depth, options) { - if (depth < 0) return '[DOMMatrix]' - - return `DOMMatrix [ - a: ${this.a} - b: ${this.b} - c: ${this.c} - d: ${this.d} - e: ${this.e} - f: ${this.f} - m11: ${this.m11} - m12: ${this.m12} - m13: ${this.m13} - m14: ${this.m14} - m21: ${this.m21} - m22: ${this.m22} - m23: ${this.m23} - m23: ${this.m23} - m31: ${this.m31} - m32: ${this.m32} - m33: ${this.m33} - m34: ${this.m34} - m41: ${this.m41} - m42: ${this.m42} - m43: ${this.m43} - m44: ${this.m44} - is2D: ${this.is2D} - isIdentity: ${this.isIdentity} ]` -} + multiplySelf(other) { + this._values = multiply(other._values, this._values) + if (!other.is2D) this._is2D = false + return this + } + + preMultiplySelf(other) { + this._values = multiply(this._values, other._values) + if (!other.is2D) this._is2D = false + return this + } + + translate(tx, ty, tz) { + return newInstance(this._values).translateSelf(tx, ty, tz) + } + + translateSelf(tx, ty, tz) { + if (typeof tx !== 'number') tx = 0 + if (typeof ty !== 'number') ty = 0 + if (typeof tz !== 'number') tz = 0 + this._values = multiply([ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + tx, ty, tz, 1 + ], this._values) + if (tz !== 0) this._is2D = false + return this + } + + scale(scaleX, scaleY, scaleZ, originX, originY, originZ) { + return newInstance(this._values).scaleSelf(scaleX, scaleY, scaleZ, originX, originY, originZ) + } + + scale3d(scale, originX, originY, originZ) { + return newInstance(this._values).scale3dSelf(scale, originX, originY, originZ) + } + + scale3dSelf(scale, originX, originY, originZ) { + return this.scaleSelf(scale, scale, scale, originX, originY, originZ) + } + + scaleSelf(scaleX, scaleY, scaleZ, originX, originY, originZ) { + // Not redundant with translate's checks because we need to negate the values later. + if (typeof originX !== 'number') originX = 0 + if (typeof originY !== 'number') originY = 0 + if (typeof originZ !== 'number') originZ = 0 + this.translateSelf(originX, originY, originZ) + if (typeof scaleX !== 'number') scaleX = 1 + if (typeof scaleY !== 'number') scaleY = scaleX + if (typeof scaleZ !== 'number') scaleZ = 1 + this._values = multiply([ + scaleX, 0, 0, 0, + 0, scaleY, 0, 0, + 0, 0, scaleZ, 0, + 0, 0, 0, 1 + ], this._values) + this.translateSelf(-originX, -originY, -originZ) + if (scaleZ !== 1 || originZ !== 0) this._is2D = false + return this + } + + rotateFromVector(x, y) { + return newInstance(this._values).rotateFromVectorSelf(x, y) + } + + rotateFromVectorSelf(x, y) { + if (typeof x !== 'number') x = 0 + if (typeof y !== 'number') y = 0 + const theta = (x === 0 && y === 0) ? 0 : Math.atan2(y, x) * DEGREE_PER_RAD; + return this.rotateSelf(theta) + } + + rotate(rotX, rotY, rotZ) { + return newInstance(this._values).rotateSelf(rotX, rotY, rotZ) + } + + rotateSelf(rotX, rotY, rotZ) { + if (rotY === undefined && rotZ === undefined) { + rotZ = rotX + rotX = rotY = 0 + } + if (typeof rotY !== 'number') rotY = 0 + if (typeof rotZ !== 'number') rotZ = 0 + if (rotX !== 0 || rotY !== 0) this._is2D = false + rotX *= RAD_PER_DEGREE + rotY *= RAD_PER_DEGREE + rotZ *= RAD_PER_DEGREE + let c, s; + c = Math.cos(rotZ) + s = Math.sin(rotZ) + this._values = multiply([ + c, s, 0, 0, + -s, c, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ], this._values) + c = Math.cos(rotY) + s = Math.sin(rotY) + this._values = multiply([ + c, 0, -s, 0, + 0, 1, 0, 0, + s, 0, c, 0, + 0, 0, 0, 1 + ], this._values) + c = Math.cos(rotX) + s = Math.sin(rotX) + this._values = multiply([ + 1, 0, 0, 0, + 0, c, s, 0, + 0, -s, c, 0, + 0, 0, 0, 1 + ], this._values) + return this + } + + rotateAxisAngle(x, y, z, angle) { + return newInstance(this._values).rotateAxisAngleSelf(x, y, z, angle) + } + + rotateAxisAngleSelf(x, y, z, angle) { + if (typeof x !== 'number') x = 0 + if (typeof y !== 'number') y = 0 + if (typeof z !== 'number') z = 0 + // Normalize axis + const length = Math.sqrt(x * x + y * y + z * z); + if (length === 0) return this + if (length !== 1) { + x /= length + y /= length + z /= length + } + angle *= RAD_PER_DEGREE + const c = Math.cos(angle); + const s = Math.sin(angle); + const t = 1 - c; + const tx = t * x; + const ty = t * y; + // NB: This is the generic transform. If the axis is a major axis, there are + // faster transforms. + this._values = multiply([ + tx * x + c, tx * y + s * z, tx * z - s * y, 0, + tx * y - s * z, ty * y + c, ty * z + s * x, 0, + tx * z + s * y, ty * z - s * x, t * z * z + c, 0, + 0, 0, 0, 1 + ], this._values) + if (x !== 0 || y !== 0) this._is2D = false + return this + } + + skewX(sx) { + return newInstance(this._values).skewXSelf(sx) + } + + skewXSelf(sx) { + if (typeof sx !== 'number') return this + const t = Math.tan(sx * RAD_PER_DEGREE); + this._values = multiply([ + 1, 0, 0, 0, + t, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ], this._values) + return this + } + + skewY(sy) { + return newInstance(this._values).skewYSelf(sy) + } + + skewYSelf(sy) { + if (typeof sy !== 'number') return this + const t = Math.tan(sy * RAD_PER_DEGREE); + this._values = multiply([ + 1, t, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ], this._values) + return this + } + + flipX() { + return newInstance(multiply([ + -1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ], this._values)) + } + + flipY() { + return newInstance(multiply([ + 1, 0, 0, 0, + 0, -1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ], this._values)) + } + + inverse() { + return newInstance(this._values).invertSelf() + } + + invertSelf() { + const m = this._values; + const inv = m.map(v => 0); + + inv[0] = m[5] * m[10] * m[15] - + m[5] * m[11] * m[14] - + m[9] * m[6] * m[15] + + m[9] * m[7] * m[14] + + m[13] * m[6] * m[11] - + m[13] * m[7] * m[10] + + inv[4] = -m[4] * m[10] * m[15] + + m[4] * m[11] * m[14] + + m[8] * m[6] * m[15] - + m[8] * m[7] * m[14] - + m[12] * m[6] * m[11] + + m[12] * m[7] * m[10] + + inv[8] = m[4] * m[9] * m[15] - + m[4] * m[11] * m[13] - + m[8] * m[5] * m[15] + + m[8] * m[7] * m[13] + + m[12] * m[5] * m[11] - + m[12] * m[7] * m[9] + + inv[12] = -m[4] * m[9] * m[14] + + m[4] * m[10] * m[13] + + m[8] * m[5] * m[14] - + m[8] * m[6] * m[13] - + m[12] * m[5] * m[10] + + m[12] * m[6] * m[9] + + // If the determinant is zero, this matrix cannot be inverted, and all + // values should be set to NaN, with the is2D flag set to false. + + const det = m[0] * inv[0] + m[1] * inv[4] + m[2] * inv[8] + m[3] * inv[12]; + + if (det === 0) { + this._values = m.map(v => NaN) + this._is2D = false + return this + } + + inv[1] = -m[1] * m[10] * m[15] + + m[1] * m[11] * m[14] + + m[9] * m[2] * m[15] - + m[9] * m[3] * m[14] - + m[13] * m[2] * m[11] + + m[13] * m[3] * m[10] + + inv[5] = m[0] * m[10] * m[15] - + m[0] * m[11] * m[14] - + m[8] * m[2] * m[15] + + m[8] * m[3] * m[14] + + m[12] * m[2] * m[11] - + m[12] * m[3] * m[10] + + inv[9] = -m[0] * m[9] * m[15] + + m[0] * m[11] * m[13] + + m[8] * m[1] * m[15] - + m[8] * m[3] * m[13] - + m[12] * m[1] * m[11] + + m[12] * m[3] * m[9] + + inv[13] = m[0] * m[9] * m[14] - + m[0] * m[10] * m[13] - + m[8] * m[1] * m[14] + + m[8] * m[2] * m[13] + + m[12] * m[1] * m[10] - + m[12] * m[2] * m[9] + + inv[2] = m[1] * m[6] * m[15] - + m[1] * m[7] * m[14] - + m[5] * m[2] * m[15] + + m[5] * m[3] * m[14] + + m[13] * m[2] * m[7] - + m[13] * m[3] * m[6] + + inv[6] = -m[0] * m[6] * m[15] + + m[0] * m[7] * m[14] + + m[4] * m[2] * m[15] - + m[4] * m[3] * m[14] - + m[12] * m[2] * m[7] + + m[12] * m[3] * m[6] + + inv[10] = m[0] * m[5] * m[15] - + m[0] * m[7] * m[13] - + m[4] * m[1] * m[15] + + m[4] * m[3] * m[13] + + m[12] * m[1] * m[7] - + m[12] * m[3] * m[5] + + inv[14] = -m[0] * m[5] * m[14] + + m[0] * m[6] * m[13] + + m[4] * m[1] * m[14] - + m[4] * m[2] * m[13] - + m[12] * m[1] * m[6] + + m[12] * m[2] * m[5] + + inv[3] = -m[1] * m[6] * m[11] + + m[1] * m[7] * m[10] + + m[5] * m[2] * m[11] - + m[5] * m[3] * m[10] - + m[9] * m[2] * m[7] + + m[9] * m[3] * m[6] + + inv[7] = m[0] * m[6] * m[11] - + m[0] * m[7] * m[10] - + m[4] * m[2] * m[11] + + m[4] * m[3] * m[10] + + m[8] * m[2] * m[7] - + m[8] * m[3] * m[6] + + inv[11] = -m[0] * m[5] * m[11] + + m[0] * m[7] * m[9] + + m[4] * m[1] * m[11] - + m[4] * m[3] * m[9] - + m[8] * m[1] * m[7] + + m[8] * m[3] * m[5] + + inv[15] = m[0] * m[5] * m[10] - + m[0] * m[6] * m[9] - + m[4] * m[1] * m[10] + + m[4] * m[2] * m[9] + + m[8] * m[1] * m[6] - + m[8] * m[2] * m[5] + + inv.forEach((v,i) => inv[i] = v/det) + this._values = inv + return this + } + + setMatrixValue(transformList) { + const temp = new DOMMatrix(transformList); + this._values = temp._values + this._is2D = temp._is2D + return this + } + + transformPoint(point) { + point = new DOMPoint(point) + const x = point.x; + const y = point.y; + const z = point.z; + const w = point.w; + const values = this._values; + const nx = values[M11] * x + values[M21] * y + values[M31] * z + values[M41] * w; + const ny = values[M12] * x + values[M22] * y + values[M32] * z + values[M42] * w; + const nz = values[M13] * x + values[M23] * y + values[M33] * z + values[M43] * w; + const nw = values[M14] * x + values[M24] * y + values[M34] * z + values[M44] * w; + return new DOMPoint(nx, ny, nz, nw) + } + + toFloat32Array() { + return Float32Array.from(this._values) + } + + toFloat64Array() { + return this._values.slice(0) + } + + static fromMatrix (init) { + if (!(init instanceof DOMMatrix)) throw new TypeError('Expected DOMMatrix') + return new DOMMatrix(init._values) + } + + static fromFloat32Array (init) { + if (!(init instanceof Float32Array)) throw new TypeError('Expected Float32Array') + return new DOMMatrix(init) + } + + static fromFloat64Array (init) { + if (!(init instanceof Float64Array)) throw new TypeError('Expected Float64Array') + return new DOMMatrix(init) + } -DOMMatrix.prototype.toString = function () { - return this.is2D ? - `matrix(${this.a}, ${this.b}, ${this.c}, ${this.d}, ${this.e}, ${this.f})` : - `matrix3d(${this._values.join(', ')})` + [util.inspect.custom || 'inspect'] (depth, options) { + if (depth < 0) return '[DOMMatrix]' + + return `DOMMatrix [ + a: ${this.a} + b: ${this.b} + c: ${this.c} + d: ${this.d} + e: ${this.e} + f: ${this.f} + m11: ${this.m11} + m12: ${this.m12} + m13: ${this.m13} + m14: ${this.m14} + m21: ${this.m21} + m22: ${this.m22} + m23: ${this.m23} + m23: ${this.m23} + m31: ${this.m31} + m32: ${this.m32} + m33: ${this.m33} + m34: ${this.m34} + m41: ${this.m41} + m42: ${this.m42} + m43: ${this.m43} + m44: ${this.m44} + is2D: ${this.is2D} + isIdentity: ${this.isIdentity} ]` + } } /** @@ -187,35 +553,35 @@ function setNumber3D(receiver, index, value) { } Object.defineProperties(DOMMatrix.prototype, { - m11: {get: function () { return this._values[M11] }, set: function (v) { return setNumber2D(this, M11, v) }}, - m12: {get: function () { return this._values[M12] }, set: function (v) { return setNumber2D(this, M12, v) }}, - m13: {get: function () { return this._values[M13] }, set: function (v) { return setNumber3D(this, M13, v) }}, - m14: {get: function () { return this._values[M14] }, set: function (v) { return setNumber3D(this, M14, v) }}, - m21: {get: function () { return this._values[M21] }, set: function (v) { return setNumber2D(this, M21, v) }}, - m22: {get: function () { return this._values[M22] }, set: function (v) { return setNumber2D(this, M22, v) }}, - m23: {get: function () { return this._values[M23] }, set: function (v) { return setNumber3D(this, M23, v) }}, - m24: {get: function () { return this._values[M24] }, set: function (v) { return setNumber3D(this, M24, v) }}, - m31: {get: function () { return this._values[M31] }, set: function (v) { return setNumber3D(this, M31, v) }}, - m32: {get: function () { return this._values[M32] }, set: function (v) { return setNumber3D(this, M32, v) }}, - m33: {get: function () { return this._values[M33] }, set: function (v) { return setNumber3D(this, M33, v) }}, - m34: {get: function () { return this._values[M34] }, set: function (v) { return setNumber3D(this, M34, v) }}, - m41: {get: function () { return this._values[M41] }, set: function (v) { return setNumber2D(this, M41, v) }}, - m42: {get: function () { return this._values[M42] }, set: function (v) { return setNumber2D(this, M42, v) }}, - m43: {get: function () { return this._values[M43] }, set: function (v) { return setNumber3D(this, M43, v) }}, - m44: {get: function () { return this._values[M44] }, set: function (v) { return setNumber3D(this, M44, v) }}, - - a: {get: function () { return this.m11 }, set: function (v) { return this.m11 = v }}, - b: {get: function () { return this.m12 }, set: function (v) { return this.m12 = v }}, - c: {get: function () { return this.m21 }, set: function (v) { return this.m21 = v }}, - d: {get: function () { return this.m22 }, set: function (v) { return this.m22 = v }}, - e: {get: function () { return this.m41 }, set: function (v) { return this.m41 = v }}, - f: {get: function () { return this.m42 }, set: function (v) { return this.m42 = v }}, - - is2D: {get: function () { return this._is2D }}, // read-only + m11: {get() { return this._values[M11] }, set(v) { return setNumber2D(this, M11, v) }}, + m12: {get() { return this._values[M12] }, set(v) { return setNumber2D(this, M12, v) }}, + m13: {get() { return this._values[M13] }, set(v) { return setNumber3D(this, M13, v) }}, + m14: {get() { return this._values[M14] }, set(v) { return setNumber3D(this, M14, v) }}, + m21: {get() { return this._values[M21] }, set(v) { return setNumber2D(this, M21, v) }}, + m22: {get() { return this._values[M22] }, set(v) { return setNumber2D(this, M22, v) }}, + m23: {get() { return this._values[M23] }, set(v) { return setNumber3D(this, M23, v) }}, + m24: {get() { return this._values[M24] }, set(v) { return setNumber3D(this, M24, v) }}, + m31: {get() { return this._values[M31] }, set(v) { return setNumber3D(this, M31, v) }}, + m32: {get() { return this._values[M32] }, set(v) { return setNumber3D(this, M32, v) }}, + m33: {get() { return this._values[M33] }, set(v) { return setNumber3D(this, M33, v) }}, + m34: {get() { return this._values[M34] }, set(v) { return setNumber3D(this, M34, v) }}, + m41: {get() { return this._values[M41] }, set(v) { return setNumber2D(this, M41, v) }}, + m42: {get() { return this._values[M42] }, set(v) { return setNumber2D(this, M42, v) }}, + m43: {get() { return this._values[M43] }, set(v) { return setNumber3D(this, M43, v) }}, + m44: {get() { return this._values[M44] }, set(v) { return setNumber3D(this, M44, v) }}, + + a: {get() { return this.m11 }, set(v) { return this.m11 = v }}, + b: {get() { return this.m12 }, set(v) { return this.m12 = v }}, + c: {get() { return this.m21 }, set(v) { return this.m21 = v }}, + d: {get() { return this.m22 }, set(v) { return this.m22 = v }}, + e: {get() { return this.m41 }, set(v) { return this.m41 = v }}, + f: {get() { return this.m42 }, set(v) { return this.m42 = v }}, + + is2D: {get() { return this._is2D }}, // read-only isIdentity: { - get: function () { - var values = this._values + get() { + const values = this._values; return values[M11] === 1 && values[M12] === 0 && values[M13] === 0 && values[M14] === 0 && values[M21] === 0 && values[M22] === 1 && values[M23] === 0 && values[M24] === 0 && values[M31] === 0 && values[M32] === 0 && values[M33] === 1 && values[M34] === 0 && @@ -230,7 +596,7 @@ Object.defineProperties(DOMMatrix.prototype, { * without copying (okay because all usages are followed by a multiply). */ function newInstance(values) { - var instance = Object.create(DOMMatrix.prototype) + const instance = Object.create(DOMMatrix.prototype); instance.constructor = DOMMatrix instance._is2D = true instance._values = values @@ -238,11 +604,11 @@ function newInstance(values) { } function multiply(A, B) { - var dest = new Float64Array(16) - for (var i = 0; i < 4; i++) { - for (var j = 0; j < 4; j++) { - var sum = 0 - for (var k = 0; k < 4; k++) { + const dest = new Float64Array(16); + for (let i = 0; i < 4; i++) { + for (let j = 0; j < 4; j++) { + let sum = 0; + for (let k = 0; k < 4; k++) { sum += A[i * 4 + k] * B[k * 4 + j] } dest[i * 4 + j] = sum @@ -251,359 +617,4 @@ function multiply(A, B) { return dest } -DOMMatrix.prototype.multiply = function (other) { - return newInstance(this._values).multiplySelf(other) -} -DOMMatrix.prototype.multiplySelf = function (other) { - this._values = multiply(other._values, this._values) - if (!other.is2D) this._is2D = false - return this -} -DOMMatrix.prototype.preMultiplySelf = function (other) { - this._values = multiply(this._values, other._values) - if (!other.is2D) this._is2D = false - return this -} - -DOMMatrix.prototype.translate = function (tx, ty, tz) { - return newInstance(this._values).translateSelf(tx, ty, tz) -} -DOMMatrix.prototype.translateSelf = function (tx, ty, tz) { - if (typeof tx !== 'number') tx = 0 - if (typeof ty !== 'number') ty = 0 - if (typeof tz !== 'number') tz = 0 - this._values = multiply([ - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - tx, ty, tz, 1 - ], this._values) - if (tz !== 0) this._is2D = false - return this -} - -DOMMatrix.prototype.scale = function (scaleX, scaleY, scaleZ, originX, originY, originZ) { - return newInstance(this._values).scaleSelf(scaleX, scaleY, scaleZ, originX, originY, originZ) -} -DOMMatrix.prototype.scale3d = function (scale, originX, originY, originZ) { - return newInstance(this._values).scale3dSelf(scale, originX, originY, originZ) -} -DOMMatrix.prototype.scale3dSelf = function (scale, originX, originY, originZ) { - return this.scaleSelf(scale, scale, scale, originX, originY, originZ) -} -DOMMatrix.prototype.scaleSelf = function (scaleX, scaleY, scaleZ, originX, originY, originZ) { - // Not redundant with translate's checks because we need to negate the values later. - if (typeof originX !== 'number') originX = 0 - if (typeof originY !== 'number') originY = 0 - if (typeof originZ !== 'number') originZ = 0 - this.translateSelf(originX, originY, originZ) - if (typeof scaleX !== 'number') scaleX = 1 - if (typeof scaleY !== 'number') scaleY = scaleX - if (typeof scaleZ !== 'number') scaleZ = 1 - this._values = multiply([ - scaleX, 0, 0, 0, - 0, scaleY, 0, 0, - 0, 0, scaleZ, 0, - 0, 0, 0, 1 - ], this._values) - this.translateSelf(-originX, -originY, -originZ) - if (scaleZ !== 1 || originZ !== 0) this._is2D = false - return this -} - -DOMMatrix.prototype.rotateFromVector = function (x, y) { - return newInstance(this._values).rotateFromVectorSelf(x, y) -} -DOMMatrix.prototype.rotateFromVectorSelf = function (x, y) { - if (typeof x !== 'number') x = 0 - if (typeof y !== 'number') y = 0 - var theta = (x === 0 && y === 0) ? 0 : Math.atan2(y, x) * DEGREE_PER_RAD - return this.rotateSelf(theta) -} -DOMMatrix.prototype.rotate = function (rotX, rotY, rotZ) { - return newInstance(this._values).rotateSelf(rotX, rotY, rotZ) -} -DOMMatrix.prototype.rotateSelf = function (rotX, rotY, rotZ) { - if (rotY === undefined && rotZ === undefined) { - rotZ = rotX - rotX = rotY = 0 - } - if (typeof rotY !== 'number') rotY = 0 - if (typeof rotZ !== 'number') rotZ = 0 - if (rotX !== 0 || rotY !== 0) this._is2D = false - rotX *= RAD_PER_DEGREE - rotY *= RAD_PER_DEGREE - rotZ *= RAD_PER_DEGREE - var c, s - c = Math.cos(rotZ) - s = Math.sin(rotZ) - this._values = multiply([ - c, s, 0, 0, - -s, c, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1 - ], this._values) - c = Math.cos(rotY) - s = Math.sin(rotY) - this._values = multiply([ - c, 0, -s, 0, - 0, 1, 0, 0, - s, 0, c, 0, - 0, 0, 0, 1 - ], this._values) - c = Math.cos(rotX) - s = Math.sin(rotX) - this._values = multiply([ - 1, 0, 0, 0, - 0, c, s, 0, - 0, -s, c, 0, - 0, 0, 0, 1 - ], this._values) - return this -} - -DOMMatrix.prototype.rotateAxisAngle = function (x, y, z, angle) { - return newInstance(this._values).rotateAxisAngleSelf(x, y, z, angle) -} -DOMMatrix.prototype.rotateAxisAngleSelf = function (x, y, z, angle) { - if (typeof x !== 'number') x = 0 - if (typeof y !== 'number') y = 0 - if (typeof z !== 'number') z = 0 - // Normalize axis - var length = Math.sqrt(x * x + y * y + z * z) - if (length === 0) return this - if (length !== 1) { - x /= length - y /= length - z /= length - } - angle *= RAD_PER_DEGREE - var c = Math.cos(angle) - var s = Math.sin(angle) - var t = 1 - c - var tx = t * x - var ty = t * y - // NB: This is the generic transform. If the axis is a major axis, there are - // faster transforms. - this._values = multiply([ - tx * x + c, tx * y + s * z, tx * z - s * y, 0, - tx * y - s * z, ty * y + c, ty * z + s * x, 0, - tx * z + s * y, ty * z - s * x, t * z * z + c, 0, - 0, 0, 0, 1 - ], this._values) - if (x !== 0 || y !== 0) this._is2D = false - return this -} - -DOMMatrix.prototype.skewX = function (sx) { - return newInstance(this._values).skewXSelf(sx) -} -DOMMatrix.prototype.skewXSelf = function (sx) { - if (typeof sx !== 'number') return this - var t = Math.tan(sx * RAD_PER_DEGREE) - this._values = multiply([ - 1, 0, 0, 0, - t, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1 - ], this._values) - return this -} - -DOMMatrix.prototype.skewY = function (sy) { - return newInstance(this._values).skewYSelf(sy) -} -DOMMatrix.prototype.skewYSelf = function (sy) { - if (typeof sy !== 'number') return this - var t = Math.tan(sy * RAD_PER_DEGREE) - this._values = multiply([ - 1, t, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1 - ], this._values) - return this -} - -DOMMatrix.prototype.flipX = function () { - return newInstance(multiply([ - -1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1 - ], this._values)) -} -DOMMatrix.prototype.flipY = function () { - return newInstance(multiply([ - 1, 0, 0, 0, - 0, -1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1 - ], this._values)) -} - -DOMMatrix.prototype.inverse = function () { - return newInstance(this._values).invertSelf() -} -DOMMatrix.prototype.invertSelf = function () { - var m = this._values - var inv = m.map(v => 0) - - inv[0] = m[5] * m[10] * m[15] - - m[5] * m[11] * m[14] - - m[9] * m[6] * m[15] + - m[9] * m[7] * m[14] + - m[13] * m[6] * m[11] - - m[13] * m[7] * m[10] - - inv[4] = -m[4] * m[10] * m[15] + - m[4] * m[11] * m[14] + - m[8] * m[6] * m[15] - - m[8] * m[7] * m[14] - - m[12] * m[6] * m[11] + - m[12] * m[7] * m[10] - - inv[8] = m[4] * m[9] * m[15] - - m[4] * m[11] * m[13] - - m[8] * m[5] * m[15] + - m[8] * m[7] * m[13] + - m[12] * m[5] * m[11] - - m[12] * m[7] * m[9] - - inv[12] = -m[4] * m[9] * m[14] + - m[4] * m[10] * m[13] + - m[8] * m[5] * m[14] - - m[8] * m[6] * m[13] - - m[12] * m[5] * m[10] + - m[12] * m[6] * m[9] - - // If the determinant is zero, this matrix cannot be inverted, and all - // values should be set to NaN, with the is2D flag set to false. - - var det = m[0] * inv[0] + m[1] * inv[4] + m[2] * inv[8] + m[3] * inv[12] - - if (det === 0) { - this._values = m.map(v => NaN) - this._is2D = false - return this - } - - inv[1] = -m[1] * m[10] * m[15] + - m[1] * m[11] * m[14] + - m[9] * m[2] * m[15] - - m[9] * m[3] * m[14] - - m[13] * m[2] * m[11] + - m[13] * m[3] * m[10] - - inv[5] = m[0] * m[10] * m[15] - - m[0] * m[11] * m[14] - - m[8] * m[2] * m[15] + - m[8] * m[3] * m[14] + - m[12] * m[2] * m[11] - - m[12] * m[3] * m[10] - - inv[9] = -m[0] * m[9] * m[15] + - m[0] * m[11] * m[13] + - m[8] * m[1] * m[15] - - m[8] * m[3] * m[13] - - m[12] * m[1] * m[11] + - m[12] * m[3] * m[9] - - inv[13] = m[0] * m[9] * m[14] - - m[0] * m[10] * m[13] - - m[8] * m[1] * m[14] + - m[8] * m[2] * m[13] + - m[12] * m[1] * m[10] - - m[12] * m[2] * m[9] - - inv[2] = m[1] * m[6] * m[15] - - m[1] * m[7] * m[14] - - m[5] * m[2] * m[15] + - m[5] * m[3] * m[14] + - m[13] * m[2] * m[7] - - m[13] * m[3] * m[6] - - inv[6] = -m[0] * m[6] * m[15] + - m[0] * m[7] * m[14] + - m[4] * m[2] * m[15] - - m[4] * m[3] * m[14] - - m[12] * m[2] * m[7] + - m[12] * m[3] * m[6] - - inv[10] = m[0] * m[5] * m[15] - - m[0] * m[7] * m[13] - - m[4] * m[1] * m[15] + - m[4] * m[3] * m[13] + - m[12] * m[1] * m[7] - - m[12] * m[3] * m[5] - - inv[14] = -m[0] * m[5] * m[14] + - m[0] * m[6] * m[13] + - m[4] * m[1] * m[14] - - m[4] * m[2] * m[13] - - m[12] * m[1] * m[6] + - m[12] * m[2] * m[5] - - inv[3] = -m[1] * m[6] * m[11] + - m[1] * m[7] * m[10] + - m[5] * m[2] * m[11] - - m[5] * m[3] * m[10] - - m[9] * m[2] * m[7] + - m[9] * m[3] * m[6] - - inv[7] = m[0] * m[6] * m[11] - - m[0] * m[7] * m[10] - - m[4] * m[2] * m[11] + - m[4] * m[3] * m[10] + - m[8] * m[2] * m[7] - - m[8] * m[3] * m[6] - - inv[11] = -m[0] * m[5] * m[11] + - m[0] * m[7] * m[9] + - m[4] * m[1] * m[11] - - m[4] * m[3] * m[9] - - m[8] * m[1] * m[7] + - m[8] * m[3] * m[5] - - inv[15] = m[0] * m[5] * m[10] - - m[0] * m[6] * m[9] - - m[4] * m[1] * m[10] + - m[4] * m[2] * m[9] + - m[8] * m[1] * m[6] - - m[8] * m[2] * m[5] - - inv.forEach((v,i) => inv[i] = v/det) - this._values = inv - return this -} - -DOMMatrix.prototype.setMatrixValue = function (transformList) { - var temp = new DOMMatrix(transformList) - this._values = temp._values - this._is2D = temp._is2D - return this -} - -DOMMatrix.prototype.transformPoint = function (point) { - point = new DOMPoint(point) - var x = point.x - var y = point.y - var z = point.z - var w = point.w - var values = this._values - var nx = values[M11] * x + values[M21] * y + values[M31] * z + values[M41] * w - var ny = values[M12] * x + values[M22] * y + values[M32] * z + values[M42] * w - var nz = values[M13] * x + values[M23] * y + values[M33] * z + values[M43] * w - var nw = values[M14] * x + values[M24] * y + values[M34] * z + values[M44] * w - return new DOMPoint(nx, ny, nz, nw) -} - -DOMMatrix.prototype.toFloat32Array = function () { - return Float32Array.from(this._values) -} - -DOMMatrix.prototype.toFloat64Array = function () { - return this._values.slice(0) -} - module.exports = {DOMMatrix, DOMPoint} diff --git a/lib/canvas.js b/lib/canvas.js index 18b47364d..07d520aec 100644 --- a/lib/canvas.js +++ b/lib/canvas.js @@ -22,7 +22,7 @@ Canvas.prototype[util.inspect.custom || 'inspect'] = function () { Canvas.prototype.getContext = function (contextType, contextAttributes) { if ('2d' == contextType) { - var ctx = this._context2d || (this._context2d = new Context2d(this, contextAttributes)); + const ctx = this._context2d || (this._context2d = new Context2d(this, contextAttributes)); this.context = ctx; ctx.canvas = this; return ctx; @@ -63,9 +63,9 @@ Canvas.prototype.toDataURL = function(a1, a2, a3){ // ['image/jpeg', opts] -> ['image/jpeg', opts, fn] // ['image/jpeg', qual] -> ['image/jpeg', {quality: qual}, fn] - var type = 'image/png'; - var opts = {}; - var fn; + let type = 'image/png'; + let opts = {}; + let fn; if ('function' === typeof a1) { fn = a1; @@ -86,14 +86,14 @@ Canvas.prototype.toDataURL = function(a1, a2, a3){ if ('function' === typeof a3) { fn = a3; } else if (undefined !== a3) { - throw new TypeError(typeof a3 + ' is not a function'); + throw new TypeError(`${typeof a3} is not a function`); } } } if (this.width === 0 || this.height === 0) { // Per spec, if the bitmap has no pixels, return this string: - var str = "data:,"; + const str = "data:,"; if (fn) { setTimeout(() => fn(null, str)); return; diff --git a/lib/jpegstream.js b/lib/jpegstream.js index a057d8983..d750f672f 100644 --- a/lib/jpegstream.js +++ b/lib/jpegstream.js @@ -6,40 +6,36 @@ * MIT Licensed */ -var Readable = require('stream').Readable; -var util = require('util'); +const { Readable } = require('stream'); +function noop() {} -var JPEGStream = module.exports = function JPEGStream(canvas, options) { - if (!(this instanceof JPEGStream)) { - throw new TypeError("Class constructors cannot be invoked without 'new'"); - } +class JPEGStream extends Readable { + constructor (canvas, options) { + super(); - if (canvas.streamJPEGSync === undefined) { - throw new Error("node-canvas was built without JPEG support."); - } + if (canvas.streamJPEGSync === undefined) { + throw new Error("node-canvas was built without JPEG support.") + } - Readable.call(this); + this.options = options + this.canvas = canvas + } - this.options = options; - this.canvas = canvas; + _read() { + // For now we're not controlling the c++ code's data emission, so we only + // call canvas.streamJPEGSync once and let it emit data at will. + this._read = noop + + this.canvas.streamJPEGSync(this.options, (err, chunk) => { + if (err) { + this.emit('error', err) + } else if (chunk) { + this.push(chunk) + } else { + this.push(null) + } + }) + } }; -util.inherits(JPEGStream, Readable); - -function noop() {} - -JPEGStream.prototype._read = function _read() { - // For now we're not controlling the c++ code's data emission, so we only - // call canvas.streamJPEGSync once and let it emit data at will. - this._read = noop; - var self = this; - self.canvas.streamJPEGSync(this.options, function(err, chunk){ - if (err) { - self.emit('error', err); - } else if (chunk) { - self.push(chunk); - } else { - self.push(null); - } - }); -}; +module.exports = JPEGStream; diff --git a/lib/parse-font.js b/lib/parse-font.js index ce03529e0..43b60b38a 100644 --- a/lib/parse-font.js +++ b/lib/parse-font.js @@ -14,13 +14,12 @@ const weights = 'bold|bolder|lighter|[1-9]00' // [ [ <‘font-style’> || || <‘font-weight’> || <‘font-stretch’> ]? // <‘font-size’> [ / <‘line-height’> ]? <‘font-family’> ] // https://drafts.csswg.org/css-fonts-3/#font-prop -const weightRe = new RegExp('(' + weights + ') +', 'i') -const styleRe = new RegExp('(' + styles + ') +', 'i') -const variantRe = new RegExp('(' + variants + ') +', 'i') -const stretchRe = new RegExp('(' + stretches + ') +', 'i') +const weightRe = new RegExp(`(${weights}) +`, 'i') +const styleRe = new RegExp(`(${styles}) +`, 'i') +const variantRe = new RegExp(`(${variants}) +`, 'i') +const stretchRe = new RegExp(`(${stretches}) +`, 'i') const sizeFamilyRe = new RegExp( - '([\\d\\.]+)(' + units + ') *' - + '((?:' + string + ')( *, *(?:' + string + '))*)') + `([\\d\\.]+)(${units}) *((?:${string})( *, *(?:${string}))*)`) /** * Cache font parsing. @@ -39,7 +38,7 @@ const defaultHeight = 16 // pt, common browser default * @api private */ -module.exports = function (str) { +module.exports = str => { // Cached if (cache[str]) return cache[str] diff --git a/lib/pdfstream.js b/lib/pdfstream.js index 6aeec53d7..6ed12883c 100644 --- a/lib/pdfstream.js +++ b/lib/pdfstream.js @@ -4,36 +4,32 @@ * Canvas - PDFStream */ -var Readable = require('stream').Readable; -var util = require('util'); - -var PDFStream = module.exports = function PDFStream(canvas, options) { - if (!(this instanceof PDFStream)) { - throw new TypeError("Class constructors cannot be invoked without 'new'"); - } - - Readable.call(this); +const { Readable } = require('stream'); +function noop() {} - this.canvas = canvas; - this.options = options; -}; +class PDFStream extends Readable { + constructor(canvas, options) { + super(); -util.inherits(PDFStream, Readable); + this.canvas = canvas; + this.options = options; + } -function noop() {} + _read() { + // For now we're not controlling the c++ code's data emission, so we only + // call canvas.streamPDFSync once and let it emit data at will. + this._read = noop; + + this.canvas.streamPDFSync((err, chunk, len) => { + if (err) { + this.emit('error', err); + } else if (len) { + this.push(chunk); + } else { + this.push(null); + } + }, this.options); + } +} -PDFStream.prototype._read = function _read() { - // For now we're not controlling the c++ code's data emission, so we only - // call canvas.streamPDFSync once and let it emit data at will. - this._read = noop; - var self = this; - self.canvas.streamPDFSync(function(err, chunk, len){ - if (err) { - self.emit('error', err); - } else if (len) { - self.push(chunk); - } else { - self.push(null); - } - }, this.options); -}; +module.exports = PDFStream; diff --git a/lib/pngstream.js b/lib/pngstream.js index 021bb7fd9..6310f545c 100644 --- a/lib/pngstream.js +++ b/lib/pngstream.js @@ -6,41 +6,37 @@ * MIT Licensed */ -var Readable = require('stream').Readable; -var util = require('util'); - -var PNGStream = module.exports = function PNGStream(canvas, options) { - if (!(this instanceof PNGStream)) { - throw new TypeError("Class constructors cannot be invoked without 'new'"); - } +const { Readable } = require('stream'); +function noop() {} - Readable.call(this); +class PNGStream extends Readable { + constructor(canvas, options) { + super(); - if (options && - options.palette instanceof Uint8ClampedArray && - options.palette.length % 4 !== 0) { - throw new Error("Palette length must be a multiple of 4."); + if (options && + options.palette instanceof Uint8ClampedArray && + options.palette.length % 4 !== 0) { + throw new Error("Palette length must be a multiple of 4."); + } + this.canvas = canvas; + this.options = options || {}; } - this.canvas = canvas; - this.options = options || {}; -}; -util.inherits(PNGStream, Readable); - -function noop() {} + _read() { + // For now we're not controlling the c++ code's data emission, so we only + // call canvas.streamPNGSync once and let it emit data at will. + this._read = noop; + + this.canvas.streamPNGSync((err, chunk, len) => { + if (err) { + this.emit('error', err); + } else if (len) { + this.push(chunk); + } else { + this.push(null); + } + }, this.options); + } +} -PNGStream.prototype._read = function _read() { - // For now we're not controlling the c++ code's data emission, so we only - // call canvas.streamPNGSync once and let it emit data at will. - this._read = noop; - var self = this; - self.canvas.streamPNGSync(function(err, chunk, len){ - if (err) { - self.emit('error', err); - } else if (len) { - self.push(chunk); - } else { - self.push(null); - } - }, self.options); -}; +module.exports = PNGStream From 604db2770179b7420a82421159773dc50829b671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Mon, 2 Aug 2021 11:32:12 +0200 Subject: [PATCH 344/474] Run standard --fix --- benchmarks/run.js | 30 +- examples/backends.js | 14 +- examples/clock.js | 20 +- examples/crop.js | 20 +- examples/fill-evenodd.js | 10 +- examples/font.js | 10 +- examples/globalAlpha.js | 12 +- examples/gradients.js | 14 +- examples/image-src-svg.js | 10 +- examples/indexed-png-alpha.js | 20 +- examples/indexed-png-image-data.js | 24 +- examples/kraken.js | 40 +- examples/live-clock.js | 10 +- examples/multi-page-pdf.js | 10 +- examples/pango-glyphs.js | 10 +- examples/ray.js | 32 +- examples/resize.js | 22 +- examples/small-pdf.js | 12 +- examples/small-svg.js | 12 +- examples/spark.js | 26 +- examples/state.js | 10 +- examples/text.js | 12 +- examples/voronoi.js | 72 +- index.js | 3 +- lib/DOMMatrix.js | 332 ++-- lib/bindings.js | 4 +- lib/canvas.js | 80 +- lib/image.js | 46 +- lib/jpegstream.js | 14 +- lib/parse-font.js | 12 +- lib/pdfstream.js | 28 +- lib/pngstream.js | 28 +- test/canvas.test.js | 2576 ++++++++++++++-------------- test/dommatrix.test.js | 140 +- test/image.test.js | 342 ++-- test/imageData.test.js | 16 +- test/public/app.js | 30 +- test/public/tests.js | 408 ++--- test/server.js | 20 +- util/has_lib.js | 14 +- util/win_jpeg_lookup.js | 10 +- 41 files changed, 2276 insertions(+), 2279 deletions(-) diff --git a/benchmarks/run.js b/benchmarks/run.js index 2f731894f..a6954da87 100644 --- a/benchmarks/run.js +++ b/benchmarks/run.js @@ -4,16 +4,16 @@ * milliseconds to complete. */ -var createCanvas = require('../').createCanvas -var canvas = createCanvas(200, 200) -var largeCanvas = createCanvas(1000, 1000) -var ctx = canvas.getContext('2d') +const { createCanvas } = require('../') +const canvas = createCanvas(200, 200) +const largeCanvas = createCanvas(1000, 1000) +const ctx = canvas.getContext('2d') -var initialTimes = 10 -var minDurationMs = 2000 +const initialTimes = 10 +const minDurationMs = 2000 -var queue = [] -var running = false +const queue = [] +let running = false function bm (label, fn) { queue.push({ label: label, fn: fn }) @@ -28,11 +28,11 @@ function next () { function run (benchmark, n, start) { running = true - var originalN = n - var fn = benchmark.fn + const originalN = n + const fn = benchmark.fn if (fn.length) { // async - var pending = n + let pending = n while (n--) { fn(function () { @@ -46,12 +46,12 @@ function run (benchmark, n, start) { } function done (benchmark, times, start, isAsync) { - var duration = Date.now() - start + const duration = Date.now() - start if (duration < minDurationMs) { run(benchmark, times * 2, Date.now()) } else { - var opsSec = times / duration * 1000 + const opsSec = times / duration * 1000 if (isAsync) { console.log(' - \x1b[33m%s\x1b[0m %s ops/sec (%s times, async)', benchmark.label, opsSec.toLocaleString(), times) } else { @@ -97,7 +97,7 @@ bm('strokeRect()', function () { }) bm('linear gradients', function () { - var lingrad = ctx.createLinearGradient(0, 50, 0, 95) + const lingrad = ctx.createLinearGradient(0, 50, 0, 95) lingrad.addColorStop(0.5, '#000') lingrad.addColorStop(1, 'rgba(0,0,0,0)') ctx.fillStyle = lingrad @@ -157,7 +157,7 @@ bm('getImageData(0,0,100,100)', function () { }) bm('PNGStream 200x200', function (done) { - var stream = canvas.createPNGStream() + const stream = canvas.createPNGStream() stream.on('data', function (chunk) { // whatever }) diff --git a/examples/backends.js b/examples/backends.js index eacc39431..dee41efd5 100644 --- a/examples/backends.js +++ b/examples/backends.js @@ -1,18 +1,18 @@ -var fs = require('fs') -var resolve = require('path').resolve +const fs = require('fs') +const { resolve } = require('path') -var Canvas = require('..') +const Canvas = require('..') -var imagebackend = new Canvas.backends.ImageBackend(800, 600) +const imagebackend = new Canvas.backends.ImageBackend(800, 600) -var canvas = new Canvas.Canvas(imagebackend) -var ctx = canvas.getContext('2d') +const canvas = new Canvas.Canvas(imagebackend) +const ctx = canvas.getContext('2d') console.log('Width: ' + canvas.width + ', Height: ' + canvas.height) ctx.fillStyle = '#00FF00' ctx.fillRect(50, 50, 100, 100) -var outPath = resolve(__dirname, 'rectangle.png') +const outPath = resolve(__dirname, 'rectangle.png') canvas.createPNGStream().pipe(fs.createWriteStream(outPath)) diff --git a/examples/clock.js b/examples/clock.js index abe3915f2..240ed9d24 100644 --- a/examples/clock.js +++ b/examples/clock.js @@ -1,6 +1,6 @@ -var fs = require('fs') -var path = require('path') -var Canvas = require('..') +const fs = require('fs') +const path = require('path') +const Canvas = require('..') function getX (angle) { return -Math.sin(angle + Math.PI) @@ -11,8 +11,8 @@ function getY (angle) { } function clock (ctx) { - var x, y, i - var now = new Date() + let x, y, i + const now = new Date() ctx.clearRect(0, 0, 320, 320) @@ -53,9 +53,9 @@ function clock (ctx) { } } - var sec = now.getSeconds() - var min = now.getMinutes() - var hr = now.getHours() % 12 + const sec = now.getSeconds() + const min = now.getMinutes() + const hr = now.getHours() % 12 ctx.fillStyle = 'black' @@ -104,8 +104,8 @@ function clock (ctx) { module.exports = clock if (require.main === module) { - var canvas = Canvas.createCanvas(320, 320) - var ctx = canvas.getContext('2d') + const canvas = Canvas.createCanvas(320, 320) + const ctx = canvas.getContext('2d') clock(ctx) diff --git a/examples/crop.js b/examples/crop.js index d58e3b08f..c1ade470a 100644 --- a/examples/crop.js +++ b/examples/crop.js @@ -1,23 +1,23 @@ -var fs = require('fs') -var path = require('path') -var Canvas = require('canvas') +const fs = require('fs') +const path = require('path') +const Canvas = require('canvas') -var img = new Canvas.Image() +const img = new Canvas.Image() img.onerror = function (err) { throw err } img.onload = function () { - var w = img.width / 2 - var h = img.height / 2 - var canvas = Canvas.createCanvas(w, h) - var ctx = canvas.getContext('2d') + const w = img.width / 2 + const h = img.height / 2 + const canvas = Canvas.createCanvas(w, h) + const ctx = canvas.getContext('2d') ctx.drawImage(img, 0, 0, w, h, 0, 0, w, h) - var out = fs.createWriteStream(path.join(__dirname, 'crop.jpg')) - var stream = canvas.createJPEGStream({ + const out = fs.createWriteStream(path.join(__dirname, 'crop.jpg')) + const stream = canvas.createJPEGStream({ bufsize: 2048, quality: 80 }) diff --git a/examples/fill-evenodd.js b/examples/fill-evenodd.js index 12e0d4ea8..3823ef4e6 100644 --- a/examples/fill-evenodd.js +++ b/examples/fill-evenodd.js @@ -1,9 +1,9 @@ -var fs = require('fs') -var path = require('path') -var Canvas = require('..') +const fs = require('fs') +const path = require('path') +const Canvas = require('..') -var canvas = Canvas.createCanvas(100, 100) -var ctx = canvas.getContext('2d') +const canvas = Canvas.createCanvas(100, 100) +const ctx = canvas.getContext('2d') ctx.fillStyle = '#f00' ctx.rect(0, 0, 100, 50) diff --git a/examples/font.js b/examples/font.js index 2a370f45e..d2a37b825 100644 --- a/examples/font.js +++ b/examples/font.js @@ -1,6 +1,6 @@ -var fs = require('fs') -var path = require('path') -var Canvas = require('..') +const fs = require('fs') +const path = require('path') +const Canvas = require('..') function fontFile (name) { return path.join(__dirname, '/pfennigFont/', name) @@ -15,8 +15,8 @@ Canvas.registerFont(fontFile('PfennigBold.ttf'), { family: 'pfennigFont', weight Canvas.registerFont(fontFile('PfennigItalic.ttf'), { family: 'pfennigFont', style: 'italic' }) Canvas.registerFont(fontFile('PfennigBoldItalic.ttf'), { family: 'pfennigFont', weight: 'bold', style: 'italic' }) -var canvas = Canvas.createCanvas(320, 320) -var ctx = canvas.getContext('2d') +const canvas = Canvas.createCanvas(320, 320) +const ctx = canvas.getContext('2d') ctx.font = 'normal normal 50px Helvetica' diff --git a/examples/globalAlpha.js b/examples/globalAlpha.js index 523af3b3d..e618c0b10 100644 --- a/examples/globalAlpha.js +++ b/examples/globalAlpha.js @@ -1,9 +1,9 @@ -var fs = require('fs') -var path = require('path') -var Canvas = require('..') +const fs = require('fs') +const path = require('path') +const Canvas = require('..') -var canvas = Canvas.createCanvas(150, 150) -var ctx = canvas.getContext('2d') +const canvas = Canvas.createCanvas(150, 150) +const ctx = canvas.getContext('2d') ctx.fillStyle = '#FD0' ctx.fillRect(0, 0, 75, 75) @@ -23,7 +23,7 @@ ctx.fillStyle = '#FFF' ctx.globalAlpha = 0.2 // Draw semi transparent circles -for (var i = 0; i < 7; i++) { +for (let i = 0; i < 7; i++) { ctx.beginPath() ctx.arc(75, 75, 10 + 10 * i, 0, Math.PI * 2, true) ctx.fill() diff --git a/examples/gradients.js b/examples/gradients.js index 5cdfddfb2..f504258c2 100644 --- a/examples/gradients.js +++ b/examples/gradients.js @@ -1,18 +1,18 @@ -var fs = require('fs') -var path = require('path') -var Canvas = require('..') +const fs = require('fs') +const path = require('path') +const Canvas = require('..') -var canvas = Canvas.createCanvas(320, 320) -var ctx = canvas.getContext('2d') +const canvas = Canvas.createCanvas(320, 320) +const ctx = canvas.getContext('2d') // Create gradients -var lingrad = ctx.createLinearGradient(0, 0, 0, 150) +const lingrad = ctx.createLinearGradient(0, 0, 0, 150) lingrad.addColorStop(0, '#00ABEB') lingrad.addColorStop(0.5, '#fff') lingrad.addColorStop(0.5, '#26C000') lingrad.addColorStop(1, '#fff') -var lingrad2 = ctx.createLinearGradient(0, 50, 0, 95) +const lingrad2 = ctx.createLinearGradient(0, 50, 0, 95) lingrad2.addColorStop(0.5, '#000') lingrad2.addColorStop(1, 'rgba(0,0,0,0)') diff --git a/examples/image-src-svg.js b/examples/image-src-svg.js index 6a6567651..88a5365ed 100644 --- a/examples/image-src-svg.js +++ b/examples/image-src-svg.js @@ -1,9 +1,9 @@ -var fs = require('fs') -var path = require('path') -var Canvas = require('..') +const fs = require('fs') +const path = require('path') +const Canvas = require('..') -var canvas = Canvas.createCanvas(500, 500) -var ctx = canvas.getContext('2d') +const canvas = Canvas.createCanvas(500, 500) +const ctx = canvas.getContext('2d') ctx.fillStyle = 'white' ctx.fillRect(0, 0, 500, 500) diff --git a/examples/indexed-png-alpha.js b/examples/indexed-png-alpha.js index 7303aa845..79a8a5d41 100644 --- a/examples/indexed-png-alpha.js +++ b/examples/indexed-png-alpha.js @@ -1,13 +1,13 @@ -var Canvas = require('..') -var fs = require('fs') -var path = require('path') -var canvas = Canvas.createCanvas(200, 200) -var ctx = canvas.getContext('2d', { pixelFormat: 'A8' }) +const Canvas = require('..') +const fs = require('fs') +const path = require('path') +const canvas = Canvas.createCanvas(200, 200) +const ctx = canvas.getContext('2d', { pixelFormat: 'A8' }) // Matches the "fillStyle" browser test, made by using alpha fillStyle value -var palette = new Uint8ClampedArray(37 * 4) -var i, j -var k = 0 +const palette = new Uint8ClampedArray(37 * 4) +let i, j +let k = 0 // First value is opaque white: palette[k++] = 255 palette[k++] = 255 @@ -23,8 +23,8 @@ for (i = 0; i < 6; i++) { } for (i = 0; i < 6; i++) { for (j = 0; j < 6; j++) { - var index = i * 6 + j + 1.5 // 0.5 to bias rounding - var fraction = index / 255 + const index = i * 6 + j + 1.5 // 0.5 to bias rounding + const fraction = index / 255 ctx.fillStyle = 'rgba(0,0,0,' + fraction + ')' ctx.fillRect(j * 25, i * 25, 25, 25) } diff --git a/examples/indexed-png-image-data.js b/examples/indexed-png-image-data.js index 12c25c2de..253835f0e 100644 --- a/examples/indexed-png-image-data.js +++ b/examples/indexed-png-image-data.js @@ -1,13 +1,13 @@ -var Canvas = require('..') -var fs = require('fs') -var path = require('path') -var canvas = Canvas.createCanvas(200, 200) -var ctx = canvas.getContext('2d', { pixelFormat: 'A8' }) +const Canvas = require('..') +const fs = require('fs') +const path = require('path') +const canvas = Canvas.createCanvas(200, 200) +const ctx = canvas.getContext('2d', { pixelFormat: 'A8' }) // Matches the "fillStyle" browser test, made by manipulating imageData -var palette = new Uint8ClampedArray(37 * 4) -var k = 0 -var i, j +const palette = new Uint8ClampedArray(37 * 4) +let k = 0 +let i, j // First value is opaque white: palette[k++] = 255 palette[k++] = 255 @@ -21,13 +21,13 @@ for (i = 0; i < 6; i++) { palette[k++] = 255 } } -var idata = ctx.getImageData(0, 0, 200, 200) +const idata = ctx.getImageData(0, 0, 200, 200) for (i = 0; i < 6; i++) { for (j = 0; j < 6; j++) { - var index = j * 6 + i + const index = j * 6 + i // fill rect: - for (var xr = j * 25; xr < j * 25 + 25; xr++) { - for (var yr = i * 25; yr < i * 25 + 25; yr++) { + for (let xr = j * 25; xr < j * 25 + 25; xr++) { + for (let yr = i * 25; yr < i * 25 + 25; yr++) { idata.data[xr * 200 + yr] = index + 1 } } diff --git a/examples/kraken.js b/examples/kraken.js index c49ef8836..ed2f25e8c 100644 --- a/examples/kraken.js +++ b/examples/kraken.js @@ -1,12 +1,12 @@ -var fs = require('fs') -var path = require('path') -var Canvas = require('..') +const fs = require('fs') +const path = require('path') +const Canvas = require('..') -var Image = Canvas.Image -var canvas = Canvas.createCanvas(400, 267) -var ctx = canvas.getContext('2d') +const Image = Canvas.Image +const canvas = Canvas.createCanvas(400, 267) +const ctx = canvas.getContext('2d') -var img = new Image() +const img = new Image() img.onload = function () { ctx.drawImage(img, 0, 0) @@ -16,13 +16,13 @@ img.onerror = err => { throw err } img.src = path.join(__dirname, 'images', 'squid.png') -var sigma = 10 // radius -var kernel, kernelSize, kernelSum +const sigma = 10 // radius +let kernel, kernelSize, kernelSum function buildKernel () { - var i, j, g - var ss = sigma * sigma - var factor = 2 * Math.PI * ss + let i, j, g + const ss = sigma * sigma + const factor = 2 * Math.PI * ss kernel = [[]] @@ -52,17 +52,17 @@ function buildKernel () { } function blurTest () { - var x, y, i, j - var r, g, b, a + let x, y, i, j + let r, g, b, a console.log('... running') - var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height) - var data = imgData.data - var width = imgData.width - var height = imgData.height + const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height) + const data = imgData.data + const width = imgData.width + const height = imgData.height - var startTime = (new Date()).getTime() + const startTime = (new Date()).getTime() for (y = 0; y < height; ++y) { for (x = 0; x < width; ++x) { @@ -91,7 +91,7 @@ function blurTest () { } } - var finishTime = Date.now() - startTime + const finishTime = Date.now() - startTime for (i = 0; i < data.length; i++) { imgData.data[i] = data[i] } diff --git a/examples/live-clock.js b/examples/live-clock.js index 9d3a643f3..365680b84 100644 --- a/examples/live-clock.js +++ b/examples/live-clock.js @@ -1,10 +1,10 @@ -var http = require('http') -var Canvas = require('..') +const http = require('http') +const Canvas = require('..') -var clock = require('./clock') +const clock = require('./clock') -var canvas = Canvas.createCanvas(320, 320) -var ctx = canvas.getContext('2d') +const canvas = Canvas.createCanvas(320, 320) +const ctx = canvas.getContext('2d') http.createServer(function (req, res) { clock(ctx) diff --git a/examples/multi-page-pdf.js b/examples/multi-page-pdf.js index ae33b68e6..46b8237e7 100644 --- a/examples/multi-page-pdf.js +++ b/examples/multi-page-pdf.js @@ -1,10 +1,10 @@ -var fs = require('fs') -var Canvas = require('..') +const fs = require('fs') +const Canvas = require('..') -var canvas = Canvas.createCanvas(500, 500, 'pdf') -var ctx = canvas.getContext('2d') +const canvas = Canvas.createCanvas(500, 500, 'pdf') +const ctx = canvas.getContext('2d') -var x, y +let x, y function reset () { x = 50 diff --git a/examples/pango-glyphs.js b/examples/pango-glyphs.js index 4047715ff..eddbbdb4a 100644 --- a/examples/pango-glyphs.js +++ b/examples/pango-glyphs.js @@ -1,9 +1,9 @@ -var fs = require('fs') -var path = require('path') -var Canvas = require('..') +const fs = require('fs') +const path = require('path') +const Canvas = require('..') -var canvas = Canvas.createCanvas(400, 100) -var ctx = canvas.getContext('2d') +const canvas = Canvas.createCanvas(400, 100) +const ctx = canvas.getContext('2d') ctx.globalAlpha = 1 ctx.font = 'normal 16px Impact' diff --git a/examples/ray.js b/examples/ray.js index 68c85246a..c8a8e5326 100644 --- a/examples/ray.js +++ b/examples/ray.js @@ -1,9 +1,9 @@ -var fs = require('fs') -var path = require('path') -var Canvas = require('..') +const fs = require('fs') +const path = require('path') +const Canvas = require('..') -var canvas = Canvas.createCanvas(243 * 4, 243) -var ctx = canvas.getContext('2d') +const canvas = Canvas.createCanvas(243 * 4, 243) +const ctx = canvas.getContext('2d') function render (level) { ctx.fillStyle = getPointColour(122, 122) @@ -12,7 +12,7 @@ function render (level) { } function renderLevel (minimumLevel, level, y) { - var x + let x for (x = 0; x < 243 / level; ++x) { drawBlock(x, y, level) @@ -51,26 +51,26 @@ function getPointColour (x, y) { x = x / 121.5 - 1 y = -y / 121.5 + 1 - var x2y2 = x * x + y * y + const x2y2 = x * x + y * y if (x2y2 > 1) { return '#000' } - var root = Math.sqrt(1 - x2y2) - var x3d = x * 0.7071067812 + root / 2 - y / 2 - var y3d = x * 0.7071067812 - root / 2 + y / 2 - var z3d = 0.7071067812 * root + 0.7071067812 * y - var brightness = -x / 2 + root * 0.7071067812 + y / 2 + const root = Math.sqrt(1 - x2y2) + const x3d = x * 0.7071067812 + root / 2 - y / 2 + const y3d = x * 0.7071067812 - root / 2 + y / 2 + const z3d = 0.7071067812 * root + 0.7071067812 * y + let brightness = -x / 2 + root * 0.7071067812 + y / 2 if (brightness < 0) brightness = 0 - var r = Math.round(brightness * 127.5 * (1 - y3d)) - var g = Math.round(brightness * 127.5 * (x3d + 1)) - var b = Math.round(brightness * 127.5 * (z3d + 1)) + const r = Math.round(brightness * 127.5 * (1 - y3d)) + const g = Math.round(brightness * 127.5 * (x3d + 1)) + const b = Math.round(brightness * 127.5 * (z3d + 1)) return 'rgb(' + r + ', ' + g + ', ' + b + ')' } -var start = new Date() +const start = new Date() render(10) ctx.translate(243, 0) diff --git a/examples/resize.js b/examples/resize.js index d50aa9231..f13a04451 100644 --- a/examples/resize.js +++ b/examples/resize.js @@ -1,22 +1,22 @@ -var fs = require('fs') -var path = require('path') -var Canvas = require('..') +const fs = require('fs') +const path = require('path') +const Canvas = require('..') -var Image = Canvas.Image +const Image = Canvas.Image -var img = new Image() -var start = new Date() +const img = new Image() +const start = new Date() img.onerror = function (err) { throw err } img.onload = function () { - var width = 100 - var height = 100 - var canvas = Canvas.createCanvas(width, height) - var ctx = canvas.getContext('2d') - var out = fs.createWriteStream(path.join(__dirname, 'resize.png')) + const width = 100 + const height = 100 + const canvas = Canvas.createCanvas(width, height) + const ctx = canvas.getContext('2d') + const out = fs.createWriteStream(path.join(__dirname, 'resize.png')) ctx.imageSmoothingEnabled = true ctx.drawImage(img, 0, 0, width, height) diff --git a/examples/small-pdf.js b/examples/small-pdf.js index f7a405309..e01134976 100644 --- a/examples/small-pdf.js +++ b/examples/small-pdf.js @@ -1,11 +1,11 @@ -var fs = require('fs') -var Canvas = require('..') +const fs = require('fs') +const Canvas = require('..') -var canvas = Canvas.createCanvas(400, 200, 'pdf') -var ctx = canvas.getContext('2d') +const canvas = Canvas.createCanvas(400, 200, 'pdf') +const ctx = canvas.getContext('2d') -var y = 80 -var x = 50 +let y = 80 +let x = 50 ctx.font = '22px Helvetica' ctx.fillText('node-canvas pdf', x, y) diff --git a/examples/small-svg.js b/examples/small-svg.js index 099ff6925..97799687e 100644 --- a/examples/small-svg.js +++ b/examples/small-svg.js @@ -1,11 +1,11 @@ -var fs = require('fs') -var Canvas = require('..') +const fs = require('fs') +const Canvas = require('..') -var canvas = Canvas.createCanvas(400, 200, 'svg') -var ctx = canvas.getContext('2d') +const canvas = Canvas.createCanvas(400, 200, 'svg') +const ctx = canvas.getContext('2d') -var y = 80 -var x = 50 +let y = 80 +let x = 50 ctx.font = '22px Helvetica' ctx.fillText('node-canvas SVG', x, y) diff --git a/examples/spark.js b/examples/spark.js index 9e99c9fb6..a369473f4 100644 --- a/examples/spark.js +++ b/examples/spark.js @@ -1,25 +1,25 @@ -var fs = require('fs') -var path = require('path') -var Canvas = require('..') +const fs = require('fs') +const path = require('path') +const Canvas = require('..') -var canvas = Canvas.createCanvas(40, 15) -var ctx = canvas.getContext('2d') +const canvas = Canvas.createCanvas(40, 15) +const ctx = canvas.getContext('2d') function spark (ctx, data) { - var len = data.length - var pad = 1 - var width = ctx.canvas.width - var height = ctx.canvas.height - var barWidth = width / len - var max = Math.max.apply(null, data) + const len = data.length + const pad = 1 + const width = ctx.canvas.width + const height = ctx.canvas.height + const barWidth = width / len + const max = Math.max.apply(null, data) ctx.fillStyle = 'rgba(0,0,255,0.5)' ctx.strokeStyle = 'red' ctx.lineWidth = 1 data.forEach(function (n, i) { - var x = i * barWidth + pad - var y = height * (n / max) + const x = i * barWidth + pad + const y = height * (n / max) ctx.lineTo(x, height - y) ctx.fillRect(x, height, barWidth - pad, -y) diff --git a/examples/state.js b/examples/state.js index 3349a1a8b..ce8eeb6d8 100644 --- a/examples/state.js +++ b/examples/state.js @@ -1,9 +1,9 @@ -var fs = require('fs') -var path = require('path') -var Canvas = require('..') +const fs = require('fs') +const path = require('path') +const Canvas = require('..') -var canvas = Canvas.createCanvas(150, 150) -var ctx = canvas.getContext('2d') +const canvas = Canvas.createCanvas(150, 150) +const ctx = canvas.getContext('2d') ctx.fillRect(0, 0, 150, 150) // Draw a rectangle with default settings ctx.save() // Save the default state diff --git a/examples/text.js b/examples/text.js index 39ddc544a..04fbcdb8b 100644 --- a/examples/text.js +++ b/examples/text.js @@ -1,9 +1,9 @@ -var fs = require('fs') -var path = require('path') -var Canvas = require('..') +const fs = require('fs') +const path = require('path') +const Canvas = require('..') -var canvas = Canvas.createCanvas(200, 200) -var ctx = canvas.getContext('2d') +const canvas = Canvas.createCanvas(200, 200) +const ctx = canvas.getContext('2d') ctx.globalAlpha = 0.2 @@ -30,7 +30,7 @@ ctx.strokeText('Wahoo', 50, 100) ctx.fillStyle = '#000' ctx.fillText('Wahoo', 49, 99) -var m = ctx.measureText('Wahoo') +const m = ctx.measureText('Wahoo') ctx.strokeStyle = '#f00' diff --git a/examples/voronoi.js b/examples/voronoi.js index 963c9922a..07eecdfbc 100644 --- a/examples/voronoi.js +++ b/examples/voronoi.js @@ -1,25 +1,25 @@ -var http = require('http') -var Canvas = require('..') +const http = require('http') +const Canvas = require('..') -var canvas = Canvas.createCanvas(1920, 1200) -var ctx = canvas.getContext('2d') +const canvas = Canvas.createCanvas(1920, 1200) +const ctx = canvas.getContext('2d') -var voronoiFactory = require('./rhill-voronoi-core-min') +const voronoiFactory = require('./rhill-voronoi-core-min') http.createServer(function (req, res) { - var x, y, v, iHalfedge + let x, y, v, iHalfedge - var voronoi = voronoiFactory() - var start = new Date() - var bbox = { xl: 0, xr: canvas.width, yt: 0, yb: canvas.height } + const voronoi = voronoiFactory() + const start = new Date() + const bbox = { xl: 0, xr: canvas.width, yt: 0, yb: canvas.height } - for (var i = 0; i < 340; i++) { + for (let i = 0; i < 340; i++) { x = Math.random() * canvas.width y = Math.random() * canvas.height voronoi.addSites([{ x: x, y: y }]) } - var diagram = voronoi.compute(bbox) + const diagram = voronoi.compute(bbox) ctx.beginPath() ctx.rect(0, 0, canvas.width, canvas.height) @@ -31,24 +31,24 @@ http.createServer(function (req, res) { ctx.strokeStyle = 'rgba(255,255,255,0.5)' ctx.lineWidth = 4 // edges - var edges = diagram.edges - var nEdges = edges.length + const edges = diagram.edges + const nEdges = edges.length - var sites = diagram.sites - var nSites = sites.length - for (var iSite = nSites - 1; iSite >= 0; iSite -= 1) { - var site = sites[iSite] + const sites = diagram.sites + const nSites = sites.length + for (let iSite = nSites - 1; iSite >= 0; iSite -= 1) { + const site = sites[iSite] ctx.rect(site.x - 0.5, site.y - 0.5, 1, 1) - var cell = diagram.cells[diagram.sites[iSite].id] + const cell = diagram.cells[diagram.sites[iSite].id] if (cell !== undefined) { - var halfedges = cell.halfedges - var nHalfedges = halfedges.length + const halfedges = cell.halfedges + const nHalfedges = halfedges.length if (nHalfedges < 3) return - var minx = canvas.width - var miny = canvas.height - var maxx = 0 - var maxy = 0 + let minx = canvas.width + let miny = canvas.height + let maxx = 0 + let maxy = 0 v = halfedges[0].getStartpoint() ctx.beginPath() @@ -63,35 +63,35 @@ http.createServer(function (req, res) { if (v.y > maxy) maxy = v.y } - var midx = (maxx + minx) / 2 - var midy = (maxy + miny) / 2 - var R = 0 + let midx = (maxx + minx) / 2 + let midy = (maxy + miny) / 2 + let R = 0 for (iHalfedge = 0; iHalfedge < nHalfedges; iHalfedge++) { v = halfedges[iHalfedge].getEndpoint() - var dx = v.x - site.x - var dy = v.y - site.y - var newR = Math.sqrt(dx * dx + dy * dy) + const dx = v.x - site.x + const dy = v.y - site.y + const newR = Math.sqrt(dx * dx + dy * dy) if (newR > R) R = newR } midx = site.x midy = site.y - var radgrad = ctx.createRadialGradient(midx + R * 0.3, midy - R * 0.3, 0, midx, midy, R) + const radgrad = ctx.createRadialGradient(midx + R * 0.3, midy - R * 0.3, 0, midx, midy, R) radgrad.addColorStop(0, '#09760b') radgrad.addColorStop(1.0, 'black') ctx.fillStyle = radgrad ctx.fill() - var radgrad2 = ctx.createRadialGradient(midx - R * 0.5, midy + R * 0.5, R * 0.1, midx, midy, R) + const radgrad2 = ctx.createRadialGradient(midx - R * 0.5, midy + R * 0.5, R * 0.1, midx, midy, R) radgrad2.addColorStop(0, 'rgba(255,255,255,0.5)') radgrad2.addColorStop(0.04, 'rgba(255,255,255,0.3)') radgrad2.addColorStop(0.05, 'rgba(255,255,255,0)') ctx.fillStyle = radgrad2 ctx.fill() - var lingrad = ctx.createLinearGradient(minx, site.y, minx + 100, site.y - 20) + const lingrad = ctx.createLinearGradient(minx, site.y, minx + 100, site.y - 20) lingrad.addColorStop(0.0, 'rgba(255,255,255,0.5)') lingrad.addColorStop(0.2, 'rgba(255,255,255,0.2)') lingrad.addColorStop(1.0, 'rgba(255,255,255,0)') @@ -101,11 +101,11 @@ http.createServer(function (req, res) { } if (nEdges) { - var edge + let edge ctx.beginPath() - for (var iEdge = nEdges - 1; iEdge >= 0; iEdge -= 1) { + for (let iEdge = nEdges - 1; iEdge >= 0; iEdge -= 1) { edge = edges[iEdge] v = edge.va ctx.moveTo(v.x, v.y) @@ -119,7 +119,7 @@ http.createServer(function (req, res) { canvas.toBuffer(function (err, buf) { if (err) throw err - var duration = new Date() - start + const duration = new Date() - start console.log('Rendered in %dms', duration) res.writeHead(200, { diff --git a/index.js b/index.js index 500fba961..4d11d9a81 100644 --- a/index.js +++ b/index.js @@ -9,8 +9,7 @@ const fs = require('fs') const PNGStream = require('./lib/pngstream') const PDFStream = require('./lib/pdfstream') const JPEGStream = require('./lib/jpegstream') -const DOMMatrix = require('./lib/DOMMatrix').DOMMatrix -const DOMPoint = require('./lib/DOMMatrix').DOMPoint +const { DOMPoint, DOMMatrix } = require('./lib/DOMMatrix') function createCanvas (width, height, type) { return new Canvas(width, height, type) diff --git a/lib/DOMMatrix.js b/lib/DOMMatrix.js index 31178b939..479e7e6e5 100644 --- a/lib/DOMMatrix.js +++ b/lib/DOMMatrix.js @@ -20,16 +20,16 @@ class DOMPoint { } // Constants to index into _values (col-major) -const M11 = 0, M12 = 1, M13 = 2, M14 = 3 -const M21 = 4, M22 = 5, M23 = 6, M24 = 7 -const M31 = 8, M32 = 9, M33 = 10, M34 = 11 -const M41 = 12, M42 = 13, M43 = 14, M44 = 15 +const M11 = 0; const M12 = 1; const M13 = 2; const M14 = 3 +const M21 = 4; const M22 = 5; const M23 = 6; const M24 = 7 +const M31 = 8; const M32 = 9; const M33 = 10; const M34 = 11 +const M41 = 12; const M42 = 13; const M43 = 14; const M44 = 15 const DEGREE_PER_RAD = 180 / Math.PI const RAD_PER_DEGREE = Math.PI / 180 -function parseMatrix(init) { - let parsed = init.replace('matrix(', ''); +function parseMatrix (init) { + let parsed = init.replace('matrix(', '') parsed = parsed.split(',', 7) // 6 + 1 to handle too many params if (parsed.length !== 6) throw new Error(`Failed to parse ${init}`) parsed = parsed.map(parseFloat) @@ -41,15 +41,15 @@ function parseMatrix(init) { ] } -function parseMatrix3d(init) { - let parsed = init.replace('matrix3d(', ''); +function parseMatrix3d (init) { + let parsed = init.replace('matrix3d(', '') parsed = parsed.split(',', 17) // 16 + 1 to handle too many params if (parsed.length !== 16) throw new Error(`Failed to parse ${init}`) return parsed.map(parseFloat) } -function parseTransform(tform) { - const type = tform.split('(', 1)[0]; +function parseTransform (tform) { + const type = tform.split('(', 1)[0] switch (type) { case 'matrix': return parseMatrix(tform) @@ -62,7 +62,7 @@ function parseTransform(tform) { } class DOMMatrix { - constructor(init) { + constructor (init) { this._is2D = true this._values = new Float64Array([ 1, 0, 0, 0, @@ -71,11 +71,11 @@ class DOMMatrix { 0, 0, 0, 1 ]) - let i; + let i if (typeof init === 'string') { // parse CSS transformList if (init === '') return // default identity matrix - const tforms = init.split(/\)\s+/, 20).map(parseTransform); + const tforms = init.split(/\)\s+/, 20).map(parseTransform) if (tforms.length === 0) return init = tforms[0] for (i = 1; i < tforms.length; i++) init = multiply(tforms[i], init) @@ -111,33 +111,33 @@ class DOMMatrix { } } - toString() { - return this.is2D ? - `matrix(${this.a}, ${this.b}, ${this.c}, ${this.d}, ${this.e}, ${this.f})` : - `matrix3d(${this._values.join(', ')})` + toString () { + return this.is2D + ? `matrix(${this.a}, ${this.b}, ${this.c}, ${this.d}, ${this.e}, ${this.f})` + : `matrix3d(${this._values.join(', ')})` } - multiply(other) { + multiply (other) { return newInstance(this._values).multiplySelf(other) } - multiplySelf(other) { + multiplySelf (other) { this._values = multiply(other._values, this._values) if (!other.is2D) this._is2D = false return this } - preMultiplySelf(other) { + preMultiplySelf (other) { this._values = multiply(this._values, other._values) if (!other.is2D) this._is2D = false return this } - translate(tx, ty, tz) { + translate (tx, ty, tz) { return newInstance(this._values).translateSelf(tx, ty, tz) } - translateSelf(tx, ty, tz) { + translateSelf (tx, ty, tz) { if (typeof tx !== 'number') tx = 0 if (typeof ty !== 'number') ty = 0 if (typeof tz !== 'number') tz = 0 @@ -151,19 +151,19 @@ class DOMMatrix { return this } - scale(scaleX, scaleY, scaleZ, originX, originY, originZ) { + scale (scaleX, scaleY, scaleZ, originX, originY, originZ) { return newInstance(this._values).scaleSelf(scaleX, scaleY, scaleZ, originX, originY, originZ) } - scale3d(scale, originX, originY, originZ) { + scale3d (scale, originX, originY, originZ) { return newInstance(this._values).scale3dSelf(scale, originX, originY, originZ) } - scale3dSelf(scale, originX, originY, originZ) { + scale3dSelf (scale, originX, originY, originZ) { return this.scaleSelf(scale, scale, scale, originX, originY, originZ) } - scaleSelf(scaleX, scaleY, scaleZ, originX, originY, originZ) { + scaleSelf (scaleX, scaleY, scaleZ, originX, originY, originZ) { // Not redundant with translate's checks because we need to negate the values later. if (typeof originX !== 'number') originX = 0 if (typeof originY !== 'number') originY = 0 @@ -183,22 +183,22 @@ class DOMMatrix { return this } - rotateFromVector(x, y) { + rotateFromVector (x, y) { return newInstance(this._values).rotateFromVectorSelf(x, y) } - rotateFromVectorSelf(x, y) { + rotateFromVectorSelf (x, y) { if (typeof x !== 'number') x = 0 if (typeof y !== 'number') y = 0 - const theta = (x === 0 && y === 0) ? 0 : Math.atan2(y, x) * DEGREE_PER_RAD; + const theta = (x === 0 && y === 0) ? 0 : Math.atan2(y, x) * DEGREE_PER_RAD return this.rotateSelf(theta) } - rotate(rotX, rotY, rotZ) { + rotate (rotX, rotY, rotZ) { return newInstance(this._values).rotateSelf(rotX, rotY, rotZ) } - rotateSelf(rotX, rotY, rotZ) { + rotateSelf (rotX, rotY, rotZ) { if (rotY === undefined && rotZ === undefined) { rotZ = rotX rotX = rotY = 0 @@ -209,7 +209,7 @@ class DOMMatrix { rotX *= RAD_PER_DEGREE rotY *= RAD_PER_DEGREE rotZ *= RAD_PER_DEGREE - let c, s; + let c, s c = Math.cos(rotZ) s = Math.sin(rotZ) this._values = multiply([ @@ -237,16 +237,16 @@ class DOMMatrix { return this } - rotateAxisAngle(x, y, z, angle) { + rotateAxisAngle (x, y, z, angle) { return newInstance(this._values).rotateAxisAngleSelf(x, y, z, angle) } - rotateAxisAngleSelf(x, y, z, angle) { + rotateAxisAngleSelf (x, y, z, angle) { if (typeof x !== 'number') x = 0 if (typeof y !== 'number') y = 0 if (typeof z !== 'number') z = 0 // Normalize axis - const length = Math.sqrt(x * x + y * y + z * z); + const length = Math.sqrt(x * x + y * y + z * z) if (length === 0) return this if (length !== 1) { x /= length @@ -254,30 +254,30 @@ class DOMMatrix { z /= length } angle *= RAD_PER_DEGREE - const c = Math.cos(angle); - const s = Math.sin(angle); - const t = 1 - c; - const tx = t * x; - const ty = t * y; + const c = Math.cos(angle) + const s = Math.sin(angle) + const t = 1 - c + const tx = t * x + const ty = t * y // NB: This is the generic transform. If the axis is a major axis, there are // faster transforms. this._values = multiply([ - tx * x + c, tx * y + s * z, tx * z - s * y, 0, - tx * y - s * z, ty * y + c, ty * z + s * x, 0, - tx * z + s * y, ty * z - s * x, t * z * z + c, 0, - 0, 0, 0, 1 + tx * x + c, tx * y + s * z, tx * z - s * y, 0, + tx * y - s * z, ty * y + c, ty * z + s * x, 0, + tx * z + s * y, ty * z - s * x, t * z * z + c, 0, + 0, 0, 0, 1 ], this._values) if (x !== 0 || y !== 0) this._is2D = false return this } - skewX(sx) { + skewX (sx) { return newInstance(this._values).skewXSelf(sx) } - skewXSelf(sx) { + skewXSelf (sx) { if (typeof sx !== 'number') return this - const t = Math.tan(sx * RAD_PER_DEGREE); + const t = Math.tan(sx * RAD_PER_DEGREE) this._values = multiply([ 1, 0, 0, 0, t, 1, 0, 0, @@ -287,13 +287,13 @@ class DOMMatrix { return this } - skewY(sy) { + skewY (sy) { return newInstance(this._values).skewYSelf(sy) } - skewYSelf(sy) { + skewYSelf (sy) { if (typeof sy !== 'number') return this - const t = Math.tan(sy * RAD_PER_DEGREE); + const t = Math.tan(sy * RAD_PER_DEGREE) this._values = multiply([ 1, t, 0, 0, 0, 1, 0, 0, @@ -303,7 +303,7 @@ class DOMMatrix { return this } - flipX() { + flipX () { return newInstance(multiply([ -1, 0, 0, 0, 0, 1, 0, 0, @@ -312,7 +312,7 @@ class DOMMatrix { ], this._values)) } - flipY() { + flipY () { return newInstance(multiply([ 1, 0, 0, 0, 0, -1, 0, 0, @@ -321,46 +321,46 @@ class DOMMatrix { ], this._values)) } - inverse() { + inverse () { return newInstance(this._values).invertSelf() } - invertSelf() { - const m = this._values; - const inv = m.map(v => 0); - - inv[0] = m[5] * m[10] * m[15] - - m[5] * m[11] * m[14] - - m[9] * m[6] * m[15] + - m[9] * m[7] * m[14] + - m[13] * m[6] * m[11] - - m[13] * m[7] * m[10] - - inv[4] = -m[4] * m[10] * m[15] + - m[4] * m[11] * m[14] + - m[8] * m[6] * m[15] - - m[8] * m[7] * m[14] - - m[12] * m[6] * m[11] + - m[12] * m[7] * m[10] - - inv[8] = m[4] * m[9] * m[15] - - m[4] * m[11] * m[13] - - m[8] * m[5] * m[15] + - m[8] * m[7] * m[13] + + invertSelf () { + const m = this._values + const inv = m.map(v => 0) + + inv[0] = m[5] * m[10] * m[15] - + m[5] * m[11] * m[14] - + m[9] * m[6] * m[15] + + m[9] * m[7] * m[14] + + m[13] * m[6] * m[11] - + m[13] * m[7] * m[10] + + inv[4] = -m[4] * m[10] * m[15] + + m[4] * m[11] * m[14] + + m[8] * m[6] * m[15] - + m[8] * m[7] * m[14] - + m[12] * m[6] * m[11] + + m[12] * m[7] * m[10] + + inv[8] = m[4] * m[9] * m[15] - + m[4] * m[11] * m[13] - + m[8] * m[5] * m[15] + + m[8] * m[7] * m[13] + m[12] * m[5] * m[11] - m[12] * m[7] * m[9] - inv[12] = -m[4] * m[9] * m[14] + - m[4] * m[10] * m[13] + - m[8] * m[5] * m[14] - - m[8] * m[6] * m[13] - + inv[12] = -m[4] * m[9] * m[14] + + m[4] * m[10] * m[13] + + m[8] * m[5] * m[14] - + m[8] * m[6] * m[13] - m[12] * m[5] * m[10] + m[12] * m[6] * m[9] // If the determinant is zero, this matrix cannot be inverted, and all // values should be set to NaN, with the is2D flag set to false. - const det = m[0] * inv[0] + m[1] * inv[4] + m[2] * inv[8] + m[3] * inv[12]; + const det = m[0] * inv[0] + m[1] * inv[4] + m[2] * inv[8] + m[3] * inv[12] if (det === 0) { this._values = m.map(v => NaN) @@ -368,59 +368,59 @@ class DOMMatrix { return this } - inv[1] = -m[1] * m[10] * m[15] + - m[1] * m[11] * m[14] + - m[9] * m[2] * m[15] - - m[9] * m[3] * m[14] - + inv[1] = -m[1] * m[10] * m[15] + + m[1] * m[11] * m[14] + + m[9] * m[2] * m[15] - + m[9] * m[3] * m[14] - m[13] * m[2] * m[11] + m[13] * m[3] * m[10] - inv[5] = m[0] * m[10] * m[15] - - m[0] * m[11] * m[14] - - m[8] * m[2] * m[15] + - m[8] * m[3] * m[14] + + inv[5] = m[0] * m[10] * m[15] - + m[0] * m[11] * m[14] - + m[8] * m[2] * m[15] + + m[8] * m[3] * m[14] + m[12] * m[2] * m[11] - m[12] * m[3] * m[10] - inv[9] = -m[0] * m[9] * m[15] + - m[0] * m[11] * m[13] + - m[8] * m[1] * m[15] - - m[8] * m[3] * m[13] - + inv[9] = -m[0] * m[9] * m[15] + + m[0] * m[11] * m[13] + + m[8] * m[1] * m[15] - + m[8] * m[3] * m[13] - m[12] * m[1] * m[11] + m[12] * m[3] * m[9] - inv[13] = m[0] * m[9] * m[14] - - m[0] * m[10] * m[13] - - m[8] * m[1] * m[14] + - m[8] * m[2] * m[13] + + inv[13] = m[0] * m[9] * m[14] - + m[0] * m[10] * m[13] - + m[8] * m[1] * m[14] + + m[8] * m[2] * m[13] + m[12] * m[1] * m[10] - m[12] * m[2] * m[9] - inv[2] = m[1] * m[6] * m[15] - - m[1] * m[7] * m[14] - - m[5] * m[2] * m[15] + - m[5] * m[3] * m[14] + + inv[2] = m[1] * m[6] * m[15] - + m[1] * m[7] * m[14] - + m[5] * m[2] * m[15] + + m[5] * m[3] * m[14] + m[13] * m[2] * m[7] - m[13] * m[3] * m[6] - inv[6] = -m[0] * m[6] * m[15] + - m[0] * m[7] * m[14] + - m[4] * m[2] * m[15] - - m[4] * m[3] * m[14] - + inv[6] = -m[0] * m[6] * m[15] + + m[0] * m[7] * m[14] + + m[4] * m[2] * m[15] - + m[4] * m[3] * m[14] - m[12] * m[2] * m[7] + m[12] * m[3] * m[6] - inv[10] = m[0] * m[5] * m[15] - - m[0] * m[7] * m[13] - - m[4] * m[1] * m[15] + - m[4] * m[3] * m[13] + + inv[10] = m[0] * m[5] * m[15] - + m[0] * m[7] * m[13] - + m[4] * m[1] * m[15] + + m[4] * m[3] * m[13] + m[12] * m[1] * m[7] - m[12] * m[3] * m[5] - inv[14] = -m[0] * m[5] * m[14] + - m[0] * m[6] * m[13] + - m[4] * m[1] * m[14] - - m[4] * m[2] * m[13] - + inv[14] = -m[0] * m[5] * m[14] + + m[0] * m[6] * m[13] + + m[4] * m[1] * m[14] - + m[4] * m[2] * m[13] - m[12] * m[1] * m[6] + m[12] * m[2] * m[5] @@ -452,37 +452,37 @@ class DOMMatrix { m[8] * m[1] * m[6] - m[8] * m[2] * m[5] - inv.forEach((v,i) => inv[i] = v/det) + inv.forEach((v, i) => { inv[i] = v / det }) this._values = inv return this } - setMatrixValue(transformList) { - const temp = new DOMMatrix(transformList); + setMatrixValue (transformList) { + const temp = new DOMMatrix(transformList) this._values = temp._values this._is2D = temp._is2D return this } - transformPoint(point) { + transformPoint (point) { point = new DOMPoint(point) - const x = point.x; - const y = point.y; - const z = point.z; - const w = point.w; - const values = this._values; - const nx = values[M11] * x + values[M21] * y + values[M31] * z + values[M41] * w; - const ny = values[M12] * x + values[M22] * y + values[M32] * z + values[M42] * w; - const nz = values[M13] * x + values[M23] * y + values[M33] * z + values[M43] * w; - const nw = values[M14] * x + values[M24] * y + values[M34] * z + values[M44] * w; + const x = point.x + const y = point.y + const z = point.z + const w = point.w + const values = this._values + const nx = values[M11] * x + values[M21] * y + values[M31] * z + values[M41] * w + const ny = values[M12] * x + values[M22] * y + values[M32] * z + values[M42] * w + const nz = values[M13] * x + values[M23] * y + values[M33] * z + values[M43] * w + const nw = values[M14] * x + values[M24] * y + values[M34] * z + values[M44] * w return new DOMPoint(nx, ny, nz, nw) } - toFloat32Array() { + toFloat32Array () { return Float32Array.from(this._values) } - toFloat64Array() { + toFloat64Array () { return this._values.slice(0) } @@ -535,57 +535,57 @@ class DOMMatrix { /** * Checks that `value` is a number and sets the value. */ -function setNumber2D(receiver, index, value) { +function setNumber2D (receiver, index, value) { if (typeof value !== 'number') throw new TypeError('Expected number') - return receiver._values[index] = value + return (receiver._values[index] = value) } /** * Checks that `value` is a number, sets `_is2D = false` if necessary and sets * the value. */ -function setNumber3D(receiver, index, value) { +function setNumber3D (receiver, index, value) { if (typeof value !== 'number') throw new TypeError('Expected number') if (index === M33 || index === M44) { if (value !== 1) receiver._is2D = false } else if (value !== 0) receiver._is2D = false - return receiver._values[index] = value + return (receiver._values[index] = value) } Object.defineProperties(DOMMatrix.prototype, { - m11: {get() { return this._values[M11] }, set(v) { return setNumber2D(this, M11, v) }}, - m12: {get() { return this._values[M12] }, set(v) { return setNumber2D(this, M12, v) }}, - m13: {get() { return this._values[M13] }, set(v) { return setNumber3D(this, M13, v) }}, - m14: {get() { return this._values[M14] }, set(v) { return setNumber3D(this, M14, v) }}, - m21: {get() { return this._values[M21] }, set(v) { return setNumber2D(this, M21, v) }}, - m22: {get() { return this._values[M22] }, set(v) { return setNumber2D(this, M22, v) }}, - m23: {get() { return this._values[M23] }, set(v) { return setNumber3D(this, M23, v) }}, - m24: {get() { return this._values[M24] }, set(v) { return setNumber3D(this, M24, v) }}, - m31: {get() { return this._values[M31] }, set(v) { return setNumber3D(this, M31, v) }}, - m32: {get() { return this._values[M32] }, set(v) { return setNumber3D(this, M32, v) }}, - m33: {get() { return this._values[M33] }, set(v) { return setNumber3D(this, M33, v) }}, - m34: {get() { return this._values[M34] }, set(v) { return setNumber3D(this, M34, v) }}, - m41: {get() { return this._values[M41] }, set(v) { return setNumber2D(this, M41, v) }}, - m42: {get() { return this._values[M42] }, set(v) { return setNumber2D(this, M42, v) }}, - m43: {get() { return this._values[M43] }, set(v) { return setNumber3D(this, M43, v) }}, - m44: {get() { return this._values[M44] }, set(v) { return setNumber3D(this, M44, v) }}, - - a: {get() { return this.m11 }, set(v) { return this.m11 = v }}, - b: {get() { return this.m12 }, set(v) { return this.m12 = v }}, - c: {get() { return this.m21 }, set(v) { return this.m21 = v }}, - d: {get() { return this.m22 }, set(v) { return this.m22 = v }}, - e: {get() { return this.m41 }, set(v) { return this.m41 = v }}, - f: {get() { return this.m42 }, set(v) { return this.m42 = v }}, - - is2D: {get() { return this._is2D }}, // read-only + m11: { get () { return this._values[M11] }, set (v) { return setNumber2D(this, M11, v) } }, + m12: { get () { return this._values[M12] }, set (v) { return setNumber2D(this, M12, v) } }, + m13: { get () { return this._values[M13] }, set (v) { return setNumber3D(this, M13, v) } }, + m14: { get () { return this._values[M14] }, set (v) { return setNumber3D(this, M14, v) } }, + m21: { get () { return this._values[M21] }, set (v) { return setNumber2D(this, M21, v) } }, + m22: { get () { return this._values[M22] }, set (v) { return setNumber2D(this, M22, v) } }, + m23: { get () { return this._values[M23] }, set (v) { return setNumber3D(this, M23, v) } }, + m24: { get () { return this._values[M24] }, set (v) { return setNumber3D(this, M24, v) } }, + m31: { get () { return this._values[M31] }, set (v) { return setNumber3D(this, M31, v) } }, + m32: { get () { return this._values[M32] }, set (v) { return setNumber3D(this, M32, v) } }, + m33: { get () { return this._values[M33] }, set (v) { return setNumber3D(this, M33, v) } }, + m34: { get () { return this._values[M34] }, set (v) { return setNumber3D(this, M34, v) } }, + m41: { get () { return this._values[M41] }, set (v) { return setNumber2D(this, M41, v) } }, + m42: { get () { return this._values[M42] }, set (v) { return setNumber2D(this, M42, v) } }, + m43: { get () { return this._values[M43] }, set (v) { return setNumber3D(this, M43, v) } }, + m44: { get () { return this._values[M44] }, set (v) { return setNumber3D(this, M44, v) } }, + + a: { get () { return this.m11 }, set (v) { return (this.m11 = v) } }, + b: { get () { return this.m12 }, set (v) { return (this.m12 = v) } }, + c: { get () { return this.m21 }, set (v) { return (this.m21 = v) } }, + d: { get () { return this.m22 }, set (v) { return (this.m22 = v) } }, + e: { get () { return this.m41 }, set (v) { return (this.m41 = v) } }, + f: { get () { return this.m42 }, set (v) { return (this.m42 = v) } }, + + is2D: { get () { return this._is2D } }, // read-only isIdentity: { - get() { - const values = this._values; - return values[M11] === 1 && values[M12] === 0 && values[M13] === 0 && values[M14] === 0 && + get () { + const values = this._values + return (values[M11] === 1 && values[M12] === 0 && values[M13] === 0 && values[M14] === 0 && values[M21] === 0 && values[M22] === 1 && values[M23] === 0 && values[M24] === 0 && values[M31] === 0 && values[M32] === 0 && values[M33] === 1 && values[M34] === 0 && - values[M41] === 0 && values[M42] === 0 && values[M43] === 0 && values[M44] === 1 + values[M41] === 0 && values[M42] === 0 && values[M43] === 0 && values[M44] === 1) } } }) @@ -595,19 +595,19 @@ Object.defineProperties(DOMMatrix.prototype, { * @param {Float64Array} values Value to assign to `_values`. This is assigned * without copying (okay because all usages are followed by a multiply). */ -function newInstance(values) { - const instance = Object.create(DOMMatrix.prototype); +function newInstance (values) { + const instance = Object.create(DOMMatrix.prototype) instance.constructor = DOMMatrix instance._is2D = true instance._values = values return instance } -function multiply(A, B) { - const dest = new Float64Array(16); +function multiply (A, B) { + const dest = new Float64Array(16) for (let i = 0; i < 4; i++) { for (let j = 0; j < 4; j++) { - let sum = 0; + let sum = 0 for (let k = 0; k < 4; k++) { sum += A[i * 4 + k] * B[k * 4 + j] } @@ -617,4 +617,4 @@ function multiply(A, B) { return dest } -module.exports = {DOMMatrix, DOMPoint} +module.exports = { DOMMatrix, DOMPoint } diff --git a/lib/bindings.js b/lib/bindings.js index c0afc9841..95ee914f3 100644 --- a/lib/bindings.js +++ b/lib/bindings.js @@ -1,3 +1,3 @@ -'use strict'; +'use strict' -module.exports = require('../build/Release/canvas.node'); +module.exports = require('../build/Release/canvas.node') diff --git a/lib/canvas.js b/lib/canvas.js index 07d520aec..03fa1a959 100644 --- a/lib/canvas.js +++ b/lib/canvas.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict' /*! * Canvas @@ -21,30 +21,30 @@ Canvas.prototype[util.inspect.custom || 'inspect'] = function () { } Canvas.prototype.getContext = function (contextType, contextAttributes) { - if ('2d' == contextType) { - const ctx = this._context2d || (this._context2d = new Context2d(this, contextAttributes)); - this.context = ctx; - ctx.canvas = this; - return ctx; + if (contextType == '2d') { + const ctx = this._context2d || (this._context2d = new Context2d(this, contextAttributes)) + this.context = ctx + ctx.canvas = this + return ctx } -}; +} Canvas.prototype.pngStream = -Canvas.prototype.createPNGStream = function(options){ - return new PNGStream(this, options); -}; +Canvas.prototype.createPNGStream = function (options) { + return new PNGStream(this, options) +} Canvas.prototype.pdfStream = -Canvas.prototype.createPDFStream = function(options){ - return new PDFStream(this, options); -}; +Canvas.prototype.createPDFStream = function (options) { + return new PDFStream(this, options) +} Canvas.prototype.jpegStream = -Canvas.prototype.createJPEGStream = function(options){ - return new JPEGStream(this, options); -}; +Canvas.prototype.createJPEGStream = function (options) { + return new JPEGStream(this, options) +} -Canvas.prototype.toDataURL = function(a1, a2, a3){ +Canvas.prototype.toDataURL = function (a1, a2, a3) { // valid arg patterns (args -> [type, opts, fn]): // [] -> ['image/png', null, null] // [qual] -> ['image/png', null, null] @@ -63,51 +63,51 @@ Canvas.prototype.toDataURL = function(a1, a2, a3){ // ['image/jpeg', opts] -> ['image/jpeg', opts, fn] // ['image/jpeg', qual] -> ['image/jpeg', {quality: qual}, fn] - let type = 'image/png'; - let opts = {}; - let fn; + let type = 'image/png' + let opts = {} + let fn - if ('function' === typeof a1) { - fn = a1; + if (typeof a1 === 'function') { + fn = a1 } else { - if ('string' === typeof a1 && FORMATS.includes(a1.toLowerCase())) { - type = a1.toLowerCase(); + if (typeof a1 === 'string' && FORMATS.includes(a1.toLowerCase())) { + type = a1.toLowerCase() } - if ('function' === typeof a2) { - fn = a2; + if (typeof a2 === 'function') { + fn = a2 } else { - if ('object' === typeof a2) { - opts = a2; - } else if ('number' === typeof a2) { - opts = {quality: Math.max(0, Math.min(1, a2))}; + if (typeof a2 === 'object') { + opts = a2 + } else if (typeof a2 === 'number') { + opts = { quality: Math.max(0, Math.min(1, a2)) } } - if ('function' === typeof a3) { - fn = a3; + if (typeof a3 === 'function') { + fn = a3 } else if (undefined !== a3) { - throw new TypeError(`${typeof a3} is not a function`); + throw new TypeError(`${typeof a3} is not a function`) } } } if (this.width === 0 || this.height === 0) { // Per spec, if the bitmap has no pixels, return this string: - const str = "data:,"; + const str = 'data:,' if (fn) { - setTimeout(() => fn(null, str)); - return; + setTimeout(() => fn(null, str)) + return } else { - return str; + return str } } if (fn) { this.toBuffer((err, buf) => { - if (err) return fn(err); - fn(null, `data:${type};base64,${buf.toString('base64')}`); + if (err) return fn(err) + fn(null, `data:${type};base64,${buf.toString('base64')}`) }, type, opts) } else { return `data:${type};base64,${this.toBuffer(type, opts).toString('base64')}` } -}; +} diff --git a/lib/image.js b/lib/image.js index 2788a5d93..c5b594f8a 100644 --- a/lib/image.js +++ b/lib/image.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict' /*! * Canvas - Image @@ -15,9 +15,9 @@ const Image = module.exports = bindings.Image const util = require('util') // Lazily loaded simple-get -let get; +let get -const {GetSource, SetSource} = bindings; +const { GetSource, SetSource } = bindings Object.defineProperty(Image.prototype, 'src', { /** @@ -30,14 +30,14 @@ Object.defineProperty(Image.prototype, 'src', { * @param {String|Buffer} val filename, buffer, data URI, URL * @api public */ - set(val) { + set (val) { if (typeof val === 'string') { if (/^\s*data:/.test(val)) { // data: URI const commaI = val.indexOf(',') // 'base64' must come before the comma const isBase64 = val.lastIndexOf('base64', commaI) !== -1 const content = val.slice(commaI + 1) - setSource(this, Buffer.from(content, isBase64 ? 'base64' : 'utf8'), val); + setSource(this, Buffer.from(content, isBase64 ? 'base64' : 'utf8'), val) } else if (/^\s*https?:\/\//.test(val)) { // remote URL const onerror = err => { if (typeof this.onerror === 'function') { @@ -47,7 +47,7 @@ Object.defineProperty(Image.prototype, 'src', { } } - if (!get) get = require('simple-get'); + if (!get) get = require('simple-get') get.concat(val, (err, res, data) => { if (err) return onerror(err) @@ -59,35 +59,35 @@ Object.defineProperty(Image.prototype, 'src', { setSource(this, data) }) } else { // local file path assumed - setSource(this, val); + setSource(this, val) } } else if (Buffer.isBuffer(val)) { - setSource(this, val); + setSource(this, val) } }, - get() { + get () { // TODO https://github.com/Automattic/node-canvas/issues/118 - return getSource(this); + return getSource(this) }, configurable: true -}); +}) // TODO || is for Node.js pre-v6.6.0 -Image.prototype[util.inspect.custom || 'inspect'] = function(){ - return '[Image' - + (this.complete ? ':' + this.width + 'x' + this.height : '') - + (this.src ? ' ' + this.src : '') - + (this.complete ? ' complete' : '') - + ']'; -}; +Image.prototype[util.inspect.custom || 'inspect'] = function () { + return '[Image' + + (this.complete ? ':' + this.width + 'x' + this.height : '') + + (this.src ? ' ' + this.src : '') + + (this.complete ? ' complete' : '') + + ']' +} -function getSource(img){ - return img._originalSource || GetSource.call(img); +function getSource (img) { + return img._originalSource || GetSource.call(img) } -function setSource(img, src, origSrc){ - SetSource.call(img, src); - img._originalSource = origSrc; +function setSource (img, src, origSrc) { + SetSource.call(img, src) + img._originalSource = origSrc } diff --git a/lib/jpegstream.js b/lib/jpegstream.js index d750f672f..701d2f870 100644 --- a/lib/jpegstream.js +++ b/lib/jpegstream.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict' /*! * Canvas - JPEGStream @@ -6,22 +6,22 @@ * MIT Licensed */ -const { Readable } = require('stream'); -function noop() {} +const { Readable } = require('stream') +function noop () {} class JPEGStream extends Readable { constructor (canvas, options) { - super(); + super() if (canvas.streamJPEGSync === undefined) { - throw new Error("node-canvas was built without JPEG support.") + throw new Error('node-canvas was built without JPEG support.') } this.options = options this.canvas = canvas } - _read() { + _read () { // For now we're not controlling the c++ code's data emission, so we only // call canvas.streamJPEGSync once and let it emit data at will. this._read = noop @@ -38,4 +38,4 @@ class JPEGStream extends Readable { } }; -module.exports = JPEGStream; +module.exports = JPEGStream diff --git a/lib/parse-font.js b/lib/parse-font.js index 43b60b38a..713db5082 100644 --- a/lib/parse-font.js +++ b/lib/parse-font.js @@ -5,11 +5,11 @@ */ const weights = 'bold|bolder|lighter|[1-9]00' - , styles = 'italic|oblique' - , variants = 'small-caps' - , stretches = 'ultra-condensed|extra-condensed|condensed|semi-condensed|semi-expanded|expanded|extra-expanded|ultra-expanded' - , units = 'px|pt|pc|in|cm|mm|%|em|ex|ch|rem|q' - , string = '\'([^\']+)\'|"([^"]+)"|[\\w\\s-]+' +const styles = 'italic|oblique' +const variants = 'small-caps' +const stretches = 'ultra-condensed|extra-condensed|condensed|semi-condensed|semi-expanded|expanded|extra-expanded|ultra-expanded' +const units = 'px|pt|pc|in|cm|mm|%|em|ex|ch|rem|q' +const string = '\'([^\']+)\'|"([^"]+)"|[\\w\\s-]+' // [ [ <‘font-style’> || || <‘font-weight’> || <‘font-stretch’> ]? // <‘font-size’> [ / <‘line-height’> ]? <‘font-family’> ] @@ -60,7 +60,7 @@ module.exports = str => { // Optional, unordered properties. let weight, style, variant, stretch // Stop search at `sizeFamily.index` - let substr = str.substring(0, sizeFamily.index) + const substr = str.substring(0, sizeFamily.index) if ((weight = weightRe.exec(substr))) font.weight = weight[1] if ((style = styleRe.exec(substr))) font.style = style[1] if ((variant = variantRe.exec(substr))) font.variant = variant[1] diff --git a/lib/pdfstream.js b/lib/pdfstream.js index 6ed12883c..8643af75b 100644 --- a/lib/pdfstream.js +++ b/lib/pdfstream.js @@ -1,35 +1,35 @@ -'use strict'; +'use strict' /*! * Canvas - PDFStream */ -const { Readable } = require('stream'); -function noop() {} +const { Readable } = require('stream') +function noop () {} class PDFStream extends Readable { - constructor(canvas, options) { - super(); + constructor (canvas, options) { + super() - this.canvas = canvas; - this.options = options; + this.canvas = canvas + this.options = options } - _read() { + _read () { // For now we're not controlling the c++ code's data emission, so we only // call canvas.streamPDFSync once and let it emit data at will. - this._read = noop; + this._read = noop this.canvas.streamPDFSync((err, chunk, len) => { if (err) { - this.emit('error', err); + this.emit('error', err) } else if (len) { - this.push(chunk); + this.push(chunk) } else { - this.push(null); + this.push(null) } - }, this.options); + }, this.options) } } -module.exports = PDFStream; +module.exports = PDFStream diff --git a/lib/pngstream.js b/lib/pngstream.js index 6310f545c..db8fdb465 100644 --- a/lib/pngstream.js +++ b/lib/pngstream.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict' /*! * Canvas - PNGStream @@ -6,36 +6,36 @@ * MIT Licensed */ -const { Readable } = require('stream'); -function noop() {} +const { Readable } = require('stream') +function noop () {} class PNGStream extends Readable { - constructor(canvas, options) { - super(); + constructor (canvas, options) { + super() if (options && options.palette instanceof Uint8ClampedArray && options.palette.length % 4 !== 0) { - throw new Error("Palette length must be a multiple of 4."); + throw new Error('Palette length must be a multiple of 4.') } - this.canvas = canvas; - this.options = options || {}; + this.canvas = canvas + this.options = options || {} } - _read() { + _read () { // For now we're not controlling the c++ code's data emission, so we only // call canvas.streamPNGSync once and let it emit data at will. - this._read = noop; + this._read = noop this.canvas.streamPNGSync((err, chunk, len) => { if (err) { - this.emit('error', err); + this.emit('error', err) } else if (len) { - this.push(chunk); + this.push(chunk) } else { - this.push(null); + this.push(null) } - }, this.options); + }, this.options) } } diff --git a/test/canvas.test.js b/test/canvas.test.js index bb9009ca6..46e167637 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -5,154 +5,156 @@ /** * Module dependencies. */ - -const createCanvas = require('../').createCanvas -const createImageData = require('../').createImageData -const loadImage = require('../').loadImage -const parseFont = require('../').parseFont -const registerFont = require('../').registerFont - const assert = require('assert') const os = require('os') -const Readable = require('stream').Readable +const path = require('path') +const { Readable } = require('stream') + +const { + createCanvas, + createImageData, + loadImage, + parseFont, + registerFont, + Canvas +} = require('../') describe('Canvas', function () { // Run with --expose-gc and uncomment this line to help find memory problems: // afterEach(gc); it('Prototype and ctor are well-shaped, don\'t hit asserts on accessors (GH-803)', function () { - const Canvas = require('../').Canvas; - var c = new Canvas(10, 10); - assert.throws(function () { Canvas.prototype.width; }, /incompatible receiver/); - assert(!c.hasOwnProperty('width')); - assert('width' in c); - assert(Canvas.prototype.hasOwnProperty('width')); - }); + const c = new Canvas(10, 10) + assert.throws(function () { Canvas.prototype.width }, /incompatible receiver/) + assert(!c.hasOwnProperty('width')) + assert('width' in c) + assert('width' in Canvas.prototype) + }) it('.parseFont()', function () { - var tests = [ - '20px Arial' - , { size: 20, unit: 'px', family: 'Arial' } - , '20pt Arial' - , { size: 26.666666666666668, unit: 'pt', family: 'Arial' } - , '20.5pt Arial' - , { size: 27.333333333333332, unit: 'pt', family: 'Arial' } - , '20% Arial' - , { size: 20, unit: '%', family: 'Arial' } // TODO I think this is a bad assertion - ZB 23-Jul-2017 - , '20mm Arial' - , { size: 75.59055118110237, unit: 'mm', family: 'Arial' } - , '20px serif' - , { size: 20, unit: 'px', family: 'serif' } - , '20px sans-serif' - , { size: 20, unit: 'px', family: 'sans-serif' } - , '20px monospace' - , { size: 20, unit: 'px', family: 'monospace' } - , '50px Arial, sans-serif' - , { size: 50, unit: 'px', family: 'Arial,sans-serif' } - , 'bold italic 50px Arial, sans-serif' - , { style: 'italic', weight: 'bold', size: 50, unit: 'px', family: 'Arial,sans-serif' } - , '50px Helvetica , Arial, sans-serif' - , { size: 50, unit: 'px', family: 'Helvetica,Arial,sans-serif' } - , '50px "Helvetica Neue", sans-serif' - , { size: 50, unit: 'px', family: 'Helvetica Neue,sans-serif' } - , '50px "Helvetica Neue", "foo bar baz" , sans-serif' - , { size: 50, unit: 'px', family: 'Helvetica Neue,foo bar baz,sans-serif' } - , "50px 'Helvetica Neue'" - , { size: 50, unit: 'px', family: 'Helvetica Neue' } - , 'italic 20px Arial' - , { size: 20, unit: 'px', style: 'italic', family: 'Arial' } - , 'oblique 20px Arial' - , { size: 20, unit: 'px', style: 'oblique', family: 'Arial' } - , 'normal 20px Arial' - , { size: 20, unit: 'px', style: 'normal', family: 'Arial' } - , '300 20px Arial' - , { size: 20, unit: 'px', weight: '300', family: 'Arial' } - , '800 20px Arial' - , { size: 20, unit: 'px', weight: '800', family: 'Arial' } - , 'bolder 20px Arial' - , { size: 20, unit: 'px', weight: 'bolder', family: 'Arial' } - , 'lighter 20px Arial' - , { size: 20, unit: 'px', weight: 'lighter', family: 'Arial' } - , 'normal normal normal 16px Impact' - , { size: 16, unit: 'px', weight: 'normal', family: 'Impact', style: 'normal', variant: 'normal' } - , 'italic small-caps bolder 16px cursive' - , { size: 16, unit: 'px', style: 'italic', variant: 'small-caps', weight: 'bolder', family: 'cursive' } - , '20px "new century schoolbook", serif' - , { size: 20, unit: 'px', family: 'new century schoolbook,serif' } - , '20px "Arial bold 300"' // synthetic case with weight keyword inside family - , { size: 20, unit: 'px', family: 'Arial bold 300', variant: 'normal' } - ]; - - for (var i = 0, len = tests.length; i < len; ++i) { - var str = tests[i++] - , expected = tests[i] - , actual = parseFont(str); - - if (!expected.style) expected.style = 'normal'; - if (!expected.weight) expected.weight = 'normal'; - if (!expected.stretch) expected.stretch = 'normal'; - if (!expected.variant) expected.variant = 'normal'; - - assert.deepEqual(actual, expected, 'Failed to parse: ' + str); + const tests = [ + '20px Arial', + { size: 20, unit: 'px', family: 'Arial' }, + '20pt Arial', + { size: 26.666666666666668, unit: 'pt', family: 'Arial' }, + '20.5pt Arial', + { size: 27.333333333333332, unit: 'pt', family: 'Arial' }, + '20% Arial', + { size: 20, unit: '%', family: 'Arial' }, // TODO I think this is a bad assertion - ZB 23-Jul-2017 + '20mm Arial', + { size: 75.59055118110237, unit: 'mm', family: 'Arial' }, + '20px serif', + { size: 20, unit: 'px', family: 'serif' }, + '20px sans-serif', + { size: 20, unit: 'px', family: 'sans-serif' }, + '20px monospace', + { size: 20, unit: 'px', family: 'monospace' }, + '50px Arial, sans-serif', + { size: 50, unit: 'px', family: 'Arial,sans-serif' }, + 'bold italic 50px Arial, sans-serif', + { style: 'italic', weight: 'bold', size: 50, unit: 'px', family: 'Arial,sans-serif' }, + '50px Helvetica , Arial, sans-serif', + { size: 50, unit: 'px', family: 'Helvetica,Arial,sans-serif' }, + '50px "Helvetica Neue", sans-serif', + { size: 50, unit: 'px', family: 'Helvetica Neue,sans-serif' }, + '50px "Helvetica Neue", "foo bar baz" , sans-serif', + { size: 50, unit: 'px', family: 'Helvetica Neue,foo bar baz,sans-serif' }, + "50px 'Helvetica Neue'", + { size: 50, unit: 'px', family: 'Helvetica Neue' }, + 'italic 20px Arial', + { size: 20, unit: 'px', style: 'italic', family: 'Arial' }, + 'oblique 20px Arial', + { size: 20, unit: 'px', style: 'oblique', family: 'Arial' }, + 'normal 20px Arial', + { size: 20, unit: 'px', style: 'normal', family: 'Arial' }, + '300 20px Arial', + { size: 20, unit: 'px', weight: '300', family: 'Arial' }, + '800 20px Arial', + { size: 20, unit: 'px', weight: '800', family: 'Arial' }, + 'bolder 20px Arial', + { size: 20, unit: 'px', weight: 'bolder', family: 'Arial' }, + 'lighter 20px Arial', + { size: 20, unit: 'px', weight: 'lighter', family: 'Arial' }, + 'normal normal normal 16px Impact', + { size: 16, unit: 'px', weight: 'normal', family: 'Impact', style: 'normal', variant: 'normal' }, + 'italic small-caps bolder 16px cursive', + { size: 16, unit: 'px', style: 'italic', variant: 'small-caps', weight: 'bolder', family: 'cursive' }, + '20px "new century schoolbook", serif', + { size: 20, unit: 'px', family: 'new century schoolbook,serif' }, + '20px "Arial bold 300"', // synthetic case with weight keyword inside family + { size: 20, unit: 'px', family: 'Arial bold 300', variant: 'normal' } + ] + + for (let i = 0, len = tests.length; i < len; ++i) { + const str = tests[i++] + const expected = tests[i] + const actual = parseFont(str) + + if (!expected.style) expected.style = 'normal' + if (!expected.weight) expected.weight = 'normal' + if (!expected.stretch) expected.stretch = 'normal' + if (!expected.variant) expected.variant = 'normal' + + assert.deepEqual(actual, expected, 'Failed to parse: ' + str) } assert.strictEqual(parseFont('Helvetica, sans'), undefined) - }); + }) it('registerFont', function () { // Minimal test to make sure nothing is thrown - registerFont('./examples/pfennigFont/Pfennig.ttf', {family: 'Pfennig'}) - registerFont('./examples/pfennigFont/PfennigBold.ttf', {family: 'Pfennig', weight: 'bold'}) - }); + registerFont('./examples/pfennigFont/Pfennig.ttf', { family: 'Pfennig' }) + registerFont('./examples/pfennigFont/PfennigBold.ttf', { family: 'Pfennig', weight: 'bold' }) + }) it('color serialization', function () { - var canvas = createCanvas(200, 200) - , ctx = canvas.getContext('2d'); + const canvas = createCanvas(200, 200) + const ctx = canvas.getContext('2d'); - ['fillStyle', 'strokeStyle', 'shadowColor'].forEach(function(prop){ - ctx[prop] = '#FFFFFF'; - assert.equal('#ffffff', ctx[prop], prop + ' #FFFFFF -> #ffffff, got ' + ctx[prop]); + ['fillStyle', 'strokeStyle', 'shadowColor'].forEach(function (prop) { + ctx[prop] = '#FFFFFF' + assert.equal('#ffffff', ctx[prop], prop + ' #FFFFFF -> #ffffff, got ' + ctx[prop]) - ctx[prop] = '#FFF'; - assert.equal('#ffffff', ctx[prop], prop + ' #FFF -> #ffffff, got ' + ctx[prop]); + ctx[prop] = '#FFF' + assert.equal('#ffffff', ctx[prop], prop + ' #FFF -> #ffffff, got ' + ctx[prop]) - ctx[prop] = 'rgba(128, 200, 128, 1)'; - assert.equal('#80c880', ctx[prop], prop + ' rgba(128, 200, 128, 1) -> #80c880, got ' + ctx[prop]); + ctx[prop] = 'rgba(128, 200, 128, 1)' + assert.equal('#80c880', ctx[prop], prop + ' rgba(128, 200, 128, 1) -> #80c880, got ' + ctx[prop]) - ctx[prop] = 'rgba(128,80,0,0.5)'; - assert.equal('rgba(128, 80, 0, 0.50)', ctx[prop], prop + ' rgba(128,80,0,0.5) -> rgba(128, 80, 0, 0.5), got ' + ctx[prop]); + ctx[prop] = 'rgba(128,80,0,0.5)' + assert.equal('rgba(128, 80, 0, 0.50)', ctx[prop], prop + ' rgba(128,80,0,0.5) -> rgba(128, 80, 0, 0.5), got ' + ctx[prop]) - ctx[prop] = 'rgba(128,80,0,0.75)'; - assert.equal('rgba(128, 80, 0, 0.75)', ctx[prop], prop + ' rgba(128,80,0,0.75) -> rgba(128, 80, 0, 0.75), got ' + ctx[prop]); + ctx[prop] = 'rgba(128,80,0,0.75)' + assert.equal('rgba(128, 80, 0, 0.75)', ctx[prop], prop + ' rgba(128,80,0,0.75) -> rgba(128, 80, 0, 0.75), got ' + ctx[prop]) - if ('shadowColor' == prop) return; + if (prop === 'shadowColor') return - var grad = ctx.createLinearGradient(0,0,0,150); - ctx[prop] = grad; - assert.strictEqual(grad, ctx[prop], prop + ' pattern getter failed'); - }); - }); + const grad = ctx.createLinearGradient(0, 0, 0, 150) + ctx[prop] = grad + assert.strictEqual(grad, ctx[prop], prop + ' pattern getter failed') + }) + }) it('color parser', function () { - var canvas = createCanvas(200, 200) - , ctx = canvas.getContext('2d'); + const canvas = createCanvas(200, 200) + const ctx = canvas.getContext('2d') - ctx.fillStyle = '#ffccaa'; - assert.equal('#ffccaa', ctx.fillStyle); + ctx.fillStyle = '#ffccaa' + assert.equal('#ffccaa', ctx.fillStyle) - ctx.fillStyle = '#FFCCAA'; - assert.equal('#ffccaa', ctx.fillStyle); + ctx.fillStyle = '#FFCCAA' + assert.equal('#ffccaa', ctx.fillStyle) - ctx.fillStyle = '#FCA'; - assert.equal('#ffccaa', ctx.fillStyle); + ctx.fillStyle = '#FCA' + assert.equal('#ffccaa', ctx.fillStyle) - ctx.fillStyle = '#fff'; - ctx.fillStyle = '#FGG'; - assert.equal('#ff0000', ctx.fillStyle); + ctx.fillStyle = '#fff' + ctx.fillStyle = '#FGG' + assert.equal('#ff0000', ctx.fillStyle) - ctx.fillStyle = '#fff'; - ctx.fillStyle = 'afasdfasdf'; - assert.equal('#ffffff', ctx.fillStyle); + ctx.fillStyle = '#fff' + ctx.fillStyle = 'afasdfasdf' + assert.equal('#ffffff', ctx.fillStyle) // #rgba and #rrggbbaa ctx.fillStyle = '#ffccaa80' @@ -164,108 +166,108 @@ describe('Canvas', function () { ctx.fillStyle = '#BEAD' assert.equal('rgba(187, 238, 170, 0.87)', ctx.fillStyle) - ctx.fillStyle = 'rgb(255,255,255)'; - assert.equal('#ffffff', ctx.fillStyle); + ctx.fillStyle = 'rgb(255,255,255)' + assert.equal('#ffffff', ctx.fillStyle) - ctx.fillStyle = 'rgb(0,0,0)'; - assert.equal('#000000', ctx.fillStyle); + ctx.fillStyle = 'rgb(0,0,0)' + assert.equal('#000000', ctx.fillStyle) - ctx.fillStyle = 'rgb( 0 , 0 , 0)'; - assert.equal('#000000', ctx.fillStyle); + ctx.fillStyle = 'rgb( 0 , 0 , 0)' + assert.equal('#000000', ctx.fillStyle) - ctx.fillStyle = 'rgba( 0 , 0 , 0, 1)'; - assert.equal('#000000', ctx.fillStyle); + ctx.fillStyle = 'rgba( 0 , 0 , 0, 1)' + assert.equal('#000000', ctx.fillStyle) - ctx.fillStyle = 'rgba( 255, 200, 90, 0.5)'; - assert.equal('rgba(255, 200, 90, 0.50)', ctx.fillStyle); + ctx.fillStyle = 'rgba( 255, 200, 90, 0.5)' + assert.equal('rgba(255, 200, 90, 0.50)', ctx.fillStyle) - ctx.fillStyle = 'rgba( 255, 200, 90, 0.75)'; - assert.equal('rgba(255, 200, 90, 0.75)', ctx.fillStyle); + ctx.fillStyle = 'rgba( 255, 200, 90, 0.75)' + assert.equal('rgba(255, 200, 90, 0.75)', ctx.fillStyle) - ctx.fillStyle = 'rgba( 255, 200, 90, 0.7555)'; - assert.equal('rgba(255, 200, 90, 0.75)', ctx.fillStyle); + ctx.fillStyle = 'rgba( 255, 200, 90, 0.7555)' + assert.equal('rgba(255, 200, 90, 0.75)', ctx.fillStyle) - ctx.fillStyle = 'rgba( 255, 200, 90, .7555)'; - assert.equal('rgba(255, 200, 90, 0.75)', ctx.fillStyle); + ctx.fillStyle = 'rgba( 255, 200, 90, .7555)' + assert.equal('rgba(255, 200, 90, 0.75)', ctx.fillStyle) - ctx.fillStyle = 'rgb(0, 0, 9000)'; - assert.equal('#0000ff', ctx.fillStyle); + ctx.fillStyle = 'rgb(0, 0, 9000)' + assert.equal('#0000ff', ctx.fillStyle) - ctx.fillStyle = 'rgba(0, 0, 0, 42.42)'; - assert.equal('#000000', ctx.fillStyle); + ctx.fillStyle = 'rgba(0, 0, 0, 42.42)' + assert.equal('#000000', ctx.fillStyle) // hsl / hsla tests - ctx.fillStyle = 'hsl(0, 0%, 0%)'; - assert.equal('#000000', ctx.fillStyle); + ctx.fillStyle = 'hsl(0, 0%, 0%)' + assert.equal('#000000', ctx.fillStyle) - ctx.fillStyle = 'hsl(3600, -10%, -10%)'; - assert.equal('#000000', ctx.fillStyle); + ctx.fillStyle = 'hsl(3600, -10%, -10%)' + assert.equal('#000000', ctx.fillStyle) - ctx.fillStyle = 'hsl(10, 100%, 42%)'; - assert.equal('#d62400', ctx.fillStyle); + ctx.fillStyle = 'hsl(10, 100%, 42%)' + assert.equal('#d62400', ctx.fillStyle) - ctx.fillStyle = 'hsl(370, 120%, 42%)'; - assert.equal('#d62400', ctx.fillStyle); + ctx.fillStyle = 'hsl(370, 120%, 42%)' + assert.equal('#d62400', ctx.fillStyle) - ctx.fillStyle = 'hsl(0, 100%, 100%)'; - assert.equal('#ffffff', ctx.fillStyle); + ctx.fillStyle = 'hsl(0, 100%, 100%)' + assert.equal('#ffffff', ctx.fillStyle) - ctx.fillStyle = 'hsl(0, 150%, 150%)'; - assert.equal('#ffffff', ctx.fillStyle); + ctx.fillStyle = 'hsl(0, 150%, 150%)' + assert.equal('#ffffff', ctx.fillStyle) - ctx.fillStyle = 'hsl(237, 76%, 25%)'; - assert.equal('#0f1470', ctx.fillStyle); + ctx.fillStyle = 'hsl(237, 76%, 25%)' + assert.equal('#0f1470', ctx.fillStyle) - ctx.fillStyle = 'hsl(240, 73%, 25%)'; - assert.equal('#11116e', ctx.fillStyle); + ctx.fillStyle = 'hsl(240, 73%, 25%)' + assert.equal('#11116e', ctx.fillStyle) - ctx.fillStyle = 'hsl(262, 32%, 42%)'; - assert.equal('#62498d', ctx.fillStyle); + ctx.fillStyle = 'hsl(262, 32%, 42%)' + assert.equal('#62498d', ctx.fillStyle) - ctx.fillStyle = 'hsla(0, 0%, 0%, 1)'; - assert.equal('#000000', ctx.fillStyle); + ctx.fillStyle = 'hsla(0, 0%, 0%, 1)' + assert.equal('#000000', ctx.fillStyle) - ctx.fillStyle = 'hsla(0, 100%, 100%, 1)'; - assert.equal('#ffffff', ctx.fillStyle); + ctx.fillStyle = 'hsla(0, 100%, 100%, 1)' + assert.equal('#ffffff', ctx.fillStyle) - ctx.fillStyle = 'hsla(120, 25%, 75%, 0.5)'; - assert.equal('rgba(175, 207, 175, 0.50)', ctx.fillStyle); + ctx.fillStyle = 'hsla(120, 25%, 75%, 0.5)' + assert.equal('rgba(175, 207, 175, 0.50)', ctx.fillStyle) - ctx.fillStyle = 'hsla(240, 75%, 25%, 0.75)'; - assert.equal('rgba(16, 16, 112, 0.75)', ctx.fillStyle); + ctx.fillStyle = 'hsla(240, 75%, 25%, 0.75)' + assert.equal('rgba(16, 16, 112, 0.75)', ctx.fillStyle) - ctx.fillStyle = 'hsla(172.0, 33.00000e0%, 42%, 1)'; - assert.equal('#488e85', ctx.fillStyle); + ctx.fillStyle = 'hsla(172.0, 33.00000e0%, 42%, 1)' + assert.equal('#488e85', ctx.fillStyle) - ctx.fillStyle = 'hsl(124.5, 76.1%, 47.6%)'; - assert.equal('#1dd62b', ctx.fillStyle); + ctx.fillStyle = 'hsl(124.5, 76.1%, 47.6%)' + assert.equal('#1dd62b', ctx.fillStyle) - ctx.fillStyle = 'hsl(1.24e2, 760e-1%, 4.7e1%)'; - assert.equal('#1dd329', ctx.fillStyle); + ctx.fillStyle = 'hsl(1.24e2, 760e-1%, 4.7e1%)' + assert.equal('#1dd329', ctx.fillStyle) // case-insensitive (#235) - ctx.fillStyle = "sILveR"; - assert.equal(ctx.fillStyle, "#c0c0c0"); - }); + ctx.fillStyle = 'sILveR' + assert.equal(ctx.fillStyle, '#c0c0c0') + }) it('Canvas#type', function () { - var canvas = createCanvas(10, 10); - assert.equal(canvas.type, 'image'); - var canvas = createCanvas(10, 10, 'pdf'); - assert.equal(canvas.type, 'pdf'); - var canvas = createCanvas(10, 10, 'svg'); - assert.equal(canvas.type, 'svg'); - var canvas = createCanvas(10, 10, 'hey'); - assert.equal(canvas.type, 'image'); - }); + let canvas = createCanvas(10, 10) + assert.equal(canvas.type, 'image') + canvas = createCanvas(10, 10, 'pdf') + assert.equal(canvas.type, 'pdf') + canvas = createCanvas(10, 10, 'svg') + assert.equal(canvas.type, 'svg') + canvas = createCanvas(10, 10, 'hey') + assert.equal(canvas.type, 'image') + }) it('Canvas#getContext("2d")', function () { - var canvas = createCanvas(200, 300) - , ctx = canvas.getContext('2d'); - assert.ok('object' == typeof ctx); - assert.equal(canvas, ctx.canvas, 'context.canvas is not canvas'); - assert.equal(ctx, canvas.context, 'canvas.context is not context'); + const canvas = createCanvas(200, 300) + const ctx = canvas.getContext('2d') + assert.ok(typeof ctx === 'object') + assert.equal(canvas, ctx.canvas, 'context.canvas is not canvas') + assert.equal(ctx, canvas.context, 'canvas.context is not context') const MAX_IMAGE_SIZE = 32767; @@ -280,159 +282,158 @@ describe('Canvas', function () { [Math.pow(2, 30), 0, 3], [Math.pow(2, 30), 1, 3], [Math.pow(2, 32), 0, 1], - [Math.pow(2, 32), 1, 1], + [Math.pow(2, 32), 1, 1] ].forEach(params => { - var width = params[0]; - var height = params[1]; - var errorLevel = params[2]; + const width = params[0] + const height = params[1] + const errorLevel = params[2] - var level = 3; + let level = 3 try { - var canvas = createCanvas(width, height); - level--; + const canvas = createCanvas(width, height) + level-- - var ctx = canvas.getContext('2d'); - level--; + const ctx = canvas.getContext('2d') + level-- - ctx.getImageData(0, 0, 1, 1); - level--; + ctx.getImageData(0, 0, 1, 1) + level-- } catch (err) {} - if (errorLevel !== null) - assert.strictEqual(level, errorLevel); - }); - }); + if (errorLevel !== null) { assert.strictEqual(level, errorLevel) } + }) + }) it('Canvas#getContext("2d", {pixelFormat: string})', function () { - var canvas, context; + let canvas, context // default: - canvas = createCanvas(10, 10); - context = canvas.getContext("2d", {pixelFormat: "RGBA32"}); - assert.equal(context.pixelFormat, "RGBA32"); + canvas = createCanvas(10, 10) + context = canvas.getContext('2d', { pixelFormat: 'RGBA32' }) + assert.equal(context.pixelFormat, 'RGBA32') - canvas = createCanvas(10, 10); - context = canvas.getContext("2d", {pixelFormat: "RGBA32"}); - assert.equal(context.pixelFormat, "RGBA32"); + canvas = createCanvas(10, 10) + context = canvas.getContext('2d', { pixelFormat: 'RGBA32' }) + assert.equal(context.pixelFormat, 'RGBA32') - canvas = createCanvas(10, 10); - context = canvas.getContext("2d", {pixelFormat: "RGB24"}); - assert.equal(context.pixelFormat, "RGB24"); + canvas = createCanvas(10, 10) + context = canvas.getContext('2d', { pixelFormat: 'RGB24' }) + assert.equal(context.pixelFormat, 'RGB24') - canvas = createCanvas(10, 10); - context = canvas.getContext("2d", {pixelFormat: "A8"}); - assert.equal(context.pixelFormat, "A8"); + canvas = createCanvas(10, 10) + context = canvas.getContext('2d', { pixelFormat: 'A8' }) + assert.equal(context.pixelFormat, 'A8') - canvas = createCanvas(10, 10); - context = canvas.getContext("2d", {pixelFormat: "A1"}); - assert.equal(context.pixelFormat, "A1"); + canvas = createCanvas(10, 10) + context = canvas.getContext('2d', { pixelFormat: 'A1' }) + assert.equal(context.pixelFormat, 'A1') - canvas = createCanvas(10, 10); - context = canvas.getContext("2d", {pixelFormat: "RGB16_565"}); - assert.equal(context.pixelFormat, "RGB16_565"); + canvas = createCanvas(10, 10) + context = canvas.getContext('2d', { pixelFormat: 'RGB16_565' }) + assert.equal(context.pixelFormat, 'RGB16_565') // Not tested: RGB30 - }); + }) it('Canvas#getContext("2d", {alpha: boolean})', function () { - var canvas, context; + let canvas, context - canvas = createCanvas(10, 10); - context = canvas.getContext("2d", {alpha: true}); - assert.equal(context.pixelFormat, "RGBA32"); + canvas = createCanvas(10, 10) + context = canvas.getContext('2d', { alpha: true }) + assert.equal(context.pixelFormat, 'RGBA32') - canvas = createCanvas(10, 10); - context = canvas.getContext("2d", {alpha: false}); - assert.equal(context.pixelFormat, "RGB24"); + canvas = createCanvas(10, 10) + context = canvas.getContext('2d', { alpha: false }) + assert.equal(context.pixelFormat, 'RGB24') // alpha takes priority: - canvas = createCanvas(10, 10); - context = canvas.getContext("2d", {pixelFormat: "RGBA32", alpha: false}); - assert.equal(context.pixelFormat, "RGB24"); - }); + canvas = createCanvas(10, 10) + context = canvas.getContext('2d', { pixelFormat: 'RGBA32', alpha: false }) + assert.equal(context.pixelFormat, 'RGB24') + }) it('Canvas#{width,height}=', function () { - const canvas = createCanvas(100, 200); - const context = canvas.getContext('2d'); - - assert.equal(canvas.width, 100); - assert.equal(canvas.height, 200); - - context.globalAlpha = .5; - context.fillStyle = '#0f0'; - context.strokeStyle = '#0f0'; - context.font = '20px arial'; - context.fillRect(0, 0, 1, 1); - - canvas.width = 50; - canvas.height = 70; - assert.equal(canvas.width, 50); - assert.equal(canvas.height, 70); - - context.font = '20px arial'; - assert.equal(context.font, '20px arial'); - canvas.width |= 0; - - assert.equal(context.lineWidth, 1); // #1095 - assert.equal(context.globalAlpha, 1); // #1292 - assert.equal(context.fillStyle, '#000000'); - assert.equal(context.strokeStyle, '#000000'); - assert.equal(context.font, '10px sans-serif'); - assert.strictEqual(context.getImageData(0, 0, 1, 1).data.join(','), '0,0,0,0'); - }); + const canvas = createCanvas(100, 200) + const context = canvas.getContext('2d') + + assert.equal(canvas.width, 100) + assert.equal(canvas.height, 200) + + context.globalAlpha = 0.5 + context.fillStyle = '#0f0' + context.strokeStyle = '#0f0' + context.font = '20px arial' + context.fillRect(0, 0, 1, 1) + + canvas.width = 50 + canvas.height = 70 + assert.equal(canvas.width, 50) + assert.equal(canvas.height, 70) + + context.font = '20px arial' + assert.equal(context.font, '20px arial') + canvas.width |= 0 + + assert.equal(context.lineWidth, 1) // #1095 + assert.equal(context.globalAlpha, 1) // #1292 + assert.equal(context.fillStyle, '#000000') + assert.equal(context.strokeStyle, '#000000') + assert.equal(context.font, '10px sans-serif') + assert.strictEqual(context.getImageData(0, 0, 1, 1).data.join(','), '0,0,0,0') + }) it('Canvas#width= (resurfacing) doesn\'t crash when fillStyle is a pattern (#1357)', function (done) { - const canvas = createCanvas(100, 200); - const ctx = canvas.getContext('2d'); + const canvas = createCanvas(100, 200) + const ctx = canvas.getContext('2d') - loadImage(`${__dirname}/fixtures/checkers.png`).then(img => { - const pattern = ctx.createPattern(img, 'repeat'); - ctx.fillStyle = pattern; - ctx.fillRect(0, 0, 300, 300); - canvas.width = 200; // cause canvas to resurface - done(); + loadImage(path.join(__dirname, '/fixtures/checkers.png')).then(img => { + const pattern = ctx.createPattern(img, 'repeat') + ctx.fillStyle = pattern + ctx.fillRect(0, 0, 300, 300) + canvas.width = 200 // cause canvas to resurface + done() }) - }); + }) it('SVG Canvas#width changes don\'t crash (#1380)', function () { const myCanvas = createCanvas(100, 100, 'svg') - myCanvas.width = 120; - }); + myCanvas.width = 120 + }) - it('Canvas#stride', function() { - var canvas = createCanvas(24, 10); - assert.ok(canvas.stride >= 24, 'canvas.stride is too short'); - assert.ok(canvas.stride < 1024, 'canvas.stride seems too long'); + it('Canvas#stride', function () { + const canvas = createCanvas(24, 10) + assert.ok(canvas.stride >= 24, 'canvas.stride is too short') + assert.ok(canvas.stride < 1024, 'canvas.stride seems too long') // TODO test stride on other formats - }); + }) it('Canvas#getContext("invalid")', function () { - assert.equal(null, createCanvas(200, 300).getContext('invalid')); - }); + assert.equal(null, createCanvas(200, 300).getContext('invalid')) + }) it('Context2d#patternQuality', function () { - var canvas = createCanvas(200, 200) - , ctx = canvas.getContext('2d'); - - assert.equal('good', ctx.patternQuality); - ctx.patternQuality = 'best'; - assert.equal('best', ctx.patternQuality); - ctx.patternQuality = 'invalid'; - assert.equal('best', ctx.patternQuality); - }); + const canvas = createCanvas(200, 200) + const ctx = canvas.getContext('2d') + + assert.equal('good', ctx.patternQuality) + ctx.patternQuality = 'best' + assert.equal('best', ctx.patternQuality) + ctx.patternQuality = 'invalid' + assert.equal('best', ctx.patternQuality) + }) it('Context2d#imageSmoothingEnabled', function () { - var canvas = createCanvas(200, 200) - , ctx = canvas.getContext('2d'); - - assert.equal(true, ctx.imageSmoothingEnabled); - ctx.imageSmoothingEnabled = false; - assert.equal('good', ctx.patternQuality); - assert.equal(false, ctx.imageSmoothingEnabled); - assert.equal('good', ctx.patternQuality); - }); + const canvas = createCanvas(200, 200) + const ctx = canvas.getContext('2d') + + assert.equal(true, ctx.imageSmoothingEnabled) + ctx.imageSmoothingEnabled = false + assert.equal('good', ctx.patternQuality) + assert.equal(false, ctx.imageSmoothingEnabled) + assert.equal('good', ctx.patternQuality) + }) it('Context2d#font=', function () { const canvas = createCanvas(200, 200) @@ -444,204 +445,204 @@ describe('Canvas', function () { ctx.font = 'Helvetica, sans' // invalid assert.equal(ctx.font, '15px Arial, sans-serif') - }); + }) it('Context2d#lineWidth=', function () { - var canvas = createCanvas(200, 200) - , ctx = canvas.getContext('2d'); - - ctx.lineWidth = 10.0; - assert.equal(10, ctx.lineWidth); - ctx.lineWidth = Infinity; - assert.equal(10, ctx.lineWidth); - ctx.lineWidth = -Infinity; - assert.equal(10, ctx.lineWidth); - ctx.lineWidth = -5; - assert.equal(10, ctx.lineWidth); - ctx.lineWidth = 0; - assert.equal(10, ctx.lineWidth); - }); + const canvas = createCanvas(200, 200) + const ctx = canvas.getContext('2d') + + ctx.lineWidth = 10.0 + assert.equal(10, ctx.lineWidth) + ctx.lineWidth = Infinity + assert.equal(10, ctx.lineWidth) + ctx.lineWidth = -Infinity + assert.equal(10, ctx.lineWidth) + ctx.lineWidth = -5 + assert.equal(10, ctx.lineWidth) + ctx.lineWidth = 0 + assert.equal(10, ctx.lineWidth) + }) it('Context2d#antiAlias=', function () { - var canvas = createCanvas(200, 200) - , ctx = canvas.getContext('2d'); - - assert.equal('default', ctx.antialias); - ctx.antialias = 'none'; - assert.equal('none', ctx.antialias); - ctx.antialias = 'gray'; - assert.equal('gray', ctx.antialias); - ctx.antialias = 'subpixel'; - assert.equal('subpixel', ctx.antialias); - ctx.antialias = 'invalid'; - assert.equal('subpixel', ctx.antialias); - ctx.antialias = 1; - assert.equal('subpixel', ctx.antialias); - }); + const canvas = createCanvas(200, 200) + const ctx = canvas.getContext('2d') + + assert.equal('default', ctx.antialias) + ctx.antialias = 'none' + assert.equal('none', ctx.antialias) + ctx.antialias = 'gray' + assert.equal('gray', ctx.antialias) + ctx.antialias = 'subpixel' + assert.equal('subpixel', ctx.antialias) + ctx.antialias = 'invalid' + assert.equal('subpixel', ctx.antialias) + ctx.antialias = 1 + assert.equal('subpixel', ctx.antialias) + }) it('Context2d#lineCap=', function () { - var canvas = createCanvas(200, 200) - , ctx = canvas.getContext('2d'); + const canvas = createCanvas(200, 200) + const ctx = canvas.getContext('2d') - assert.equal('butt', ctx.lineCap); - ctx.lineCap = 'round'; - assert.equal('round', ctx.lineCap); - }); + assert.equal('butt', ctx.lineCap) + ctx.lineCap = 'round' + assert.equal('round', ctx.lineCap) + }) it('Context2d#lineJoin=', function () { - var canvas = createCanvas(200, 200) - , ctx = canvas.getContext('2d'); + const canvas = createCanvas(200, 200) + const ctx = canvas.getContext('2d') - assert.equal('miter', ctx.lineJoin); - ctx.lineJoin = 'round'; - assert.equal('round', ctx.lineJoin); - }); + assert.equal('miter', ctx.lineJoin) + ctx.lineJoin = 'round' + assert.equal('round', ctx.lineJoin) + }) it('Context2d#globalAlpha=', function () { - var canvas = createCanvas(200, 200) - , ctx = canvas.getContext('2d'); + const canvas = createCanvas(200, 200) + const ctx = canvas.getContext('2d') - assert.equal(1, ctx.globalAlpha); + assert.equal(1, ctx.globalAlpha) ctx.globalAlpha = 0.5 - assert.equal(0.5, ctx.globalAlpha); - }); + assert.equal(0.5, ctx.globalAlpha) + }) it('Context2d#isPointInPath()', function () { - var canvas = createCanvas(200, 200) - , ctx = canvas.getContext('2d'); - - ctx.rect(5,5,100,100); - ctx.rect(50,100,10,10); - assert.ok(ctx.isPointInPath(10,10)); - assert.ok(ctx.isPointInPath(10,50)); - assert.ok(ctx.isPointInPath(100,100)); - assert.ok(ctx.isPointInPath(105,105)); - assert.ok(!ctx.isPointInPath(106,105)); - assert.ok(!ctx.isPointInPath(150,150)); - - assert.ok(ctx.isPointInPath(50,110)); - assert.ok(ctx.isPointInPath(60,110)); - assert.ok(!ctx.isPointInPath(70,110)); - assert.ok(!ctx.isPointInPath(50,120)); - }); + const canvas = createCanvas(200, 200) + const ctx = canvas.getContext('2d') + + ctx.rect(5, 5, 100, 100) + ctx.rect(50, 100, 10, 10) + assert.ok(ctx.isPointInPath(10, 10)) + assert.ok(ctx.isPointInPath(10, 50)) + assert.ok(ctx.isPointInPath(100, 100)) + assert.ok(ctx.isPointInPath(105, 105)) + assert.ok(!ctx.isPointInPath(106, 105)) + assert.ok(!ctx.isPointInPath(150, 150)) + + assert.ok(ctx.isPointInPath(50, 110)) + assert.ok(ctx.isPointInPath(60, 110)) + assert.ok(!ctx.isPointInPath(70, 110)) + assert.ok(!ctx.isPointInPath(50, 120)) + }) it('Context2d#textAlign', function () { - var canvas = createCanvas(200,200) - , ctx = canvas.getContext('2d'); - - assert.equal('start', ctx.textAlign); - ctx.textAlign = 'center'; - assert.equal('center', ctx.textAlign); - ctx.textAlign = 'right'; - assert.equal('right', ctx.textAlign); - ctx.textAlign = 'end'; - assert.equal('end', ctx.textAlign); - ctx.textAlign = 'fail'; - assert.equal('end', ctx.textAlign); - }); + const canvas = createCanvas(200, 200) + const ctx = canvas.getContext('2d') + + assert.equal('start', ctx.textAlign) + ctx.textAlign = 'center' + assert.equal('center', ctx.textAlign) + ctx.textAlign = 'right' + assert.equal('right', ctx.textAlign) + ctx.textAlign = 'end' + assert.equal('end', ctx.textAlign) + ctx.textAlign = 'fail' + assert.equal('end', ctx.textAlign) + }) describe('#toBuffer', function () { it('Canvas#toBuffer()', function () { - var buf = createCanvas(200,200).toBuffer(); - assert.equal('PNG', buf.slice(1,4).toString()); - }); + const buf = createCanvas(200, 200).toBuffer() + assert.equal('PNG', buf.slice(1, 4).toString()) + }) it('Canvas#toBuffer("image/png")', function () { - var buf = createCanvas(200,200).toBuffer('image/png'); - assert.equal('PNG', buf.slice(1,4).toString()); - }); + const buf = createCanvas(200, 200).toBuffer('image/png') + assert.equal('PNG', buf.slice(1, 4).toString()) + }) it('Canvas#toBuffer("image/png", {resolution: 96})', function () { - const buf = createCanvas(200, 200).toBuffer('image/png', {resolution: 96}); + const buf = createCanvas(200, 200).toBuffer('image/png', { resolution: 96 }) // 3780 ppm ~= 96 ppi - let foundpHYs = false; + let foundpHYs = false for (let i = 0; i < buf.length - 12; i++) { if (buf[i] === 0x70 && buf[i + 1] === 0x48 && buf[i + 2] === 0x59 && buf[i + 3] === 0x73) { // pHYs - foundpHYs = true; - assert.equal(buf[i + 4], 0); - assert.equal(buf[i + 5], 0); - assert.equal(buf[i + 6], 0x0e); - assert.equal(buf[i + 7], 0xc4); // x - assert.equal(buf[i + 8], 0); - assert.equal(buf[i + 9], 0); - assert.equal(buf[i + 10], 0x0e); - assert.equal(buf[i + 11], 0xc4); // y + foundpHYs = true + assert.equal(buf[i + 4], 0) + assert.equal(buf[i + 5], 0) + assert.equal(buf[i + 6], 0x0e) + assert.equal(buf[i + 7], 0xc4) // x + assert.equal(buf[i + 8], 0) + assert.equal(buf[i + 9], 0) + assert.equal(buf[i + 10], 0x0e) + assert.equal(buf[i + 11], 0xc4) // y } } - assert.ok(foundpHYs, "missing pHYs header"); + assert.ok(foundpHYs, 'missing pHYs header') }) it('Canvas#toBuffer("image/png", {compressionLevel: 5})', function () { - var buf = createCanvas(200,200).toBuffer('image/png', {compressionLevel: 5}); - assert.equal('PNG', buf.slice(1,4).toString()); - }); + const buf = createCanvas(200, 200).toBuffer('image/png', { compressionLevel: 5 }) + assert.equal('PNG', buf.slice(1, 4).toString()) + }) it('Canvas#toBuffer("image/jpeg")', function () { - var buf = createCanvas(200,200).toBuffer('image/jpeg'); - assert.equal(buf[0], 0xff); - assert.equal(buf[1], 0xd8); - assert.equal(buf[buf.byteLength - 2], 0xff); - assert.equal(buf[buf.byteLength - 1], 0xd9); - }); + const buf = createCanvas(200, 200).toBuffer('image/jpeg') + assert.equal(buf[0], 0xff) + assert.equal(buf[1], 0xd8) + assert.equal(buf[buf.byteLength - 2], 0xff) + assert.equal(buf[buf.byteLength - 1], 0xd9) + }) it('Canvas#toBuffer("image/jpeg", {quality: 0.95})', function () { - var buf = createCanvas(200,200).toBuffer('image/jpeg', {quality: 0.95}); - assert.equal(buf[0], 0xff); - assert.equal(buf[1], 0xd8); - assert.equal(buf[buf.byteLength - 2], 0xff); - assert.equal(buf[buf.byteLength - 1], 0xd9); - }); + const buf = createCanvas(200, 200).toBuffer('image/jpeg', { quality: 0.95 }) + assert.equal(buf[0], 0xff) + assert.equal(buf[1], 0xd8) + assert.equal(buf[buf.byteLength - 2], 0xff) + assert.equal(buf[buf.byteLength - 1], 0xd9) + }) it('Canvas#toBuffer(callback)', function (done) { - createCanvas(200, 200).toBuffer(function(err, buf){ - assert.ok(!err); - assert.equal('PNG', buf.slice(1,4).toString()); - done(); - }); - }); + createCanvas(200, 200).toBuffer(function (err, buf) { + assert.ok(!err) + assert.equal('PNG', buf.slice(1, 4).toString()) + done() + }) + }) it('Canvas#toBuffer(callback, "image/jpeg")', function (done) { - createCanvas(200,200).toBuffer(function (err, buf) { - assert.ok(!err); - assert.equal(buf[0], 0xff); - assert.equal(buf[1], 0xd8); - assert.equal(buf[buf.byteLength - 2], 0xff); - assert.equal(buf[buf.byteLength - 1], 0xd9); - done(); - }, 'image/jpeg'); - }); + createCanvas(200, 200).toBuffer(function (err, buf) { + assert.ok(!err) + assert.equal(buf[0], 0xff) + assert.equal(buf[1], 0xd8) + assert.equal(buf[buf.byteLength - 2], 0xff) + assert.equal(buf[buf.byteLength - 1], 0xd9) + done() + }, 'image/jpeg') + }) it('Canvas#toBuffer(callback, "image/jpeg", {quality: 0.95})', function (done) { - createCanvas(200,200).toBuffer(function (err, buf) { - assert.ok(!err); - assert.equal(buf[0], 0xff); - assert.equal(buf[1], 0xd8); - assert.equal(buf[buf.byteLength - 2], 0xff); - assert.equal(buf[buf.byteLength - 1], 0xd9); - done(); - }, 'image/jpeg', {quality: 0.95}); - }); + createCanvas(200, 200).toBuffer(function (err, buf) { + assert.ok(!err) + assert.equal(buf[0], 0xff) + assert.equal(buf[1], 0xd8) + assert.equal(buf[buf.byteLength - 2], 0xff) + assert.equal(buf[buf.byteLength - 1], 0xd9) + done() + }, 'image/jpeg', { quality: 0.95 }) + }) - describe('#toBuffer("raw")', function() { - var canvas = createCanvas(11, 10) - , ctx = canvas.getContext('2d'); + describe('#toBuffer("raw")', function () { + const canvas = createCanvas(11, 10) + const ctx = canvas.getContext('2d') - ctx.clearRect(0, 0, 11, 10); + ctx.clearRect(0, 0, 11, 10) - ctx.fillStyle = 'rgba(200, 200, 200, 0.505)'; - ctx.fillRect(0, 0, 5, 5); + ctx.fillStyle = 'rgba(200, 200, 200, 0.505)' + ctx.fillRect(0, 0, 5, 5) - ctx.fillStyle = 'red'; - ctx.fillRect(5, 0, 5, 5); + ctx.fillStyle = 'red' + ctx.fillRect(5, 0, 5, 5) - ctx.fillStyle = '#00ff00'; - ctx.fillRect(0, 5, 5, 5); + ctx.fillStyle = '#00ff00' + ctx.fillRect(0, 5, 5, 5) - ctx.fillStyle = 'black'; - ctx.fillRect(5, 5, 4, 5); + ctx.fillStyle = 'black' + ctx.fillRect(5, 5, 4, 5) /** Output: * *****RRRRR- @@ -656,307 +657,307 @@ describe('Canvas', function () { * GGGGGBBBB-- */ - var buf = canvas.toBuffer('raw'); - var stride = canvas.stride; + const buf = canvas.toBuffer('raw') + const stride = canvas.stride - var endianness = os.endianness(); + const endianness = os.endianness() - function assertPixel(u32, x, y, message) { - var expected = '0x' + u32.toString(16); + function assertPixel (u32, x, y, message) { + const expected = '0x' + u32.toString(16) // Buffer doesn't have readUInt32(): it only has readUInt32LE() and // readUInt32BE(). - var px = buf['readUInt32' + endianness](y * stride + x * 4); - var actual = '0x' + px.toString(16); + const px = buf['readUInt32' + endianness](y * stride + x * 4) + const actual = '0x' + px.toString(16) - assert.equal(actual, expected, message); + assert.equal(actual, expected, message) } - it('should have the correct size', function() { - assert.equal(buf.length, stride * 10); - }); - - it('does not premultiply alpha', function() { - assertPixel(0x80646464, 0, 0, 'first semitransparent pixel'); - assertPixel(0x80646464, 4, 4, 'last semitransparent pixel'); - }); - - it('draws red', function() { - assertPixel(0xffff0000, 5, 0, 'first red pixel'); - assertPixel(0xffff0000, 9, 4, 'last red pixel'); - }); - - it('draws green', function() { - assertPixel(0xff00ff00, 0, 5, 'first green pixel'); - assertPixel(0xff00ff00, 4, 9, 'last green pixel'); - }); - - it('draws black', function() { - assertPixel(0xff000000, 5, 5, 'first black pixel'); - assertPixel(0xff000000, 8, 9, 'last black pixel'); - }); - - it('leaves undrawn pixels black, transparent', function() { - assertPixel(0x0, 9, 5, 'first undrawn pixel'); - assertPixel(0x0, 9, 9, 'last undrawn pixel'); - }); - - it('is immutable', function() { - ctx.fillStyle = 'white'; - ctx.fillRect(0, 0, 10, 10); - canvas.toBuffer('raw'); // (side-effect: flushes canvas) - assertPixel(0xffff0000, 5, 0, 'first red pixel'); - }); - }); - }); + it('should have the correct size', function () { + assert.equal(buf.length, stride * 10) + }) + + it('does not premultiply alpha', function () { + assertPixel(0x80646464, 0, 0, 'first semitransparent pixel') + assertPixel(0x80646464, 4, 4, 'last semitransparent pixel') + }) + + it('draws red', function () { + assertPixel(0xffff0000, 5, 0, 'first red pixel') + assertPixel(0xffff0000, 9, 4, 'last red pixel') + }) + + it('draws green', function () { + assertPixel(0xff00ff00, 0, 5, 'first green pixel') + assertPixel(0xff00ff00, 4, 9, 'last green pixel') + }) + + it('draws black', function () { + assertPixel(0xff000000, 5, 5, 'first black pixel') + assertPixel(0xff000000, 8, 9, 'last black pixel') + }) + + it('leaves undrawn pixels black, transparent', function () { + assertPixel(0x0, 9, 5, 'first undrawn pixel') + assertPixel(0x0, 9, 9, 'last undrawn pixel') + }) + + it('is immutable', function () { + ctx.fillStyle = 'white' + ctx.fillRect(0, 0, 10, 10) + canvas.toBuffer('raw') // (side-effect: flushes canvas) + assertPixel(0xffff0000, 5, 0, 'first red pixel') + }) + }) + }) describe('#toDataURL()', function () { - var canvas = createCanvas(200, 200) - , ctx = canvas.getContext('2d'); + const canvas = createCanvas(200, 200) + const ctx = canvas.getContext('2d') - ctx.fillRect(0,0,100,100); - ctx.fillStyle = 'red'; - ctx.fillRect(100,0,100,100); + ctx.fillRect(0, 0, 100, 100) + ctx.fillStyle = 'red' + ctx.fillRect(100, 0, 100, 100) it('toDataURL() works and defaults to PNG', function () { - assert.ok(canvas.toDataURL().startsWith('data:image/png;base64,')); - }); + assert.ok(canvas.toDataURL().startsWith('data:image/png;base64,')) + }) it('toDataURL(0.5) works and defaults to PNG', function () { - assert.ok(canvas.toDataURL(0.5).startsWith('data:image/png;base64,')); - }); + assert.ok(canvas.toDataURL(0.5).startsWith('data:image/png;base64,')) + }) it('toDataURL(undefined) works and defaults to PNG', function () { - assert.ok(canvas.toDataURL(undefined).startsWith('data:image/png;base64,')); - }); + assert.ok(canvas.toDataURL(undefined).startsWith('data:image/png;base64,')) + }) it('toDataURL("image/png") works', function () { - assert.ok(canvas.toDataURL('image/png').startsWith('data:image/png;base64,')); - }); + assert.ok(canvas.toDataURL('image/png').startsWith('data:image/png;base64,')) + }) it('toDataURL("image/png", 0.5) works', function () { - assert.ok(canvas.toDataURL('image/png').startsWith('data:image/png;base64,')); - }); + assert.ok(canvas.toDataURL('image/png').startsWith('data:image/png;base64,')) + }) it('toDataURL("iMaGe/PNg") works', function () { - assert.ok(canvas.toDataURL('iMaGe/PNg').startsWith('data:image/png;base64,')); - }); + assert.ok(canvas.toDataURL('iMaGe/PNg').startsWith('data:image/png;base64,')) + }) it('toDataURL("image/jpeg") works', function () { - assert.ok(canvas.toDataURL('image/jpeg').startsWith('data:image/jpeg;base64,')); - }); + assert.ok(canvas.toDataURL('image/jpeg').startsWith('data:image/jpeg;base64,')) + }) it('toDataURL(function (err, str) {...}) works and defaults to PNG', function (done) { - createCanvas(200,200).toDataURL(function(err, str){ - assert.ifError(err); - assert.ok(0 === str.indexOf('data:image/png;base64,')); - done(); - }); - }); + createCanvas(200, 200).toDataURL(function (err, str) { + assert.ifError(err) + assert.ok(str.indexOf('data:image/png;base64,') === 0) + done() + }) + }) it('toDataURL(function (err, str) {...}) is async even with no canvas data', function (done) { - createCanvas().toDataURL(function(err, str){ - assert.ifError(err); - assert.ok('data:,' === str); - done(); - }); - }); + createCanvas().toDataURL(function (err, str) { + assert.ifError(err) + assert.ok(str === 'data:,') + done() + }) + }) it('toDataURL(0.5, function (err, str) {...}) works and defaults to PNG', function (done) { - createCanvas(200,200).toDataURL(0.5, function(err, str){ - assert.ifError(err); - assert.ok(0 === str.indexOf('data:image/png;base64,')); - done(); - }); - }); + createCanvas(200, 200).toDataURL(0.5, function (err, str) { + assert.ifError(err) + assert.ok(str.indexOf('data:image/png;base64,') === 0) + done() + }) + }) it('toDataURL(undefined, function (err, str) {...}) works and defaults to PNG', function (done) { - createCanvas(200,200).toDataURL(undefined, function(err, str){ - assert.ifError(err); - assert.ok(0 === str.indexOf('data:image/png;base64,')); - done(); - }); - }); + createCanvas(200, 200).toDataURL(undefined, function (err, str) { + assert.ifError(err) + assert.ok(str.indexOf('data:image/png;base64,') === 0) + done() + }) + }) it('toDataURL("image/png", function (err, str) {...}) works', function (done) { - createCanvas(200,200).toDataURL('image/png', function(err, str){ - assert.ifError(err); - assert.ok(0 === str.indexOf('data:image/png;base64,')); - done(); - }); - }); + createCanvas(200, 200).toDataURL('image/png', function (err, str) { + assert.ifError(err) + assert.ok(str.indexOf('data:image/png;base64,') === 0) + done() + }) + }) it('toDataURL("image/png", 0.5, function (err, str) {...}) works', function (done) { - createCanvas(200,200).toDataURL('image/png', 0.5, function(err, str){ - assert.ifError(err); - assert.ok(0 === str.indexOf('data:image/png;base64,')); - done(); - }); - }); + createCanvas(200, 200).toDataURL('image/png', 0.5, function (err, str) { + assert.ifError(err) + assert.ok(str.indexOf('data:image/png;base64,') === 0) + done() + }) + }) it('toDataURL("image/png", {}) works', function () { - assert.ok(canvas.toDataURL('image/png', {}).startsWith('data:image/png;base64,')); - }); + assert.ok(canvas.toDataURL('image/png', {}).startsWith('data:image/png;base64,')) + }) it('toDataURL("image/jpeg", {}) works', function () { - assert.ok(canvas.toDataURL('image/jpeg', {}).startsWith('data:image/jpeg;base64,')); - }); + assert.ok(canvas.toDataURL('image/jpeg', {}).startsWith('data:image/jpeg;base64,')) + }) it('toDataURL("image/jpeg", function (err, str) {...}) works', function (done) { - createCanvas(200,200).toDataURL('image/jpeg', function(err, str){ - assert.ifError(err); - assert.ok(0 === str.indexOf('data:image/jpeg;base64,')); - done(); - }); - }); + createCanvas(200, 200).toDataURL('image/jpeg', function (err, str) { + assert.ifError(err) + assert.ok(str.indexOf('data:image/jpeg;base64,') === 0) + done() + }) + }) it('toDataURL("iMAge/JPEG", function (err, str) {...}) works', function (done) { - createCanvas(200,200).toDataURL('iMAge/JPEG', function(err, str){ - assert.ifError(err); - assert.ok(0 === str.indexOf('data:image/jpeg;base64,')); - done(); - }); - }); + createCanvas(200, 200).toDataURL('iMAge/JPEG', function (err, str) { + assert.ifError(err) + assert.ok(str.indexOf('data:image/jpeg;base64,') === 0) + done() + }) + }) it('toDataURL("image/jpeg", undefined, function (err, str) {...}) works', function (done) { - createCanvas(200,200).toDataURL('image/jpeg', undefined, function(err, str){ - assert.ifError(err); - assert.ok(0 === str.indexOf('data:image/jpeg;base64,')); - done(); - }); - }); + createCanvas(200, 200).toDataURL('image/jpeg', undefined, function (err, str) { + assert.ifError(err) + assert.ok(str.indexOf('data:image/jpeg;base64,') === 0) + done() + }) + }) it('toDataURL("image/jpeg", 0.5, function (err, str) {...}) works', function (done) { - createCanvas(200,200).toDataURL('image/jpeg', 0.5, function(err, str){ - assert.ifError(err); - assert.ok(0 === str.indexOf('data:image/jpeg;base64,')); - done(); - }); - }); + createCanvas(200, 200).toDataURL('image/jpeg', 0.5, function (err, str) { + assert.ifError(err) + assert.ok(str.indexOf('data:image/jpeg;base64,') === 0) + done() + }) + }) it('toDataURL("image/jpeg", opts, function (err, str) {...}) works', function (done) { - createCanvas(200,200).toDataURL('image/jpeg', {quality: 100}, function(err, str){ - assert.ifError(err); - assert.ok(0 === str.indexOf('data:image/jpeg;base64,')); - done(); - }); - }); - }); + createCanvas(200, 200).toDataURL('image/jpeg', { quality: 100 }, function (err, str) { + assert.ifError(err) + assert.ok(str.indexOf('data:image/jpeg;base64,') === 0) + done() + }) + }) + }) describe('Context2d#createImageData(width, height)', function () { - it("works", function () { - var canvas = createCanvas(20, 20) - , ctx = canvas.getContext('2d'); - - var imageData = ctx.createImageData(2,6); - assert.equal(2, imageData.width); - assert.equal(6, imageData.height); - assert.equal(2 * 6 * 4, imageData.data.length); - - assert.equal(0, imageData.data[0]); - assert.equal(0, imageData.data[1]); - assert.equal(0, imageData.data[2]); - assert.equal(0, imageData.data[3]); - }); - - it("works, A8 format", function () { - var canvas = createCanvas(20, 20) - , ctx = canvas.getContext('2d', {pixelFormat: "A8"}); - - var imageData = ctx.createImageData(2,6); - assert.equal(2, imageData.width); - assert.equal(6, imageData.height); - assert.equal(2 * 6 * 1, imageData.data.length); - - assert.equal(0, imageData.data[0]); - assert.equal(0, imageData.data[1]); - assert.equal(0, imageData.data[2]); - assert.equal(0, imageData.data[3]); - }); - - it("works, A1 format", function () { - var canvas = createCanvas(20, 20) - , ctx = canvas.getContext('2d', {pixelFormat: "A1"}); - - var imageData = ctx.createImageData(2,6); - assert.equal(2, imageData.width); - assert.equal(6, imageData.height); - assert.equal(Math.ceil(2 * 6 / 8), imageData.data.length); - - assert.equal(0, imageData.data[0]); - assert.equal(0, imageData.data[1]); - }); - - it("works, RGB24 format", function () { - var canvas = createCanvas(20, 20) - , ctx = canvas.getContext('2d', {pixelFormat: "RGB24"}); - - var imageData = ctx.createImageData(2,6); - assert.equal(2, imageData.width); - assert.equal(6, imageData.height); - assert.equal(2 * 6 * 4, imageData.data.length); - - assert.equal(0, imageData.data[0]); - assert.equal(0, imageData.data[1]); - assert.equal(0, imageData.data[2]); - assert.equal(0, imageData.data[3]); - }); - - it("works, RGB16_565 format", function () { - var canvas = createCanvas(20, 20) - , ctx = canvas.getContext('2d', {pixelFormat: "RGB16_565"}); - - var imageData = ctx.createImageData(2,6); - assert(imageData.data instanceof Uint16Array); - assert.equal(2, imageData.width); - assert.equal(6, imageData.height); - assert.equal(2 * 6, imageData.data.length); - - assert.equal(0, imageData.data[0]); - assert.equal(0, imageData.data[1]); - }); - }); + it('works', function () { + const canvas = createCanvas(20, 20) + const ctx = canvas.getContext('2d') + + const imageData = ctx.createImageData(2, 6) + assert.equal(2, imageData.width) + assert.equal(6, imageData.height) + assert.equal(2 * 6 * 4, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + }) + + it('works, A8 format', function () { + const canvas = createCanvas(20, 20) + const ctx = canvas.getContext('2d', { pixelFormat: 'A8' }) + + const imageData = ctx.createImageData(2, 6) + assert.equal(2, imageData.width) + assert.equal(6, imageData.height) + assert.equal(2 * 6 * 1, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + }) + + it('works, A1 format', function () { + const canvas = createCanvas(20, 20) + const ctx = canvas.getContext('2d', { pixelFormat: 'A1' }) + + const imageData = ctx.createImageData(2, 6) + assert.equal(2, imageData.width) + assert.equal(6, imageData.height) + assert.equal(Math.ceil(2 * 6 / 8), imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + }) + + it('works, RGB24 format', function () { + const canvas = createCanvas(20, 20) + const ctx = canvas.getContext('2d', { pixelFormat: 'RGB24' }) + + const imageData = ctx.createImageData(2, 6) + assert.equal(2, imageData.width) + assert.equal(6, imageData.height) + assert.equal(2 * 6 * 4, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + }) + + it('works, RGB16_565 format', function () { + const canvas = createCanvas(20, 20) + const ctx = canvas.getContext('2d', { pixelFormat: 'RGB16_565' }) + + const imageData = ctx.createImageData(2, 6) + assert(imageData.data instanceof Uint16Array) + assert.equal(2, imageData.width) + assert.equal(6, imageData.height) + assert.equal(2 * 6, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + }) + }) describe('Context2d#measureText()', function () { it('Context2d#measureText().width', function () { - var canvas = createCanvas(20, 20) - , ctx = canvas.getContext('2d'); + const canvas = createCanvas(20, 20) + const ctx = canvas.getContext('2d') - assert.ok(ctx.measureText('foo').width); - assert.ok(ctx.measureText('foo').width != ctx.measureText('foobar').width); - assert.ok(ctx.measureText('foo').width != ctx.measureText(' foo').width); - }); + assert.ok(ctx.measureText('foo').width) + assert.ok(ctx.measureText('foo').width !== ctx.measureText('foobar').width) + assert.ok(ctx.measureText('foo').width !== ctx.measureText(' foo').width) + }) it('works', function () { - var canvas = createCanvas(20, 20) - var ctx = canvas.getContext('2d') - ctx.font = "20px Arial" + const canvas = createCanvas(20, 20) + const ctx = canvas.getContext('2d') + ctx.font = '20px Arial' - ctx.textBaseline = "alphabetic" - var metrics = ctx.measureText("Alphabet") + ctx.textBaseline = 'alphabetic' + let metrics = ctx.measureText('Alphabet') // Actual value depends on font library version. Have observed values // between 0 and 0.769. - assert.ok(metrics.alphabeticBaseline >= 0 && metrics.alphabeticBaseline <= 1); + assert.ok(metrics.alphabeticBaseline >= 0 && metrics.alphabeticBaseline <= 1) // Positive = going up from the baseline assert.ok(metrics.actualBoundingBoxAscent > 0) // Positive = going down from the baseline assert.ok(metrics.actualBoundingBoxDescent > 0) // ~4-5 - ctx.textBaseline = "bottom" - metrics = ctx.measureText("Alphabet") - assert.strictEqual(ctx.textBaseline, "bottom") + ctx.textBaseline = 'bottom' + metrics = ctx.measureText('Alphabet') + assert.strictEqual(ctx.textBaseline, 'bottom') assert.ok(metrics.alphabeticBaseline > 0) // ~4-5 assert.ok(metrics.actualBoundingBoxAscent > 0) // On the baseline or slightly above assert.ok(metrics.actualBoundingBoxDescent <= 0) - }); - }); + }) + }) it('Context2d#fillText()', function () { [ [['A', 10, 10], true], [['A', 10, 10, undefined], true], - [['A', 10, 10, NaN], false], + [['A', 10, 10, NaN], false] ].forEach(([args, shouldDraw]) => { const canvas = createCanvas(20, 20) const ctx = canvas.getContext('2d') @@ -970,297 +971,297 @@ describe('Canvas', function () { }) it('Context2d#currentTransform', function () { - var canvas = createCanvas(20, 20); - var ctx = canvas.getContext('2d'); - - ctx.scale(0.1, 0.3); - var mat1 = ctx.currentTransform; - assert.equal(mat1.a, 0.1); - assert.equal(mat1.b, 0); - assert.equal(mat1.c, 0); - assert.equal(mat1.d, 0.3); - assert.equal(mat1.e, 0); - assert.equal(mat1.f, 0); - - ctx.resetTransform(); - var mat2 = ctx.currentTransform; - assert.equal(mat2.a, 1); - assert.equal(mat2.d, 1); - - ctx.currentTransform = mat1; - var mat3 = ctx.currentTransform; - assert.equal(mat3.a, 0.1); - assert.equal(mat3.d, 0.3); - - assert.deepEqual(ctx.currentTransform, ctx.getTransform()); - - ctx.setTransform(ctx.getTransform()); - assert.deepEqual(mat3, ctx.getTransform()); - - ctx.setTransform(mat3.a, mat3.b, mat3.c, mat3.d, mat3.e, mat3.f); - assert.deepEqual(mat3, ctx.getTransform()); - }); + const canvas = createCanvas(20, 20) + const ctx = canvas.getContext('2d') + + ctx.scale(0.1, 0.3) + const mat1 = ctx.currentTransform + assert.equal(mat1.a, 0.1) + assert.equal(mat1.b, 0) + assert.equal(mat1.c, 0) + assert.equal(mat1.d, 0.3) + assert.equal(mat1.e, 0) + assert.equal(mat1.f, 0) + + ctx.resetTransform() + const mat2 = ctx.currentTransform + assert.equal(mat2.a, 1) + assert.equal(mat2.d, 1) + + ctx.currentTransform = mat1 + const mat3 = ctx.currentTransform + assert.equal(mat3.a, 0.1) + assert.equal(mat3.d, 0.3) + + assert.deepEqual(ctx.currentTransform, ctx.getTransform()) + + ctx.setTransform(ctx.getTransform()) + assert.deepEqual(mat3, ctx.getTransform()) + + ctx.setTransform(mat3.a, mat3.b, mat3.c, mat3.d, mat3.e, mat3.f) + assert.deepEqual(mat3, ctx.getTransform()) + }) it('Context2d#createImageData(ImageData)', function () { - var canvas = createCanvas(20, 20) - , ctx = canvas.getContext('2d'); + const canvas = createCanvas(20, 20) + const ctx = canvas.getContext('2d') - var imageData = ctx.createImageData(ctx.createImageData(2, 6)); - assert.equal(2, imageData.width); - assert.equal(6, imageData.height); - assert.equal(2 * 6 * 4, imageData.data.length); - }); + const imageData = ctx.createImageData(ctx.createImageData(2, 6)) + assert.equal(2, imageData.width) + assert.equal(6, imageData.height) + assert.equal(2 * 6 * 4, imageData.data.length) + }) describe('Context2d#getImageData()', function () { - function createTestCanvas(useAlpha, attributes) { - var canvas = createCanvas(3, 6); - var ctx = canvas.getContext('2d', attributes); + function createTestCanvas (useAlpha, attributes) { + const canvas = createCanvas(3, 6) + const ctx = canvas.getContext('2d', attributes) - ctx.fillStyle = useAlpha ? 'rgba(255,0,0,0.25)' : '#f00'; - ctx.fillRect(0,0,1,6); + ctx.fillStyle = useAlpha ? 'rgba(255,0,0,0.25)' : '#f00' + ctx.fillRect(0, 0, 1, 6) - ctx.fillStyle = useAlpha ? 'rgba(0,255,0,0.5)' : '#0f0'; - ctx.fillRect(1,0,1,6); + ctx.fillStyle = useAlpha ? 'rgba(0,255,0,0.5)' : '#0f0' + ctx.fillRect(1, 0, 1, 6) - ctx.fillStyle = useAlpha ? 'rgba(0,0,255,0.75)' : '#00f'; - ctx.fillRect(2,0,1,6); + ctx.fillStyle = useAlpha ? 'rgba(0,0,255,0.75)' : '#00f' + ctx.fillRect(2, 0, 1, 6) - return ctx; + return ctx } - it("works, full width, RGBA32", function () { - var ctx = createTestCanvas(); - var imageData = ctx.getImageData(0,0,3,6); - - assert.equal(3, imageData.width); - assert.equal(6, imageData.height); - assert.equal(3 * 6 * 4, imageData.data.length); - - assert.equal(255, imageData.data[0]); - assert.equal(0, imageData.data[1]); - assert.equal(0, imageData.data[2]); - assert.equal(255, imageData.data[3]); - - assert.equal(0, imageData.data[4]); - assert.equal(255, imageData.data[5]); - assert.equal(0, imageData.data[6]); - assert.equal(255, imageData.data[7]); - - assert.equal(0, imageData.data[8]); - assert.equal(0, imageData.data[9]); - assert.equal(255, imageData.data[10]); - assert.equal(255, imageData.data[11]); - }); - - it("works, full width, RGB24", function () { - var ctx = createTestCanvas(false, {pixelFormat: "RGB24"}); - var imageData = ctx.getImageData(0,0,3,6); - assert.equal(3, imageData.width); - assert.equal(6, imageData.height); - assert.equal(3 * 6 * 4, imageData.data.length); - - assert.equal(255, imageData.data[0]); - assert.equal(0, imageData.data[1]); - assert.equal(0, imageData.data[2]); - assert.equal(255, imageData.data[3]); - - assert.equal(0, imageData.data[4]); - assert.equal(255, imageData.data[5]); - assert.equal(0, imageData.data[6]); - assert.equal(255, imageData.data[7]); - - assert.equal(0, imageData.data[8]); - assert.equal(0, imageData.data[9]); - assert.equal(255, imageData.data[10]); - assert.equal(255, imageData.data[11]); - }); - - it("works, full width, RGB16_565", function () { - var ctx = createTestCanvas(false, {pixelFormat: "RGB16_565"}); - var imageData = ctx.getImageData(0,0,3,6); - assert.equal(3, imageData.width); - assert.equal(6, imageData.height); - assert.equal(3 * 6, imageData.data.length); - - assert.equal((255 & 0b11111) << 11, imageData.data[0]); - assert.equal((255 & 0b111111) << 5, imageData.data[1]); - assert.equal((255 & 0b11111), imageData.data[2]); - - assert.equal((255 & 0b11111) << 11, imageData.data[3]); - assert.equal((255 & 0b111111) << 5, imageData.data[4]); - assert.equal((255 & 0b11111), imageData.data[5]); - }); - - it("works, full width, A8", function () { - var ctx = createTestCanvas(true, {pixelFormat: "A8"}); - var imageData = ctx.getImageData(0,0,3,6); - assert.equal(3, imageData.width); - assert.equal(6, imageData.height); - assert.equal(3 * 6, imageData.data.length); - - assert.equal(63, imageData.data[0]); - assert.equal(127, imageData.data[1]); - assert.equal(191, imageData.data[2]); - - assert.equal(63, imageData.data[3]); - assert.equal(127, imageData.data[4]); - assert.equal(191, imageData.data[5]); - }); - - it("works, full width, A1"); - - it("works, full width, RGB30"); - - it("works, slice, RGBA32", function () { - var ctx = createTestCanvas(); - var imageData = ctx.getImageData(0,0,2,1); - assert.equal(2, imageData.width); - assert.equal(1, imageData.height); - assert.equal(8, imageData.data.length); - - assert.equal(255, imageData.data[0]); - assert.equal(0, imageData.data[1]); - assert.equal(0, imageData.data[2]); - assert.equal(255, imageData.data[3]); - - assert.equal(0, imageData.data[4]); - assert.equal(255, imageData.data[5]); - assert.equal(0, imageData.data[6]); - assert.equal(255, imageData.data[7]); - }); - - it("works, slice, RGB24", function () { - var ctx = createTestCanvas(false, {pixelFormat: "RGB24"}); - var imageData = ctx.getImageData(0,0,2,1); - assert.equal(2, imageData.width); - assert.equal(1, imageData.height); - assert.equal(8, imageData.data.length); - - assert.equal(255, imageData.data[0]); - assert.equal(0, imageData.data[1]); - assert.equal(0, imageData.data[2]); - assert.equal(255, imageData.data[3]); - - assert.equal(0, imageData.data[4]); - assert.equal(255, imageData.data[5]); - assert.equal(0, imageData.data[6]); - assert.equal(255, imageData.data[7]); - }); - - it("works, slice, RGB16_565", function () { - var ctx = createTestCanvas(false, {pixelFormat: "RGB16_565"}); - var imageData = ctx.getImageData(0,0,2,1); - assert.equal(2, imageData.width); - assert.equal(1, imageData.height); - assert.equal(2 * 1, imageData.data.length); - - assert.equal((255 & 0b11111) << 11, imageData.data[0]); - assert.equal((255 & 0b111111) << 5, imageData.data[1]); - }); - - it("works, slice, A8", function () { - var ctx = createTestCanvas(true, {pixelFormat: "A8"}); - var imageData = ctx.getImageData(0,0,2,1); - assert.equal(2, imageData.width); - assert.equal(1, imageData.height); - assert.equal(2 * 1, imageData.data.length); - - assert.equal(63, imageData.data[0]); - assert.equal(127, imageData.data[1]); - }); - - it("works, slice, A1"); - - it("works, slice, RGB30"); - - it("works, assignment", function () { - var ctx = createTestCanvas(); - var data = ctx.getImageData(0,0,5,5).data; - data[0] = 50; - assert.equal(50, data[0]); - data[0] = 280; - assert.equal(255, data[0]); - data[0] = -4444; - assert.equal(0, data[0]); - }); - - it("throws if indexes are invalid", function () { - var ctx = createTestCanvas(); - assert.throws(function () { ctx.getImageData(0, 0, 0, 0); }, /IndexSizeError/); - }); - }); + it('works, full width, RGBA32', function () { + const ctx = createTestCanvas() + const imageData = ctx.getImageData(0, 0, 3, 6) + + assert.equal(3, imageData.width) + assert.equal(6, imageData.height) + assert.equal(3 * 6 * 4, imageData.data.length) + + assert.equal(255, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(255, imageData.data[3]) + + assert.equal(0, imageData.data[4]) + assert.equal(255, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + + assert.equal(0, imageData.data[8]) + assert.equal(0, imageData.data[9]) + assert.equal(255, imageData.data[10]) + assert.equal(255, imageData.data[11]) + }) + + it('works, full width, RGB24', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB24' }) + const imageData = ctx.getImageData(0, 0, 3, 6) + assert.equal(3, imageData.width) + assert.equal(6, imageData.height) + assert.equal(3 * 6 * 4, imageData.data.length) + + assert.equal(255, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(255, imageData.data[3]) + + assert.equal(0, imageData.data[4]) + assert.equal(255, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + + assert.equal(0, imageData.data[8]) + assert.equal(0, imageData.data[9]) + assert.equal(255, imageData.data[10]) + assert.equal(255, imageData.data[11]) + }) + + it('works, full width, RGB16_565', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB16_565' }) + const imageData = ctx.getImageData(0, 0, 3, 6) + assert.equal(3, imageData.width) + assert.equal(6, imageData.height) + assert.equal(3 * 6, imageData.data.length) + + assert.equal((255 & 0b11111) << 11, imageData.data[0]) + assert.equal((255 & 0b111111) << 5, imageData.data[1]) + assert.equal((255 & 0b11111), imageData.data[2]) + + assert.equal((255 & 0b11111) << 11, imageData.data[3]) + assert.equal((255 & 0b111111) << 5, imageData.data[4]) + assert.equal((255 & 0b11111), imageData.data[5]) + }) + + it('works, full width, A8', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + const imageData = ctx.getImageData(0, 0, 3, 6) + assert.equal(3, imageData.width) + assert.equal(6, imageData.height) + assert.equal(3 * 6, imageData.data.length) + + assert.equal(63, imageData.data[0]) + assert.equal(127, imageData.data[1]) + assert.equal(191, imageData.data[2]) + + assert.equal(63, imageData.data[3]) + assert.equal(127, imageData.data[4]) + assert.equal(191, imageData.data[5]) + }) + + it('works, full width, A1') + + it('works, full width, RGB30') + + it('works, slice, RGBA32', function () { + const ctx = createTestCanvas() + const imageData = ctx.getImageData(0, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(255, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(255, imageData.data[3]) + + assert.equal(0, imageData.data[4]) + assert.equal(255, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + }) + + it('works, slice, RGB24', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB24' }) + const imageData = ctx.getImageData(0, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(255, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(255, imageData.data[3]) + + assert.equal(0, imageData.data[4]) + assert.equal(255, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + }) + + it('works, slice, RGB16_565', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB16_565' }) + const imageData = ctx.getImageData(0, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(2 * 1, imageData.data.length) + + assert.equal((255 & 0b11111) << 11, imageData.data[0]) + assert.equal((255 & 0b111111) << 5, imageData.data[1]) + }) + + it('works, slice, A8', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + const imageData = ctx.getImageData(0, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(2 * 1, imageData.data.length) + + assert.equal(63, imageData.data[0]) + assert.equal(127, imageData.data[1]) + }) + + it('works, slice, A1') + + it('works, slice, RGB30') + + it('works, assignment', function () { + const ctx = createTestCanvas() + const data = ctx.getImageData(0, 0, 5, 5).data + data[0] = 50 + assert.equal(50, data[0]) + data[0] = 280 + assert.equal(255, data[0]) + data[0] = -4444 + assert.equal(0, data[0]) + }) + + it('throws if indexes are invalid', function () { + const ctx = createTestCanvas() + assert.throws(function () { ctx.getImageData(0, 0, 0, 0) }, /IndexSizeError/) + }) + }) it('Context2d#createPattern(Canvas)', function () { - var pattern = createCanvas(2,2) - , checkers = pattern.getContext('2d'); + let pattern = createCanvas(2, 2) + const checkers = pattern.getContext('2d') // white - checkers.fillStyle = '#fff'; - checkers.fillRect(0,0,2,2); + checkers.fillStyle = '#fff' + checkers.fillRect(0, 0, 2, 2) // black - checkers.fillStyle = '#000'; - checkers.fillRect(0,0,1,1); - checkers.fillRect(1,1,1,1); + checkers.fillStyle = '#000' + checkers.fillRect(0, 0, 1, 1) + checkers.fillRect(1, 1, 1, 1) - var imageData = checkers.getImageData(0,0,2,2); - assert.equal(2, imageData.width); - assert.equal(2, imageData.height); - assert.equal(16, imageData.data.length); + let imageData = checkers.getImageData(0, 0, 2, 2) + assert.equal(2, imageData.width) + assert.equal(2, imageData.height) + assert.equal(16, imageData.data.length) // (0,0) black - assert.equal(0, imageData.data[0]); - assert.equal(0, imageData.data[1]); - assert.equal(0, imageData.data[2]); - assert.equal(255, imageData.data[3]); + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(255, imageData.data[3]) // (1,0) white - assert.equal(255, imageData.data[4]); - assert.equal(255, imageData.data[5]); - assert.equal(255, imageData.data[6]); - assert.equal(255, imageData.data[7]); + assert.equal(255, imageData.data[4]) + assert.equal(255, imageData.data[5]) + assert.equal(255, imageData.data[6]) + assert.equal(255, imageData.data[7]) // (0,1) white - assert.equal(255, imageData.data[8]); - assert.equal(255, imageData.data[9]); - assert.equal(255, imageData.data[10]); - assert.equal(255, imageData.data[11]); + assert.equal(255, imageData.data[8]) + assert.equal(255, imageData.data[9]) + assert.equal(255, imageData.data[10]) + assert.equal(255, imageData.data[11]) // (1,1) black - assert.equal(0, imageData.data[12]); - assert.equal(0, imageData.data[13]); - assert.equal(0, imageData.data[14]); - assert.equal(255, imageData.data[15]); - - var canvas = createCanvas(20, 20) - , ctx = canvas.getContext('2d') - , pattern = ctx.createPattern(pattern); - - ctx.fillStyle = pattern; - ctx.fillRect(0,0,20,20); - - var imageData = ctx.getImageData(0,0,20,20); - assert.equal(20, imageData.width); - assert.equal(20, imageData.height); - assert.equal(1600, imageData.data.length); - - var i=0, b = true; - while(i { - var canvas = createCanvas(20, 20) - , ctx = canvas.getContext('2d') - , pattern = ctx.createPattern(img); + return loadImage(path.join(__dirname, '/fixtures/checkers.png')).then((img) => { + const canvas = createCanvas(20, 20) + const ctx = canvas.getContext('2d') + const pattern = ctx.createPattern(img) - ctx.fillStyle = pattern; - ctx.fillRect(0,0,20,20); + ctx.fillStyle = pattern + ctx.fillRect(0, 0, 20, 20) - var imageData = ctx.getImageData(0,0,20,20); - assert.equal(20, imageData.width); - assert.equal(20, imageData.height); - assert.equal(1600, imageData.data.length); + const imageData = ctx.getImageData(0, 0, 20, 20) + assert.equal(20, imageData.width) + assert.equal(20, imageData.height) + assert.equal(1600, imageData.data.length) - var i=0, b = true; - while (i= 6) - obj[Symbol.toPrimitive] = function () { return 0.89; }; - else - obj.valueOf = function () { return 0.89; }; - ctx.resetTransform(); - testAngle(obj, 0.89); + const obj = Object.create(null) + if (+process.version.match(/\d+/) >= 6) { obj[Symbol.toPrimitive] = function () { return 0.89 } } else { obj.valueOf = function () { return 0.89 } } + ctx.resetTransform() + testAngle(obj, 0.89) // NaN - ctx.resetTransform(); - ctx.rotate(0.91); - testAngle(NaN, 0.91); + ctx.resetTransform() + ctx.rotate(0.91) + testAngle(NaN, 0.91) // Infinite value - ctx.resetTransform(); - ctx.rotate(0.94); - testAngle(-Infinity, 0.94); + ctx.resetTransform() + ctx.rotate(0.94) + testAngle(-Infinity, 0.94) - function testAngle(angle, expected){ - ctx.rotate(angle); + function testAngle (angle, expected) { + ctx.rotate(angle) - var mat = ctx.currentTransform; - var sin = Math.sin(expected); - var cos = Math.cos(expected); + const mat = ctx.currentTransform + const sin = Math.sin(expected) + const cos = Math.cos(expected) - assert.ok(Math.abs(mat.m11 - cos) < Number.EPSILON); - assert.ok(Math.abs(mat.m12 - sin) < Number.EPSILON); - assert.ok(Math.abs(mat.m21 + sin) < Number.EPSILON); - assert.ok(Math.abs(mat.m22 - cos) < Number.EPSILON); + assert.ok(Math.abs(mat.m11 - cos) < Number.EPSILON) + assert.ok(Math.abs(mat.m12 - sin) < Number.EPSILON) + assert.ok(Math.abs(mat.m21 + sin) < Number.EPSILON) + assert.ok(Math.abs(mat.m22 - cos) < Number.EPSILON) } - }); + }) it('Context2d#drawImage()', function () { - var canvas = createCanvas(500, 500); - var ctx = canvas.getContext('2d'); + const canvas = createCanvas(500, 500) + const ctx = canvas.getContext('2d') // Drawing canvas to itself - ctx.fillStyle = 'white'; - ctx.fillRect(0, 0, 500, 500); - ctx.fillStyle = 'black'; - ctx.fillRect(5, 5, 10, 10); - ctx.drawImage(canvas, 20, 20); - - var imgd = ctx.getImageData(0, 0, 500, 500); - var data = imgd.data; - var count = 0; - - for(var i = 0; i < 500 * 500 * 4; i += 4){ - if(data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0) - count++; + ctx.fillStyle = 'white' + ctx.fillRect(0, 0, 500, 500) + ctx.fillStyle = 'black' + ctx.fillRect(5, 5, 10, 10) + ctx.drawImage(canvas, 20, 20) + + let imgd = ctx.getImageData(0, 0, 500, 500) + let data = imgd.data + let count = 0 + + for (let i = 0; i < 500 * 500 * 4; i += 4) { + if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0) { count++ } } - assert.strictEqual(count, 10 * 10 * 2); + assert.strictEqual(count, 10 * 10 * 2) // Drawing zero-width image - ctx.drawImage(canvas, 0, 0, 0, 0, 0, 0, 0, 0); - ctx.drawImage(canvas, 0, 0, 0, 0, 1, 1, 1, 1); - ctx.drawImage(canvas, 1, 1, 1, 1, 0, 0, 0, 0); - ctx.fillStyle = 'white'; - ctx.fillRect(0, 0, 500, 500); - - imgd = ctx.getImageData(0, 0, 500, 500); - data = imgd.data; - count = 0; - - for(i = 0; i < 500 * 500 * 4; i += 4){ - if(data[i] === 255 && data[i + 1] === 255 && data[i + 2] === 255) - count++; + ctx.drawImage(canvas, 0, 0, 0, 0, 0, 0, 0, 0) + ctx.drawImage(canvas, 0, 0, 0, 0, 1, 1, 1, 1) + ctx.drawImage(canvas, 1, 1, 1, 1, 0, 0, 0, 0) + ctx.fillStyle = 'white' + ctx.fillRect(0, 0, 500, 500) + + imgd = ctx.getImageData(0, 0, 500, 500) + data = imgd.data + count = 0 + + for (let i = 0; i < 500 * 500 * 4; i += 4) { + if (data[i] === 255 && data[i + 1] === 255 && data[i + 2] === 255) { count++ } } - assert.strictEqual(count, 500 * 500); - }); + assert.strictEqual(count, 500 * 500) + }) it('Context2d#SetFillColor()', function () { - var canvas = createCanvas(2, 2); - var ctx = canvas.getContext('2d'); + const canvas = createCanvas(2, 2) + const ctx = canvas.getContext('2d') - ctx.fillStyle = '#808080'; - ctx.fillRect(0, 0, 2, 2); - var data = ctx.getImageData(0, 0, 2, 2).data; + ctx.fillStyle = '#808080' + ctx.fillRect(0, 0, 2, 2) + const data = ctx.getImageData(0, 0, 2, 2).data data.forEach(function (byte, index) { - if (index + 1 & 3) - assert.strictEqual(byte, 128); - else - assert.strictEqual(byte, 255); - }); - }); - -}); + if (index + 1 & 3) { assert.strictEqual(byte, 128) } else { assert.strictEqual(byte, 255) } + }) + }) +}) diff --git a/test/dommatrix.test.js b/test/dommatrix.test.js index 8e1571713..2c29e73eb 100644 --- a/test/dommatrix.test.js +++ b/test/dommatrix.test.js @@ -2,34 +2,34 @@ 'use stricit' -const DOMMatrix = require('../').DOMMatrix +const {DOMMatrix} = require('../') const assert = require('assert') // This doesn't need to be precise; we're not testing the engine's trig // implementations. const TOLERANCE = 0.001 -function assertApprox(actual, expected, tolerance) { +function assertApprox (actual, expected, tolerance) { if (typeof tolerance !== 'number') tolerance = TOLERANCE assert.ok(expected > actual - tolerance && expected < actual + tolerance, `Expected ${expected} to equal ${actual} +/- ${tolerance}`) } -function assertApproxDeep(actual, expected, tolerance) { +function assertApproxDeep (actual, expected, tolerance) { expected.forEach(function (value, index) { assertApprox(actual[index], value) }) } describe('DOMMatrix', function () { - var Avals = [4,5,1,8, 0,3,6,1, 3,5,0,9, 2,4,6,1] - var Bvals = [1,5,1,0, 0,3,6,1, 3,5,7,2, 2,0,6,1] - var Xvals = [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,0] - var AxB = new Float64Array([7,25,31,22, 20,43,24,58, 37,73,45,94, 28,44,8,71]) - var BxA = new Float64Array([23,40,89,15, 20,39,66,16, 21,30,87,14, 22,52,74,17]) + const Avals = [4, 5, 1, 8, 0, 3, 6, 1, 3, 5, 0, 9, 2, 4, 6, 1] + const Bvals = [1, 5, 1, 0, 0, 3, 6, 1, 3, 5, 7, 2, 2, 0, 6, 1] + const Xvals = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0] + const AxB = new Float64Array([7, 25, 31, 22, 20, 43, 24, 58, 37, 73, 45, 94, 28, 44, 8, 71]) + const BxA = new Float64Array([23, 40, 89, 15, 20, 39, 66, 16, 21, 30, 87, 14, 22, 52, 74, 17]) describe('constructor, general', function () { it('aliases a,b,c,d,e,f properly', function () { - var y = new DOMMatrix(Avals) + const y = new DOMMatrix(Avals) assert.strictEqual(y.a, y.m11) assert.strictEqual(y.b, y.m12) assert.strictEqual(y.c, y.m21) @@ -39,7 +39,7 @@ describe('DOMMatrix', function () { }) it('parses lists of transforms per spec', function () { - var y = new DOMMatrix('matrix(1, -2, 3.2, 4.5e2, 3.5E-1, +2) matrix(1, 2, 4, 1, 0, 0)') + const y = new DOMMatrix('matrix(1, -2, 3.2, 4.5e2, 3.5E-1, +2) matrix(1, 2, 4, 1, 0, 0)') assert.strictEqual(y.a, 7.4) assert.strictEqual(y.b, 898) assert.strictEqual(y.c, 7.2) @@ -50,7 +50,7 @@ describe('DOMMatrix', function () { }) it('parses matrix2d(<16 numbers>) per spec', function () { - var y = new DOMMatrix('matrix3d(1, -0, 0, 0, -2.12, 1, 0, 0, 3e2, 0, +1, 1.252, 0, 0, 0, 1)') + const y = new DOMMatrix('matrix3d(1, -0, 0, 0, -2.12, 1, 0, 0, 3e2, 0, +1, 1.252, 0, 0, 0, 1)') assert.deepEqual(y.toFloat64Array(), new Float64Array([ 1, 0, 0, 0, -2.12, 1, 0, 0, @@ -61,7 +61,7 @@ describe('DOMMatrix', function () { }) it('sets is2D to true if matrix2d(<16 numbers>) is 2D', function () { - var y = new DOMMatrix('matrix3d(1, 2, 0, 0, 3, 4, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)') + const y = new DOMMatrix('matrix3d(1, 2, 0, 0, 3, 4, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)') assert.deepEqual(y.toFloat64Array(), new Float64Array([ 1, 2, 0, 0, 3, 4, 0, 0, @@ -78,9 +78,9 @@ describe('DOMMatrix', function () { describe('multiply', function () { it('performs self.other, returning a new DOMMatrix', function () { - var A = new DOMMatrix(Avals) - var B = new DOMMatrix(Bvals) - var C = B.multiply(A) + const A = new DOMMatrix(Avals) + const B = new DOMMatrix(Bvals) + const C = B.multiply(A) assert.deepEqual(C.toFloat64Array(), BxA) assert.notStrictEqual(A, C) assert.notStrictEqual(B, C) @@ -89,9 +89,9 @@ describe('DOMMatrix', function () { describe('multiplySelf', function () { it('performs self.other, mutating self', function () { - var A = new DOMMatrix(Avals) - var B = new DOMMatrix(Bvals) - var C = B.multiplySelf(A) + const A = new DOMMatrix(Avals) + const B = new DOMMatrix(Bvals) + const C = B.multiplySelf(A) assert.deepEqual(C.toFloat64Array(), BxA) assert.strictEqual(C, B) }) @@ -99,9 +99,9 @@ describe('DOMMatrix', function () { describe('preMultiplySelf', function () { it('performs other.self, mutating self', function () { - var A = new DOMMatrix(Avals) - var B = new DOMMatrix(Bvals) - var C = B.preMultiplySelf(A) + const A = new DOMMatrix(Avals) + const B = new DOMMatrix(Bvals) + const C = B.preMultiplySelf(A) assert.deepEqual(C.toFloat64Array(), AxB) assert.strictEqual(C, B) }) @@ -111,7 +111,7 @@ describe('DOMMatrix', function () { describe('translateSelf', function () { it('works, 1 arg', function () { - var A = new DOMMatrix() + const A = new DOMMatrix() A.translateSelf(1) assert.deepEqual(A.toFloat64Array(), new Float64Array([ 1, 0, 0, 0, @@ -122,8 +122,8 @@ describe('DOMMatrix', function () { }) it('works, 2 args', function () { - var A = new DOMMatrix(Avals) - var C = A.translateSelf(2, 5) + const A = new DOMMatrix(Avals) + const C = A.translateSelf(2, 5) assert.deepEqual(C.toFloat64Array(), new Float64Array([ 4, 5, 1, 8, 0, 3, 6, 1, @@ -133,7 +133,7 @@ describe('DOMMatrix', function () { }) it('works, 3 args', function () { - var A = new DOMMatrix() + const A = new DOMMatrix() A.translateSelf(1, 2, 3) assert.deepEqual(A.toFloat64Array(), new Float64Array([ 1, 0, 0, 0, @@ -145,7 +145,7 @@ describe('DOMMatrix', function () { }) describe('scale', function () { - var x = new DOMMatrix() + const x = new DOMMatrix() it('works, 1 arg', function () { assert.deepEqual(x.scale(2).toFloat64Array(), new Float64Array([ 2, 0, 0, 0, @@ -218,7 +218,7 @@ describe('DOMMatrix', function () { describe('scaleSelf', function () {}) describe('scale3d', function () { - var x = new DOMMatrix(Avals) + const x = new DOMMatrix(Avals) it('works, 0 args', function () { assert.deepEqual(x.scale3d().toFloat64Array(), new Float64Array(Avals)) @@ -265,8 +265,8 @@ describe('DOMMatrix', function () { describe('rotate', function () { it('works, 1 arg', function () { - var x = new DOMMatrix() - var y = x.rotate(70) + const x = new DOMMatrix() + const y = x.rotate(70) assertApproxDeep(y.toFloat64Array(), new Float64Array([ 0.3420201, 0.9396926, 0, 0, -0.939692, 0.3420201, 0, 0, @@ -276,8 +276,8 @@ describe('DOMMatrix', function () { }) it('works, 2 args', function () { - var x = new DOMMatrix() - var y = x.rotate(70, 30) + const x = new DOMMatrix() + const y = x.rotate(70, 30) assertApproxDeep(y.toFloat64Array(), new Float64Array([ 0.8660254, 0, -0.5, 0, 0.4698463, 0.3420201, 0.8137976, 0, @@ -288,8 +288,8 @@ describe('DOMMatrix', function () { }) it('works, 3 args', function () { - var x = new DOMMatrix() - var y = x.rotate(70, 30, 50) + const x = new DOMMatrix() + const y = x.rotate(70, 30, 50) assertApproxDeep(y.toFloat64Array(), new Float64Array([ 0.5566703, 0.6634139, -0.5, 0, 0.0400087, 0.5797694, 0.8137976, 0, @@ -301,7 +301,7 @@ describe('DOMMatrix', function () { describe('rotateSelf', function () {}) describe('rotateFromVector', function () { - var x = new DOMMatrix(Avals) + const x = new DOMMatrix(Avals) it('works, no args and x/y=0', function () { assert.deepEqual(x.rotateFromVector().toFloat64Array(), new Float64Array(Avals)) assert.deepEqual(x.rotateFromVector(5).toFloat64Array(), new Float64Array(Avals)) @@ -309,8 +309,8 @@ describe('DOMMatrix', function () { }) it('works', function () { - var y = x.rotateFromVector(4, 2).toFloat64Array() - var expect = new Float64Array([ + const y = x.rotateFromVector(4, 2).toFloat64Array() + const expect = new Float64Array([ 3.5777087, 5.8137767, 3.5777087, 7.6026311, -1.7888543, 0.4472135, 4.9193495, -2.6832815, 3, 5, 0, 9, @@ -324,15 +324,15 @@ describe('DOMMatrix', function () { describe('rotateAxisAngle', function () { it('works, 0 args', function () { - var x = new DOMMatrix(Avals) - var y = x.rotateAxisAngle().toFloat64Array() + const x = new DOMMatrix(Avals) + const y = x.rotateAxisAngle().toFloat64Array() assert.deepEqual(y, new Float64Array(Avals)) }) it('works, 4 args', function () { - var x = new DOMMatrix(Avals) - var y = x.rotateAxisAngle(2, 4, 1, 35).toFloat64Array() - var expect = new Float64Array([ + const x = new DOMMatrix(Avals) + const y = x.rotateAxisAngle(2, 4, 1, 35).toFloat64Array() + const expect = new Float64Array([ 1.9640922, 2.4329989, 2.0179538, 2.6719387, 0.6292488, 4.0133545, 5.6853755, 3.0697681, 4.5548203, 6.0805840, -0.7774101, 11.3770500, @@ -346,9 +346,9 @@ describe('DOMMatrix', function () { describe('skewX', function () { it('works', function () { - var x = new DOMMatrix(Avals) - var y = x.skewX(30).toFloat64Array() - var expect = new Float64Array([ + const x = new DOMMatrix(Avals) + const y = x.skewX(30).toFloat64Array() + const expect = new Float64Array([ 4, 5, 1, 8, 2.3094010, 5.8867513, 6.5773502, 5.6188021, 3, 5, 0, 9, @@ -362,9 +362,9 @@ describe('DOMMatrix', function () { describe('skewY', function () { it('works', function () { - var x = new DOMMatrix(Avals) - var y = x.skewY(30).toFloat64Array() - var expect = new Float64Array([ + const x = new DOMMatrix(Avals) + const y = x.skewY(30).toFloat64Array() + const expect = new Float64Array([ 4, 6.7320508, 4.4641016, 8.5773502, 0, 3, 6, 1, 3, 5, 0, 9, @@ -378,9 +378,9 @@ describe('DOMMatrix', function () { describe('flipX', function () { it('works', function () { - var x = new DOMMatrix() + const x = new DOMMatrix() x.rotateSelf(70) - var y = x.flipX() + const y = x.flipX() assertApprox(y.a, -0.34202) assertApprox(y.b, -0.93969) assertApprox(y.c, -0.93969) @@ -392,9 +392,9 @@ describe('DOMMatrix', function () { describe('flipY', function () { it('works', function () { - var x = new DOMMatrix() + const x = new DOMMatrix() x.rotateSelf(70) - var y = x.flipY() + const y = x.flipY() assertApprox(y.a, 0.34202) assertApprox(y.b, 0.93969) assertApprox(y.c, 0.93969) @@ -405,8 +405,8 @@ describe('DOMMatrix', function () { }) describe('invertSelf', function () { - it('works for invertible matrices', function() { - var d = new DOMMatrix(Avals) + it('works for invertible matrices', function () { + const d = new DOMMatrix(Avals) d.invertSelf() assertApprox(d.m11, 0.9152542372881356) assertApprox(d.m12, -0.01694915254237288) @@ -426,8 +426,8 @@ describe('DOMMatrix', function () { assertApprox(d.m44, -0.6610169491525424) }) - it('works for non-invertible matrices', function() { - var d = new DOMMatrix(Xvals) + it('works for non-invertible matrices', function () { + const d = new DOMMatrix(Xvals) d.invertSelf() assert.strictEqual(isNaN(d.m11), true) assert.strictEqual(isNaN(d.m12), true) @@ -450,9 +450,9 @@ describe('DOMMatrix', function () { }) describe('inverse', function () { - it('preserves the original DOMMatrix', function() { - var d = new DOMMatrix(Avals) - var d2 = d.inverse() + it('preserves the original DOMMatrix', function () { + const d = new DOMMatrix(Avals) + const d2 = d.inverse() assert.strictEqual(d.m11, Avals[0]) assert.strictEqual(d.m12, Avals[1]) assert.strictEqual(d.m13, Avals[2]) @@ -487,9 +487,9 @@ describe('DOMMatrix', function () { assertApprox(d2.m44, -0.6610169491525424) }) - it('preserves the original DOMMatrix for non-invertible matrices', function() { - var d = new DOMMatrix(Xvals) - var d2 = d.inverse() + it('preserves the original DOMMatrix for non-invertible matrices', function () { + const d = new DOMMatrix(Xvals) + const d2 = d.inverse() assert.strictEqual(d.m11, Xvals[0]) assert.strictEqual(d.m12, Xvals[1]) assert.strictEqual(d.m13, Xvals[2]) @@ -528,15 +528,15 @@ describe('DOMMatrix', function () { describe('transformPoint', function () { it('works', function () { - var x = new DOMMatrix() - var r = x.transformPoint({x: 1, y: 2, z: 3}) + const x = new DOMMatrix() + let r = x.transformPoint({ x: 1, y: 2, z: 3 }) assert.strictEqual(r.x, 1) assert.strictEqual(r.y, 2) assert.strictEqual(r.z, 3) assert.strictEqual(r.w, 1) x.rotateSelf(70) - r = x.transformPoint({x: 2, y: 3, z: 4}) + r = x.transformPoint({ x: 2, y: 3, z: 4 }) assertApprox(r.x, -2.13503) assertApprox(r.y, 2.905445) assert.strictEqual(r.z, 4) @@ -546,8 +546,8 @@ describe('DOMMatrix', function () { describe('toFloat32Array', function () { it('works', function () { - var x = new DOMMatrix() - var y = x.toFloat32Array() + const x = new DOMMatrix() + const y = x.toFloat32Array() assert.ok(y instanceof Float32Array) assert.deepEqual(y, new Float32Array([ 1, 0, 0, 0, @@ -560,8 +560,8 @@ describe('DOMMatrix', function () { describe('toFloat64Array', function () { it('works', function () { - var x = new DOMMatrix() - var y = x.toFloat64Array() + const x = new DOMMatrix() + const y = x.toFloat64Array() assert.ok(y instanceof Float64Array) assert.deepEqual(y, new Float64Array([ 1, 0, 0, 0, @@ -574,12 +574,12 @@ describe('DOMMatrix', function () { describe('toString', function () { it('works, 2d', function () { - var x = new DOMMatrix() + const x = new DOMMatrix() assert.equal(x.toString(), 'matrix(1, 0, 0, 1, 0, 0)') }) it('works, 3d', function () { - var x = new DOMMatrix() + const x = new DOMMatrix() x.m31 = 1 assert.equal(x.is2D, false) assert.equal(x.toString(), diff --git a/test/image.test.js b/test/image.test.js index 9607d0131..8d54dd90f 100644 --- a/test/image.test.js +++ b/test/image.test.js @@ -5,38 +5,37 @@ /** * Module dependencies. */ - -const {createCanvas, loadImage, rsvgVersion} = require('../'); -const Image = require('../').Image -const HAVE_SVG = rsvgVersion !== undefined; - const assert = require('assert') const assertRejects = require('assert-rejects') const fs = require('fs') const path = require('path') -const png_checkers = `${__dirname}/fixtures/checkers.png` -const png_clock = `${__dirname}/fixtures/clock.png` -const jpg_chrome = `${__dirname}/fixtures/chrome.jpg` -const jpg_face = `${__dirname}/fixtures/face.jpeg` -const svg_tree = `${__dirname}/fixtures/tree.svg` -const bmp_dir = `${__dirname}/fixtures/bmp` +const { createCanvas, loadImage, rsvgVersion, Image } = require('../') +const HAVE_SVG = rsvgVersion !== undefined + + +const pngCheckers = path.join(__dirname, '/fixtures/checkers.png') +const pngClock = path.join(__dirname, '/fixtures/clock.png') +const jpgChrome = path.join(__dirname, '/fixtures/chrome.jpg') +const jpgFace = path.join(__dirname, '/fixtures/face.jpeg') +const svgTree = path.join(__dirname, '/fixtures/tree.svg') +const bmpDir = path.join(__dirname, '/fixtures/bmp') describe('Image', function () { it('Prototype and ctor are well-shaped, don\'t hit asserts on accessors (GH-803)', function () { - var img = new Image(); - assert.throws(function () { Image.prototype.width; }, /incompatible receiver/); - assert(!img.hasOwnProperty('width')); - assert('width' in img); - assert(Image.prototype.hasOwnProperty('width')); - }); + const img = new Image() + assert.throws(function () { Image.prototype.width }, /incompatible receiver/) + assert(!img.hasOwnProperty('width')) + assert('width' in img) + assert(Image.prototype.hasOwnProperty('width')) + }) it('loads JPEG image', function () { - return loadImage(jpg_face).then((img) => { + return loadImage(jpgFace).then((img) => { assert.strictEqual(img.onerror, null) assert.strictEqual(img.onload, null) - assert.strictEqual(img.src, jpg_face) + assert.strictEqual(img.src, jpgFace) assert.strictEqual(img.width, 485) assert.strictEqual(img.height, 401) assert.strictEqual(img.complete, true) @@ -44,7 +43,7 @@ describe('Image', function () { }) it('loads JPEG data URL', function () { - const base64Encoded = fs.readFileSync(jpg_face, 'base64') + const base64Encoded = fs.readFileSync(jpgFace, 'base64') const dataURL = `data:image/png;base64,${base64Encoded}` return loadImage(dataURL).then((img) => { @@ -59,11 +58,11 @@ describe('Image', function () { }) it('loads PNG image', function () { - return loadImage(png_clock).then((img) => { + return loadImage(pngClock).then((img) => { assert.strictEqual(img.onerror, null) assert.strictEqual(img.onload, null) - assert.strictEqual(img.src, png_clock) + assert.strictEqual(img.src, pngClock) assert.strictEqual(img.width, 320) assert.strictEqual(img.height, 320) assert.strictEqual(img.complete, true) @@ -71,7 +70,7 @@ describe('Image', function () { }) it('loads PNG data URL', function () { - const base64Encoded = fs.readFileSync(png_clock, 'base64') + const base64Encoded = fs.readFileSync(pngClock, 'base64') const dataURL = `data:image/png;base64,${base64Encoded}` return loadImage(dataURL).then((img) => { @@ -86,7 +85,7 @@ describe('Image', function () { }) it('detects invalid PNG', function (done) { - if (process.platform === 'win32') this.skip(); // TODO + if (process.platform === 'win32') this.skip() // TODO const img = new Image() img.onerror = () => { assert.strictEqual(img.complete, true) @@ -102,8 +101,8 @@ describe('Image', function () { throw new MyError() } assert.throws(() => { - img.src = jpg_face - }, MyError); + img.src = jpgFace + }, MyError) }) it('propagates exceptions thrown by onerror', function () { @@ -114,14 +113,14 @@ describe('Image', function () { } assert.throws(() => { img.src = Buffer.from('', 'hex') - }, MyError); + }, MyError) }) it('loads SVG data URL base64', function () { - if (!HAVE_SVG) this.skip(); - const base64Enc = fs.readFileSync(svg_tree, 'base64') + if (!HAVE_SVG) this.skip() + const base64Enc = fs.readFileSync(svgTree, 'base64') const dataURL = `data:image/svg+xml;base64,${base64Enc}` - return loadImage(dataURL).then((img) => { + return loadImage(dataURL).then((img) => { assert.strictEqual(img.onerror, null) assert.strictEqual(img.onload, null) assert.strictEqual(img.width, 200) @@ -131,10 +130,10 @@ describe('Image', function () { }) it('loads SVG data URL utf8', function () { - if (!HAVE_SVG) this.skip(); - const utf8Encoded = fs.readFileSync(svg_tree, 'utf8') + if (!HAVE_SVG) this.skip() + const utf8Encoded = fs.readFileSync(svgTree, 'utf8') const dataURL = `data:image/svg+xml;utf8,${utf8Encoded}` - return loadImage(dataURL).then((img) => { + return loadImage(dataURL).then((img) => { assert.strictEqual(img.onerror, null) assert.strictEqual(img.onload, null) assert.strictEqual(img.width, 200) @@ -144,19 +143,19 @@ describe('Image', function () { }) it('calls Image#onload multiple times', function () { - return loadImage(png_clock).then((img) => { + return loadImage(pngClock).then((img) => { let onloadCalled = 0 img.onload = () => { onloadCalled += 1 } - img.src = png_checkers - assert.strictEqual(img.src, png_checkers) + img.src = pngCheckers + assert.strictEqual(img.src, pngCheckers) assert.strictEqual(img.complete, true) assert.strictEqual(img.width, 2) assert.strictEqual(img.height, 2) - img.src = png_clock - assert.strictEqual(img.src, png_clock) + img.src = pngClock + assert.strictEqual(img.src, pngClock) assert.strictEqual(true, img.complete) assert.strictEqual(320, img.width) assert.strictEqual(320, img.height) @@ -166,13 +165,13 @@ describe('Image', function () { onloadCalled = 0 img.onload = () => { onloadCalled += 1 } - img.src = png_clock + img.src = pngClock assert.strictEqual(onloadCalled, 1) }) }) it('handles errors', function () { - return assertRejects(loadImage(`${png_clock}fail`), Error) + return assertRejects(loadImage(`${pngClock}fail`), Error) }) it('returns a nice, coded error for fopen failures', function (done) { @@ -190,34 +189,34 @@ describe('Image', function () { it('captures errors from libjpeg', function (done) { const img = new Image() img.onerror = err => { - assert.equal(err.message, "JPEG datastream contains no image") + assert.equal(err.message, 'JPEG datastream contains no image') assert.strictEqual(img.complete, true) done() } - img.src = `${__dirname}/fixtures/159-crash1.jpg` + img.src = path.join(__dirname, '/fixtures/159-crash1.jpg') }) it('calls Image#onerror multiple times', function () { - return loadImage(png_clock).then((img) => { + return loadImage(pngClock).then((img) => { let onloadCalled = 0 let onerrorCalled = 0 img.onload = () => { onloadCalled += 1 } img.onerror = () => { onerrorCalled += 1 } - img.src = `${png_clock}s1` - assert.strictEqual(img.src, `${png_clock}s1`) + img.src = `${pngClock}s1` + assert.strictEqual(img.src, `${pngClock}s1`) - img.src = `${png_clock}s2` - assert.strictEqual(img.src, `${png_clock}s2`) + img.src = `${pngClock}s2` + assert.strictEqual(img.src, `${pngClock}s2`) assert.strictEqual(onerrorCalled, 2) onerrorCalled = 0 img.onerror = () => { onerrorCalled += 1 } - img.src = `${png_clock}s3` - assert.strictEqual(img.src, `${png_clock}s3`) + img.src = `${pngClock}s3` + assert.strictEqual(img.src, `${pngClock}s3`) assert.strictEqual(onerrorCalled, 1) assert.strictEqual(onloadCalled, 0) @@ -225,19 +224,19 @@ describe('Image', function () { }) it('Image#{width,height}', function () { - return loadImage(png_clock).then((img) => { + return loadImage(pngClock).then((img) => { img.src = '' assert.strictEqual(img.width, 0) assert.strictEqual(img.height, 0) - img.src = png_clock + img.src = pngClock assert.strictEqual(img.width, 320) assert.strictEqual(img.height, 320) }) }) it('Image#src set empty buffer', function () { - return loadImage(png_clock).then((img) => { + return loadImage(pngClock).then((img) => { let onerrorCalled = 0 img.onerror = () => { onerrorCalled += 1 } @@ -251,14 +250,14 @@ describe('Image', function () { }) }) - it('should unbind Image#onload', function() { - return loadImage(png_clock).then((img) => { + it('should unbind Image#onload', function () { + return loadImage(pngClock).then((img) => { let onloadCalled = 0 img.onload = () => { onloadCalled += 1 } - img.src = png_checkers - assert.strictEqual(img.src, png_checkers) + img.src = pngCheckers + assert.strictEqual(img.src, pngCheckers) assert.strictEqual(img.complete, true) assert.strictEqual(img.width, 2) assert.strictEqual(img.height, 2) @@ -268,8 +267,8 @@ describe('Image', function () { onloadCalled = 0 img.onload = null - img.src = png_clock - assert.strictEqual(img.src, png_clock) + img.src = pngClock + assert.strictEqual(img.src, pngClock) assert.strictEqual(img.complete, true) assert.strictEqual(img.width, 320) assert.strictEqual(img.height, 320) @@ -278,27 +277,27 @@ describe('Image', function () { }) }) - it('should unbind Image#onerror', function() { - return loadImage(png_clock).then((img) => { + it('should unbind Image#onerror', function () { + return loadImage(pngClock).then((img) => { let onloadCalled = 0 let onerrorCalled = 0 img.onload = () => { onloadCalled += 1 } img.onerror = () => { onerrorCalled += 1 } - img.src = `${png_clock}s1` - assert.strictEqual(img.src, `${png_clock}s1`) + img.src = `${pngClock}s1` + assert.strictEqual(img.src, `${pngClock}s1`) - img.src = `${png_clock}s2` - assert.strictEqual(img.src, `${png_clock}s2`) + img.src = `${pngClock}s2` + assert.strictEqual(img.src, `${pngClock}s2`) assert.strictEqual(onerrorCalled, 2) onerrorCalled = 0 img.onerror = null - img.src = `${png_clock}s3` - assert.strictEqual(img.src, `${png_clock}s3`) + img.src = `${pngClock}s3` + assert.strictEqual(img.src, `${pngClock}s3`) assert.strictEqual(onloadCalled, 0) assert.strictEqual(onerrorCalled, 0) @@ -314,7 +313,7 @@ describe('Image', function () { return copy } - const source = fs.readFileSync(jpg_chrome) + const source = fs.readFileSync(jpgChrome) const corruptSources = [ withIncreasedByte(source, 0), @@ -335,68 +334,68 @@ describe('Image', function () { }) it('does not contain `source` property', function () { - var keys = Reflect.ownKeys(Image.prototype); - assert.ok(!keys.includes('source')); - assert.ok(!keys.includes('getSource')); - assert.ok(!keys.includes('setSource')); - }); + const keys = Reflect.ownKeys(Image.prototype) + assert.ok(!keys.includes('source')) + assert.ok(!keys.includes('getSource')) + assert.ok(!keys.includes('setSource')) + }) describe('supports BMP', function () { it('parses 1-bit image', function (done) { - let img = new Image(); + const img = new Image() img.onload = () => { - assert.strictEqual(img.width, 111); - assert.strictEqual(img.height, 72); - done(); - }; + assert.strictEqual(img.width, 111) + assert.strictEqual(img.height, 72) + done() + } - img.onerror = err => { throw err; }; - img.src = path.join(bmp_dir, '1-bit.bmp'); - }); + img.onerror = err => { throw err } + img.src = path.join(bmpDir, '1-bit.bmp') + }) it('parses 4-bit image', function (done) { - let img = new Image(); + const img = new Image() img.onload = () => { - assert.strictEqual(img.width, 32); - assert.strictEqual(img.height, 32); - done(); - }; + assert.strictEqual(img.width, 32) + assert.strictEqual(img.height, 32) + done() + } - img.onerror = err => { throw err; }; - img.src = path.join(bmp_dir, '4-bit.bmp'); - }); + img.onerror = err => { throw err } + img.src = path.join(bmpDir, '4-bit.bmp') + }) - it('parses 8-bit image'); + it('parses 8-bit image') it('parses 24-bit image', function (done) { - let img = new Image(); + const img = new Image() img.onload = () => { - assert.strictEqual(img.width, 2); - assert.strictEqual(img.height, 2); + assert.strictEqual(img.width, 2) + assert.strictEqual(img.height, 2) testImgd(img, [ 0, 0, 255, 255, 0, 255, 0, 255, 255, 0, 0, 255, - 255, 255, 255, 255, - ]); + 255, 255, 255, 255 + ]) - done(); - }; + done() + } - img.onerror = err => { throw err; }; - img.src = path.join(bmp_dir, '24-bit.bmp'); - }); + img.onerror = err => { throw err } + img.src = path.join(bmpDir, '24-bit.bmp') + }) it('parses 32-bit image', function (done) { - let img = new Image(); + const img = new Image() img.onload = () => { - assert.strictEqual(img.width, 4); - assert.strictEqual(img.height, 2); + assert.strictEqual(img.width, 4) + assert.strictEqual(img.height, 2) testImgd(img, [ 0, 0, 255, 255, @@ -406,116 +405,117 @@ describe('Image', function () { 0, 0, 255, 127, 0, 255, 0, 127, 255, 0, 0, 127, - 255, 255, 255, 127, - ]); - - done(); - }; + 255, 255, 255, 127 + ]) - img.onerror = err => { throw err; }; - img.src = fs.readFileSync(path.join(bmp_dir, '32-bit.bmp')); // Also tests loading from buffer - }); + done() + } + + img.onerror = err => { throw err } + img.src = fs.readFileSync(path.join(bmpDir, '32-bit.bmp')) // Also tests loading from buffer + }) it('parses minimal BMP', function (done) { - let img = new Image(); + const img = new Image() img.onload = () => { - assert.strictEqual(img.width, 1); - assert.strictEqual(img.height, 1); + assert.strictEqual(img.width, 1) + assert.strictEqual(img.height, 1) testImgd(img, [ - 255, 0, 0, 255, - ]); - - done(); - }; + 255, 0, 0, 255 + ]) - img.onerror = err => { throw err; }; - img.src = path.join(bmp_dir, 'min.bmp'); - }); + done() + } + + img.onerror = err => { throw err } + img.src = path.join(bmpDir, 'min.bmp') + }) it('properly handles negative height', function (done) { - let img = new Image(); + const img = new Image() img.onload = () => { - assert.strictEqual(img.width, 1); - assert.strictEqual(img.height, 2); + assert.strictEqual(img.width, 1) + assert.strictEqual(img.height, 2) testImgd(img, [ 255, 0, 0, 255, - 0, 255, 0, 255, - ]); - - done(); - }; + 0, 255, 0, 255 + ]) - img.onerror = err => { throw err; }; - img.src = path.join(bmp_dir, 'negative-height.bmp'); - }); + done() + } + + img.onerror = err => { throw err } + img.src = path.join(bmpDir, 'negative-height.bmp') + }) it('color palette', function (done) { - let img = new Image(); + const img = new Image() img.onload = () => { - assert.strictEqual(img.width, 32); - assert.strictEqual(img.height, 32); - done(); - }; + assert.strictEqual(img.width, 32) + assert.strictEqual(img.height, 32) + done() + } - img.onerror = err => { throw err; }; - img.src = path.join(bmp_dir, 'palette.bmp'); - }); + img.onerror = err => { throw err } + img.src = path.join(bmpDir, 'palette.bmp') + }) it('V3 header', function (done) { - let img = new Image(); + const img = new Image() img.onload = () => { - assert.strictEqual(img.width, 256); - assert.strictEqual(img.height, 192); - done(); - }; + assert.strictEqual(img.width, 256) + assert.strictEqual(img.height, 192) + done() + } - img.onerror = err => { throw err; }; - img.src = path.join(bmp_dir, 'v3-header.bmp'); - }); + img.onerror = err => { throw err } + img.src = path.join(bmpDir, 'v3-header.bmp') + }) - it('V5 header'); + it('V5 header') it('catches BMP errors', function (done) { - let img = new Image(); + const img = new Image() img.onload = () => { - throw new Error('Invalid image should not be loaded properly'); - }; + throw new Error('Invalid image should not be loaded properly') + } img.onerror = err => { - let msg = 'Error while processing file header - unexpected end of file'; - assert.strictEqual(err.message, msg); - done(); - }; + const msg = 'Error while processing file header - unexpected end of file' + assert.strictEqual(err.message, msg) + done() + } - img.src = Buffer.from('BM'); - }); + img.src = Buffer.from('BM') + }) it('BMP bomb', function (done) { - let img = new Image(); + const img = new Image() img.onload = () => { - throw new Error('Invalid image should not be loaded properly'); - }; + throw new Error('Invalid image should not be loaded properly') + } img.onerror = err => { - done(); - }; + if (!err) throw new Error('Expected a error') + done() + } - img.src = path.join(bmp_dir, 'bomb.bmp'); - }); + img.src = path.join(bmpDir, 'bomb.bmp') + }) - function testImgd(img, data){ - let ctx = createCanvas(img.width, img.height).getContext('2d'); - ctx.drawImage(img, 0, 0); - var actualData = ctx.getImageData(0, 0, img.width, img.height).data; - assert.strictEqual(String(actualData), String(data)); + function testImgd (img, data) { + const ctx = createCanvas(img.width, img.height).getContext('2d') + ctx.drawImage(img, 0, 0) + const actualData = ctx.getImageData(0, 0, img.width, img.height).data + assert.strictEqual(String(actualData), String(data)) } - }); + }) }) diff --git a/test/imageData.test.js b/test/imageData.test.js index cfffe9aaf..13bbcab1b 100644 --- a/test/imageData.test.js +++ b/test/imageData.test.js @@ -2,15 +2,15 @@ 'use strict' -const createImageData = require('../').createImageData -const ImageData = require('../').ImageData; +const {createImageData} = require('../') +const {ImageData} = require('../') const assert = require('assert') describe('ImageData', function () { it('Prototype and ctor are well-shaped, don\'t hit asserts on accessors (GH-803)', function () { - assert.throws(function () { ImageData.prototype.width; }, /incompatible receiver/); - }); + assert.throws(function () { ImageData.prototype.width }, /incompatible receiver/) + }) it('should throw with invalid numeric arguments', function () { assert.throws(() => { createImageData(0, 0) }, /width is zero/) @@ -33,7 +33,7 @@ describe('ImageData', function () { assert.throws(() => { createImageData(new Uint8ClampedArray(3), 0) }, /source width is zero/) // Note: Some errors thrown by browsers are not thrown by node-canvas // because our ImageData can support different BPPs. - }); + }) it('should construct with Uint8ClampedArray', function () { let data, imageData @@ -51,7 +51,7 @@ describe('ImageData', function () { assert.strictEqual(imageData.height, 4) assert(imageData.data instanceof Uint8ClampedArray) assert.strictEqual(imageData.data.length, 48) - }); + }) it('should construct with Uint16Array', function () { let data = new Uint16Array(2 * 3 * 2) @@ -67,5 +67,5 @@ describe('ImageData', function () { assert.strictEqual(imagedata.height, 4) assert(imagedata.data instanceof Uint16Array) assert.strictEqual(imagedata.data.length, 24) - }); -}); + }) +}) diff --git a/test/public/app.js b/test/public/app.js index 0a2eafd07..b25ea11a2 100644 --- a/test/public/app.js +++ b/test/public/app.js @@ -19,10 +19,10 @@ function pdfLink (name) { } function localRendering (name, callback) { - var canvas = create('canvas', { width: 200, height: 200, title: name }) - var tests = window.tests - var ctx = canvas.getContext('2d', { alpha: true }) - var initialFillStyle = ctx.fillStyle + const canvas = create('canvas', { width: 200, height: 200, title: name }) + const tests = window.tests + const ctx = canvas.getContext('2d', { alpha: true }) + const initialFillStyle = ctx.fillStyle ctx.fillStyle = 'white' ctx.fillRect(0, 0, 200, 200) ctx.fillStyle = initialFillStyle @@ -36,12 +36,12 @@ function localRendering (name, callback) { } function getDifference (canvas, image, outputCanvas) { - var imgCanvas = create('canvas', { width: 200, height: 200 }) - var ctx = imgCanvas.getContext('2d', { alpha: true }) - var output = outputCanvas.getContext('2d', { alpha: true }).getImageData(0, 0, 200, 200) + const imgCanvas = create('canvas', { width: 200, height: 200 }) + const ctx = imgCanvas.getContext('2d', { alpha: true }) + const output = outputCanvas.getContext('2d', { alpha: true }).getImageData(0, 0, 200, 200) ctx.drawImage(image, 0, 0, 200, 200) - var imageDataCanvas = ctx.getImageData(0, 0, 200, 200).data - var imageDataGolden = canvas.getContext('2d', { alpha: true }).getImageData(0, 0, 200, 200).data + const imageDataCanvas = ctx.getImageData(0, 0, 200, 200).data + const imageDataGolden = canvas.getContext('2d', { alpha: true }).getImageData(0, 0, 200, 200).data window.pixelmatch(imageDataCanvas, imageDataGolden, output.data, 200, 200, { includeAA: false, threshold: 0.15 @@ -51,16 +51,16 @@ function getDifference (canvas, image, outputCanvas) { } function clearTests () { - var table = document.getElementById('tests') + const table = document.getElementById('tests') if (table) document.body.removeChild(table) } function runTests () { clearTests() - var testNames = Object.keys(window.tests) + const testNames = Object.keys(window.tests) - var table = create('table', { id: 'tests' }, [ + const table = create('table', { id: 'tests' }, [ create('thead', {}, [ create('th', { textContent: 'node-canvas' }), create('th', { textContent: 'browser canvas' }), @@ -68,9 +68,9 @@ function runTests () { create('th', { textContent: '' }) ]), create('tbody', {}, testNames.map(function (name) { - var img = create('img') - var canvasOuput = create('canvas', { width: 200, height: 200, title: name }) - var canvas = localRendering(name, function () { + const img = create('img') + const canvasOuput = create('canvas', { width: 200, height: 200, title: name }) + const canvas = localRendering(name, function () { img.onload = function () { getDifference(canvas, img, canvasOuput) } diff --git a/test/public/tests.js b/test/public/tests.js index bdbeeca4e..bbf3c6050 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -1,7 +1,7 @@ -var DOMMatrix -var Image -var imageSrc -var tests = {} +let DOMMatrix +let Image +let imageSrc +const tests = {} if (typeof module !== 'undefined' && module.exports) { module.exports = tests @@ -35,7 +35,7 @@ tests['fillRect()'] = function (ctx) { } function renderLevel (minimumLevel, level, y) { - var x + let x for (x = 0; x < 243 / level; ++x) { drawBlock(x, y, level) } @@ -72,17 +72,17 @@ tests['fillRect()'] = function (ctx) { function getPointColour (x, y) { x = x / 121.5 - 1 y = -y / 121.5 + 1 - var x2y2 = x * x + y * y + const x2y2 = x * x + y * y if (x2y2 > 1) { return '#000' } - var root = Math.sqrt(1 - x2y2) - var x3d = x * 0.7071067812 + root / 2 - y / 2 - var y3d = x * 0.7071067812 - root / 2 + y / 2 - var z3d = 0.7071067812 * root + 0.7071067812 * y - var brightness = -x / 2 + root * 0.7071067812 + y / 2 + const root = Math.sqrt(1 - x2y2) + const x3d = x * 0.7071067812 + root / 2 - y / 2 + const y3d = x * 0.7071067812 - root / 2 + y / 2 + const z3d = 0.7071067812 * root + 0.7071067812 * y + let brightness = -x / 2 + root * 0.7071067812 + y / 2 if (brightness < 0) brightness = 0 return ( 'rgb(' + Math.round(brightness * 127.5 * (1 - y3d)) + @@ -125,15 +125,15 @@ tests['arc()'] = function (ctx) { } tests['arc() 2'] = function (ctx) { - for (var i = 0; i < 4; i++) { - for (var j = 0; j < 3; j++) { + for (let i = 0; i < 4; i++) { + for (let j = 0; j < 3; j++) { ctx.beginPath() - var x = 25 + j * 50 // x coordinate - var y = 25 + i * 50 // y coordinate - var radius = 20 // Arc radius - var startAngle = 0 // Starting point on circle - var endAngle = Math.PI + (Math.PI * j) / 2 // End point on circle - var anticlockwise = (i % 2) === 1 // clockwise or anticlockwise + const x = 25 + j * 50 // x coordinate + const y = 25 + i * 50 // y coordinate + const radius = 20 // Arc radius + const startAngle = 0 // Starting point on circle + const endAngle = Math.PI + (Math.PI * j) / 2 // End point on circle + const anticlockwise = (i % 2) === 1 // clockwise or anticlockwise ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise) @@ -166,48 +166,48 @@ tests['arcTo()'] = function (ctx) { } tests['ellipse() 1'] = function (ctx) { - var n = 8 - for (var i = 0; i < n; i++) { + const n = 8 + for (let i = 0; i < n; i++) { ctx.beginPath() - var a = i * 2 * Math.PI / n - var x = 100 + 50 * Math.cos(a) - var y = 100 + 50 * Math.sin(a) + const a = i * 2 * Math.PI / n + const x = 100 + 50 * Math.cos(a) + const y = 100 + 50 * Math.sin(a) ctx.ellipse(x, y, 10, 15, a, 0, 2 * Math.PI) ctx.stroke() } } tests['ellipse() 2'] = function (ctx) { - var n = 8 - for (var i = 0; i < n; i++) { + const n = 8 + for (let i = 0; i < n; i++) { ctx.beginPath() - var a = i * 2 * Math.PI / n - var x = 100 + 50 * Math.cos(a) - var y = 100 + 50 * Math.sin(a) + const a = i * 2 * Math.PI / n + const x = 100 + 50 * Math.cos(a) + const y = 100 + 50 * Math.sin(a) ctx.ellipse(x, y, 10, 15, a, 0, a) ctx.stroke() } } tests['ellipse() 3'] = function (ctx) { - var n = 8 - for (var i = 0; i < n; i++) { + const n = 8 + for (let i = 0; i < n; i++) { ctx.beginPath() - var a = i * 2 * Math.PI / n - var x = 100 + 50 * Math.cos(a) - var y = 100 + 50 * Math.sin(a) + const a = i * 2 * Math.PI / n + const x = 100 + 50 * Math.cos(a) + const y = 100 + 50 * Math.sin(a) ctx.ellipse(x, y, 10, 15, a, 0, a, true) ctx.stroke() } } tests['ellipse() 4'] = function (ctx) { - var n = 8 - for (var i = 0; i < n; i++) { + const n = 8 + for (let i = 0; i < n; i++) { ctx.beginPath() - var a = i * 2 * Math.PI / n - var x = 100 + 50 * Math.cos(a) - var y = 100 + 50 * Math.sin(a) + const a = i * 2 * Math.PI / n + const x = 100 + 50 * Math.cos(a) + const y = 100 + 50 * Math.sin(a) ctx.ellipse(x, y, 10, 15, a, a, 0, true) ctx.stroke() } @@ -238,12 +238,12 @@ tests['quadraticCurveTo()'] = function (ctx) { } tests['transform()'] = function (ctx) { - var sin = Math.sin(Math.PI / 6) - var cos = Math.cos(Math.PI / 6) + const sin = Math.sin(Math.PI / 6) + const cos = Math.cos(Math.PI / 6) ctx.translate(100, 100) ctx.scale(0.5, 0.5) - var c = 0 - for (var i = 0; i <= 12; i++) { + let c = 0 + for (let i = 0; i <= 12; i++) { c = Math.floor(255 / 12 * i) ctx.fillStyle = 'rgb(' + c + ',' + c + ',' + c + ')' ctx.fillRect(0, 0, 100, 10) @@ -261,11 +261,11 @@ tests['rotate()'] = function (ctx) { tests['rotate() 2'] = function (ctx) { ctx.translate(75, 75) - for (var i = 1; i < 6; i++) { // Loop through rings (from inside to out) + for (let i = 1; i < 6; i++) { // Loop through rings (from inside to out) ctx.save() ctx.fillStyle = 'rgb(' + (51 * i) + ',' + (255 - 51 * i) + ',255)' - for (var j = 0; j < i * 6; j++) { // draw individual dots + for (let j = 0; j < i * 6; j++) { // draw individual dots ctx.rotate(Math.PI * 2 / (i * 6)) ctx.beginPath() ctx.arc(0, i * 12.5, 5, 0, Math.PI * 2, true) @@ -278,8 +278,8 @@ tests['rotate() 2'] = function (ctx) { tests['translate()'] = function (ctx) { ctx.fillRect(0, 0, 300, 300) - for (var i = 0; i < 3; i++) { - for (var j = 0; j < 3; j++) { + for (let i = 0; i < 3; i++) { + for (let j = 0; j < 3; j++) { ctx.save() ctx.strokeStyle = '#9CFF00' ctx.translate(50 + j * 100, 50 + i * 100) @@ -288,15 +288,18 @@ tests['translate()'] = function (ctx) { } } function drawSpirograph (ctx, R, r, O) { - var x1 = R - O - var y1 = 0 - var i = 1 + let x1 = R - O + let y1 = 0 + let i = 1 + let x2 + let y2 + ctx.beginPath() ctx.moveTo(x1, y1) do { if (i > 20000) break - var x2 = (R + r) * Math.cos(i * Math.PI / 72) - (r + O) * Math.cos(((R + r) / r) * (i * Math.PI / 72)) - var y2 = (R + r) * Math.sin(i * Math.PI / 72) - (r + O) * Math.sin(((R + r) / r) * (i * Math.PI / 72)) + x2 = (R + r) * Math.cos(i * Math.PI / 72) - (r + O) * Math.cos(((R + r) / r) * (i * Math.PI / 72)) + y2 = (R + r) * Math.sin(i * Math.PI / 72) - (r + O) * Math.sin(((R + r) / r) * (i * Math.PI / 72)) ctx.lineTo(x2, y2) x1 = x2 y1 = y2 @@ -357,15 +360,18 @@ tests['scale()'] = function (ctx) { drawSpirograph(ctx, 22, 6, 5) ctx.restore() function drawSpirograph (ctx, R, r, O) { - var x1 = R - O - var y1 = 0 - var i = 1 + let x1 = R - O + let y1 = 0 + let i = 1 + let x2 + let y2 + ctx.beginPath() ctx.moveTo(x1, y1) do { if (i > 20000) break - var x2 = (R + r) * Math.cos(i * Math.PI / 72) - (r + O) * Math.cos(((R + r) / r) * (i * Math.PI / 72)) - var y2 = (R + r) * Math.sin(i * Math.PI / 72) - (r + O) * Math.sin(((R + r) / r) * (i * Math.PI / 72)) + x2 = (R + r) * Math.cos(i * Math.PI / 72) - (r + O) * Math.cos(((R + r) / r) * (i * Math.PI / 72)) + y2 = (R + r) * Math.sin(i * Math.PI / 72) - (r + O) * Math.sin(((R + r) / r) * (i * Math.PI / 72)) ctx.lineTo(x2, y2) x1 = x2 y1 = y2 @@ -400,7 +406,7 @@ tests['clip() 2'] = function (ctx) { ctx.clip() // draw background - var lingrad = ctx.createLinearGradient(0, -75, 0, 75) + const lingrad = ctx.createLinearGradient(0, -75, 0, 75) lingrad.addColorStop(0, '#232256') lingrad.addColorStop(1, '#143778') @@ -408,7 +414,7 @@ tests['clip() 2'] = function (ctx) { ctx.fillRect(-75, -75, 150, 150) // draw stars - for (var j = 1; j < 50; j++) { + for (let j = 1; j < 50; j++) { ctx.save() ctx.fillStyle = '#fff' ctx.translate(75 - Math.floor(Math.random() * 150), 75 - Math.floor(Math.random() * 150)) @@ -419,7 +425,7 @@ tests['clip() 2'] = function (ctx) { ctx.save() ctx.beginPath() ctx.moveTo(r, 0) - for (var i = 0; i < 9; i++) { + for (let i = 0; i < 9; i++) { ctx.rotate(Math.PI / 5) if ((i % 2) === 0) { ctx.lineTo((r / 0.525731) * 0.200811, 0) @@ -434,9 +440,9 @@ tests['clip() 2'] = function (ctx) { } tests['createPattern()'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { - var pattern = ctx.createPattern(img, 'repeat') + const pattern = ctx.createPattern(img, 'repeat') ctx.scale(0.1, 0.1) ctx.fillStyle = pattern ctx.fillRect(100, 100, 800, 800) @@ -449,9 +455,9 @@ tests['createPattern()'] = function (ctx, done) { } tests['createPattern() with globalAlpha'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { - var pattern = ctx.createPattern(img, 'repeat') + const pattern = ctx.createPattern(img, 'repeat') ctx.scale(0.1, 0.1) ctx.globalAlpha = 0.6 ctx.fillStyle = pattern @@ -466,7 +472,7 @@ tests['createPattern() with globalAlpha'] = function (ctx, done) { } tests['createPattern() no-repeat'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.scale(0.1, 0.1) ctx.lineStyle = 'black' @@ -550,13 +556,13 @@ tests['createPattern() then setTransform with no-repeat'] = function (ctx, done) } tests['createLinearGradient()'] = function (ctx) { - var lingrad = ctx.createLinearGradient(0, 0, 0, 150) + const lingrad = ctx.createLinearGradient(0, 0, 0, 150) lingrad.addColorStop(0, '#00ABEB') lingrad.addColorStop(0.5, '#fff') lingrad.addColorStop(0.5, '#26C000') lingrad.addColorStop(1, '#fff') - var lingrad2 = ctx.createLinearGradient(0, 50, 0, 95) + const lingrad2 = ctx.createLinearGradient(0, 50, 0, 95) lingrad2.addColorStop(0.5, '#000') lingrad2.addColorStop(1, 'rgba(0,0,0,0)') @@ -571,7 +577,7 @@ tests['createLinearGradient()'] = function (ctx) { ctx.fillStyle = ctx.fillStyle // eslint-disable-line no-self-assign ctx.fillRect(65, 65, 20, 20) - var lingrad3 = ctx.createLinearGradient(0, 0, 200, 0) + const lingrad3 = ctx.createLinearGradient(0, 0, 200, 0) lingrad3.addColorStop(0, 'rgba(0,255,0,0.5)') lingrad3.addColorStop(0.33, 'rgba(255,255,0,0.5)') lingrad3.addColorStop(0.66, 'rgba(0,255,255,0.5)') @@ -581,7 +587,7 @@ tests['createLinearGradient()'] = function (ctx) { } tests['createLinearGradient() with opacity'] = function (ctx) { - var lingrad = ctx.createLinearGradient(0, 0, 0, 200) + const lingrad = ctx.createLinearGradient(0, 0, 0, 200) lingrad.addColorStop(0, '#00FF00') lingrad.addColorStop(0.33, '#FF0000') lingrad.addColorStop(0.66, '#0000FF') @@ -603,7 +609,7 @@ tests['createLinearGradient() with opacity'] = function (ctx) { } tests['createLinearGradient() and transforms'] = function (ctx) { - var lingrad = ctx.createLinearGradient(0, -100, 0, 100) + const lingrad = ctx.createLinearGradient(0, -100, 0, 100) lingrad.addColorStop(0, '#00FF00') lingrad.addColorStop(0.33, '#FF0000') lingrad.addColorStop(0.66, '#0000FF') @@ -624,22 +630,22 @@ tests['createLinearGradient() and transforms'] = function (ctx) { tests['createRadialGradient()'] = function (ctx) { // Create gradients - var radgrad = ctx.createRadialGradient(45, 45, 10, 52, 50, 30) + const radgrad = ctx.createRadialGradient(45, 45, 10, 52, 50, 30) radgrad.addColorStop(0, '#A7D30C') radgrad.addColorStop(0.9, '#019F62') radgrad.addColorStop(1, 'rgba(1,159,98,0)') - var radgrad2 = ctx.createRadialGradient(105, 105, 20, 112, 120, 50) + const radgrad2 = ctx.createRadialGradient(105, 105, 20, 112, 120, 50) radgrad2.addColorStop(0, '#FF5F98') radgrad2.addColorStop(0.75, '#FF0188') radgrad2.addColorStop(1, 'rgba(255,1,136,0)') - var radgrad3 = ctx.createRadialGradient(95, 15, 15, 102, 20, 40) + const radgrad3 = ctx.createRadialGradient(95, 15, 15, 102, 20, 40) radgrad3.addColorStop(0, '#00C9FF') radgrad3.addColorStop(0.8, '#00B5E2') radgrad3.addColorStop(1, 'rgba(0,201,255,0)') - var radgrad4 = ctx.createRadialGradient(0, 150, 50, 0, 140, 90) + const radgrad4 = ctx.createRadialGradient(0, 150, 50, 0, 140, 90) radgrad4.addColorStop(0, '#F4F201') radgrad4.addColorStop(0.8, '#E4C700') radgrad4.addColorStop(1, 'rgba(228,199,0,0)') @@ -655,7 +661,7 @@ tests['createRadialGradient()'] = function (ctx) { ctx.fillRect(0, 0, 150, 150) } -tests['globalAlpha'] = function (ctx) { +tests.globalAlpha = function (ctx) { ctx.globalAlpha = 0.5 ctx.fillStyle = 'rgba(0,0,0,0.5)' ctx.strokeRect(0, 0, 50, 50) @@ -681,25 +687,25 @@ tests['globalAlpha 2'] = function (ctx) { ctx.globalAlpha = 0.2 - for (var i = 0; i < 7; i++) { + for (let i = 0; i < 7; i++) { ctx.beginPath() ctx.arc(75, 75, 10 + 10 * i, 0, Math.PI * 2, true) ctx.fill() } } -tests['fillStyle'] = function (ctx) { - for (var i = 0; i < 6; i++) { - for (var j = 0; j < 6; j++) { +tests.fillStyle = function (ctx) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 6; j++) { ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' ctx.fillRect(j * 25, i * 25, 25, 25) } } } -tests['strokeStyle'] = function (ctx) { - for (var i = 0; i < 6; i++) { - for (var j = 0; j < 6; j++) { +tests.strokeStyle = function (ctx) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 6; j++) { ctx.strokeStyle = 'rgb(0,' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ')' ctx.beginPath() @@ -724,7 +730,7 @@ tests['fill with stroke'] = function (ctx) { tests['floating point coordinates'] = function (ctx) { ctx.lineCap = 'square' - for (var i = 0; i < 70; i += 3.05) { + for (let i = 0; i < 70; i += 3.05) { ctx.rect(i + 3, 10.5, 0, 130) ctx.moveTo(i + 77, 10.5) ctx.lineTo(i + 77, 140.5) @@ -732,8 +738,8 @@ tests['floating point coordinates'] = function (ctx) { ctx.stroke() } -tests['lineWidth'] = function (ctx) { - for (var i = 0; i < 10; i++) { +tests.lineWidth = function (ctx) { + for (let i = 0; i < 10; i++) { ctx.lineWidth = 1 + i ctx.beginPath() ctx.moveTo(5 + i * 14, 5) @@ -743,7 +749,7 @@ tests['lineWidth'] = function (ctx) { } tests['line caps'] = function (ctx) { - var lineCap = ['butt', 'round', 'square'] + const lineCap = ['butt', 'round', 'square'] ctx.strokeStyle = '#09f' ctx.beginPath() @@ -754,7 +760,7 @@ tests['line caps'] = function (ctx) { ctx.stroke() ctx.strokeStyle = 'black' - for (var i = 0; i < lineCap.length; i++) { + for (let i = 0; i < lineCap.length; i++) { ctx.lineWidth = 15 ctx.lineCap = lineCap[i] ctx.beginPath() @@ -765,9 +771,9 @@ tests['line caps'] = function (ctx) { } tests['line join'] = function (ctx) { - var lineJoin = ['round', 'bevel', 'miter'] + const lineJoin = ['round', 'bevel', 'miter'] ctx.lineWidth = 10 - for (var i = 0; i < lineJoin.length; i++) { + for (let i = 0; i < lineJoin.length; i++) { ctx.lineJoin = lineJoin[i] ctx.beginPath() ctx.moveTo(-5, 5 + i * 40) @@ -788,7 +794,7 @@ tests['lineCap default'] = function (ctx) { ctx.stroke() } -tests['lineCap'] = function (ctx) { +tests.lineCap = function (ctx) { ctx.beginPath() ctx.lineWidth = 10.0 ctx.lineCap = 'round' @@ -798,7 +804,7 @@ tests['lineCap'] = function (ctx) { ctx.stroke() } -tests['lineJoin'] = function (ctx) { +tests.lineJoin = function (ctx) { ctx.beginPath() ctx.lineWidth = 10.0 ctx.lineJoin = 'round' @@ -808,7 +814,7 @@ tests['lineJoin'] = function (ctx) { ctx.stroke() } -tests['states'] = function (ctx) { +tests.states = function (ctx) { ctx.save() ctx.rect(50, 50, 100, 100) ctx.stroke() @@ -890,7 +896,7 @@ tests['fillText()'] = function (ctx) { ctx.lineTo(10, 10) ctx.fillText('Awesome!', 50, 100) - var te = ctx.measureText('Awesome!') + const te = ctx.measureText('Awesome!') ctx.strokeStyle = 'rgba(0,0,0,0.5)' ctx.lineTo(50, 102) @@ -922,7 +928,7 @@ tests['fillText() maxWidth argument'] = function (ctx) { ctx.font = 'Helvetica, sans' ctx.fillText('Drawing text can be fun!', 0, 20) - for (var i = 1; i < 6; i++) { + for (let i = 1; i < 6; i++) { ctx.fillText('Drawing text can be fun!', 0, 20 * (7 - i), i * 20) } @@ -950,7 +956,7 @@ tests['fillText() maxWidth argument + textAlign center (#1253)'] = function (ctx ctx.textAlign = 'center' ctx.fillText('Drawing text can be fun!', 100, 20) - for (var i = 1; i < 6; i++) { + for (let i = 1; i < 6; i++) { ctx.fillText('Drawing text can be fun!', 100, 20 * (7 - i), i * 20) } @@ -962,7 +968,7 @@ tests['fillText() maxWidth argument + textAlign right'] = function (ctx) { ctx.textAlign = 'right' ctx.fillText('Drawing text can be fun!', 200, 20) - for (var i = 1; i < 6; i++) { + for (let i = 1; i < 6; i++) { ctx.fillText('Drawing text can be fun!', 200, 20 * (7 - i), i * 20) } @@ -990,7 +996,7 @@ tests['strokeText() maxWidth argument'] = function (ctx) { ctx.font = 'Helvetica, sans' ctx.strokeText('Drawing text can be fun!', 0, 20) - for (var i = 1; i < 6; i++) { + for (let i = 1; i < 6; i++) { ctx.strokeText('Drawing text can be fun!', 0, 20 * (7 - i), i * 20) } @@ -1374,8 +1380,8 @@ const gco = [ gco.forEach(op => { tests['globalCompositeOperator ' + op] = function (ctx, done) { - var img1 = new Image() - var img2 = new Image() + const img1 = new Image() + const img2 = new Image() img1.onload = function () { img2.onload = function () { ctx.globalAlpha = 0.7 @@ -1392,8 +1398,8 @@ gco.forEach(op => { gco.forEach(op => { tests['9 args, transform, globalCompositeOperator ' + op] = function (ctx, done) { - var img1 = new Image() - var img2 = new Image() + const img1 = new Image() + const img2 = new Image() img1.onload = function () { img2.onload = function () { ctx.globalAlpha = 0.7 @@ -1412,8 +1418,8 @@ gco.forEach(op => { }) tests['drawImage issue #1249'] = function (ctx, done) { - var img1 = new Image() - var img2 = new Image() + const img1 = new Image() + const img2 = new Image() img1.onload = function () { img2.onload = function () { ctx.drawImage(img1, 0, 0, 200, 200) @@ -1427,7 +1433,7 @@ tests['drawImage issue #1249'] = function (ctx, done) { } tests['drawImage 9 arguments big numbers'] = function (ctx, done) { - var img = new Image() + const img = new Image() ctx.imageSmoothingEnabled = false img.onload = function () { // we use big numbers because is over the max canvas allowed @@ -1444,8 +1450,8 @@ tests['drawImage 9 arguments big numbers'] = function (ctx, done) { } tests['known bug #416'] = function (ctx, done) { - var img1 = new Image() - var img2 = new Image() + const img1 = new Image() + const img2 = new Image() img1.onload = function () { img2.onload = function () { ctx.drawImage(img1, 0, 0) @@ -1464,7 +1470,7 @@ tests['known bug #416'] = function (ctx, done) { img1.src = imageSrc('existing.png') } -tests['shadowBlur'] = function (ctx) { +tests.shadowBlur = function (ctx) { ctx.fillRect(150, 10, 20, 20) ctx.lineTo(20, 5) @@ -1490,7 +1496,7 @@ tests['shadowBlur'] = function (ctx) { ctx.fillRect(150, 150, 20, 20) } -tests['shadowColor'] = function (ctx) { +tests.shadowColor = function (ctx) { ctx.fillRect(150, 10, 20, 20) ctx.lineTo(20, 5) @@ -1797,7 +1803,7 @@ tests['shadow transform text'] = function (ctx) { } tests['shadow image'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.shadowColor = '#f3ac22' ctx.shadowBlur = 2 @@ -1811,7 +1817,7 @@ tests['shadow image'] = function (ctx, done) { } tests['shadow image with crop'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.shadowColor = '#000' ctx.shadowBlur = 4 @@ -1829,7 +1835,7 @@ tests['shadow image with crop'] = function (ctx, done) { } tests['shadow image with crop and zoom'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.shadowColor = '#000' ctx.shadowBlur = 4 @@ -1856,7 +1862,7 @@ tests['drawImage canvas over canvas'] = function (ctx) { } tests['scaled shadow image'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.shadowColor = '#f3ac22' ctx.shadowBlur = 2 @@ -1870,7 +1876,7 @@ tests['scaled shadow image'] = function (ctx, done) { } tests['smoothing disabled image'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.imageSmoothingEnabled = false ctx.patternQuality = 'good' @@ -1885,11 +1891,11 @@ tests['smoothing disabled image'] = function (ctx, done) { } tests['createPattern() with globalAlpha and smoothing off scaling down'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.imageSmoothingEnabled = false ctx.patternQuality = 'good' - var pattern = ctx.createPattern(img, 'repeat') + const pattern = ctx.createPattern(img, 'repeat') ctx.scale(0.1, 0.1) ctx.globalAlpha = 0.95 ctx.fillStyle = pattern @@ -1904,11 +1910,11 @@ tests['createPattern() with globalAlpha and smoothing off scaling down'] = funct } tests['createPattern() with globalAlpha and smoothing off scaling up'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.imageSmoothingEnabled = false ctx.patternQuality = 'good' - var pattern = ctx.createPattern(img, 'repeat') + const pattern = ctx.createPattern(img, 'repeat') ctx.scale(20, 20) ctx.globalAlpha = 0.95 ctx.fillStyle = pattern @@ -1923,7 +1929,7 @@ tests['createPattern() with globalAlpha and smoothing off scaling up'] = functio } tests['smoothing and gradients (gradients are not influenced by patternQuality)'] = function (ctx) { - var grad1 = ctx.createLinearGradient(0, 0, 10, 10) + const grad1 = ctx.createLinearGradient(0, 0, 10, 10) grad1.addColorStop(0, 'yellow') grad1.addColorStop(0.25, 'red') grad1.addColorStop(0.75, 'blue') @@ -1949,13 +1955,13 @@ tests['shadow integration'] = function (ctx) { ctx.shadowColor = '#eee' ctx.lineWidth = 3 - var grad1 = ctx.createLinearGradient(105, 0, 200, 100) + const grad1 = ctx.createLinearGradient(105, 0, 200, 100) grad1.addColorStop(0, 'yellow') grad1.addColorStop(0.25, 'red') grad1.addColorStop(0.75, 'blue') grad1.addColorStop(1, 'limegreen') - var grad2 = ctx.createRadialGradient(50, 50, 10, 50, 50, 50) + const grad2 = ctx.createRadialGradient(50, 50, 10, 50, 50, 50) grad2.addColorStop(0, 'yellow') grad2.addColorStop(0.25, 'red') grad2.addColorStop(0.75, 'blue') @@ -1999,7 +2005,7 @@ tests['font state'] = function (ctx) { } tests['drawImage(img,0,0)'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.drawImage(img, 0, 0) done(null) @@ -2009,7 +2015,7 @@ tests['drawImage(img,0,0)'] = function (ctx, done) { } tests['drawImage(img) jpeg'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.drawImage(img, 0, 0, 100, 100) done(null) @@ -2020,7 +2026,7 @@ tests['drawImage(img) jpeg'] = function (ctx, done) { tests['drawImage(img) YCCK JPEG (#425)'] = function (ctx, done) { // This also provides coverage for CMYK JPEGs - var img = new Image() + const img = new Image() img.onload = function () { ctx.drawImage(img, 0, 0, 100, 100) done(null) @@ -2030,7 +2036,7 @@ tests['drawImage(img) YCCK JPEG (#425)'] = function (ctx, done) { } tests['drawImage(img) grayscale JPEG'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.drawImage(img, 0, 0, 100, 100) done(null) @@ -2040,7 +2046,7 @@ tests['drawImage(img) grayscale JPEG'] = function (ctx, done) { } tests['drawImage(img) svg'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.drawImage(img, 0, 0, 100, 100) done(null) @@ -2050,7 +2056,7 @@ tests['drawImage(img) svg'] = function (ctx, done) { } tests['drawImage(img) svg with scaling from drawImage'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.drawImage(img, -800, -800, 1000, 1000) done(null) @@ -2060,7 +2066,7 @@ tests['drawImage(img) svg with scaling from drawImage'] = function (ctx, done) { } tests['drawImage(img) svg with scaling from ctx'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.scale(100, 100) ctx.drawImage(img, -8, -8, 10, 10) @@ -2071,7 +2077,7 @@ tests['drawImage(img) svg with scaling from ctx'] = function (ctx, done) { } tests['drawImage(img,x,y)'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.drawImage(img, 5, 25) done(null) @@ -2081,7 +2087,7 @@ tests['drawImage(img,x,y)'] = function (ctx, done) { } tests['drawImage(img,x,y,w,h) scale down'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.drawImage(img, 25, 25, 10, 10) done(null) @@ -2091,7 +2097,7 @@ tests['drawImage(img,x,y,w,h) scale down'] = function (ctx, done) { } tests['drawImage(img,x,y,w,h) scale down in a scaled up context'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.scale(20, 20) ctx.drawImage(img, 0, 0, 10, 10) @@ -2102,7 +2108,7 @@ tests['drawImage(img,x,y,w,h) scale down in a scaled up context'] = function (ct } tests['drawImage(img,x,y,w,h) scale up'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.drawImage(img, 0, 0, 200, 200) done(null) @@ -2112,7 +2118,7 @@ tests['drawImage(img,x,y,w,h) scale up'] = function (ctx, done) { } tests['drawImage(img,x,y,w,h) scale vertical'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.drawImage(img, 0, 0, img.width, 200) done(null) @@ -2122,7 +2128,7 @@ tests['drawImage(img,x,y,w,h) scale vertical'] = function (ctx, done) { } tests['drawImage(img,sx,sy,sw,sh,x,y,w,h)'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.drawImage(img, 13, 13, 45, 45, 25, 25, img.width / 2, img.height / 2) done(null) @@ -2132,7 +2138,7 @@ tests['drawImage(img,sx,sy,sw,sh,x,y,w,h)'] = function (ctx, done) { } tests['drawImage(img,0,0) globalAlpha'] = function (ctx, done) { - var img = new Image() + const img = new Image() ctx.fillRect(50, 50, 30, 30) ctx.globalAlpha = 0.5 img.onload = function () { @@ -2147,7 +2153,7 @@ tests['drawImage(img,0,0) clip'] = function (ctx, done) { ctx.arc(50, 50, 50, 0, Math.PI * 2, false) ctx.stroke() ctx.clip() - var img = new Image() + const img = new Image() ctx.fillRect(50, 50, 30, 30) ctx.globalAlpha = 0.5 img.onload = function () { @@ -2159,120 +2165,120 @@ tests['drawImage(img,0,0) clip'] = function (ctx, done) { } tests['putImageData()'] = function (ctx) { - for (var i = 0; i < 6; i++) { - for (var j = 0; j < 6; j++) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 6; j++) { ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' ctx.fillRect(j * 25, i * 25, 25, 25) } } - var data = ctx.getImageData(0, 0, 50, 50) + const data = ctx.getImageData(0, 0, 50, 50) ctx.putImageData(data, 10, 10) } tests['putImageData() 1'] = function (ctx) { - for (var i = 0; i < 6; i++) { - for (var j = 0; j < 6; j++) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 6; j++) { ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' ctx.fillRect(j * 25, i * 25, 25, 25) } } - var data = ctx.getImageData(0, 0, 50, 50) + const data = ctx.getImageData(0, 0, 50, 50) ctx.putImageData(data, -10, -10) } tests['putImageData() 2'] = function (ctx) { - for (var i = 0; i < 6; i++) { - for (var j = 0; j < 6; j++) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 6; j++) { ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' ctx.fillRect(j * 25, i * 25, 25, 25) } } - var data = ctx.getImageData(25, 25, 50, 50) + const data = ctx.getImageData(25, 25, 50, 50) ctx.putImageData(data, 10, 10) } tests['putImageData() 3'] = function (ctx) { - for (var i = 0; i < 6; i++) { - for (var j = 0; j < 6; j++) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 6; j++) { ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' ctx.fillRect(j * 25, i * 25, 25, 25) } } - var data = ctx.getImageData(10, 25, 10, 50) + const data = ctx.getImageData(10, 25, 10, 50) ctx.putImageData(data, 50, 10) } tests['putImageData() 4'] = function (ctx) { - for (var i = 0; i < 6; i++) { - for (var j = 0; j < 6; j++) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 6; j++) { ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' ctx.fillRect(j * 25, i * 25, 25, 25) } } ctx.strokeRect(30, 30, 30, 30) - var data = ctx.getImageData(0, 0, 50, 50) + const data = ctx.getImageData(0, 0, 50, 50) ctx.putImageData(data, 30, 30, 10, 10, 30, 30) } tests['putImageData() 5'] = function (ctx) { - for (var i = 0; i < 6; i++) { - for (var j = 0; j < 6; j++) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 6; j++) { ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' ctx.fillRect(j * 25, i * 25, 25, 25) } } ctx.strokeRect(60, 60, 50, 30) - var data = ctx.getImageData(0, 0, 50, 50) + const data = ctx.getImageData(0, 0, 50, 50) ctx.putImageData(data, 60, 60, 0, 0, 50, 30) } tests['putImageData() 6'] = function (ctx) { - for (var i = 0; i < 6; i++) { - for (var j = 0; j < 6; j++) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 6; j++) { ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' ctx.fillRect(j * 25, i * 25, 25, 25) } } ctx.strokeRect(60, 60, 50, 30) - var data = ctx.getImageData(0, 0, 50, 50) + const data = ctx.getImageData(0, 0, 50, 50) ctx.putImageData(data, 60, 60, 10, 0, 35, 30) } tests['putImageData() 7'] = function (ctx) { - for (var i = 0; i < 6; i++) { - for (var j = 0; j < 6; j++) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 6; j++) { ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' ctx.fillRect(j * 25, i * 25, 25, 25) } } ctx.strokeRect(60, 60, 50, 30) ctx.translate(20, 20) - var data = ctx.getImageData(0, 0, 50, 50) + const data = ctx.getImageData(0, 0, 50, 50) ctx.putImageData(data, 60, 60, 10, 20, 35, -10) } tests['putImageData() 8'] = function (ctx) { - for (var i = 0; i < 6; i++) { - for (var j = 0; j < 6; j++) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 6; j++) { ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' ctx.fillRect(j * 25, i * 25, 25, 25) } } ctx.translate(20, 20) - var data = ctx.getImageData(0, 0, 50, 50) + const data = ctx.getImageData(0, 0, 50, 50) ctx.putImageData(data, -10, -10, 0, 20, 35, 30) } tests['putImageData() 9'] = function (ctx) { - for (var i = 0; i < 6; i++) { - for (var j = 0; j < 6; j++) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 6; j++) { ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' ctx.fillRect(j * 25, i * 25, 25, 25) } } ctx.translate(20, 20) - var data = ctx.getImageData(0, 0, 50, 50) + const data = ctx.getImageData(0, 0, 50, 50) ctx.putImageData(data, -10, -10, 0, 20, 500, 500) } @@ -2284,7 +2290,7 @@ tests['putImageData() 10'] = function (ctx) { ctx.fillStyle = 'rgba(0,0,255,1)' ctx.fillRect(100, 0, 50, 100) - var data = ctx.getImageData(0, 0, 120, 20) + const data = ctx.getImageData(0, 0, 120, 20) ctx.putImageData(data, 20, 120) } @@ -2296,7 +2302,7 @@ tests['putImageData() alpha'] = function (ctx) { ctx.fillStyle = 'rgba(0,0,255,0.5)' ctx.fillRect(100, 0, 50, 100) - var data = ctx.getImageData(0, 0, 120, 20) + const data = ctx.getImageData(0, 0, 120, 20) ctx.putImageData(data, 20, 120) } @@ -2308,7 +2314,7 @@ tests['putImageData() alpha 2'] = function (ctx) { ctx.fillStyle = 'rgba(0,0,255,0.75)' ctx.fillRect(100, 0, 50, 100) - var data = ctx.getImageData(0, 0, 120, 20) + const data = ctx.getImageData(0, 0, 120, 20) ctx.putImageData(data, 20, 120) } @@ -2321,19 +2327,19 @@ tests['putImageData() globalAlpha'] = function (ctx) { ctx.fillStyle = '#00f' ctx.fillRect(100, 0, 50, 100) - var data = ctx.getImageData(0, 0, 120, 20) + const data = ctx.getImageData(0, 0, 120, 20) ctx.putImageData(data, 20, 120) } tests['putImageData() png data'] = function (ctx, done) { - var img = new Image() + const img = new Image() ctx.fillRect(50, 50, 30, 30) img.onload = function () { ctx.drawImage(img, 0, 0, 200, 200) - var imageData = ctx.getImageData(0, 0, 50, 50) - var data = imageData.data + const imageData = ctx.getImageData(0, 0, 50, 50) + const data = imageData.data if (data instanceof Uint8ClampedArray) { - for (var i = 0, len = data.length; i < len; i += 4) { + for (let i = 0, len = data.length; i < len; i += 4) { data[i + 3] = 80 } } @@ -2347,14 +2353,14 @@ tests['putImageData() png data'] = function (ctx, done) { } tests['putImageData() png data 2'] = function (ctx, done) { - var img = new Image() + const img = new Image() ctx.fillRect(50, 50, 30, 30) img.onload = function () { ctx.drawImage(img, 0, 0, 200, 200) - var imageData = ctx.getImageData(0, 0, 50, 50) - var data = imageData.data + const imageData = ctx.getImageData(0, 0, 50, 50) + const data = imageData.data if (data instanceof Uint8ClampedArray) { - for (var i = 0, len = data.length; i < len; i += 4) { + for (let i = 0, len = data.length; i < len; i += 4) { data[i + 3] = 80 } } @@ -2368,14 +2374,14 @@ tests['putImageData() png data 2'] = function (ctx, done) { } tests['putImageData() png data 3'] = function (ctx, done) { - var img = new Image() + const img = new Image() ctx.fillRect(50, 50, 30, 30) img.onload = function () { ctx.drawImage(img, 0, 0, 200, 200) - var imageData = ctx.getImageData(0, 0, 50, 50) - var data = imageData.data + const imageData = ctx.getImageData(0, 0, 50, 50) + const data = imageData.data if (data instanceof Uint8ClampedArray) { - for (var i = 0, len = data.length; i < len; i += 4) { + for (let i = 0, len = data.length; i < len; i += 4) { data[i + 0] = data[i + 0] * 0.2 data[i + 1] = data[i + 1] * 0.2 data[i + 2] = data[i + 2] * 0.2 @@ -2388,12 +2394,12 @@ tests['putImageData() png data 3'] = function (ctx, done) { img.src = imageSrc('state.png') } -tests['setLineDash'] = function (ctx) { +tests.setLineDash = function (ctx) { ctx.setLineDash([10, 5, 25, 15]) ctx.lineWidth = 14 - var y = 5 - var line = function (lineDash, color) { + let y = 5 + const line = function (lineDash, color) { ctx.setLineDash(lineDash) if (color) ctx.strokeStyle = color ctx.beginPath() @@ -2412,7 +2418,7 @@ tests['setLineDash'] = function (ctx) { line([10, 10, NaN]) line((function () { ctx.setLineDash([8]) - var a = ctx.getLineDash() + const a = ctx.getLineDash() a[0] -= 3 a.push(20) return a @@ -2422,12 +2428,12 @@ tests['setLineDash'] = function (ctx) { line([0, 3, 0, 0], 'green') // should be empty } -tests['lineDashOffset'] = function (ctx) { +tests.lineDashOffset = function (ctx) { ctx.setLineDash([10, 5, 25, 15]) ctx.lineWidth = 4 - var y = 5 - var line = function (lineDashOffset, color) { + let y = 5 + const line = function (lineDashOffset, color) { ctx.lineDashOffset = lineDashOffset if (color) ctx.strokeStyle = color ctx.beginPath() @@ -2449,18 +2455,18 @@ tests['lineDashOffset'] = function (ctx) { line(60, 'orange') line(-Infinity) line(70, 'purple') - line(void 0) + line(undefined) line(80, 'black') line(ctx.lineDashOffset + 10) - for (var i = 0; i < 10; i++) { + for (let i = 0; i < 10; i++) { line(90 + i / 5, 'red') } } tests['fillStyle=\'hsl(...)\''] = function (ctx) { - for (var i = 0; i < 6; i++) { - for (var j = 0; j < 6; j++) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 6; j++) { ctx.fillStyle = 'hsl(' + (360 - 60 * i) + ',' + (100 - 16.66 * j) + '%,' + (50 + (i + j) * (50 / 12)) + '%)' ctx.fillRect(j * 25, i * 25, 25, 25) } @@ -2468,8 +2474,8 @@ tests['fillStyle=\'hsl(...)\''] = function (ctx) { } tests['fillStyle=\'hsla(...)\''] = function (ctx) { - for (var i = 0; i < 6; i++) { - for (var j = 0; j < 6; j++) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 6; j++) { ctx.fillStyle = 'hsla(' + (360 - 60 * i) + ',' + (100 - 16.66 * j) + '%,50%,' + (1 - 0.16 * j) + ')' ctx.fillRect(j * 25, i * 25, 25, 25) } @@ -2506,7 +2512,7 @@ tests['rotated baseline'] = function (ctx) { ctx.textBaseline = 'bottom' ctx.translate(100, 100) - for (var i = 0; i < 16; i++) { + for (let i = 0; i < 16; i++) { ctx.fillText('Hello world!', -50, -50) ctx.rotate(-Math.PI / 8) } @@ -2520,7 +2526,7 @@ tests['rotated and scaled baseline'] = function (ctx) { ctx.translate(100, 100) ctx.scale(0.1, 0.2) - for (var i = 0; i < 16; i++) { + for (let i = 0; i < 16; i++) { ctx.fillText('Hello world!', -50 / 0.1, -50 / 0.2) ctx.rotate(-Math.PI / 8) } @@ -2534,7 +2540,7 @@ tests['rotated and skewed baseline'] = function (ctx) { ctx.translate(100, 100) ctx.transform(1, 1, 0, 1, 1, 1) - for (var i = 0; i < 16; i++) { + for (let i = 0; i < 16; i++) { ctx.fillText('Hello world!', -50, -50) ctx.rotate(-Math.PI / 8) } @@ -2551,7 +2557,7 @@ tests['rotated, scaled and skewed baseline'] = function (ctx) { ctx.scale(0.1, 0.2) ctx.transform(1, 1, 0, 1, 1, 1) - for (var i = 0; i < 16; i++) { + for (let i = 0; i < 16; i++) { ctx.fillText('Hello world!', -50 / 0.1, -50 / 0.2) ctx.rotate(-Math.PI / 8) } @@ -2565,7 +2571,7 @@ tests['measureText()'] = function (ctx) { ctx.fillText(text, x, y) ctx.strokeStyle = 'red' ctx.beginPath(); ctx.moveTo(0, y + 0.5); ctx.lineTo(200, y + 0.5); ctx.stroke() - var metrics = ctx.measureText(text) + const metrics = ctx.measureText(text) ctx.strokeStyle = 'blue' ctx.strokeRect( x - metrics.actualBoundingBoxLeft + 0.5, @@ -2617,7 +2623,7 @@ tests['image sampling (#1084)'] = function (ctx, done) { } tests['drawImage reflection bug'] = function (ctx, done) { - var img1 = new Image() + const img1 = new Image() img1.onload = function () { ctx.drawImage(img1, 60, 30, 150, 150, 0, 0, 200, 200) done() @@ -2626,7 +2632,7 @@ tests['drawImage reflection bug'] = function (ctx, done) { } tests['drawImage reflection bug with skewing'] = function (ctx, done) { - var img1 = new Image() + const img1 = new Image() img1.onload = function () { ctx.transform(1.2, 1, 1.8, 1.3, 0, 0) ctx.drawImage(img1, 60, 30, 150, 150, 0, 0, 200, 200) diff --git a/test/server.js b/test/server.js index d583a7748..fe2a53218 100644 --- a/test/server.js +++ b/test/server.js @@ -1,19 +1,19 @@ -var path = require('path') -var express = require('express') +const path = require('path') +const express = require('express') -var Canvas = require('../') -var tests = require('./public/tests') +const Canvas = require('../') +const tests = require('./public/tests') -var app = express() -var port = parseInt(process.argv[2] || '4000', 10) +const app = express() +const port = parseInt(process.argv[2] || '4000', 10) function renderTest (canvas, name, cb) { if (!tests[name]) { throw new Error('Unknown test: ' + name) } - var ctx = canvas.getContext('2d', { pixelFormat: 'RGBA32' }) - var initialFillStyle = ctx.fillStyle + const ctx = canvas.getContext('2d', { pixelFormat: 'RGBA32' }) + const initialFillStyle = ctx.fillStyle ctx.fillStyle = 'white' ctx.fillRect(0, 0, 200, 200) ctx.fillStyle = initialFillStyle @@ -37,7 +37,7 @@ app.get('/pixelmatch.js', function (req, res) { }) app.get('/render', function (req, res, next) { - var canvas = Canvas.createCanvas(200, 200) + const canvas = Canvas.createCanvas(200, 200) renderTest(canvas, req.query.name, function (err) { if (err) return next(err) @@ -48,7 +48,7 @@ app.get('/render', function (req, res, next) { }) app.get('/pdf', function (req, res, next) { - var canvas = Canvas.createCanvas(200, 200, 'pdf') + const canvas = Canvas.createCanvas(200, 200, 'pdf') renderTest(canvas, req.query.name, function (err) { if (err) return next(err) diff --git a/util/has_lib.js b/util/has_lib.js index 40bbf0847..02d709064 100644 --- a/util/has_lib.js +++ b/util/has_lib.js @@ -1,8 +1,8 @@ -var query = process.argv[2] -var fs = require('fs') -var childProcess = require('child_process') +const query = process.argv[2] +const fs = require('fs') +const childProcess = require('child_process') -var SYSTEM_PATHS = [ +const SYSTEM_PATHS = [ '/lib', '/usr/lib', '/usr/lib64', @@ -23,8 +23,8 @@ var SYSTEM_PATHS = [ * @return {boolean} exists */ function hasSystemLib (lib) { - var libName = 'lib' + lib + '.+(so|dylib)' - var libNameRegex = new RegExp(libName) + const libName = 'lib' + lib + '.+(so|dylib)' + const libNameRegex = new RegExp(libName) // Try using ldconfig on linux systems if (hasLdconfig()) { @@ -40,7 +40,7 @@ function hasSystemLib (lib) { // Try checking common library locations return SYSTEM_PATHS.some(function (systemPath) { try { - var dirListing = fs.readdirSync(systemPath) + const dirListing = fs.readdirSync(systemPath) return dirListing.some(function (file) { return libNameRegex.test(file) }) diff --git a/util/win_jpeg_lookup.js b/util/win_jpeg_lookup.js index 82869945a..79815f650 100644 --- a/util/win_jpeg_lookup.js +++ b/util/win_jpeg_lookup.js @@ -1,21 +1,21 @@ -var fs = require('fs') -var paths = ['C:/libjpeg-turbo'] +const fs = require('fs') +const paths = ['C:/libjpeg-turbo'] if (process.arch === 'x64') { paths.unshift('C:/libjpeg-turbo64') } -paths.forEach(function(path){ +paths.forEach(function (path) { if (exists(path)) { process.stdout.write(path) process.exit() } }) -function exists(path) { +function exists (path) { try { return fs.lstatSync(path).isDirectory() - } catch(e) { + } catch (e) { return false } } From a721d5170e7874045f4ce942991dc716a5fb1127 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Wed, 8 Dec 2021 00:38:19 -0800 Subject: [PATCH 345/474] Clean up isnan/isinf, use isfinite Cleans up funky C code and uses std::isfinite, which also compiles to fewer instructions (https://godbolt.org/z/qMT8bMTKn). --- CHANGELOG.md | 1 + src/CanvasRenderingContext2d.cc | 19 +++---------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29a7d2136..6558bf06f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * Changed `DOMPoint()` constructor to check for parameter nullability. * Changed `DOMMatrix.js` to use string literals for non-special cases. * Remove semicolons from Dommatrix.js. +* Clean up inf/nan macros and slightly speed up argument checking. ### Added * Added `deregisterAllFonts` method to free up memory and reduce font conflicts. ### Fixed diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 0bb867c73..f3415a7ed 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -21,17 +21,6 @@ using namespace v8; -// Windows doesn't support the C99 names for these -#ifdef _MSC_VER -#define isnan(x) _isnan(x) -#define isinf(x) (!_finite(x)) -#endif - -#ifndef isnan -#define isnan(x) std::isnan(x) -#define isinf(x) std::isinf(x) -#endif - Nan::Persistent Context2d::constructor; /* @@ -77,9 +66,7 @@ inline static bool checkArgs(const Nan::FunctionCallbackInfo &info, doubl double val = Nan::To(info[i]).FromMaybe(0); if (areArgsValid) { - if (val != val || - val == std::numeric_limits::infinity() || - val == -std::numeric_limits::infinity()) { + if (!std::isfinite(val)) { // We should continue the loop instead of returning immediately // See https://html.spec.whatwg.org/multipage/canvas.html @@ -2787,7 +2774,7 @@ NAN_METHOD(Context2d::SetLineDash) { if (!d->IsNumber()) return; a[i] = Nan::To(d).FromMaybe(0); if (a[i] == 0) zero_dashes++; - if (a[i] < 0 || isnan(a[i]) || isinf(a[i])) return; + if (a[i] < 0 || !std::isfinite(a[i])) return; } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -2827,7 +2814,7 @@ NAN_METHOD(Context2d::GetLineDash) { */ NAN_SETTER(Context2d::SetLineDashOffset) { double offset = Nan::To(value).FromMaybe(0); - if (isnan(offset) || isinf(offset)) return; + if (!std::isfinite(offset)) return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); From aed721dc6808f708344c8a224d05948b5fa4eb13 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Wed, 8 Dec 2021 11:52:05 -0700 Subject: [PATCH 346/474] Update nan v14 requires 2.14.1. The semver range in package.json already allowed for that version, but this requires at least that version. None of the other changes (2.14.2, 2.15.0) matter for us. --- CHANGELOG.md | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6558bf06f..1a03cdd69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * Changed `DOMPoint()` constructor to check for parameter nullability. * Changed `DOMMatrix.js` to use string literals for non-special cases. * Remove semicolons from Dommatrix.js. +* Update nan to v2.15.0 to ensure Node.js v14+ support. * Clean up inf/nan macros and slightly speed up argument checking. ### Added * Added `deregisterAllFonts` method to free up memory and reduce font conflicts. diff --git a/package.json b/package.json index 1c21c402f..68e1ad816 100644 --- a/package.json +++ b/package.json @@ -48,8 +48,8 @@ ], "types": "types/index.d.ts", "dependencies": { - "nan": "^2.14.0", "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.15.0", "simple-get": "^3.0.3" }, "devDependencies": { From d603479a07da8e7a5d27ed1ba7a155988d7c3282 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 30 Dec 2021 16:51:08 -0800 Subject: [PATCH 347/474] v2.9.0 --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a03cdd69..a1df850f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ project adheres to [Semantic Versioning](http://semver.org/). (Unreleased) ================== ### Changed +### Added +### Fixed + +2.9.0 +================== +### Changed * Refactor functions to classes. * Changed `DOMPoint()` constructor to check for parameter nullability. * Changed `DOMMatrix.js` to use string literals for non-special cases. diff --git a/package.json b/package.json index 68e1ad816..b833e7168 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.8.0", + "version": "2.9.0", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 5fc80e7dd6efce094d0ca1cc84c0573327246336 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Fri, 31 Dec 2021 12:47:06 -0800 Subject: [PATCH 348/474] bug: stringify CanvasPattern, ImageData, CanvasGradient like browsers Fixes #1646 Fixes #1639 --- CHANGELOG.md | 1 + lib/bindings.js | 12 ++++++- lib/pattern.js | 4 +++ test/canvas.test.js | 72 +++++++++++++++++++++++++----------------- test/imageData.test.js | 5 +++ 5 files changed, 64 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1df850f4..e87890de5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed +* Stringify CanvasGradient, CanvasPattern and ImageData like browsers do. (#1639, #1646) 2.9.0 ================== diff --git a/lib/bindings.js b/lib/bindings.js index 95ee914f3..c638a5878 100644 --- a/lib/bindings.js +++ b/lib/bindings.js @@ -1,3 +1,13 @@ 'use strict' -module.exports = require('../build/Release/canvas.node') +const bindings = require('../build/Release/canvas.node') + +module.exports = bindings + +bindings.ImageData.prototype.toString = function () { + return '[object ImageData]' +} + +bindings.CanvasGradient.prototype.toString = function () { + return '[object CanvasGradient]' +} diff --git a/lib/pattern.js b/lib/pattern.js index 3f86f8700..1de497681 100644 --- a/lib/pattern.js +++ b/lib/pattern.js @@ -11,3 +11,7 @@ const { DOMMatrix } = require('./DOMMatrix') bindings.CanvasPatternInit(DOMMatrix) module.exports = bindings.CanvasPattern + +bindings.CanvasPattern.prototype.toString = function () { + return '[object CanvasPattern]' +} diff --git a/test/canvas.test.js b/test/canvas.test.js index 46e167637..c2a2eda80 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -1317,37 +1317,44 @@ describe('Canvas', function () { }) }); - it('Context2d#createPattern(Image)', function () { - return loadImage(path.join(__dirname, '/fixtures/checkers.png')).then((img) => { - const canvas = createCanvas(20, 20) - const ctx = canvas.getContext('2d') - const pattern = ctx.createPattern(img) + it('Context2d#createPattern(Image)', async function () { + const img = await loadImage(path.join(__dirname, '/fixtures/checkers.png')); + const canvas = createCanvas(20, 20) + const ctx = canvas.getContext('2d') + const pattern = ctx.createPattern(img) - ctx.fillStyle = pattern - ctx.fillRect(0, 0, 20, 20) - - const imageData = ctx.getImageData(0, 0, 20, 20) - assert.equal(20, imageData.width) - assert.equal(20, imageData.height) - assert.equal(1600, imageData.data.length) - - let i = 0; let b = true - while (i < imageData.data.length) { - if (b) { - assert.equal(0, imageData.data[i++]) - assert.equal(0, imageData.data[i++]) - assert.equal(0, imageData.data[i++]) - assert.equal(255, imageData.data[i++]) - } else { - assert.equal(255, imageData.data[i++]) - assert.equal(255, imageData.data[i++]) - assert.equal(255, imageData.data[i++]) - assert.equal(255, imageData.data[i++]) - } - // alternate b, except when moving to a new row - b = i % (imageData.width * 4) === 0 ? b : !b + ctx.fillStyle = pattern + ctx.fillRect(0, 0, 20, 20) + + const imageData = ctx.getImageData(0, 0, 20, 20) + assert.equal(20, imageData.width) + assert.equal(20, imageData.height) + assert.equal(1600, imageData.data.length) + + let i = 0; let b = true + while (i < imageData.data.length) { + if (b) { + assert.equal(0, imageData.data[i++]) + assert.equal(0, imageData.data[i++]) + assert.equal(0, imageData.data[i++]) + assert.equal(255, imageData.data[i++]) + } else { + assert.equal(255, imageData.data[i++]) + assert.equal(255, imageData.data[i++]) + assert.equal(255, imageData.data[i++]) + assert.equal(255, imageData.data[i++]) } - }) + // alternate b, except when moving to a new row + b = i % (imageData.width * 4) === 0 ? b : !b + } + }) + + it('CanvasPattern stringifies as [object CanvasPattern]', async function () { + const img = await loadImage(path.join(__dirname, '/fixtures/checkers.png')); + const canvas = createCanvas(20, 20) + const ctx = canvas.getContext('2d') + const pattern = ctx.createPattern(img) + assert.strictEqual(pattern.toString(), '[object CanvasPattern]') }) it('Context2d#createLinearGradient()', function () { @@ -1380,6 +1387,13 @@ describe('Canvas', function () { assert.equal(255, imageData.data[i + 3]) }) + it('CanvasGradient stringifies as [object CanvasGradient]', function () { + const canvas = createCanvas(20, 1) + const ctx = canvas.getContext('2d') + const gradient = ctx.createLinearGradient(1, 1, 19, 1) + assert.strictEqual(gradient.toString(), '[object CanvasGradient]') + }) + describe('Context2d#putImageData()', function () { it('throws for invalid arguments', function () { const canvas = createCanvas(2, 1) diff --git a/test/imageData.test.js b/test/imageData.test.js index 13bbcab1b..d3c84c29a 100644 --- a/test/imageData.test.js +++ b/test/imageData.test.js @@ -12,6 +12,11 @@ describe('ImageData', function () { assert.throws(function () { ImageData.prototype.width }, /incompatible receiver/) }) + it('stringifies as [object ImageData]', function () { + const imageData = createImageData(2, 3) + assert.strictEqual(imageData.toString(), '[object ImageData]') + }) + it('should throw with invalid numeric arguments', function () { assert.throws(() => { createImageData(0, 0) }, /width is zero/) assert.throws(() => { createImageData(1, 0) }, /height is zero/) From 83a8df1592ebf442dcce3af659f3eff6a57f5087 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 30 Dec 2021 18:08:22 -0800 Subject: [PATCH 349/474] Add missing cctype include for toupper Fixes #1848 --- CHANGELOG.md | 1 + src/Util.h | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e87890de5..467e95ba2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Added ### Fixed * Stringify CanvasGradient, CanvasPattern and ImageData like browsers do. (#1639, #1646) +* Add missing include for `toupper`. 2.9.0 ================== diff --git a/src/Util.h b/src/Util.h index 8f935359d..dba6883a2 100644 --- a/src/Util.h +++ b/src/Util.h @@ -2,6 +2,7 @@ #include #include +#include // Wrapper around Nan::SetAccessor that makes it easier to change the last // argument (signature). Getters/setters must be accessed only when there is From 0bccc05b384cc1f3e969cd9ed7859a95376ec88c Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 30 Dec 2021 18:40:50 -0800 Subject: [PATCH 350/474] bug: fix process crash in getImageData for PDF/SVG canvases Fixes #1853 --- CHANGELOG.md | 1 + src/CanvasRenderingContext2d.cc | 8 ++++---- test/canvas.test.js | 20 ++++++++++++++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 467e95ba2..84f155f0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Fixed * Stringify CanvasGradient, CanvasPattern and ImageData like browsers do. (#1639, #1646) * Add missing include for `toupper`. +* Throw an error instead of crashing the process if `getImageData` or `putImageData` is called on a PDF or SVG canvas (#1853) 2.9.0 ================== diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index f3415a7ed..b98fe4520 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -937,8 +937,8 @@ NAN_METHOD(Context2d::PutImageData) { } #endif default: { - Nan::ThrowError("Invalid pixel format"); - break; + Nan::ThrowError("Invalid pixel format or not an image canvas"); + return; } } @@ -1111,8 +1111,8 @@ NAN_METHOD(Context2d::GetImageData) { #endif default: { // Unlikely - Nan::ThrowError("Invalid pixel format"); - break; + Nan::ThrowError("Invalid pixel format or not an image canvas"); + return; } } diff --git a/test/canvas.test.js b/test/canvas.test.js index c2a2eda80..b4c8eb7e1 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -1189,6 +1189,14 @@ describe('Canvas', function () { const ctx = createTestCanvas() assert.throws(function () { ctx.getImageData(0, 0, 0, 0) }, /IndexSizeError/) }) + + it('throws if canvas is a PDF canvas (#1853)', function () { + const canvas = createCanvas(3, 6, 'pdf') + const ctx = canvas.getContext('2d') + assert.throws(() => { + ctx.getImageData(0, 0, 3, 6) + }) + }) }) it('Context2d#createPattern(Canvas)', function () { @@ -1402,6 +1410,18 @@ describe('Canvas', function () { assert.throws(function () { ctx.putImageData(undefined, 0, 0) }, TypeError) }) + it('throws if canvas is a PDF canvas (#1853)', function () { + const canvas = createCanvas(3, 6, 'pdf') + const ctx = canvas.getContext('2d') + const srcImageData = createImageData(new Uint8ClampedArray([ + 1, 2, 3, 255, 5, 6, 7, 255, + 0, 1, 2, 255, 4, 5, 6, 255 + ]), 2) + assert.throws(() => { + ctx.putImageData(srcImageData, -1, -1) + }) + }) + it('works for negative source values', function () { const canvas = createCanvas(2, 2) const ctx = canvas.getContext('2d') From 6fd0fa55343aa244e7097f6ee50b271a8636ee63 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Thu, 24 Feb 2022 14:12:43 -0500 Subject: [PATCH 351/474] make types compatible with typescript 4.6 (#1986) --- CHANGELOG.md | 1 + types/index.d.ts | 6 ------ types/test.ts | 1 - 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84f155f0d..1e33918a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * Stringify CanvasGradient, CanvasPattern and ImageData like browsers do. (#1639, #1646) * Add missing include for `toupper`. * Throw an error instead of crashing the process if `getImageData` or `putImageData` is called on a PDF or SVG canvas (#1853) +* Compatibility with Typescript 4.6 2.9.0 ================== diff --git a/types/index.d.ts b/types/index.d.ts index c667dfbf1..f613281e2 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -166,12 +166,6 @@ declare class NodeCanvasRenderingContext2D extends CanvasRenderingContext2D { */ textDrawingMode: 'path' | 'glyph' - /** _'saturate' is non-standard._ */ - globalCompositeOperation: 'saturate' | 'clear' | 'copy' | 'destination' | 'source-over' | 'destination-over' | - 'source-in' | 'destination-in' | 'source-out' | 'destination-out' | 'source-atop' | 'destination-atop' | - 'xor' | 'lighter' | 'multiply' | 'screen' | 'overlay' | 'darken' | 'lighten' | 'color-dodge' | 'color-burn' | - 'hard-light' | 'soft-light' | 'difference' | 'exclusion' | 'hue' | 'saturation' | 'color' | 'luminosity' - /** _Non-standard_. Sets the antialiasing mode. */ antialias: 'default' | 'gray' | 'none' | 'subpixel' diff --git a/types/test.ts b/types/test.ts index 8cff19419..bfbe26429 100644 --- a/types/test.ts +++ b/types/test.ts @@ -16,7 +16,6 @@ ctx.currentTransform = ctx.getTransform() ctx.quality = 'best' ctx.textDrawingMode = 'glyph' -ctx.globalCompositeOperation = 'saturate' const grad: Canvas.CanvasGradient = ctx.createLinearGradient(0, 1, 2, 3) grad.addColorStop(0.1, 'red') From 009d5942c6d0a1f1c1ec421e701bd62e3334409a Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Wed, 9 Feb 2022 23:16:28 +0000 Subject: [PATCH 352/474] replace some remaining glib calls --- src/Canvas.cc | 10 ++++----- src/register_font.cc | 48 +++++++++++--------------------------------- 2 files changed, 17 insertions(+), 41 deletions(-) diff --git a/src/Canvas.cc b/src/Canvas.cc index bd39d4329..9270031f2 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -706,9 +706,9 @@ NAN_METHOD(Canvas::StreamJPEGSync) { char * str_value(Local val, const char *fallback, bool can_be_number) { if (val->IsString() || (can_be_number && val->IsNumber())) { - return g_strdup(*Nan::Utf8String(val)); + return strdup(*Nan::Utf8String(val)); } else if (fallback) { - return g_strdup(fallback); + return strdup(fallback); } else { return NULL; } @@ -765,9 +765,9 @@ NAN_METHOD(Canvas::RegisterFont) { Nan::ThrowError(GENERIC_FACE_ERROR); } - g_free(family); - g_free(weight); - g_free(style); + free(family); + free(weight); + free(style); } NAN_METHOD(Canvas::DeregisterAllFonts) { diff --git a/src/register_font.cc b/src/register_font.cc index 1b8632dd2..927a66b4b 100644 --- a/src/register_font.cc +++ b/src/register_font.cc @@ -94,35 +94,12 @@ to_utf8(FT_Byte* buf, FT_UInt len, FT_UShort pid, FT_UShort eid) { * system, fall back to the other */ -typedef struct _NameDef { - const char *buf; - int rank; // the higher the more desirable -} NameDef; - -gint -_name_def_compare(gconstpointer a, gconstpointer b) { - return ((NameDef*)a)->rank > ((NameDef*)b)->rank ? -1 : 1; -} - -// Some versions of GTK+ do not have this, particualrly the one we -// currently link to in node-canvas's wiki -void -_free_g_list_item(gpointer data, gpointer user_data) { - NameDef *d = (NameDef *)data; - free((void *)(d->buf)); -} - -void -_g_list_free_full(GList *list) { - g_list_foreach(list, _free_g_list_item, NULL); - g_list_free(list); -} - char * get_family_name(FT_Face face) { FT_SfntName name; - GList *list = NULL; - char *utf8name = NULL; + + int best_rank = -1; + char* best_buf = NULL; for (unsigned i = 0; i < FT_Get_Sfnt_Name_Count(face); ++i) { FT_Get_Sfnt_Name(face, i, &name); @@ -131,20 +108,19 @@ get_family_name(FT_Face face) { char *buf = to_utf8(name.string, name.string_len, name.platform_id, name.encoding_id); if (buf) { - NameDef *d = (NameDef*)malloc(sizeof(NameDef)); - d->buf = (const char*)buf; - d->rank = GET_NAME_RANK(name); - - list = g_list_insert_sorted(list, (gpointer)d, _name_def_compare); + int rank = GET_NAME_RANK(name); + if (rank > best_rank) { + best_rank = rank; + if (best_buf) free(best_buf); + best_buf = buf; + } else { + free(buf); + } } } } - GList *best_def = g_list_first(list); - if (best_def) utf8name = (char*) strdup(((NameDef*)best_def->data)->buf); - if (list) _g_list_free_full(list); - - return utf8name; + return best_buf; } PangoWeight From ddce10f478a7fe15a312e17dd41d1225efcead6d Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sat, 7 Aug 2021 15:53:55 +0000 Subject: [PATCH 353/474] select fonts via postscript name on Linux greatly improves font matching accuracy Fixes #1572 --- CHANGELOG.md | 1 + src/register_font.cc | 69 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e33918a2..3f2a55d0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * Add missing include for `toupper`. * Throw an error instead of crashing the process if `getImageData` or `putImageData` is called on a PDF or SVG canvas (#1853) * Compatibility with Typescript 4.6 +* Near-perfect font matching on Linux (#1572) 2.9.0 ================== diff --git a/src/register_font.cc b/src/register_font.cc index 927a66b4b..18ba0252d 100644 --- a/src/register_font.cc +++ b/src/register_font.cc @@ -1,5 +1,6 @@ #include "register_font.h" +#include #include #include #include @@ -10,6 +11,7 @@ #include #else #include +#include #endif #include @@ -32,11 +34,29 @@ #define PREFERRED_ENCODING_ID TT_MS_ID_UNICODE_CS #endif +// With PangoFcFontMaps (the pango font module on Linux) we're able to add a +// hook that lets us get perfect matching. Tie the conditions for enabling that +// feature to one variable +#if !defined(__APPLE__) && !defined(_WIN32) && PANGO_VERSION_CHECK(1, 47, 0) +#define PERFECT_MATCHES_ENABLED +#endif + #define IS_PREFERRED_ENC(X) \ X.platform_id == PREFERRED_PLATFORM_ID && X.encoding_id == PREFERRED_ENCODING_ID +#ifdef PERFECT_MATCHES_ENABLED +// On Linux-like OSes using FontConfig, the PostScript name ranks higher than +// preferred family and family name since we'll use it to get perfect font +// matching (see fc_font_map_substitute_hook) +#define GET_NAME_RANK(X) \ + ((IS_PREFERRED_ENC(X) ? 1 : 0) << 2) | \ + ((X.name_id == TT_NAME_ID_PS_NAME ? 1 : 0) << 1) | \ + (X.name_id == TT_NAME_ID_PREFERRED_FAMILY ? 1 : 0) +#else #define GET_NAME_RANK(X) \ - (IS_PREFERRED_ENC(X) ? 1 : 0) + (X.name_id == TT_NAME_ID_PREFERRED_FAMILY ? 1 : 0) + ((IS_PREFERRED_ENC(X) ? 1 : 0) << 1) | \ + (X.name_id == TT_NAME_ID_PREFERRED_FAMILY ? 1 : 0) +#endif /* * Return a UTF-8 encoded string given a TrueType name buf+len @@ -104,15 +124,31 @@ get_family_name(FT_Face face) { for (unsigned i = 0; i < FT_Get_Sfnt_Name_Count(face); ++i) { FT_Get_Sfnt_Name(face, i, &name); - if (name.name_id == TT_NAME_ID_FONT_FAMILY || name.name_id == TT_NAME_ID_PREFERRED_FAMILY) { - char *buf = to_utf8(name.string, name.string_len, name.platform_id, name.encoding_id); + if ( + name.name_id == TT_NAME_ID_FONT_FAMILY || +#ifdef PERFECT_MATCHES_ENABLED + name.name_id == TT_NAME_ID_PS_NAME || +#endif + name.name_id == TT_NAME_ID_PREFERRED_FAMILY + ) { + int rank = GET_NAME_RANK(name); - if (buf) { - int rank = GET_NAME_RANK(name); - if (rank > best_rank) { + if (rank > best_rank) { + char *buf = to_utf8(name.string, name.string_len, name.platform_id, name.encoding_id); + if (buf) { best_rank = rank; if (best_buf) free(best_buf); best_buf = buf; + +#ifdef PERFECT_MATCHES_ENABLED + // Prepend an '@' to the postscript name + if (name.name_id == TT_NAME_ID_PS_NAME) { + std::string best_buf_modified = "@"; + best_buf_modified += best_buf; + free(best_buf); + best_buf = strdup(best_buf_modified.c_str()); + } +#endif } else { free(buf); } @@ -209,6 +245,21 @@ get_pango_font_description(unsigned char* filepath) { return NULL; } +#ifdef PERFECT_MATCHES_ENABLED +static void +fc_font_map_substitute_hook(FcPattern *pat, gpointer data) { + FcChar8 *family; + + for (int i = 0; FcPatternGetString(pat, FC_FAMILY, i, &family) == FcResultMatch; i++) { + if (family[0] == '@') { + FcPatternAddString(pat, FC_POSTSCRIPT_NAME, (FcChar8 *)family + 1); + FcPatternRemove(pat, FC_FAMILY, i); + i -= 1; + } + } +} +#endif + /* * Register font with the OS */ @@ -233,6 +284,12 @@ register_font(unsigned char *filepath) { // font families. pango_cairo_font_map_set_default(NULL); +#ifdef PERFECT_MATCHES_ENABLED + PangoFontMap* map = pango_cairo_font_map_get_default(); + PangoFcFontMap* fc_map = PANGO_FC_FONT_MAP(map); + pango_fc_font_map_set_default_substitute(fc_map, fc_font_map_substitute_hook, NULL, NULL); +#endif + return true; } From d7c4673c4539235a7f583495f58cbf0b282eeb1e Mon Sep 17 00:00:00 2001 From: tignear Date: Mon, 9 Aug 2021 01:13:39 +0900 Subject: [PATCH 354/474] Add support for multi-byte font path on Windows --- CHANGELOG.md | 1 + .../pfennigMultiByte\360\237\232\200.ttf" | Bin 0 -> 308868 bytes src/register_font.cc | 93 +++++++++++++++++- test/canvas.test.js | 10 +- 4 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 "examples/pfennigFont/pfennigMultiByte\360\237\232\200.ttf" diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f2a55d0d..e6d13bf0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * Throw an error instead of crashing the process if `getImageData` or `putImageData` is called on a PDF or SVG canvas (#1853) * Compatibility with Typescript 4.6 * Near-perfect font matching on Linux (#1572) +* Fix multi-byte font path support on Windows. 2.9.0 ================== diff --git "a/examples/pfennigFont/pfennigMultiByte\360\237\232\200.ttf" "b/examples/pfennigFont/pfennigMultiByte\360\237\232\200.ttf" new file mode 100644 index 0000000000000000000000000000000000000000..bbbe4636d3ae6e45b9964c2dd87cadd2a646a78b GIT binary patch literal 308868 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Ì>PVhnKZMoah4}SDSp8jyUq6J^ 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 #elif defined(_WIN32) #include +#include #else #include #include @@ -205,6 +206,41 @@ get_pango_style(FT_Long flags) { } } +#ifdef _WIN32 +std::unique_ptr +u8ToWide(const char* str) { + int iBufferSize = MultiByteToWideChar(CP_UTF8, 0, str, -1, (wchar_t*)NULL, 0); + if(!iBufferSize){ + return nullptr; + } + std::unique_ptr wpBufWString = std::unique_ptr{ new wchar_t[static_cast(iBufferSize)] }; + if(!MultiByteToWideChar(CP_UTF8, 0, str, -1, wpBufWString.get(), iBufferSize)){ + return nullptr; + } + return wpBufWString; +} + +static unsigned long +stream_read_func(FT_Stream stream, unsigned long offset, unsigned char* buffer, unsigned long count){ + HANDLE hFile = reinterpret_cast(stream->descriptor.pointer); + DWORD numberOfBytesRead; + OVERLAPPED overlapped; + overlapped.Offset = offset; + overlapped.OffsetHigh = 0; + overlapped.hEvent = NULL; + if(!ReadFile(hFile, buffer, count, &numberOfBytesRead, &overlapped)){ + return 0; + } + return numberOfBytesRead; +}; + +static void +stream_close_func(FT_Stream stream){ + HANDLE hFile = reinterpret_cast(stream->descriptor.pointer); + CloseHandle(hFile); +} +#endif + /* * Return a PangoFontDescription that will resolve to the font file */ @@ -214,8 +250,47 @@ get_pango_font_description(unsigned char* filepath) { FT_Library library; FT_Face face; PangoFontDescription *desc = pango_font_description_new(); - +#ifdef _WIN32 + // FT_New_Face use fopen. + // Unable to find the file when supplied the multibyte string path on the Windows platform and throw error "Could not parse font file". + // This workaround fixes this by reading the font file uses win32 wide character API. + std::unique_ptr wFilepath = u8ToWide((char*)filepath); + if(!wFilepath){ + return NULL; + } + HANDLE hFile = CreateFileW( + wFilepath.get(), + GENERIC_READ, + FILE_SHARE_READ, + NULL, + OPEN_EXISTING, + NULL, + NULL + ); + if(!hFile){ + return NULL; + } + LARGE_INTEGER liSize; + if(!GetFileSizeEx(hFile, &liSize)) { + CloseHandle(hFile); + return NULL; + } + FT_Open_Args args; + args.flags = FT_OPEN_STREAM; + FT_StreamRec stream; + stream.base = NULL; + stream.size = liSize.QuadPart; + stream.pos = 0; + stream.descriptor.pointer = hFile; + stream.read = stream_read_func; + stream.close = stream_close_func; + args.stream = &stream; + if ( + !FT_Init_FreeType(&library) && + !FT_Open_Face(library, &args, 0, &face)) { +#else if (!FT_Init_FreeType(&library) && !FT_New_Face(library, (const char*)filepath, 0, &face)) { +#endif TT_OS2 *table = (TT_OS2*)FT_Get_Sfnt_Table(face, FT_SFNT_OS2); if (table) { char *family = get_family_name(face); @@ -239,7 +314,6 @@ get_pango_font_description(unsigned char* filepath) { return desc; } } - pango_font_description_free(desc); return NULL; @@ -272,7 +346,13 @@ register_font(unsigned char *filepath) { CFURLRef filepathUrl = CFURLCreateFromFileSystemRepresentation(NULL, filepath, strlen((char*)filepath), false); success = CTFontManagerRegisterFontsForURL(filepathUrl, kCTFontManagerScopeProcess, NULL); #elif defined(_WIN32) - success = AddFontResourceEx((LPCSTR)filepath, FR_PRIVATE, 0) != 0; + std::unique_ptr wFilepath = u8ToWide((char*)filepath); + if(wFilepath){ + success = AddFontResourceExW(wFilepath.get(), FR_PRIVATE, 0) != 0; + }else{ + success = false; + } + #else success = FcConfigAppFontAddFile(FcConfigGetCurrent(), (FcChar8 *)(filepath)); #endif @@ -306,7 +386,12 @@ deregister_font(unsigned char *filepath) { CFURLRef filepathUrl = CFURLCreateFromFileSystemRepresentation(NULL, filepath, strlen((char*)filepath), false); success = CTFontManagerUnregisterFontsForURL(filepathUrl, kCTFontManagerScopeProcess, NULL); #elif defined(_WIN32) - success = RemoveFontResourceExA((LPCSTR)filepath, FR_PRIVATE, 0) != 0; + std::unique_ptr wFilepath = u8ToWide((char*)filepath); + if(wFilepath){ + success = RemoveFontResourceExW(wFilepath.get(), FR_PRIVATE, 0) != 0; + }else{ + success = false; + } #else FcConfigAppFontClear(FcConfigGetCurrent()); success = true; diff --git a/test/canvas.test.js b/test/canvas.test.js index b4c8eb7e1..e8998fa1f 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -16,7 +16,8 @@ const { loadImage, parseFont, registerFont, - Canvas + Canvas, + deregisterAllFonts } = require('../') describe('Canvas', function () { @@ -105,7 +106,12 @@ describe('Canvas', function () { // Minimal test to make sure nothing is thrown registerFont('./examples/pfennigFont/Pfennig.ttf', { family: 'Pfennig' }) registerFont('./examples/pfennigFont/PfennigBold.ttf', { family: 'Pfennig', weight: 'bold' }) - }) + + // Test to multi byte file path support + registerFont('./examples/pfennigFont/pfennigMultiByte🚀.ttf', { family: 'Pfennig' }) + + deregisterAllFonts() + }); it('color serialization', function () { const canvas = createCanvas(200, 200) From 427377178eb7f660fc924bc8a7f71a2ab3109511 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 19 Mar 2022 11:01:24 -0700 Subject: [PATCH 355/474] Fix CI Use latest setup-node and pin to Windows 2019. Windows 2022 needs dealing with later. --- .github/workflows/ci.yaml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e565aa439..173955036 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,7 +15,7 @@ jobs: matrix: node: [10, 12, 14, 16] steps: - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} - uses: actions/checkout@v2 @@ -30,12 +30,12 @@ jobs: Windows: name: Test on Windows - runs-on: windows-latest + runs-on: windows-2019 strategy: matrix: node: [10, 12, 14, 16] steps: - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} - uses: actions/checkout@v2 @@ -45,6 +45,8 @@ jobs: Expand-Archive gtk.zip -DestinationPath "C:\GTK" Invoke-WebRequest "https://downloads.sourceforge.net/project/libjpeg-turbo/2.0.4/libjpeg-turbo-2.0.4-vc64.exe" -OutFile "libjpeg.exe" -UserAgent NativeHost .\libjpeg.exe /S + npm install -g node-gyp@latest + npm prefix -g | % {npm config set node_gyp "$_\node_modules\node-gyp\bin\node-gyp.js"} - name: Install run: npm install --build-from-source - name: Test @@ -57,7 +59,7 @@ jobs: matrix: node: [10, 12, 14, 16] steps: - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} - uses: actions/checkout@v2 @@ -68,13 +70,13 @@ jobs: - name: Install run: npm install --build-from-source - name: Test - run: npm test + run: npm test Lint: name: Lint runs-on: ubuntu-latest steps: - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v3 with: node-version: 14 - uses: actions/checkout@v2 From 9d8da5bf1a272ee3e14637feeef545b622822a03 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 19 Mar 2022 11:44:31 -0700 Subject: [PATCH 356/474] v2.9.1 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6d13bf0e..b32b16f52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed + +2.9.1 +================== +### Fixed * Stringify CanvasGradient, CanvasPattern and ImageData like browsers do. (#1639, #1646) * Add missing include for `toupper`. * Throw an error instead of crashing the process if `getImageData` or `putImageData` is called on a PDF or SVG canvas (#1853) diff --git a/package.json b/package.json index b833e7168..5f382675e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.9.0", + "version": "2.9.1", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 3f3af3af3d590869b799a7ae630854623147c7a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?A=2E=20Rom=C3=A1n?= Date: Thu, 16 Jun 2022 10:18:03 +0200 Subject: [PATCH 357/474] fix: resolved inconsistent exports in ESM (#2047) * feat: add ESM support * docs: updated CHANGELOG * refactor: destructure once Co-authored-by: Mohammed Keyvanzadeh * fix: use `exports.[name] = value` instead Co-authored-by: Mohammed Keyvanzadeh --- CHANGELOG.md | 1 + index.js | 64 +++++++++++++++++++++++++--------------------------- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b32b16f52..f104c3d9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed +* Fixed ESM exports. 2.9.1 ================== diff --git a/index.js b/index.js index 4d11d9a81..0cb14a1ed 100644 --- a/index.js +++ b/index.js @@ -55,40 +55,38 @@ function deregisterAllFonts () { return Canvas._deregisterAllFonts() } -module.exports = { - Canvas, - Context2d: CanvasRenderingContext2D, // Legacy/compat export - CanvasRenderingContext2D, - CanvasGradient: bindings.CanvasGradient, - CanvasPattern, - Image, - ImageData: bindings.ImageData, - PNGStream, - PDFStream, - JPEGStream, - DOMMatrix, - DOMPoint, +exports.Canvas = Canvas +exports.Context2d = CanvasRenderingContext2D // Legacy/compat export +exports.CanvasRenderingContext2D = CanvasRenderingContext2D +exports.CanvasGradient = bindings.CanvasGradient +exports.CanvasPattern = CanvasPattern +exports.Image = Image +exports.ImageData = bindings.ImageData +exports.PNGStream = PNGStream +exports.PDFStream = PDFStream +exports.JPEGStream = JPEGStream +exports.DOMMatrix = DOMMatrix +exports.DOMPoint = DOMPoint - registerFont, - deregisterAllFonts, - parseFont, +exports.registerFont = registerFont +exports.deregisterAllFonts = deregisterAllFonts +exports.parseFont = parseFont - createCanvas, - createImageData, - loadImage, +exports.createCanvas = createCanvas +exports.createImageData = createImageData +exports.loadImage = loadImage - backends: bindings.Backends, +exports.backends = bindings.Backends - /** Library version. */ - version: packageJson.version, - /** Cairo version. */ - cairoVersion: bindings.cairoVersion, - /** jpeglib version. */ - jpegVersion: bindings.jpegVersion, - /** gif_lib version. */ - gifVersion: bindings.gifVersion ? bindings.gifVersion.replace(/[^.\d]/g, '') : undefined, - /** freetype version. */ - freetypeVersion: bindings.freetypeVersion, - /** rsvg version. */ - rsvgVersion: bindings.rsvgVersion -} +/** Library version. */ +exports.version = packageJson.version +/** Cairo version. */ +exports.cairoVersion = bindings.cairoVersion +/** jpeglib version. */ +exports.jpegVersion = bindings.jpegVersion +/** gif_lib version. */ +exports.gifVersion = bindings.gifVersion ? bindings.gifVersion.replace(/[^.\d]/g, '') : undefined +/** freetype version. */ +exports.freetypeVersion = bindings.freetypeVersion +/** rsvg version. */ +exports.rsvgVersion = bindings.rsvgVersion From 1f2b156a2c1da29118d55c2a1747a04a87e02d7a Mon Sep 17 00:00:00 2001 From: Ingo Fischer Date: Tue, 21 Jun 2022 01:15:11 +0200 Subject: [PATCH 358/474] Replace binary for rebuild cases (#1982) --- CHANGELOG.md | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f104c3d9b..6f2fc10d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * Compatibility with Typescript 4.6 * Near-perfect font matching on Linux (#1572) * Fix multi-byte font path support on Windows. +* Allow rebuild of this library 2.9.0 ================== diff --git a/package.json b/package.json index 5f382675e..55d95d2ed 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "test": "mocha test/*.test.js", "pretest-server": "node-gyp build", "test-server": "node test/server.js", - "install": "node-pre-gyp install --fallback-to-build", + "install": "node-pre-gyp install --fallback-to-build --update-binary", "dtslint": "dtslint types" }, "binary": { From d4dc2a87c3843b44dfdb8e26c738c5f38e4cadf8 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 23 Jun 2022 15:08:19 -0700 Subject: [PATCH 359/474] v2.9.2 --- CHANGELOG.md | 7 ++++++- package.json | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f2fc10d8..611b485d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,12 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed -* Fixed ESM exports. + +2.9.2 +================== +### Fixed +* All exports now work when Canvas is used in ES Modules (ESM). ([#2047](https://github.com/Automattic/node-canvas/pull/2047)) +* `npm rebuild` will now re-fetch prebuilt binaries to avoid `NODE_MODULE_VERSION` mismatch errors. ([#1982](https://github.com/Automattic/node-canvas/pull/1982)) 2.9.1 ================== diff --git a/package.json b/package.json index 55d95d2ed..e0cb3777b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.9.1", + "version": "2.9.2", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 6fa9f38e00b9fd30332cc1765fb13d473eb0184b Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Fri, 24 Jun 2022 15:07:37 +0000 Subject: [PATCH 360/474] improve multi-family output in font desc resolver the problem was exposed by #1987, but was always there fixes #2041 --- CHANGELOG.md | 1 + src/Canvas.cc | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 611b485d6..f55b6e50f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed +* Wrong fonts used when calling `registerFont` multiple times with the same family name (#2041) 2.9.2 ================== diff --git a/src/Canvas.cc b/src/Canvas.cc index 9270031f2..3e339f033 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -879,18 +879,21 @@ Canvas::ResolveFontDescription(const PangoFontDescription *desc) { if (streq_casein(family, pangofamily)) { const char* sys_desc_family_name = pango_font_description_get_family(ff.sys_desc); bool unseen = seen_families.find(sys_desc_family_name) == seen_families.end(); + bool better = best.user_desc == nullptr || pango_font_description_better_match(desc, best.user_desc, ff.user_desc); // Avoid sending duplicate SFNT font names due to a bug in Pango for macOS: // https://bugzilla.gnome.org/show_bug.cgi?id=762873 if (unseen) { seen_families.insert(sys_desc_family_name); - if (renamed_families.size()) renamed_families += ','; - renamed_families += sys_desc_family_name; - } - if (first && (best.user_desc == nullptr || pango_font_description_better_match(desc, best.user_desc, ff.user_desc))) { - best = ff; + if (better) { + renamed_families = string(sys_desc_family_name) + (renamed_families.size() ? "," : "") + renamed_families; + } else { + renamed_families = renamed_families + (renamed_families.size() ? "," : "") + sys_desc_family_name; + } } + + if (first && better) best = ff; } } From 7a8a60661ff13c744010996e9b75ff4bcaffb496 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Fri, 24 Jun 2022 12:22:28 -0700 Subject: [PATCH 361/474] v2.9.3 --- CHANGELOG.md | 6 +++++- package.json | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f55b6e50f..583abd886 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,11 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed -* Wrong fonts used when calling `registerFont` multiple times with the same family name (#2041) + +2.9.3 +================== +### Fixed +* Wrong fonts used when calling `registerFont` multiple times with the same family name ([#2041](https://github.com/Automattic/node-canvas/issues/2041)) 2.9.2 ================== diff --git a/package.json b/package.json index e0cb3777b..e8a80dab7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.9.2", + "version": "2.9.3", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 64fdf185dc898837973b2613887442021d3b3c46 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Mon, 27 Jun 2022 10:07:40 -0400 Subject: [PATCH 362/474] export pangoVersion to help debugging --- CHANGELOG.md | 1 + index.js | 2 ++ src/init.cc | 2 ++ 3 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 583abd886..6672a88ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ project adheres to [Semantic Versioning](http://semver.org/). (Unreleased) ================== ### Changed +* Export `pangoVersion` ### Added ### Fixed diff --git a/index.js b/index.js index 0cb14a1ed..f605077f3 100644 --- a/index.js +++ b/index.js @@ -90,3 +90,5 @@ exports.gifVersion = bindings.gifVersion ? bindings.gifVersion.replace(/[^.\d]/g exports.freetypeVersion = bindings.freetypeVersion /** rsvg version. */ exports.rsvgVersion = bindings.rsvgVersion +/** pango version. */ +exports.pangoVersion = bindings.pangoVersion diff --git a/src/init.cc b/src/init.cc index 816ba5837..fd143973e 100644 --- a/src/init.cc +++ b/src/init.cc @@ -84,6 +84,8 @@ NAN_MODULE_INIT(init) { Nan::Set(target, Nan::New("rsvgVersion").ToLocalChecked(), Nan::New(LIBRSVG_VERSION).ToLocalChecked()).Check(); #endif + Nan::Set(target, Nan::New("pangoVersion").ToLocalChecked(), Nan::New(PANGO_VERSION_STRING).ToLocalChecked()).Check(); + char freetype_version[10]; snprintf(freetype_version, 10, "%d.%d.%d", FREETYPE_MAJOR, FREETYPE_MINOR, FREETYPE_PATCH); Nan::Set(target, Nan::New("freetypeVersion").ToLocalChecked(), Nan::New(freetype_version).ToLocalChecked()).Check(); From b0d4f44b5acf148b9b0a28f2354635a2eabc5b68 Mon Sep 17 00:00:00 2001 From: Marco Antonio Dominguez Date: Wed, 6 Jul 2022 21:12:45 -0400 Subject: [PATCH 363/474] Update instructions for OSX local build While testing the solution for a local build, I got the next output: ```sh Package pixman-1 was not found in the pkg-config search path. Perhaps you should add the directory containing `pixman-1.pc' to the PKG_CONFIG_PATH environment variable No package 'pixman-1' found gyp: Call to 'pkg-config pixman-1 --libs' returned exit status 1 while in binding.gyp. while trying to load binding.gyp ``` After updating the command to the following: `brew install pkg-config cairo pango libpng jpeg giflib librsvg pixman`, I was able to generate a succesful build. ```sh brew install pkg-config cairo pango libpng jpeg giflib librsvg pixman; cd /node_modules/canva && npx node-gyp rebuild; ``` --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index b27549962..992904ce3 100644 --- a/Readme.md +++ b/Readme.md @@ -23,7 +23,7 @@ For detailed installation information, see the [wiki](https://github.com/Automat OS | Command ----- | ----- -OS X | Using [Homebrew](https://brew.sh/):
`brew install pkg-config cairo pango libpng jpeg giflib librsvg` +OS X | Using [Homebrew](https://brew.sh/):
`brew install pkg-config cairo pango libpng jpeg giflib librsvg pixman` Ubuntu | `sudo apt-get install build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev` Fedora | `sudo yum install gcc-c++ cairo-devel pango-devel libjpeg-turbo-devel giflib-devel` Solaris | `pkgin install cairo pango pkg-config xproto renderproto kbproto xextproto` From 52551952c3d78ff12110880a2101ab980dd7bf6c Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Wed, 6 Jul 2022 21:49:46 -0700 Subject: [PATCH 364/474] Parse rgba(r,g,b) correctly `rgb()` and `rgba()` are supposed to have identical grammar and behavior: https://www.w3.org/TR/css-color-4/#rgb-functions. Fixes #2029 --- CHANGELOG.md | 1 + src/color.cc | 5 +++-- test/canvas.test.js | 3 +++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6672a88ae..85f8c6e42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * Export `pangoVersion` ### Added ### Fixed +* `rgba(r,g,b)` with no alpha should parse as opaque, not transparent. ([#2029](https://github.com/Automattic/node-canvas/issues/2029)) 2.9.3 ================== diff --git a/src/color.cc b/src/color.cc index 8c41f1135..1ea96e195 100644 --- a/src/color.cc +++ b/src/color.cc @@ -225,12 +225,13 @@ parse_clipped_percentage(const char** pStr, float *pFraction) { #define LIGHTNESS(NAME) SATURATION(NAME) #define ALPHA(NAME) \ - if (*str >= '1' && *str <= '9') { \ + if (*str >= '1' && *str <= '9') { \ NAME = 1; \ } else { \ if ('0' == *str) ++str; \ if ('.' == *str) { \ ++str; \ + NAME = 0; \ float n = .1f; \ while (*str >= '0' && *str <= '9') { \ NAME += (*str++ - '0') * n; \ @@ -630,7 +631,7 @@ rgba_from_rgba_string(const char *str, short *ok) { str += 5; WHITESPACE; uint8_t r = 0, g = 0, b = 0; - float a = 0; + float a = 1.f; CHANNEL(r); WHITESPACE_OR_COMMA; CHANNEL(g); diff --git a/test/canvas.test.js b/test/canvas.test.js index e8998fa1f..a81de892e 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -202,6 +202,9 @@ describe('Canvas', function () { ctx.fillStyle = 'rgba(0, 0, 0, 42.42)' assert.equal('#000000', ctx.fillStyle) + ctx.fillStyle = 'rgba(255, 250, 255)'; + assert.equal('#fffaff', ctx.fillStyle); + // hsl / hsla tests ctx.fillStyle = 'hsl(0, 0%, 0%)' From f8d4949cfbea3d764bbcced947ac248c0d6014a7 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 7 Jul 2022 09:22:01 -0700 Subject: [PATCH 365/474] Fix FITLER/FILTER typo in index.d.ts Fixes #2072 --- CHANGELOG.md | 1 + types/index.d.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85f8c6e42..8437833d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Added ### Fixed * `rgba(r,g,b)` with no alpha should parse as opaque, not transparent. ([#2029](https://github.com/Automattic/node-canvas/issues/2029)) +* Typo in `PngConfig.filters` types. ([#2072](https://github.com/Automattic/node-canvas/issues/2072)) 2.9.3 ================== diff --git a/types/index.d.ts b/types/index.d.ts index f613281e2..04691c4ed 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -7,12 +7,12 @@ export interface PngConfig { /** Specifies the ZLIB compression level. Defaults to 6. */ compressionLevel?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 /** - * Any bitwise combination of `PNG_FILTER_NONE`, `PNG_FITLER_SUB`, + * Any bitwise combination of `PNG_FILTER_NONE`, `PNG_FILTER_SUB`, * `PNG_FILTER_UP`, `PNG_FILTER_AVG` and `PNG_FILTER_PATETH`; or one of * `PNG_ALL_FILTERS` or `PNG_NO_FILTERS` (all are properties of the canvas * instance). These specify which filters *may* be used by libpng. During * encoding, libpng will select the best filter from this list of allowed - * filters. Defaults to `canvas.PNG_ALL_FITLERS`. + * filters. Defaults to `canvas.PNG_ALL_FILTERS`. */ filters?: number /** From c6a154673831a37d1aebe8fcc094ebe3adf87f3e Mon Sep 17 00:00:00 2001 From: Calvin Storoschuk Date: Mon, 27 Jun 2022 21:16:58 -0400 Subject: [PATCH 366/474] fix repeat-x/y support in createPattern() --- CHANGELOG.md | 1 + src/CanvasRenderingContext2d.cc | 32 +++++++++++++++++++++++++++++++- test/public/tests.js | 18 ++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8437833d5..07322e45d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Fixed * `rgba(r,g,b)` with no alpha should parse as opaque, not transparent. ([#2029](https://github.com/Automattic/node-canvas/issues/2029)) * Typo in `PngConfig.filters` types. ([#2072](https://github.com/Automattic/node-canvas/issues/2072)) +* `createPattern()` always used "repeat" mode; now supports "repeat-x" and "repeat-y". ([#2066](https://github.com/Automattic/node-canvas/issues/2066)) 2.9.3 ================== diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index b98fe4520..2bd0533f5 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -367,6 +367,7 @@ Context2d::setFillRule(v8::Local value) { void Context2d::fill(bool preserve) { cairo_pattern_t *new_pattern; + bool needsRestore = false; if (state->fillPattern) { if (state->globalAlpha < 1) { new_pattern = create_transparent_pattern(state->fillPattern, state->globalAlpha); @@ -381,10 +382,36 @@ Context2d::fill(bool preserve) { cairo_set_source(_context, state->fillPattern); } repeat_type_t repeat = Pattern::get_repeat_type_for_cairo_pattern(state->fillPattern); - if (NO_REPEAT == repeat) { + if (repeat == NO_REPEAT) { cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_NONE); + } else if (repeat == REPEAT) { + cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_REPEAT); } else { + cairo_save(_context); + cairo_path_t *savedPath = cairo_copy_path(_context); + cairo_surface_t *patternSurface = nullptr; + cairo_pattern_get_surface(cairo_get_source(_context), &patternSurface); + + double width, height; + if (repeat == REPEAT_X) { + double x1, x2; + cairo_path_extents(_context, &x1, nullptr, &x2, nullptr); + width = x2 - x1; + height = cairo_image_surface_get_height(patternSurface); + } else { + double y1, y2; + cairo_path_extents(_context, nullptr, &y1, nullptr, &y2); + width = cairo_image_surface_get_width(patternSurface); + height = y2 - y1; + } + + cairo_new_path(_context); + cairo_rectangle(_context, 0, 0, width, height); + cairo_clip(_context); + cairo_append_path(_context, savedPath); + cairo_path_destroy(savedPath); cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_REPEAT); + needsRestore = true; } } else if (state->fillGradient) { if (state->globalAlpha < 1) { @@ -412,6 +439,9 @@ Context2d::fill(bool preserve) { ? shadow(cairo_fill) : cairo_fill(_context); } + if (needsRestore) { + cairo_restore(_context); + } } /* diff --git a/test/public/tests.js b/test/public/tests.js index bbf3c6050..e079ad827 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -471,6 +471,24 @@ tests['createPattern() with globalAlpha'] = function (ctx, done) { img.src = imageSrc('face.jpeg') } +tests['createPattern() repeat-x and repeat-y'] = function (ctx, done) { + const img = new Image() + img.onload = function () { + ctx.scale(0.1, 0.1) + ctx.lineStyle = 'black' + ctx.lineWidth = 10 + ctx.fillStyle = ctx.createPattern(img, 'repeat-x') + ctx.fillRect(0, 0, 900, 900) + ctx.strokeRect(0, 0, 900, 900) + ctx.translate(1000, 1000) + ctx.fillStyle = ctx.createPattern(img, 'repeat-y') + ctx.fillRect(0, 0, 900, 900) + ctx.strokeRect(0, 0, 900, 900) + done() + } + img.src = imageSrc('face.jpeg') +} + tests['createPattern() no-repeat'] = function (ctx, done) { const img = new Image() img.onload = function () { From bdc497a2b34bc22b99a8c75d1d9989f3451b7464 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Fri, 5 Aug 2022 02:52:27 -0700 Subject: [PATCH 367/474] Use node-gyp 8.x for Win CI v9 dropped Node.js v10 support. --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 173955036..3c92ea1c8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -45,7 +45,7 @@ jobs: Expand-Archive gtk.zip -DestinationPath "C:\GTK" Invoke-WebRequest "https://downloads.sourceforge.net/project/libjpeg-turbo/2.0.4/libjpeg-turbo-2.0.4-vc64.exe" -OutFile "libjpeg.exe" -UserAgent NativeHost .\libjpeg.exe /S - npm install -g node-gyp@latest + npm install -g node-gyp@8 npm prefix -g | % {npm config set node_gyp "$_\node_modules\node-gyp\bin\node-gyp.js"} - name: Install run: npm install --build-from-source From 288f4bfa1dbd5cdb750ffaa315f07d434fe0dcf6 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sun, 24 Jul 2022 00:24:46 -0700 Subject: [PATCH 368/474] add WPT tests --- package.json | 3 + test/wpt/drawing-text-to-the-canvas.yaml | 1061 ++++ test/wpt/fill-and-stroke-styles.yaml | 2244 +++++++++ test/wpt/generate.js | 260 + .../generated/drawing-text-to-the-canvas.js | 1122 +++++ test/wpt/generated/line-styles.js | 1136 +++++ test/wpt/generated/meta.js | 92 + test/wpt/generated/path-objects.js | 4352 +++++++++++++++++ test/wpt/generated/pixel-manipulation.js | 1448 ++++++ test/wpt/generated/shadows.js | 1203 +++++ test/wpt/generated/text-styles.js | 614 +++ test/wpt/generated/the-canvas-element.js | 273 ++ test/wpt/generated/the-canvas-state.js | 206 + test/wpt/generated/transformations.js | 675 +++ test/wpt/line-styles.yaml | 1017 ++++ test/wpt/meta.yaml | 555 +++ test/wpt/path-objects.yaml | 3646 ++++++++++++++ test/wpt/pixel-manipulation.yaml | 1145 +++++ test/wpt/shadows.yaml | 1150 +++++ test/wpt/text-styles.yaml | 525 ++ test/wpt/the-canvas-element.yaml | 169 + test/wpt/the-canvas-state.yaml | 107 + test/wpt/transformations.yaml | 402 ++ 23 files changed, 23405 insertions(+) create mode 100644 test/wpt/drawing-text-to-the-canvas.yaml create mode 100644 test/wpt/fill-and-stroke-styles.yaml create mode 100644 test/wpt/generate.js create mode 100644 test/wpt/generated/drawing-text-to-the-canvas.js create mode 100644 test/wpt/generated/line-styles.js create mode 100644 test/wpt/generated/meta.js create mode 100644 test/wpt/generated/path-objects.js create mode 100644 test/wpt/generated/pixel-manipulation.js create mode 100644 test/wpt/generated/shadows.js create mode 100644 test/wpt/generated/text-styles.js create mode 100644 test/wpt/generated/the-canvas-element.js create mode 100644 test/wpt/generated/the-canvas-state.js create mode 100644 test/wpt/generated/transformations.js create mode 100644 test/wpt/line-styles.yaml create mode 100644 test/wpt/meta.yaml create mode 100644 test/wpt/path-objects.yaml create mode 100644 test/wpt/pixel-manipulation.yaml create mode 100644 test/wpt/shadows.yaml create mode 100644 test/wpt/text-styles.yaml create mode 100644 test/wpt/the-canvas-element.yaml create mode 100644 test/wpt/the-canvas-state.yaml create mode 100644 test/wpt/transformations.yaml diff --git a/package.json b/package.json index e8a80dab7..5d4185d5e 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,8 @@ "test": "mocha test/*.test.js", "pretest-server": "node-gyp build", "test-server": "node test/server.js", + "generate-wpt": "node ./test/wpt/generate.js", + "test-wpt": "mocha test/wpt/generated/*.js", "install": "node-pre-gyp install --fallback-to-build --update-binary", "dtslint": "dtslint types" }, @@ -57,6 +59,7 @@ "assert-rejects": "^1.0.0", "dtslint": "^4.0.7", "express": "^4.16.3", + "js-yaml": "^4.1.0", "mocha": "^5.2.0", "pixelmatch": "^4.0.2", "standard": "^12.0.1", diff --git a/test/wpt/drawing-text-to-the-canvas.yaml b/test/wpt/drawing-text-to-the-canvas.yaml new file mode 100644 index 000000000..e0f0d4f72 --- /dev/null +++ b/test/wpt/drawing-text-to-the-canvas.yaml @@ -0,0 +1,1061 @@ +- name: 2d.text.draw.fill.basic + desc: fillText draws filled text + manual: + testing: + - 2d.text.draw + - 2d.text.draw.fill + code: | + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('PASS', 5, 35); + expected: &passfill | + size 100 50 + cr.set_source_rgb(0, 0, 0) + cr.rectangle(0, 0, 100, 50) + cr.fill() + cr.set_source_rgb(0, 1, 0) + cr.select_font_face("Arial") + cr.set_font_size(35) + cr.translate(5, 35) + cr.text_path("PASS") + cr.fill() + +- name: 2d.text.draw.fill.unaffected + desc: fillText does not start a new path or subpath + testing: + - 2d.text.draw.fill + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('FAIL', 5, 35); + + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 5,45 == 0,255,0,255; + expected: green + +- name: 2d.text.draw.fill.rtl + desc: fillText respects Right-To-Left Override characters + manual: + testing: + - 2d.text.draw + code: | + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('\u202eFAIL \xa0 \xa0 SSAP', 5, 35); + expected: *passfill +- name: 2d.text.draw.fill.maxWidth.large + desc: fillText handles maxWidth correctly + manual: + testing: + - 2d.text.draw.maxwidth + code: | + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('PASS', 5, 35, 200); + expected: *passfill +- name: 2d.text.draw.fill.maxWidth.small + desc: fillText handles maxWidth correctly + testing: + - 2d.text.draw.maxwidth + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('fail fail fail fail fail', -100, 35, 90); + _assertGreen(ctx, 100, 50); + expected: green + +- name: 2d.text.draw.fill.maxWidth.zero + desc: fillText handles maxWidth correctly + testing: + - 2d.text.draw.maxwidth + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('fail fail fail fail fail', 5, 35, 0); + _assertGreen(ctx, 100, 50); + expected: green + +- name: 2d.text.draw.fill.maxWidth.negative + desc: fillText handles maxWidth correctly + testing: + - 2d.text.draw.maxwidth + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('fail fail fail fail fail', 5, 35, -1); + _assertGreen(ctx, 100, 50); + expected: green + +- name: 2d.text.draw.fill.maxWidth.NaN + desc: fillText handles maxWidth correctly + testing: + - 2d.text.draw.maxwidth + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('fail fail fail fail fail', 5, 35, NaN); + _assertGreen(ctx, 100, 50); + expected: green + +- name: 2d.text.draw.stroke.basic + desc: strokeText draws stroked text + manual: + testing: + - 2d.text.draw + - 2d.text.draw.stroke + code: | + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.fillStyle = '#f00'; + ctx.lineWidth = 1; + ctx.font = '35px Arial, sans-serif'; + ctx.strokeText('PASS', 5, 35); + expected: | + size 100 50 + cr.set_source_rgb(0, 0, 0) + cr.rectangle(0, 0, 100, 50) + cr.fill() + cr.set_source_rgb(0, 1, 0) + cr.select_font_face("Arial") + cr.set_font_size(35) + cr.set_line_width(1) + cr.translate(5, 35) + cr.text_path("PASS") + cr.stroke() + +- name: 2d.text.draw.stroke.unaffected + desc: strokeText does not start a new path or subpath + testing: + - 2d.text.draw.stroke + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + + ctx.font = '35px Arial, sans-serif'; + ctx.strokeStyle = '#f00'; + ctx.strokeText('FAIL', 5, 35); + + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 5,45 == 0,255,0,255; + expected: green + +- name: 2d.text.draw.kern.consistent + desc: Stroked and filled text should have exactly the same kerning so it overlaps + manual: + testing: + - 2d.text.draw + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 3; + ctx.font = '20px Arial, sans-serif'; + ctx.fillText('VAVAVAVAVAVAVA', -50, 25); + ctx.fillText('ToToToToToToTo', -50, 45); + ctx.strokeText('VAVAVAVAVAVAVA', -50, 25); + ctx.strokeText('ToToToToToToTo', -50, 45); + expected: green + +# CanvasTest is: +# A = (0, 0) to (1em, 0.75em) (above baseline) +# B = (0, 0) to (1em, -0.25em) (below baseline) +# C = (0, -0.25em) to (1em, 0.75em) (the em square) plus some Xs above and below +# D = (0, -0.25em) to (1em, 0.75em) (the em square) plus some Xs left and right +# E = (0, -0.25em) to (1em, 0.75em) (the em square) +# space = empty, 1em wide +# +# At 50px, "E" will fill the canvas vertically +# At 67px, "A" will fill the canvas vertically +# +# Ideographic baseline is 0.125em above alphabetic +# Mathematical baseline is 0.375em above alphabetic +# Hanging baseline is 0.500em above alphabetic + +# WebKit doesn't block onload on font loads, so we try to make it a bit more reliable +# by waiting with step_timeout after load before drawing + +- name: 2d.text.draw.fill.maxWidth.fontface + desc: fillText works on @font-face fonts + testing: + - 2d.text.draw.maxwidth + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillText('EEEE', -50, 37.5, 40); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.fill.maxWidth.bound + desc: fillText handles maxWidth based on line size, not bounding box size + testing: + - 2d.text.draw.maxwidth + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText('DD', 0, 37.5, 100); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.fontface + testing: + - 2d.text.font.fontface + fonts: + - CanvasTest + code: | + ctx.font = '67px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText('AA', 0, 50); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.fontface.repeat + desc: Draw with the font immediately, then wait a bit until and draw again. (This + crashes some version of WebKit.) + testing: + - 2d.text.font.fontface + fonts: + - CanvasTest + fonthack: 0 + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.font = '67px CanvasTest'; + ctx.fillStyle = '#0f0'; + ctx.fillText('AA', 0, 50); + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillText('AA', 0, 50); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.fontface.notinpage + desc: '@font-face fonts should work even if they are not used in the page' + testing: + - 2d.text.font.fontface + fonts: + - CanvasTest + fonthack: 0 + code: | + ctx.font = '67px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText('AA', 0, 50); + @assert pixel 5,5 ==~ 0,255,0,255; @moz-todo + @assert pixel 95,5 ==~ 0,255,0,255; @moz-todo + @assert pixel 25,25 ==~ 0,255,0,255; @moz-todo + @assert pixel 75,25 ==~ 0,255,0,255; @moz-todo + }), 500); + expected: green + +- name: 2d.text.draw.align.left + desc: textAlign left is the left of the first em square (not the bounding box) + testing: + - 2d.text.align.left + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'left'; + ctx.fillText('DD', 0, 37.5); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + @assert pixel 5,45 ==~ 0,255,0,255; + @assert pixel 95,45 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.align.right + desc: textAlign right is the right of the last em square (not the bounding box) + testing: + - 2d.text.align.right + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'right'; + ctx.fillText('DD', 100, 37.5); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + @assert pixel 5,45 ==~ 0,255,0,255; + @assert pixel 95,45 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.align.start.ltr + desc: textAlign start with ltr is the left edge + testing: + - 2d.text.align.left + fonts: + - CanvasTest + canvas: width="100" height="50" dir="ltr" + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'start'; + ctx.fillText('DD', 0, 37.5); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + @assert pixel 5,45 ==~ 0,255,0,255; + @assert pixel 95,45 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.align.start.rtl + desc: textAlign start with rtl is the right edge + testing: + - 2d.text.align.right + - 2d.text.draw.direction + fonts: + - CanvasTest + canvas: width="100" height="50" dir="rtl" + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'start'; + ctx.fillText('DD', 100, 37.5); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + @assert pixel 5,45 ==~ 0,255,0,255; + @assert pixel 95,45 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.align.end.ltr + desc: textAlign end with ltr is the right edge + testing: + - 2d.text.align.right + fonts: + - CanvasTest + canvas: width="100" height="50" dir="ltr" + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'end'; + ctx.fillText('DD', 100, 37.5); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + @assert pixel 5,45 ==~ 0,255,0,255; + @assert pixel 95,45 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.align.end.rtl + desc: textAlign end with rtl is the left edge + testing: + - 2d.text.align.left + - 2d.text.draw.direction + fonts: + - CanvasTest + canvas: width="100" height="50" dir="rtl" + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'end'; + ctx.fillText('DD', 0, 37.5); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + @assert pixel 5,45 ==~ 0,255,0,255; + @assert pixel 95,45 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.align.center + desc: textAlign center is the center of the em squares (not the bounding box) + testing: + - 2d.text.align.center + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'center'; + ctx.fillText('DD', 50, 37.5); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + @assert pixel 5,45 ==~ 0,255,0,255; + @assert pixel 95,45 ==~ 0,255,0,255; + }), 500); + expected: green + + +- name: 2d.text.draw.space.basic + desc: U+0020 is rendered the correct size (1em wide) + testing: + - 2d.text.draw.spaces + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText('E EE', -100, 37.5); + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.space.collapse.nonspace + desc: Non-space characters are not converted to U+0020 and collapsed + testing: + - 2d.text.draw.spaces + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText('E\x0b EE', -150, 37.5); + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.measure.width.basic + desc: The width of character is same as font used + testing: + - 2d.text.measure + fonts: + - CanvasTest + code: | + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + @assert ctx.measureText('A').width === 50; + @assert ctx.measureText('AA').width === 100; + @assert ctx.measureText('ABCD').width === 200; + + ctx.font = '100px CanvasTest'; + @assert ctx.measureText('A').width === 100; + }), 500); + }); + +- name: 2d.text.measure.width.empty + desc: The empty string has zero width + testing: + - 2d.text.measure + fonts: + - CanvasTest + code: | + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + @assert ctx.measureText("").width === 0; + }), 500); + }); + +- name: 2d.text.measure.advances + desc: Testing width advances + testing: + - 2d.text.measure.advances + fonts: + - CanvasTest + code: | + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + ctx.direction = 'ltr'; + ctx.align = 'left' + // Some platforms may return '-0'. + @assert Math.abs(ctx.measureText('Hello').advances[0]) === 0; + // Different platforms may render text slightly different. + @assert ctx.measureText('Hello').advances[1] >= 36; + @assert ctx.measureText('Hello').advances[2] >= 58; + @assert ctx.measureText('Hello').advances[3] >= 70; + @assert ctx.measureText('Hello').advances[4] >= 80; + + var tm = ctx.measureText('Hello'); + @assert ctx.measureText('Hello').advances[0] === tm.advances[0]; + @assert ctx.measureText('Hello').advances[1] === tm.advances[1]; + @assert ctx.measureText('Hello').advances[2] === tm.advances[2]; + @assert ctx.measureText('Hello').advances[3] === tm.advances[3]; + @assert ctx.measureText('Hello').advances[4] === tm.advances[4]; + }), 500); + }); + +- name: 2d.text.measure.actualBoundingBox + desc: Testing actualBoundingBox + testing: + - 2d.text.measure.actualBoundingBox + fonts: + - CanvasTest + code: | + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + ctx.direction = 'ltr'; + ctx.align = 'left' + ctx.baseline = 'alphabetic' + // Different platforms may render text slightly different. + // Values that are nominally expected to be zero might actually vary by a pixel or so + // if the UA accounts for antialiasing at glyph edges, so we allow a slight deviation. + @assert Math.abs(ctx.measureText('A').actualBoundingBoxLeft) <= 1; + @assert ctx.measureText('A').actualBoundingBoxRight >= 50; + @assert ctx.measureText('A').actualBoundingBoxAscent >= 35; + @assert Math.abs(ctx.measureText('A').actualBoundingBoxDescent) <= 1; + + @assert ctx.measureText('D').actualBoundingBoxLeft >= 48; + @assert ctx.measureText('D').actualBoundingBoxLeft <= 52; + @assert ctx.measureText('D').actualBoundingBoxRight >= 75; + @assert ctx.measureText('D').actualBoundingBoxRight <= 80; + @assert ctx.measureText('D').actualBoundingBoxAscent >= 35; + @assert ctx.measureText('D').actualBoundingBoxAscent <= 40; + @assert ctx.measureText('D').actualBoundingBoxDescent >= 12; + @assert ctx.measureText('D').actualBoundingBoxDescent <= 15; + + @assert Math.abs(ctx.measureText('ABCD').actualBoundingBoxLeft) <= 1; + @assert ctx.measureText('ABCD').actualBoundingBoxRight >= 200; + @assert ctx.measureText('ABCD').actualBoundingBoxAscent >= 85; + @assert ctx.measureText('ABCD').actualBoundingBoxDescent >= 37; + }), 500); + }); + +- name: 2d.text.measure.fontBoundingBox + desc: Testing fontBoundingBox + testing: + - 2d.text.measure.fontBoundingBox + fonts: + - CanvasTest + code: | + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + ctx.direction = 'ltr'; + ctx.align = 'left' + @assert ctx.measureText('A').fontBoundingBoxAscent === 85; + @assert ctx.measureText('A').fontBoundingBoxDescent === 39; + + @assert ctx.measureText('ABCD').fontBoundingBoxAscent === 85; + @assert ctx.measureText('ABCD').fontBoundingBoxDescent === 39; + }), 500); + }); + +- name: 2d.text.measure.fontBoundingBox.ahem + desc: Testing fontBoundingBox for font ahem + testing: + - 2d.text.measure.fontBoundingBox + fonts: + - Ahem + code: | + deferTest(); + var f = new FontFace("Ahem", "/fonts/Ahem.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px Ahem'; + ctx.direction = 'ltr'; + ctx.align = 'left' + @assert ctx.measureText('A').fontBoundingBoxAscent === 40; + @assert ctx.measureText('A').fontBoundingBoxDescent === 10; + + @assert ctx.measureText('ABCD').fontBoundingBoxAscent === 40; + @assert ctx.measureText('ABCD').fontBoundingBoxDescent === 10; + }), 500); + }); + +- name: 2d.text.measure.emHeights + desc: Testing emHeights + testing: + - 2d.text.measure.emHeights + fonts: + - CanvasTest + code: | + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + ctx.direction = 'ltr'; + ctx.align = 'left' + @assert ctx.measureText('A').emHeightAscent === 37.5; + @assert ctx.measureText('A').emHeightDescent === 12.5; + @assert ctx.measureText('A').emHeightDescent + ctx.measureText('A').emHeightAscent === 50; + + @assert ctx.measureText('ABCD').emHeightAscent === 37.5; + @assert ctx.measureText('ABCD').emHeightDescent === 12.5; + @assert ctx.measureText('ABCD').emHeightDescent + ctx.measureText('ABCD').emHeightAscent === 50; + }), 500); + }); + +- name: 2d.text.measure.baselines + desc: Testing baselines + testing: + - 2d.text.measure.baselines + fonts: + - CanvasTest + code: | + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + ctx.direction = 'ltr'; + ctx.align = 'left' + @assert Math.abs(ctx.measureText('A').getBaselines().alphabetic) === 0; + @assert ctx.measureText('A').getBaselines().ideographic === -39; + @assert ctx.measureText('A').getBaselines().hanging === 68; + + @assert Math.abs(ctx.measureText('ABCD').getBaselines().alphabetic) === 0; + @assert ctx.measureText('ABCD').getBaselines().ideographic === -39; + @assert ctx.measureText('ABCD').getBaselines().hanging === 68; + }), 500); + }); + +- name: 2d.text.drawing.style.spacing + desc: Testing letter spacing and word spacing + testing: + - 2d.text.drawing.style.spacing + code: | + @assert ctx.letterSpacing === '0px'; + @assert ctx.wordSpacing === '0px'; + + ctx.letterSpacing = '3px'; + @assert ctx.letterSpacing === '3px'; + @assert ctx.wordSpacing === '0px'; + + ctx.wordSpacing = '5px'; + @assert ctx.letterSpacing === '3px'; + @assert ctx.wordSpacing === '5px'; + + ctx.letterSpacing = '-1px'; + ctx.wordSpacing = '-1px'; + @assert ctx.letterSpacing === '-1px'; + @assert ctx.wordSpacing === '-1px'; + + ctx.letterSpacing = '1PX'; + ctx.wordSpacing = '1EM'; + @assert ctx.letterSpacing === '1px'; + @assert ctx.wordSpacing === '1em'; + +- name: 2d.text.drawing.style.nonfinite.spacing + desc: Testing letter spacing and word spacing with nonfinite inputs + testing: + - 2d.text.drawing.style.spacing + code: | + @assert ctx.letterSpacing === '0px'; + @assert ctx.wordSpacing === '0px'; + + function test_word_spacing(value) { + ctx.wordSpacing = value; + ctx.letterSpacing = value; + @assert ctx.wordSpacing === '0px'; + @assert ctx.letterSpacing === '0px'; + } + @nonfinite test_word_spacing(<0 NaN Infinity -Infinity>); + +- name: 2d.text.drawing.style.invalid.spacing + desc: Testing letter spacing and word spacing with invalid units + testing: + - 2d.text.drawing.style.spacing + code: | + @assert ctx.letterSpacing === '0px'; + @assert ctx.wordSpacing === '0px'; + + function test_word_spacing(value) { + ctx.wordSpacing = value; + ctx.letterSpacing = value; + @assert ctx.wordSpacing === '0px'; + @assert ctx.letterSpacing === '0px'; + } + @nonfinite test_word_spacing(< '0s' '1min' '1deg' '1pp'>); + +- name: 2d.text.drawing.style.letterSpacing.measure + desc: Testing letter spacing and word spacing + testing: + - 2d.text.drawing.style.spacing + code: | + @assert ctx.letterSpacing === '0px'; + @assert ctx.wordSpacing === '0px'; + var width_normal = ctx.measureText('Hello World').width; + + function test_letter_spacing(value, difference_spacing, epsilon) { + ctx.letterSpacing = value; + @assert ctx.letterSpacing === value; + @assert ctx.wordSpacing === '0px'; + width_with_letter_spacing = ctx.measureText('Hello World').width; + assert_approx_equals(width_with_letter_spacing, width_normal + difference_spacing, epsilon, "letter spacing doesn't work."); + } + + // The first value is the letter Spacing to be set, the second value the + // change in length of string 'Hello World', note that there are 11 letters + // in 'hello world', so the length difference is always letterSpacing * 11. + // and the third value is the acceptable differencee for the length change, + // note that unit such as 1cm/1mm doesn't map to an exact pixel value. + test_cases = [['3px', 33, 0], + ['5px', 55, 0], + ['-2px', -22, 0], + ['1em', 110, 0], + ['1in', 1056, 0], + ['-0.1cm', -41.65, 0.2], + ['-0.6mm', -24,95, 0.2]] + + for (const test_case of test_cases) { + test_letter_spacing(test_case[0], test_case[1], test_case[2]); + } + +- name: 2d.text.drawing.style.wordSpacing.measure + desc: Testing if word spacing is working properly + testing: + - 2d.text.drawing.style.spacing + code: | + @assert ctx.letterSpacing === '0px'; + @assert ctx.wordSpacing === '0px'; + var width_normal = ctx.measureText('Hello World, again').width; + + function test_word_spacing(value, difference_spacing, epsilon) { + ctx.wordSpacing = value; + @assert ctx.letterSpacing === '0px'; + @assert ctx.wordSpacing === value; + width_with_word_spacing = ctx.measureText('Hello World, again').width; + assert_approx_equals(width_with_word_spacing, width_normal + difference_spacing, epsilon, "word spacing doesn't work."); + } + + // The first value is the word Spacing to be set, the second value the + // change in length of string 'Hello World', note that there are 2 words + // in 'Hello World, again', so the length difference is always wordSpacing * 2. + // and the third value is the acceptable differencee for the length change, + // note that unit such as 1cm/1mm doesn't map to an exact pixel value. + test_cases = [['3px', 6, 0], + ['5px', 10, 0], + ['-2px', -4, 0], + ['1em', 20, 0], + ['1in', 192, 0], + ['-0.1cm', -7.57, 0.2], + ['-0.6mm', -4.54, 0.2]] + + for (const test_case of test_cases) { + test_word_spacing(test_case[0], test_case[1], test_case[2]); + } + +- name: 2d.text.drawing.style.letterSpacing.change.font + desc: Set letter spacing and word spacing to font dependent value and verify it works after font change. + testing: + - 2d.text.drawing.style.spacing + code: | + @assert ctx.letterSpacing === '0px'; + @assert ctx.wordSpacing === '0px'; + // Get the width for 'Hello World' at default size, 10px. + var width_normal = ctx.measureText('Hello World').width; + + ctx.letterSpacing = '1em'; + @assert ctx.letterSpacing === '1em'; + // 1em = 10px. Add 10px after each letter in "Hello World", + // makes it 110px longer. + var width_with_spacing = ctx.measureText('Hello World').width; + @assert width_with_spacing === width_normal + 110; + + // Changing font to 20px. Without resetting the spacing, 1em letterSpacing + // is now 20px, so it's suppose to be 220px longer without any letterSpacing set. + ctx.font = '20px serif'; + width_with_spacing = ctx.measureText('Hello World').width; + // Now calculate the reference spacing for "Hello World" with no spacing. + ctx.letterSpacing = '0em'; + width_normal = ctx.measureText('Hello World').width; + @assert width_with_spacing === width_normal + 220; + +- name: 2d.text.drawing.style.wordSpacing.change.font + desc: Set word spacing and word spacing to font dependent value and verify it works after font change. + testing: + - 2d.text.drawing.style.spacing + code: | + @assert ctx.letterSpacing === '0px'; + @assert ctx.wordSpacing === '0px'; + // Get the width for 'Hello World, again' at default size, 10px. + var width_normal = ctx.measureText('Hello World, again').width; + + ctx.wordSpacing = '1em'; + @assert ctx.wordSpacing === '1em'; + // 1em = 10px. Add 10px after each word in "Hello World, again", + // makes it 20px longer. + var width_with_spacing = ctx.measureText('Hello World, again').width; + @assert width_with_spacing === width_normal + 20; + + // Changing font to 20px. Without resetting the spacing, 1em wordSpacing + // is now 20px, so it's suppose to be 40px longer without any wordSpacing set. + ctx.font = '20px serif'; + width_with_spacing = ctx.measureText('Hello World, again').width; + // Now calculate the reference spacing for "Hello World, again" with no spacing. + ctx.wordSpacing = '0em'; + width_normal = ctx.measureText('Hello World, again').width; + @assert width_with_spacing === width_normal + 40; + +- name: 2d.text.drawing.style.fontKerning + desc: Testing basic functionalities of fontKerning for canvas + testing: + - 2d.text.drawing.style.fontKerning + code: | + @assert ctx.fontKerning === "auto"; + ctx.fontKerning = "normal"; + @assert ctx.fontKerning === "normal"; + width_normal = ctx.measureText("TAWATAVA").width; + ctx.fontKerning = "none"; + @assert ctx.fontKerning === "none"; + width_none = ctx.measureText("TAWATAVA").width; + @assert width_normal < width_none; + +- name: 2d.text.drawing.style.fontKerning.with.uppercase + desc: Testing basic functionalities of fontKerning for canvas + testing: + - 2d.text.drawing.style.fontKerning + code: | + @assert ctx.fontKerning === "auto"; + ctx.fontKerning = "Normal"; + @assert ctx.fontKerning === "normal"; + ctx.fontKerning = "Auto"; + ctx.fontKerning = "normal"; + @assert ctx.fontKerning === "normal"; + ctx.fontKerning = "Auto"; + ctx.fontKerning = "noRmal"; + @assert ctx.fontKerning === "normal"; + ctx.fontKerning = "Auto"; + ctx.fontKerning = "NoRMal"; + @assert ctx.fontKerning === "normal"; + ctx.fontKerning = "Auto"; + ctx.fontKerning = "NORMAL"; + @assert ctx.fontKerning === "normal"; + + ctx.fontKerning = "None"; + @assert ctx.fontKerning === "none"; + ctx.fontKerning = "Auto"; + ctx.fontKerning = "none"; + @assert ctx.fontKerning === "none"; + ctx.fontKerning = "Auto"; + ctx.fontKerning = "nOne"; + @assert ctx.fontKerning === "none"; + ctx.fontKerning = "Auto"; + ctx.fontKerning = "nonE"; + @assert ctx.fontKerning === "none"; + ctx.fontKerning = "Auto"; + ctx.fontKerning = "NONE"; + @assert ctx.fontKerning === "none"; + +- name: 2d.text.drawing.style.fontVariant.settings + desc: Testing basic functionalities of fontKerning for canvas + testing: + - 2d.text.drawing.style.fontVariantCaps + code: | + // Setting fontVariantCaps with lower cases + @assert ctx.fontVariantCaps === "normal"; + + ctx.fontVariantCaps = "normal"; + @assert ctx.fontVariantCaps === "normal"; + + ctx.fontVariantCaps = "small-caps"; + @assert ctx.fontVariantCaps === "small-caps"; + + ctx.fontVariantCaps = "all-small-caps"; + @assert ctx.fontVariantCaps === "all-small-caps"; + + ctx.fontVariantCaps = "petite-caps"; + @assert ctx.fontVariantCaps === "petite-caps"; + + ctx.fontVariantCaps = "all-petite-caps"; + @assert ctx.fontVariantCaps === "all-petite-caps"; + + ctx.fontVariantCaps = "unicase"; + @assert ctx.fontVariantCaps === "unicase"; + + ctx.fontVariantCaps = "titling-caps"; + @assert ctx.fontVariantCaps === "titling-caps"; + + // Setting fontVariantCaps with lower cases and upper cases word. + ctx.fontVariantCaps = "nORmal"; + @assert ctx.fontVariantCaps === "normal"; + + ctx.fontVariantCaps = "smaLL-caps"; + @assert ctx.fontVariantCaps === "small-caps"; + + ctx.fontVariantCaps = "all-small-CAPS"; + @assert ctx.fontVariantCaps === "all-small-caps"; + + ctx.fontVariantCaps = "pEtitE-caps"; + @assert ctx.fontVariantCaps === "petite-caps"; + + ctx.fontVariantCaps = "All-Petite-Caps"; + @assert ctx.fontVariantCaps === "all-petite-caps"; + + ctx.fontVariantCaps = "uNIcase"; + @assert ctx.fontVariantCaps === "unicase"; + + ctx.fontVariantCaps = "titling-CAPS"; + @assert ctx.fontVariantCaps === "titling-caps"; + + // Setting fontVariantCaps with non-existing font variant. + ctx.fontVariantCaps = "abcd"; + @assert ctx.fontVariantCaps === "titling-caps"; + +- name: 2d.text.drawing.style.textRendering.settings + desc: Testing basic functionalities of textRendering in Canvas + testing: + - 2d.text.drawing.style.textRendering + code: | + // Setting textRendering with lower cases + @assert ctx.textRendering === "auto"; + + ctx.textRendering = "auto"; + @assert ctx.textRendering === "auto"; + + ctx.textRendering = "optimizespeed"; + @assert ctx.textRendering === "optimizeSpeed"; + + ctx.textRendering = "optimizelegibility"; + @assert ctx.textRendering === "optimizeLegibility"; + + ctx.textRendering = "geometricprecision"; + @assert ctx.textRendering === "geometricPrecision"; + + // Setting textRendering with lower cases and upper cases word. + ctx.textRendering = "aUto"; + @assert ctx.textRendering === "auto"; + + ctx.textRendering = "OPtimizeSpeed"; + @assert ctx.textRendering === "optimizeSpeed"; + + ctx.textRendering = "OPtimizELEgibility"; + @assert ctx.textRendering === "optimizeLegibility"; + + ctx.textRendering = "GeometricPrecision"; + @assert ctx.textRendering === "geometricPrecision"; + + // Setting textRendering with non-existing font variant. + ctx.textRendering = "abcd"; + @assert ctx.textRendering === "geometricPrecision"; + +# TODO: shadows, alpha, composite, clip \ No newline at end of file diff --git a/test/wpt/fill-and-stroke-styles.yaml b/test/wpt/fill-and-stroke-styles.yaml new file mode 100644 index 000000000..88a36119d --- /dev/null +++ b/test/wpt/fill-and-stroke-styles.yaml @@ -0,0 +1,2244 @@ +- name: 2d.fillStyle.parse.current.basic + desc: currentColor is computed from the canvas element + testing: + - 2d.colors.parse + - 2d.currentColor.onset + code: | + canvas.setAttribute('style', 'color: #0f0'); + ctx.fillStyle = '#f00'; + ctx.fillStyle = 'currentColor'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.fillStyle.parse.current.changed + desc: currentColor is computed when the attribute is set, not when it is painted + testing: + - 2d.colors.parse + - 2d.currentColor.onset + code: | + canvas.setAttribute('style', 'color: #0f0'); + ctx.fillStyle = '#f00'; + ctx.fillStyle = 'currentColor'; + canvas.setAttribute('style', 'color: #f00'); + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.fillStyle.parse.current.removed + desc: currentColor is solid black when the canvas element is not in a document + testing: + - 2d.colors.parse + - 2d.currentColor.outofdoc + code: | + // Try not to let it undetectably incorrectly pick up opaque-black + // from other parts of the document: + document.body.parentNode.setAttribute('style', 'color: #f00'); + document.body.setAttribute('style', 'color: #f00'); + canvas.setAttribute('style', 'color: #f00'); + + var canvas2 = document.createElement('canvas'); + var ctx2 = canvas2.getContext('2d'); + ctx2.fillStyle = '#f00'; + ctx2.fillStyle = 'currentColor'; + ctx2.fillRect(0, 0, 100, 50); + ctx.drawImage(canvas2, 0, 0); + + document.body.parentNode.removeAttribute('style'); + document.body.removeAttribute('style'); + + @assert pixel 50,25 == 0,0,0,255; + expected: | + size 100 50 + cr.set_source_rgb(0, 0, 0) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.fillStyle.invalidstring + testing: + - 2d.colors.invalidstring + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillStyle = 'invalid'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.fillStyle.invalidtype + testing: + - 2d.colors.invalidtype + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillStyle = null; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.fillStyle.get.solid + testing: + - 2d.colors.getcolor + - 2d.serializecolor.solid + code: | + ctx.fillStyle = '#fa0'; + @assert ctx.fillStyle === '#ffaa00'; + +- name: 2d.fillStyle.get.semitransparent + testing: + - 2d.colors.getcolor + - 2d.serializecolor.transparent + code: | + ctx.fillStyle = 'rgba(255,255,255,0.45)'; + @assert ctx.fillStyle =~ /^rgba\(255, 255, 255, 0\.4\d+\)$/; + +- name: 2d.fillStyle.get.halftransparent + testing: + - 2d.colors.getcolor + - 2d.serializecolor.transparent + code: | + ctx.fillStyle = 'rgba(255,255,255,0.5)'; + @assert ctx.fillStyle === 'rgba(255, 255, 255, 0.5)'; + +- name: 2d.fillStyle.get.transparent + testing: + - 2d.colors.getcolor + - 2d.serializecolor.transparent + code: | + ctx.fillStyle = 'rgba(0,0,0,0)'; + @assert ctx.fillStyle === 'rgba(0, 0, 0, 0)'; + +- name: 2d.fillStyle.default + testing: + - 2d.colors.default + code: | + @assert ctx.fillStyle === '#000000'; + +- name: 2d.fillStyle.toStringFunctionCallback + desc: Passing a function in to ctx.fillStyle or ctx.strokeStyle with a toString callback works as specified + testing: + 2d.colors.toStringFunctionCallback + code: | + ctx.fillStyle = { toString: function() { return "#008000"; } }; + @assert ctx.fillStyle === "#008000"; + ctx.fillStyle = {}; + @assert ctx.fillStyle === "#008000"; + ctx.fillStyle = 800000; + @assert ctx.fillStyle === "#008000"; + @assert throws TypeError ctx.fillStyle = { toString: function() { throw new TypeError; } }; + ctx.strokeStyle = { toString: function() { return "#008000"; } }; + @assert ctx.strokeStyle === "#008000"; + ctx.strokeStyle = {}; + @assert ctx.strokeStyle === "#008000"; + ctx.strokeStyle = 800000; + @assert ctx.strokeStyle === "#008000"; + @assert throws TypeError ctx.strokeStyle = { toString: function() { throw new TypeError; } }; + +- name: 2d.strokeStyle.default + testing: + - 2d.colors.default + code: | + @assert ctx.strokeStyle === '#000000'; + + +- name: 2d.gradient.object.type + desc: window.CanvasGradient exists and has the right properties + testing: + - 2d.canvasGradient.type + notes: &bindings Defined in "Web IDL" (draft) + code: | + @assert window.CanvasGradient !== undefined; + @assert window.CanvasGradient.prototype.addColorStop !== undefined; + +- name: 2d.gradient.object.return + desc: createLinearGradient() and createRadialGradient() returns objects implementing + CanvasGradient + testing: + - 2d.gradient.linear.return + - 2d.gradient.radial.return + code: | + window.CanvasGradient.prototype.thisImplementsCanvasGradient = true; + + var g1 = ctx.createLinearGradient(0, 0, 100, 0); + @assert g1.addColorStop !== undefined; + @assert g1.thisImplementsCanvasGradient === true; + + var g2 = ctx.createRadialGradient(0, 0, 10, 0, 0, 20); + @assert g2.addColorStop !== undefined; + @assert g2.thisImplementsCanvasGradient === true; + +- name: 2d.gradient.interpolate.solid + testing: + - 2d.gradient.interpolate.linear + code: | + var g = ctx.createLinearGradient(0, 0, 100, 0); + g.addColorStop(0, '#0f0'); + g.addColorStop(1, '#0f0'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.gradient.interpolate.color + testing: + - 2d.gradient.interpolate.linear + code: | + var g = ctx.createLinearGradient(0, 0, 100, 0); + g.addColorStop(0, '#ff0'); + g.addColorStop(1, '#00f'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 25,25 ==~ 191,191,63,255 +/- 3; + @assert pixel 50,25 ==~ 127,127,127,255 +/- 3; + @assert pixel 75,25 ==~ 63,63,191,255 +/- 3; + expected: | + size 100 50 + g = cairo.LinearGradient(0, 0, 100, 0) + g.add_color_stop_rgb(0, 1,1,0) + g.add_color_stop_rgb(1, 0,0,1) + cr.set_source(g) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.gradient.interpolate.alpha + testing: + - 2d.gradient.interpolate.linear + code: | + ctx.fillStyle = '#ff0'; + ctx.fillRect(0, 0, 100, 50); + var g = ctx.createLinearGradient(0, 0, 100, 0); + g.addColorStop(0, 'rgba(0,0,255, 0)'); + g.addColorStop(1, 'rgba(0,0,255, 1)'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 25,25 ==~ 191,191,63,255 +/- 3; + @assert pixel 50,25 ==~ 127,127,127,255 +/- 3; + @assert pixel 75,25 ==~ 63,63,191,255 +/- 3; + expected: | + size 100 50 + g = cairo.LinearGradient(0, 0, 100, 0) + g.add_color_stop_rgb(0, 1,1,0) + g.add_color_stop_rgb(1, 0,0,1) + cr.set_source(g) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.gradient.interpolate.coloralpha + testing: + - 2d.gradient.interpolate.alpha + code: | + var g = ctx.createLinearGradient(0, 0, 100, 0); + g.addColorStop(0, 'rgba(255,255,0, 0)'); + g.addColorStop(1, 'rgba(0,0,255, 1)'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 25,25 ==~ 190,190,65,65 +/- 3; + @assert pixel 50,25 ==~ 126,126,128,128 +/- 3; + @assert pixel 75,25 ==~ 62,62,192,192 +/- 3; + expected: | + size 100 50 + g = cairo.LinearGradient(0, 0, 100, 0) + g.add_color_stop_rgba(0, 1,1,0, 0) + g.add_color_stop_rgba(1, 0,0,1, 1) + cr.set_source(g) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.gradient.interpolate.outside + testing: + - 2d.gradient.outside.first + - 2d.gradient.outside.last + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createLinearGradient(25, 0, 75, 0); + g.addColorStop(0.4, '#0f0'); + g.addColorStop(0.6, '#0f0'); + + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 20,25 ==~ 0,255,0,255; + @assert pixel 50,25 ==~ 0,255,0,255; + @assert pixel 80,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.gradient.interpolate.zerosize.fill + testing: + - 2d.gradient.linear.zerosize + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createLinearGradient(50, 25, 50, 25); // zero-length line (undefined direction) + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.rect(0, 0, 100, 50); + ctx.fill(); + @assert pixel 40,20 == 0,255,0,255; + expected: green + +- name: 2d.gradient.interpolate.zerosize.stroke + testing: + - 2d.gradient.linear.zerosize + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createLinearGradient(50, 25, 50, 25); // zero-length line (undefined direction) + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#f00'); + ctx.strokeStyle = g; + ctx.rect(20, 20, 60, 10); + ctx.stroke(); + @assert pixel 19,19 == 0,255,0,255; + @assert pixel 20,19 == 0,255,0,255; + @assert pixel 21,19 == 0,255,0,255; + @assert pixel 19,20 == 0,255,0,255; + @assert pixel 20,20 == 0,255,0,255; + @assert pixel 21,20 == 0,255,0,255; + @assert pixel 19,21 == 0,255,0,255; + @assert pixel 20,21 == 0,255,0,255; + @assert pixel 21,21 == 0,255,0,255; + expected: green + +- name: 2d.gradient.interpolate.zerosize.fillRect + testing: + - 2d.gradient.linear.zerosize + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createLinearGradient(50, 25, 50, 25); // zero-length line (undefined direction) + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 40,20 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.gradient.interpolate.zerosize.strokeRect + testing: + - 2d.gradient.linear.zerosize + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createLinearGradient(50, 25, 50, 25); // zero-length line (undefined direction) + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#f00'); + ctx.strokeStyle = g; + ctx.strokeRect(20, 20, 60, 10); + @assert pixel 19,19 == 0,255,0,255; + @assert pixel 20,19 == 0,255,0,255; + @assert pixel 21,19 == 0,255,0,255; + @assert pixel 19,20 == 0,255,0,255; + @assert pixel 20,20 == 0,255,0,255; + @assert pixel 21,20 == 0,255,0,255; + @assert pixel 19,21 == 0,255,0,255; + @assert pixel 20,21 == 0,255,0,255; + @assert pixel 21,21 == 0,255,0,255; + expected: green + +- name: 2d.gradient.interpolate.zerosize.fillText + testing: + - 2d.gradient.linear.zerosize + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createLinearGradient(50, 25, 50, 25); // zero-length line (undefined direction) + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.font = '100px sans-serif'; + ctx.fillText("AA", 0, 50); + _assertGreen(ctx, 100, 50); + expected: green + +- name: 2d.gradient.interpolate.zerosize.strokeText + testing: + - 2d.gradient.linear.zerosize + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createLinearGradient(50, 25, 50, 25); // zero-length line (undefined direction) + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#f00'); + ctx.strokeStyle = g; + ctx.font = '100px sans-serif'; + ctx.strokeText("AA", 0, 50); + _assertGreen(ctx, 100, 50); + expected: green + + +- name: 2d.gradient.interpolate.vertical + testing: + - 2d.gradient.interpolate.linear + code: | + var g = ctx.createLinearGradient(0, 0, 0, 50); + g.addColorStop(0, '#ff0'); + g.addColorStop(1, '#00f'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,12 ==~ 191,191,63,255 +/- 10; + @assert pixel 50,25 ==~ 127,127,127,255 +/- 5; + @assert pixel 50,37 ==~ 63,63,191,255 +/- 10; + expected: | + size 100 50 + g = cairo.LinearGradient(0, 0, 0, 50) + g.add_color_stop_rgb(0, 1,1,0) + g.add_color_stop_rgb(1, 0,0,1) + cr.set_source(g) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.gradient.interpolate.multiple + testing: + - 2d.gradient.interpolate.linear + code: | + canvas.width = 200; + var g = ctx.createLinearGradient(0, 0, 200, 0); + g.addColorStop(0, '#ff0'); + g.addColorStop(0.5, '#0ff'); + g.addColorStop(1, '#f0f'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 200, 50); + @assert pixel 50,25 ==~ 127,255,127,255 +/- 3; + @assert pixel 100,25 ==~ 0,255,255,255 +/- 3; + @assert pixel 150,25 ==~ 127,127,255,255 +/- 3; + expected: | + size 200 50 + g = cairo.LinearGradient(0, 0, 200, 0) + g.add_color_stop_rgb(0.0, 1,1,0) + g.add_color_stop_rgb(0.5, 0,1,1) + g.add_color_stop_rgb(1.0, 1,0,1) + cr.set_source(g) + cr.rectangle(0, 0, 200, 50) + cr.fill() + +- name: 2d.gradient.interpolate.overlap + testing: + - 2d.gradient.interpolate.overlap + code: | + canvas.width = 200; + var g = ctx.createLinearGradient(0, 0, 200, 0); + g.addColorStop(0, '#f00'); + g.addColorStop(0, '#ff0'); + g.addColorStop(0.25, '#00f'); + g.addColorStop(0.25, '#0f0'); + g.addColorStop(0.25, '#0f0'); + g.addColorStop(0.25, '#0f0'); + g.addColorStop(0.25, '#ff0'); + g.addColorStop(0.5, '#00f'); + g.addColorStop(0.5, '#0f0'); + g.addColorStop(0.75, '#00f'); + g.addColorStop(0.75, '#f00'); + g.addColorStop(0.75, '#ff0'); + g.addColorStop(0.5, '#0f0'); + g.addColorStop(0.5, '#0f0'); + g.addColorStop(0.5, '#ff0'); + g.addColorStop(1, '#00f'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 200, 50); + @assert pixel 49,25 ==~ 0,0,255,255 +/- 16; + @assert pixel 51,25 ==~ 255,255,0,255 +/- 16; + @assert pixel 99,25 ==~ 0,0,255,255 +/- 16; + @assert pixel 101,25 ==~ 255,255,0,255 +/- 16; + @assert pixel 149,25 ==~ 0,0,255,255 +/- 16; + @assert pixel 151,25 ==~ 255,255,0,255 +/- 16; + expected: | + size 200 50 + g = cairo.LinearGradient(0, 0, 50, 0) + g.add_color_stop_rgb(0, 1,1,0) + g.add_color_stop_rgb(1, 0,0,1) + cr.set_source(g) + cr.rectangle(0, 0, 50, 50) + cr.fill() + + g = cairo.LinearGradient(50, 0, 100, 0) + g.add_color_stop_rgb(0, 1,1,0) + g.add_color_stop_rgb(1, 0,0,1) + cr.set_source(g) + cr.rectangle(50, 0, 50, 50) + cr.fill() + + g = cairo.LinearGradient(100, 0, 150, 0) + g.add_color_stop_rgb(0, 1,1,0) + g.add_color_stop_rgb(1, 0,0,1) + cr.set_source(g) + cr.rectangle(100, 0, 50, 50) + cr.fill() + + g = cairo.LinearGradient(150, 0, 200, 0) + g.add_color_stop_rgb(0, 1,1,0) + g.add_color_stop_rgb(1, 0,0,1) + cr.set_source(g) + cr.rectangle(150, 0, 50, 50) + cr.fill() + +- name: 2d.gradient.interpolate.overlap2 + testing: + - 2d.gradient.interpolate.overlap + code: | + var g = ctx.createLinearGradient(0, 0, 100, 0); + var ps = [ 0, 1/10, 1/4, 1/3, 1/2, 3/4, 1 ]; + for (var p = 0; p < ps.length; ++p) + { + g.addColorStop(ps[p], '#0f0'); + for (var i = 0; i < 15; ++i) + g.addColorStop(ps[p], '#f00'); + g.addColorStop(ps[p], '#0f0'); + } + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 30,25 == 0,255,0,255; + @assert pixel 40,25 == 0,255,0,255; + @assert pixel 60,25 == 0,255,0,255; + @assert pixel 80,25 == 0,255,0,255; + expected: green + +- name: 2d.gradient.empty + testing: + - 2d.gradient.empty + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + var g = ctx.createLinearGradient(0, 0, 0, 50); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.gradient.object.update + testing: + - 2d.gradient.update + code: | + var g = ctx.createLinearGradient(-100, 0, 200, 0); + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + g.addColorStop(0.1, '#0f0'); + g.addColorStop(0.9, '#0f0'); + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.gradient.object.compare + testing: + - 2d.gradient.object + code: | + var g1 = ctx.createLinearGradient(0, 0, 100, 0); + var g2 = ctx.createLinearGradient(0, 0, 100, 0); + @assert g1 !== g2; + ctx.fillStyle = g1; + @assert ctx.fillStyle === g1; + +- name: 2d.gradient.object.crosscanvas + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + var g = document.createElement('canvas').getContext('2d').createLinearGradient(0, 0, 100, 0); + g.addColorStop(0, '#0f0'); + g.addColorStop(1, '#0f0'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.gradient.object.current + testing: + - 2d.currentColor.gradient + code: | + canvas.setAttribute('style', 'color: #f00'); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createLinearGradient(0, 0, 100, 0); + g.addColorStop(0, 'currentColor'); + g.addColorStop(1, 'currentColor'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 ==~ 0,0,0,255; + expected: | + size 100 50 + cr.set_source_rgb(0, 0, 0) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.gradient.object.invalidoffset + testing: + - 2d.gradient.invalidoffset + code: | + var g = ctx.createLinearGradient(0, 0, 100, 0); + @assert throws INDEX_SIZE_ERR g.addColorStop(-1, '#000'); + @assert throws INDEX_SIZE_ERR g.addColorStop(2, '#000'); + @assert throws TypeError g.addColorStop(Infinity, '#000'); + @assert throws TypeError g.addColorStop(-Infinity, '#000'); + @assert throws TypeError g.addColorStop(NaN, '#000'); + +- name: 2d.gradient.object.invalidcolor + testing: + - 2d.gradient.invalidcolor + code: | + var g = ctx.createLinearGradient(0, 0, 100, 0); + @assert throws SYNTAX_ERR g.addColorStop(0, ""); + @assert throws SYNTAX_ERR g.addColorStop(0, 'rgb(NaN%, NaN%, NaN%)'); + @assert throws SYNTAX_ERR g.addColorStop(0, 'null'); + @assert throws SYNTAX_ERR g.addColorStop(0, 'undefined'); + @assert throws SYNTAX_ERR g.addColorStop(0, null); + @assert throws SYNTAX_ERR g.addColorStop(0, undefined); + + var g = ctx.createRadialGradient(0, 0, 0, 100, 0, 0); + @assert throws SYNTAX_ERR g.addColorStop(0, ""); + @assert throws SYNTAX_ERR g.addColorStop(0, 'rgb(NaN%, NaN%, NaN%)'); + @assert throws SYNTAX_ERR g.addColorStop(0, 'null'); + @assert throws SYNTAX_ERR g.addColorStop(0, 'undefined'); + @assert throws SYNTAX_ERR g.addColorStop(0, null); + @assert throws SYNTAX_ERR g.addColorStop(0, undefined); + + +- name: 2d.gradient.linear.nonfinite + desc: createLinearGradient() throws TypeError if arguments are not finite + notes: *bindings + testing: + - 2d.gradient.linear.nonfinite + code: | + @nonfinite @assert throws TypeError ctx.createLinearGradient(<0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <1 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>); + +- name: 2d.gradient.linear.transform.1 + desc: Linear gradient coordinates are relative to the coordinate space at the time + of filling + testing: + - 2d.gradient.linear.transform + code: | + var g = ctx.createLinearGradient(0, 0, 200, 0); + g.addColorStop(0, '#f00'); + g.addColorStop(0.25, '#0f0'); + g.addColorStop(0.75, '#0f0'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.translate(-50, 0); + ctx.fillRect(50, 0, 100, 50); + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.gradient.linear.transform.2 + desc: Linear gradient coordinates are relative to the coordinate space at the time + of filling + testing: + - 2d.gradient.linear.transform + code: | + ctx.translate(100, 0); + var g = ctx.createLinearGradient(0, 0, 200, 0); + g.addColorStop(0, '#f00'); + g.addColorStop(0.25, '#0f0'); + g.addColorStop(0.75, '#0f0'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.translate(-150, 0); + ctx.fillRect(50, 0, 100, 50); + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.gradient.linear.transform.3 + desc: Linear gradient transforms do not experience broken caching effects + testing: + - 2d.gradient.linear.transform + code: | + var g = ctx.createLinearGradient(0, 0, 200, 0); + g.addColorStop(0, '#f00'); + g.addColorStop(0.25, '#0f0'); + g.addColorStop(0.75, '#0f0'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + ctx.translate(-50, 0); + ctx.fillRect(50, 0, 100, 50); + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.negative + desc: createRadialGradient() throws INDEX_SIZE_ERR if either radius is negative + testing: + - 2d.gradient.radial.negative + code: | + @assert throws INDEX_SIZE_ERR ctx.createRadialGradient(0, 0, -0.1, 0, 0, 1); + @assert throws INDEX_SIZE_ERR ctx.createRadialGradient(0, 0, 1, 0, 0, -0.1); + @assert throws INDEX_SIZE_ERR ctx.createRadialGradient(0, 0, -0.1, 0, 0, -0.1); + +- name: 2d.gradient.radial.nonfinite + desc: createRadialGradient() throws TypeError if arguments are not finite + notes: *bindings + testing: + - 2d.gradient.radial.nonfinite + code: | + @nonfinite @assert throws TypeError ctx.createRadialGradient(<0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <1 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <1 Infinity -Infinity NaN>); + +- name: 2d.gradient.radial.inside1 + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(50, 25, 100, 50, 25, 200); + g.addColorStop(0, '#0f0'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.inside2 + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(50, 25, 200, 50, 25, 100); + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#0f0'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.inside3 + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(50, 25, 200, 50, 25, 100); + g.addColorStop(0, '#f00'); + g.addColorStop(0.993, '#f00'); + g.addColorStop(1, '#0f0'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.outside1 + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(200, 25, 10, 200, 25, 20); + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#0f0'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.outside2 + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(200, 25, 20, 200, 25, 10); + g.addColorStop(0, '#0f0'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.outside3 + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(200, 25, 20, 200, 25, 10); + g.addColorStop(0, '#0f0'); + g.addColorStop(0.001, '#f00'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.touch1 + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(150, 25, 50, 200, 25, 100); + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; @moz-todo + @assert pixel 50,1 == 0,255,0,255; @moz-todo + @assert pixel 98,1 == 0,255,0,255; @moz-todo + @assert pixel 1,25 == 0,255,0,255; @moz-todo + @assert pixel 50,25 == 0,255,0,255; @moz-todo + @assert pixel 98,25 == 0,255,0,255; @moz-todo + @assert pixel 1,48 == 0,255,0,255; @moz-todo + @assert pixel 50,48 == 0,255,0,255; @moz-todo + @assert pixel 98,48 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.gradient.radial.touch2 + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(-80, 25, 70, 0, 25, 150); + g.addColorStop(0, '#f00'); + g.addColorStop(0.01, '#0f0'); + g.addColorStop(0.99, '#0f0'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.touch3 + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(120, -15, 25, 140, -30, 50); + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; @moz-todo + @assert pixel 50,1 == 0,255,0,255; @moz-todo + @assert pixel 98,1 == 0,255,0,255; @moz-todo + @assert pixel 1,25 == 0,255,0,255; @moz-todo + @assert pixel 50,25 == 0,255,0,255; @moz-todo + @assert pixel 98,25 == 0,255,0,255; @moz-todo + @assert pixel 1,48 == 0,255,0,255; @moz-todo + @assert pixel 50,48 == 0,255,0,255; @moz-todo + @assert pixel 98,48 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.gradient.radial.equal + testing: + - 2d.gradient.radial.equal + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(50, 25, 20, 50, 25, 20); + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; @moz-todo + @assert pixel 50,1 == 0,255,0,255; @moz-todo + @assert pixel 98,1 == 0,255,0,255; @moz-todo + @assert pixel 1,25 == 0,255,0,255; @moz-todo + @assert pixel 50,25 == 0,255,0,255; @moz-todo + @assert pixel 98,25 == 0,255,0,255; @moz-todo + @assert pixel 1,48 == 0,255,0,255; @moz-todo + @assert pixel 50,48 == 0,255,0,255; @moz-todo + @assert pixel 98,48 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.gradient.radial.cone.behind + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(120, 25, 10, 211, 25, 100); + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; @moz-todo + @assert pixel 50,1 == 0,255,0,255; @moz-todo + @assert pixel 98,1 == 0,255,0,255; @moz-todo + @assert pixel 1,25 == 0,255,0,255; @moz-todo + @assert pixel 50,25 == 0,255,0,255; @moz-todo + @assert pixel 98,25 == 0,255,0,255; @moz-todo + @assert pixel 1,48 == 0,255,0,255; @moz-todo + @assert pixel 50,48 == 0,255,0,255; @moz-todo + @assert pixel 98,48 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.gradient.radial.cone.front + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(311, 25, 10, 210, 25, 100); + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#0f0'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.cone.bottom + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(210, 25, 100, 230, 25, 101); + g.addColorStop(0, '#0f0'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.cone.top + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(230, 25, 100, 100, 25, 101); + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#0f0'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.cone.beside + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(0, 100, 40, 100, 100, 50); + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; @moz-todo + @assert pixel 50,1 == 0,255,0,255; @moz-todo + @assert pixel 98,1 == 0,255,0,255; @moz-todo + @assert pixel 1,25 == 0,255,0,255; @moz-todo + @assert pixel 50,25 == 0,255,0,255; @moz-todo + @assert pixel 98,25 == 0,255,0,255; @moz-todo + @assert pixel 1,48 == 0,255,0,255; @moz-todo + @assert pixel 50,48 == 0,255,0,255; @moz-todo + @assert pixel 98,48 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.gradient.radial.cone.cylinder + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(210, 25, 100, 230, 25, 100); + g.addColorStop(0, '#0f0'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.cone.shape1 + testing: + - 2d.gradient.radial.rendering + code: | + var tol = 1; // tolerance to avoid antialiasing artifacts + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(30+tol, 40); + ctx.lineTo(110, -20+tol); + ctx.lineTo(110, 100-tol); + ctx.fill(); + + var g = ctx.createRadialGradient(30+10*5/2, 40, 10*3/2, 30+10*15/4, 40, 10*9/4); + g.addColorStop(0, '#0f0'); + g.addColorStop(1, '#0f0'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.cone.shape2 + testing: + - 2d.gradient.radial.rendering + code: | + var tol = 1; // tolerance to avoid antialiasing artifacts + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(30+10*5/2, 40, 10*3/2, 30+10*15/4, 40, 10*9/4); + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(30-tol, 40); + ctx.lineTo(110, -20-tol); + ctx.lineTo(110, 100+tol); + ctx.fill(); + + @assert pixel 1,1 == 0,255,0,255; @moz-todo + @assert pixel 50,1 == 0,255,0,255; @moz-todo + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; @moz-todo + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; @moz-todo + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.transform.1 + desc: Radial gradient coordinates are relative to the coordinate space at the time + of filling + testing: + - 2d.gradient.radial.transform + code: | + var g = ctx.createRadialGradient(0, 0, 0, 0, 0, 11.2); + g.addColorStop(0, '#0f0'); + g.addColorStop(0.5, '#0f0'); + g.addColorStop(0.51, '#f00'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.translate(50, 25); + ctx.scale(10, 10); + ctx.fillRect(-5, -2.5, 10, 5); + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.transform.2 + desc: Radial gradient coordinates are relative to the coordinate space at the time + of filling + testing: + - 2d.gradient.radial.transform + code: | + ctx.translate(100, 0); + var g = ctx.createRadialGradient(0, 0, 0, 0, 0, 11.2); + g.addColorStop(0, '#0f0'); + g.addColorStop(0.5, '#0f0'); + g.addColorStop(0.51, '#f00'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.translate(-50, 25); + ctx.scale(10, 10); + ctx.fillRect(-5, -2.5, 10, 5); + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.transform.3 + desc: Radial gradient transforms do not experience broken caching effects + testing: + - 2d.gradient.radial.transform + code: | + var g = ctx.createRadialGradient(0, 0, 0, 0, 0, 11.2); + g.addColorStop(0, '#0f0'); + g.addColorStop(0.5, '#0f0'); + g.addColorStop(0.51, '#f00'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + ctx.translate(50, 25); + ctx.scale(10, 10); + ctx.fillRect(-5, -2.5, 10, 5); + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.gradient.conic.positive.rotation + desc: Conic gradient with positive rotation + code: | + const g = ctx.createConicGradient(3*Math.PI/2, 50, 25); + // It's red in the upper right region and green on the lower left region + g.addColorStop(0, "#f00"); + g.addColorStop(0.25, "#0f0"); + g.addColorStop(0.50, "#0f0"); + g.addColorStop(0.75, "#f00"); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 25,15 == 255,0,0,255; + @assert pixel 75,40 == 0,255,0,255; + expected: green + +- name: 2d.gradient.conic.negative.rotation + desc: Conic gradient with negative rotation + code: | + const g = ctx.createConicGradient(-Math.PI/2, 50, 25); + // It's red in the upper right region and green on the lower left region + g.addColorStop(0, "#f00"); + g.addColorStop(0.25, "#0f0"); + g.addColorStop(0.50, "#0f0"); + g.addColorStop(0.75, "#f00"); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 25,15 == 255,0,0,255; + @assert pixel 75,40 == 0,255,0,255; + expected: green + +- name: 2d.gradient.conic.invalid.inputs + desc: Conic gradient function with invalid inputs + code: | + @nonfinite @assert throws TypeError ctx.createConicGradient(<0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <1 Infinity -Infinity NaN>); + + const g = ctx.createConicGradient(0, 0, 25); + @nonfinite @assert throws TypeError g.addColorStop(, <'#f00'>); + @nonfinite @assert throws SYNTAX_ERR g.addColorStop(<0>, ); + +- name: 2d.pattern.basic.type + testing: + - 2d.pattern.return + images: + - green.png + code: | + @assert window.CanvasPattern !== undefined; + + window.CanvasPattern.prototype.thisImplementsCanvasPattern = true; + + var img = document.getElementById('green.png'); + var pattern = ctx.createPattern(img, 'no-repeat'); + @assert pattern.thisImplementsCanvasPattern; + +- name: 2d.pattern.basic.image + testing: + - 2d.pattern.painting + images: + - green.png + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + var img = document.getElementById('green.png'); + var pattern = ctx.createPattern(img, 'no-repeat'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.basic.canvas + testing: + - 2d.pattern.painting + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var ctx2 = canvas2.getContext('2d'); + ctx2.fillStyle = '#0f0'; + ctx2.fillRect(0, 0, 100, 50); + + var pattern = ctx.createPattern(canvas2, 'no-repeat'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.basic.zerocanvas + testing: + - 2d.pattern.zerocanvas + code: | + canvas.width = 0; + canvas.height = 10; + @assert canvas.width === 0; + @assert canvas.height === 10; + @assert throws INVALID_STATE_ERR ctx.createPattern(canvas, 'repeat'); + + canvas.width = 10; + canvas.height = 0; + @assert canvas.width === 10; + @assert canvas.height === 0; + @assert throws INVALID_STATE_ERR ctx.createPattern(canvas, 'repeat'); + + canvas.width = 0; + canvas.height = 0; + @assert canvas.width === 0; + @assert canvas.height === 0; + @assert throws INVALID_STATE_ERR ctx.createPattern(canvas, 'repeat'); + +- name: 2d.pattern.basic.nocontext + testing: + - 2d.pattern.painting + code: | + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var pattern = ctx.createPattern(canvas2, 'no-repeat'); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.transform.identity + testing: + - 2d.pattern.transform + code: | + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var pattern = ctx.createPattern(canvas2, 'no-repeat'); + pattern.setTransform(new DOMMatrix()); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.transform.infinity + testing: + - 2d.pattern.transform + code: | + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var pattern = ctx.createPattern(canvas2, 'no-repeat'); + pattern.setTransform({a: Infinity}); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.transform.invalid + testing: + - 2d.pattern.transform + code: | + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var pattern = ctx.createPattern(canvas2, 'no-repeat'); + @assert throws TypeError pattern.setTransform({a: 1, m11: 2}); + +- name: 2d.pattern.image.undefined + testing: + - 2d.pattern.IDL + notes: *bindings + code: | + @assert throws TypeError ctx.createPattern(undefined, 'repeat'); + +- name: 2d.pattern.image.null + testing: + - 2d.pattern.IDL + notes: *bindings + code: | + @assert throws TypeError ctx.createPattern(null, 'repeat'); + +- name: 2d.pattern.image.string + testing: + - 2d.pattern.IDL + notes: *bindings + code: | + @assert throws TypeError ctx.createPattern('../images/red.png', 'repeat'); + +- name: 2d.pattern.image.incomplete.nosrc + testing: + - 2d.pattern.incomplete.image + code: | + var img = new Image(); + @assert ctx.createPattern(img, 'repeat') === null; + +- name: 2d.pattern.image.incomplete.immediate + testing: + - 2d.pattern.incomplete.image + images: + - red.png + code: | + var img = new Image(); + img.src = '../images/red.png'; + // This triggers the "update the image data" algorithm. + // The image will not go to the "completely available" state + // until a fetch task in the networking task source is processed, + // so the image must not be fully decodable yet: + @assert ctx.createPattern(img, 'repeat') === null; @moz-todo + +- name: 2d.pattern.image.incomplete.reload + testing: + - 2d.pattern.incomplete.image + images: + - yellow.png + - red.png + code: | + var img = document.getElementById('yellow.png'); + img.src = '../images/red.png'; + // This triggers the "update the image data" algorithm, + // and resets the image to the "unavailable" state. + // The image will not go to the "completely available" state + // until a fetch task in the networking task source is processed, + // so the image must not be fully decodable yet: + @assert ctx.createPattern(img, 'repeat') === null; @moz-todo + +- name: 2d.pattern.image.incomplete.emptysrc + testing: + - 2d.pattern.incomplete.image + images: + - red.png + mozilla: {throws: !!null ''} + code: | + var img = document.getElementById('red.png'); + img.src = ""; + @assert ctx.createPattern(img, 'repeat') === null; + +- name: 2d.pattern.image.incomplete.removedsrc + testing: + - 2d.pattern.incomplete.image + images: + - red.png + mozilla: {throws: !!null ''} + code: | + var img = document.getElementById('red.png'); + img.removeAttribute('src'); + @assert ctx.createPattern(img, 'repeat') === null; + +- name: 2d.pattern.image.broken + testing: + - 2d.pattern.broken.image + images: + - broken.png + code: | + var img = document.getElementById('broken.png'); + @assert ctx.createPattern(img, 'repeat') === null; + +- name: 2d.pattern.image.nonexistent + testing: + - 2d.pattern.nonexistent.image + images: + - no-such-image-really.png + code: | + var img = document.getElementById('no-such-image-really.png'); + @assert throws INVALID_STATE_ERR ctx.createPattern(img, 'repeat'); + +- name: 2d.pattern.svgimage.nonexistent + testing: + - 2d.pattern.nonexistent.svgimage + svgimages: + - no-such-image-really.png + code: | + var img = document.getElementById('no-such-image-really.png'); + @assert throws INVALID_STATE_ERR ctx.createPattern(img, 'repeat'); + +- name: 2d.pattern.image.nonexistent-but-loading + testing: + - 2d.pattern.nonexistent-but-loading.image + code: | + var img = document.createElement("img"); + img.src = "/images/no-such-image-really.png"; + @assert ctx.createPattern(img, 'repeat') === null; + var img = document.createElementNS("http://www.w3.org/2000/svg", "image"); + img.src = "/images/no-such-image-really.png"; + @assert ctx.createPattern(img, 'repeat') === null; + +- name: 2d.pattern.image.nosrc + testing: + - 2d.pattern.nosrc.image + code: | + var img = document.createElement("img"); + @assert ctx.createPattern(img, 'repeat') === null; + var img = document.createElementNS("http://www.w3.org/2000/svg", "image"); + @assert ctx.createPattern(img, 'repeat') === null; + +- name: 2d.pattern.image.zerowidth + testing: + - 2d.pattern.zerowidth.image + images: + - red-zerowidth.svg + code: | + var img = document.getElementById('red-zerowidth.svg'); + @assert ctx.createPattern(img, 'repeat') === null; + +- name: 2d.pattern.image.zeroheight + testing: + - 2d.pattern.zeroheight.image + images: + - red-zeroheight.svg + code: | + var img = document.getElementById('red-zeroheight.svg'); + @assert ctx.createPattern(img, 'repeat') === null; + +- name: 2d.pattern.svgimage.zerowidth + testing: + - 2d.pattern.zerowidth.svgimage + svgimages: + - red-zerowidth.svg + code: | + var img = document.getElementById('red-zerowidth.svg'); + @assert ctx.createPattern(img, 'repeat') === null; + +- name: 2d.pattern.svgimage.zeroheight + testing: + - 2d.pattern.zeroheight.svgimage + svgimages: + - red-zeroheight.svg + code: | + var img = document.getElementById('red-zeroheight.svg'); + @assert ctx.createPattern(img, 'repeat') === null; + +- name: 2d.pattern.repeat.empty + testing: + - 2d.pattern.missing + images: + - green-1x1.png + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + var img = document.getElementById('green-1x1.png'); + var pattern = ctx.createPattern(img, ""); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 200, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.repeat.null + testing: + - 2d.pattern.unrecognised + code: | + @assert ctx.createPattern(canvas, null) != null; + +- name: 2d.pattern.repeat.undefined + testing: + - 2d.pattern.unrecognised + code: | + @assert throws SYNTAX_ERR ctx.createPattern(canvas, undefined); + +- name: 2d.pattern.repeat.unrecognised + testing: + - 2d.pattern.unrecognised + code: | + @assert throws SYNTAX_ERR ctx.createPattern(canvas, "invalid"); + +- name: 2d.pattern.repeat.unrecognisednull + testing: + - 2d.pattern.unrecognised + code: | + @assert throws SYNTAX_ERR ctx.createPattern(canvas, "null"); + +- name: 2d.pattern.repeat.case + testing: + - 2d.pattern.exact + code: | + @assert throws SYNTAX_ERR ctx.createPattern(canvas, "Repeat"); + +- name: 2d.pattern.repeat.nullsuffix + testing: + - 2d.pattern.exact + code: | + @assert throws SYNTAX_ERR ctx.createPattern(canvas, "repeat\0"); + +- name: 2d.pattern.modify.image1 + testing: + - 2d.pattern.modify + images: + - green.png + code: | + var img = document.getElementById('green.png'); + var pattern = ctx.createPattern(img, 'no-repeat'); + deferTest(); + img.onload = t.step_func_done(function () + { + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + }); + img.src = '/images/red.png'; + expected: green + +- name: 2d.pattern.modify.image2 + testing: + - 2d.pattern.modify + images: + - green.png + code: | + var img = document.getElementById('green.png'); + var pattern = ctx.createPattern(img, 'no-repeat'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#00f'; + ctx.fillRect(0, 0, 100, 50); + deferTest(); + img.onload = t.step_func_done(function () + { + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + }); + img.src = '/images/red.png'; + expected: green + +- name: 2d.pattern.modify.canvas1 + testing: + - 2d.pattern.modify + code: | + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var ctx2 = canvas2.getContext('2d'); + ctx2.fillStyle = '#0f0'; + ctx2.fillRect(0, 0, 100, 50); + + var pattern = ctx.createPattern(canvas2, 'no-repeat'); + + ctx2.fillStyle = '#f00'; + ctx2.fillRect(0, 0, 100, 50); + + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.modify.canvas2 + testing: + - 2d.pattern.modify + code: | + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var ctx2 = canvas2.getContext('2d'); + ctx2.fillStyle = '#0f0'; + ctx2.fillRect(0, 0, 100, 50); + + var pattern = ctx.createPattern(canvas2, 'no-repeat'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx2.fillStyle = '#f00'; + ctx2.fillRect(0, 0, 100, 50); + + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.crosscanvas + images: + - green.png + code: | + var img = document.getElementById('green.png'); + + var pattern = document.createElement('canvas').getContext('2d').createPattern(img, 'no-repeat'); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.norepeat.basic + testing: + - 2d.pattern.painting + images: + - green.png + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var img = document.getElementById('green.png'); + var pattern = ctx.createPattern(img, 'no-repeat'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.norepeat.outside + testing: + - 2d.pattern.painting + images: + - red.png + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var img = document.getElementById('red.png'); + var pattern = ctx.createPattern(img, 'no-repeat'); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = pattern; + ctx.fillRect(0, -50, 100, 50); + ctx.fillRect(-100, 0, 100, 50); + ctx.fillRect(0, 50, 100, 50); + ctx.fillRect(100, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.norepeat.coord1 + testing: + - 2d.pattern.painting + images: + - green.png + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(50, 0, 50, 50); + + var img = document.getElementById('green.png'); + var pattern = ctx.createPattern(img, 'no-repeat'); + ctx.fillStyle = pattern; + ctx.translate(50, 0); + ctx.fillRect(-50, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.norepeat.coord2 + testing: + - 2d.pattern.painting + images: + - green.png + code: | + var img = document.getElementById('green.png'); + var pattern = ctx.createPattern(img, 'no-repeat'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 50, 50); + + ctx.fillStyle = '#f00'; + ctx.fillRect(50, 0, 50, 50); + + ctx.fillStyle = pattern; + ctx.translate(50, 0); + ctx.fillRect(-50, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.norepeat.coord3 + testing: + - 2d.pattern.painting + images: + - red.png + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var img = document.getElementById('red.png'); + var pattern = ctx.createPattern(img, 'no-repeat'); + ctx.fillStyle = pattern; + ctx.translate(50, 25); + ctx.fillRect(-50, -25, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 25); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.repeat.basic + testing: + - 2d.pattern.painting + images: + - green-16x16.png + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var img = document.getElementById('green-16x16.png'); + var pattern = ctx.createPattern(img, 'repeat'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.repeat.outside + testing: + - 2d.pattern.painting + images: + - green-16x16.png + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var img = document.getElementById('green-16x16.png'); + var pattern = ctx.createPattern(img, 'repeat'); + ctx.fillStyle = pattern; + ctx.translate(50, 25); + ctx.fillRect(-50, -25, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.repeat.coord1 + testing: + - 2d.pattern.painting + images: + - rgrg-256x256.png + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var img = document.getElementById('rgrg-256x256.png'); + var pattern = ctx.createPattern(img, 'repeat'); + ctx.fillStyle = pattern; + ctx.translate(-128, -78); + ctx.fillRect(128, 78, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.repeat.coord2 + testing: + - 2d.pattern.painting + images: + - ggrr-256x256.png + code: | + var img = document.getElementById('ggrr-256x256.png'); + var pattern = ctx.createPattern(img, 'repeat'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.repeat.coord3 + testing: + - 2d.pattern.painting + images: + - rgrg-256x256.png + code: | + var img = document.getElementById('rgrg-256x256.png'); + var pattern = ctx.createPattern(img, 'repeat'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(-128, -78); + ctx.fillRect(128, 78, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.repeatx.basic + testing: + - 2d.pattern.painting + images: + - green-16x16.png + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 16); + + var img = document.getElementById('green-16x16.png'); + var pattern = ctx.createPattern(img, 'repeat-x'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.repeatx.outside + testing: + - 2d.pattern.painting + images: + - red-16x16.png + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var img = document.getElementById('red-16x16.png'); + var pattern = ctx.createPattern(img, 'repeat-x'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 16); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.repeatx.coord1 + testing: + - 2d.pattern.painting + images: + - red-16x16.png + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var img = document.getElementById('red-16x16.png'); + var pattern = ctx.createPattern(img, 'repeat-x'); + ctx.fillStyle = pattern; + ctx.translate(0, 16); + ctx.fillRect(0, -16, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 16); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.repeaty.basic + testing: + - 2d.pattern.painting + images: + - green-16x16.png + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 16, 50); + + var img = document.getElementById('green-16x16.png'); + var pattern = ctx.createPattern(img, 'repeat-y'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.repeaty.outside + testing: + - 2d.pattern.painting + images: + - red-16x16.png + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var img = document.getElementById('red-16x16.png'); + var pattern = ctx.createPattern(img, 'repeat-y'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 16, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.repeaty.coord1 + testing: + - 2d.pattern.painting + images: + - red-16x16.png + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var img = document.getElementById('red-16x16.png'); + var pattern = ctx.createPattern(img, 'repeat-y'); + ctx.fillStyle = pattern; + ctx.translate(48, 0); + ctx.fillRect(-48, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 16, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.orientation.image + desc: Image patterns do not get flipped when painted + testing: + - 2d.pattern.painting + images: + - rrgg-256x256.png + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var img = document.getElementById('rrgg-256x256.png'); + var pattern = ctx.createPattern(img, 'no-repeat'); + ctx.fillStyle = pattern; + ctx.save(); + ctx.translate(0, -103); + ctx.fillRect(0, 103, 100, 50); + ctx.restore(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 25); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.orientation.canvas + desc: Canvas patterns do not get flipped when painted + testing: + - 2d.pattern.painting + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var ctx2 = canvas2.getContext('2d'); + ctx2.fillStyle = '#f00'; + ctx2.fillRect(0, 0, 100, 25); + ctx2.fillStyle = '#0f0'; + ctx2.fillRect(0, 25, 100, 25); + + var pattern = ctx.createPattern(canvas2, 'no-repeat'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 25); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + + +- name: 2d.pattern.animated.gif + desc: createPattern() of an animated GIF draws the first frame + testing: + - 2d.pattern.animated.image + images: + - anim-gr.gif + code: | + deferTest(); + step_timeout(function () { + var pattern = ctx.createPattern(document.getElementById('anim-gr.gif'), 'repeat'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 50, 50); + step_timeout(t.step_func_done(function () { + ctx.fillRect(50, 0, 50, 50); + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + }), 250); + }, 250); + expected: green + +- name: 2d.fillStyle.CSSRGB + desc: CSSRGB works as color input + testing: + - 2d.colors.CSSRGB + code: | + ctx.fillStyle = new CSSRGB(1, 0, 1); + @assert ctx.fillStyle === '#ff00ff'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 255,0,255,255; + + const color = new CSSRGB(0, CSS.percent(50), 0); + ctx.fillStyle = color; + @assert ctx.fillStyle === '#008000'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,128,0,255; + color.g = 0; + ctx.fillStyle = color; + @assert ctx.fillStyle === '#000000'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,0,0,255; + + color.alpha = 0; + ctx.fillStyle = color; + @assert ctx.fillStyle === 'rgba(0, 0, 0, 0)'; + ctx.reset(); + color.alpha = 0.5; + ctx.fillStyle = color; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,0,0,128; + + ctx.fillStyle = new CSSHSL(CSS.deg(0), 1, 1).toRGB(); + @assert ctx.fillStyle === '#ffffff'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 255,255,255,255; + + color.alpha = 1; + color.g = 1; + ctx.fillStyle = color; + ctx.fillRect(0, 0, 100, 50); + expected: green + +- name: 2d.fillStyle.CSSHSL + desc: CSSHSL works as color input + testing: + - 2d.colors.CSSHSL + code: | + ctx.fillStyle = new CSSHSL(CSS.deg(180), 0.5, 0.5); + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 ==~ 64,191,191,255 +/- 3; + + const color = new CSSHSL(CSS.deg(180), 1, 1); + ctx.fillStyle = color; + @assert ctx.fillStyle === '#ffffff'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 255,255,255,255; + color.l = 0.5; + ctx.fillStyle = color; + @assert ctx.fillStyle === '#00ffff'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,255,255; + + ctx.fillStyle = new CSSRGB(1, 0, 1).toHSL(); + @assert ctx.fillStyle === '#ff00ff'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 255,0,255,255; + + color.h = CSS.deg(120); + color.s = 1; + color.l = 0.5; + ctx.fillStyle = color; + ctx.fillRect(0, 0, 100, 50); + expected: green diff --git a/test/wpt/generate.js b/test/wpt/generate.js new file mode 100644 index 000000000..4921d2277 --- /dev/null +++ b/test/wpt/generate.js @@ -0,0 +1,260 @@ +// This file is a port of gentestutils.py from +// https://github.com/web-platform-tests/wpt/tree/master/html/canvas/tools + +const yaml = require("js-yaml"); +const fs = require("fs"); + +const yamlFiles = fs.readdirSync(__dirname).filter(f => f.endsWith(".yaml")); +// Files that should be skipped: +const SKIP_FILES = new Set("meta.yaml"); +// Tests that should be skipped (e.g. because they cause hangs or V8 crashes): +const SKIP_TESTS = new Set([ + "2d.path.arc.nonfinite", // https://github.com/Automattic/node-canvas/issues/2055 + "2d.imageData.create2.negative", + "2d.imageData.create2.zero", + "2d.imageData.create2.nonfinite", + "2d.imageData.create1.zero", + "2d.imageData.create2.double", + "2d.imageData.get.source.outside", + "2d.imageData.get.source.negative", + "2d.imageData.get.double", + "2d.imageData.get.large.crash", // expected +]); + +function expandNonfinite(method, argstr, tail) { + // argstr is ", ..." (where usually + // 'invalid' is Infinity/-Infinity/NaN) + const args = []; + for (const arg of argstr.split(', ')) { + const [, a] = arg.match(/<(.*)>/); + args.push(a.split(' ')); + } + const calls = []; + // Start with the valid argument list + const call = []; + for (let i = 0; i < args.length; i++) { + call.push(args[i][0]); + } + // For each argument alone, try setting it to all its invalid values: + for (let i = 0; i < args.length; i++) { + for (let j = 1; j < args[i].length; j++) { + const c2 = [...call] + c2[i] = args[i][j]; + calls.push(c2); + } + } + // For all combinations of >= 2 arguments, try setting them to their first + // invalid values. (Don't do all invalid values, because the number of + // combinations explodes.) + const f = (c, start, depth) => { + for (let i = start; i < args.length; i++) { + if (args[i].length > 1) { + const a = args[i][1] + const c2 = [...c] + c2[i] = a + if (depth > 0) + calls.push(c2) + f(c2, i+1, depth+1) + } + } + }; + f(call, 0, 0); + + return calls.map(c => `${method}(${c.join(", ")})${tail}`).join("\n\t\t"); +} + +function simpleEscapeJS(str) { + return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"') +} + +function escapeJS(str) { + str = simpleEscapeJS(str) + str = str.replace(/\[(\w+)\]/g, '[\\""+($1)+"\\"]') // kind of an ugly hack, for nicer failure-message output + return str +} + +/** @type {string} test */ +function convert(test) { + let code = test.code; + if (!code) return ""; + // Indent it + code = code.trim().replace(/^/gm, "\t\t"); + + code = code.replace(/@nonfinite ([^(]+)\(([^)]+)\)(.*)/g, (match, g1, g2, g3) => { + return expandNonfinite(g1, g2, g3); + }); + + code = code.replace(/@assert pixel (\d+,\d+) == (\d+,\d+,\d+,\d+);/g, + "_assertPixel(canvas, $1, $2);"); + + code = code.replace(/@assert pixel (\d+,\d+) ==~ (\d+,\d+,\d+,\d+);/g, + "_assertPixelApprox(canvas, $1, $2);"); + + code = code.replace(/@assert pixel (\d+,\d+) ==~ (\d+,\d+,\d+,\d+) \+\/- (\d+);/g, + "_assertPixelApprox(canvas, $1, $2, $3);"); + + code = code.replace(/@assert throws (\S+_ERR) (.*);/g, + 'assert.throws(function() { $2; }, /$1/);'); + + code = code.replace(/@assert throws (\S+Error) (.*);/g, + 'assert.throws(function() { $2; }, $1);'); + + code = code.replace(/@assert (.*) === (.*);/g, (match, g1, g2) => { + return `assert.strictEqual(${g1}, ${g2}, "${escapeJS(g1)}", "${escapeJS(g2)}")`; + }); + + code = code.replace(/@assert (.*) !== (.*);/g, (match, g1, g2) => { + return `assert.notStrictEqual(${g1}, ${g2}, "${escapeJS(g1)}", "${escapeJS(g2)}");`; + }); + + code = code.replace(/@assert (.*) =~ (.*);/g, (match, g1, g2) => { + return `assert.match(${g1}, ${g2});`; + }); + + code = code.replace(/@assert (.*);/g, (match, g1) => { + return `assert(${g1}, "${escapeJS(g1)}");`; + }); + + code = code.replace(/ @moz-todo/g, ""); + + code = code.replace(/@moz-UniversalBrowserRead;/g, ""); + + if (code.includes("@")) + throw new Error("@ found in code; generation failed"); + + const name = test.name.replace(/"/g, /\"/); + + const skip = SKIP_TESTS.has(name) ? ".skip" : ""; + + return ` + it${skip}("${name}", function () {${test.desc ? `\n\t\t// ${test.desc}` : ""} + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + +${code} + }); +` +} + + +for (const filename of yamlFiles) { + if (SKIP_FILES.has(filename)) + continue; + + let tests; + try { + const content = fs.readFileSync(`${__dirname}/${filename}`, "utf8"); + tests = yaml.load(content, { + filename, + // schema: yaml.DEFAULT_SCHEMA + }); + } catch (ex) { + console.error(ex.toString()); + continue; + } + + const out = fs.createWriteStream(`${__dirname}/generated/${filename.replace(".yaml", ".js")}`); + + out.write(`// THIS FILE WAS AUTO-GENERATED. DO NOT EDIT BY HAND. + +const assert = require('assert'); +const path = require('path'); + +const { + createCanvas, + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + CanvasPattern, + CanvasGradient +} = require('../../..'); + +const window = { + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + Uint8ClampedArray, + CanvasPattern, + CanvasGradient +}; + +const document = { + createElement(type, ...args) { + if (type !== "canvas") + throw new Error(\`createElement(\${type}) not supported\`); + return createCanvas(...args); + } +}; + +function _getPixel(canvas, x, y) { + const ctx = canvas.getContext('2d'); + const imgdata = ctx.getImageData(x, y, 1, 1); + return [ imgdata.data[0], imgdata.data[1], imgdata.data[2], imgdata.data[3] ]; +} + +function _assertApprox(actual, expected, epsilon=0, msg="") { + assert(typeof actual === "number", "actual should be a number but got a \${typeof type_actual}"); + + // The epsilon math below does not place nice with NaN and Infinity + // But in this case Infinity = Infinity and NaN = NaN + if (isFinite(actual) || isFinite(expected)) { + assert(Math.abs(actual - expected) <= epsilon, + \`expected \${actual} to equal \${expected} +/- \${epsilon}. \${msg}\`); + } else { + assert.strictEqual(actual, expected); + } +} + +function _assertPixel(canvas, x, y, r, g, b, a, pos, color) { + const c = _getPixel(canvas, x,y); + assert.strictEqual(c[0], r, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[1], g, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[2], b, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[3], a, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function _assertPixelApprox(canvas, x, y, r, g, b, a, pos, color, tolerance) { + const c = _getPixel(canvas, x,y); + _assertApprox(c[0], r, tolerance, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[1], g, tolerance, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[2], b, tolerance, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[3], a, tolerance, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function assert_throws_js(Type, fn) { + assert.throws(fn, Type); +} + +// Used by font tests to allow fonts to load. +function deferTest() {} + +class Test { + // Two cases of this in the tests, look unnecessary. + done() {} + // Used by font tests to allow fonts to load. + step_func_done(func) { func(); } + // Used for image onload callback. + step_func(func) { func(); } +} + +function step_timeout(result, time) { + // Nothing; code needs to be converted for this to work. +} + +describe("WPT: ${filename.replace(".yaml", "")}", function () { +`); + + for (const test of tests) { + out.write(convert(test)); + } + + out.write(`}); +`) + + out.end(); +} diff --git a/test/wpt/generated/drawing-text-to-the-canvas.js b/test/wpt/generated/drawing-text-to-the-canvas.js new file mode 100644 index 000000000..38cddc45b --- /dev/null +++ b/test/wpt/generated/drawing-text-to-the-canvas.js @@ -0,0 +1,1122 @@ +// THIS FILE WAS AUTO-GENERATED. DO NOT EDIT BY HAND. + +const assert = require('assert'); +const path = require('path'); + +const { + createCanvas, + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + CanvasPattern, + CanvasGradient +} = require('../../..'); + +const window = { + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + Uint8ClampedArray, + CanvasPattern, + CanvasGradient +}; + +const document = { + createElement(type, ...args) { + if (type !== "canvas") + throw new Error(`createElement(${type}) not supported`); + return createCanvas(...args); + } +}; + +function _getPixel(canvas, x, y) { + const ctx = canvas.getContext('2d'); + const imgdata = ctx.getImageData(x, y, 1, 1); + return [ imgdata.data[0], imgdata.data[1], imgdata.data[2], imgdata.data[3] ]; +} + +function _assertApprox(actual, expected, epsilon=0, msg="") { + assert(typeof actual === "number", "actual should be a number but got a ${typeof type_actual}"); + + // The epsilon math below does not place nice with NaN and Infinity + // But in this case Infinity = Infinity and NaN = NaN + if (isFinite(actual) || isFinite(expected)) { + assert(Math.abs(actual - expected) <= epsilon, + `expected ${actual} to equal ${expected} +/- ${epsilon}. ${msg}`); + } else { + assert.strictEqual(actual, expected); + } +} + +function _assertPixel(canvas, x, y, r, g, b, a, pos, color) { + const c = _getPixel(canvas, x,y); + assert.strictEqual(c[0], r, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[1], g, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[2], b, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[3], a, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function _assertPixelApprox(canvas, x, y, r, g, b, a, pos, color, tolerance) { + const c = _getPixel(canvas, x,y); + _assertApprox(c[0], r, tolerance, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[1], g, tolerance, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[2], b, tolerance, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[3], a, tolerance, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function assert_throws_js(Type, fn) { + assert.throws(fn, Type); +} + +// Used by font tests to allow fonts to load. +function deferTest() {} + +class Test { + // Two cases of this in the tests, look unnecessary. + done() {} + // Used by font tests to allow fonts to load. + step_func_done(func) { func(); } + // Used for image onload callback. + step_func(func) { func(); } +} + +function step_timeout(result, time) { + // Nothing; code needs to be converted for this to work. +} + +describe("WPT: drawing-text-to-the-canvas", function () { + + it("2d.text.draw.fill.basic", function () { + // fillText draws filled text + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('PASS', 5, 35); + }); + + it("2d.text.draw.fill.unaffected", function () { + // fillText does not start a new path or subpath + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('FAIL', 5, 35); + + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 5,45, 0,255,0,255); + }); + + it("2d.text.draw.fill.rtl", function () { + // fillText respects Right-To-Left Override characters + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('\u202eFAIL \xa0 \xa0 SSAP', 5, 35); + }); + + it("2d.text.draw.fill.maxWidth.large", function () { + // fillText handles maxWidth correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('PASS', 5, 35, 200); + }); + + it("2d.text.draw.fill.maxWidth.small", function () { + // fillText handles maxWidth correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('fail fail fail fail fail', -100, 35, 90); + _assertGreen(ctx, 100, 50); + }); + + it("2d.text.draw.fill.maxWidth.zero", function () { + // fillText handles maxWidth correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('fail fail fail fail fail', 5, 35, 0); + _assertGreen(ctx, 100, 50); + }); + + it("2d.text.draw.fill.maxWidth.negative", function () { + // fillText handles maxWidth correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('fail fail fail fail fail', 5, 35, -1); + _assertGreen(ctx, 100, 50); + }); + + it("2d.text.draw.fill.maxWidth.NaN", function () { + // fillText handles maxWidth correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('fail fail fail fail fail', 5, 35, NaN); + _assertGreen(ctx, 100, 50); + }); + + it("2d.text.draw.stroke.basic", function () { + // strokeText draws stroked text + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.fillStyle = '#f00'; + ctx.lineWidth = 1; + ctx.font = '35px Arial, sans-serif'; + ctx.strokeText('PASS', 5, 35); + }); + + it("2d.text.draw.stroke.unaffected", function () { + // strokeText does not start a new path or subpath + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + + ctx.font = '35px Arial, sans-serif'; + ctx.strokeStyle = '#f00'; + ctx.strokeText('FAIL', 5, 35); + + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 5,45, 0,255,0,255); + }); + + it("2d.text.draw.kern.consistent", function () { + // Stroked and filled text should have exactly the same kerning so it overlaps + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 3; + ctx.font = '20px Arial, sans-serif'; + ctx.fillText('VAVAVAVAVAVAVA', -50, 25); + ctx.fillText('ToToToToToToTo', -50, 45); + ctx.strokeText('VAVAVAVAVAVAVA', -50, 25); + ctx.strokeText('ToToToToToToTo', -50, 45); + }); + + it("2d.text.draw.fill.maxWidth.fontface", function () { + // fillText works on @font-face fonts + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillText('EEEE', -50, 37.5, 40); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.fill.maxWidth.bound", function () { + // fillText handles maxWidth based on line size, not bounding box size + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText('DD', 0, 37.5, 100); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.fontface", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '67px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText('AA', 0, 50); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.fontface.repeat", function () { + // Draw with the font immediately, then wait a bit until and draw again. (This crashes some version of WebKit.) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.font = '67px CanvasTest'; + ctx.fillStyle = '#0f0'; + ctx.fillText('AA', 0, 50); + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillText('AA', 0, 50); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.fontface.notinpage", function () { + // @font-face fonts should work even if they are not used in the page + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '67px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText('AA', 0, 50); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.align.left", function () { + // textAlign left is the left of the first em square (not the bounding box) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'left'; + ctx.fillText('DD', 0, 37.5); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + _assertPixelApprox(canvas, 5,45, 0,255,0,255); + _assertPixelApprox(canvas, 95,45, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.align.right", function () { + // textAlign right is the right of the last em square (not the bounding box) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'right'; + ctx.fillText('DD', 100, 37.5); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + _assertPixelApprox(canvas, 5,45, 0,255,0,255); + _assertPixelApprox(canvas, 95,45, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.align.start.ltr", function () { + // textAlign start with ltr is the left edge + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'start'; + ctx.fillText('DD', 0, 37.5); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + _assertPixelApprox(canvas, 5,45, 0,255,0,255); + _assertPixelApprox(canvas, 95,45, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.align.start.rtl", function () { + // textAlign start with rtl is the right edge + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'start'; + ctx.fillText('DD', 100, 37.5); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + _assertPixelApprox(canvas, 5,45, 0,255,0,255); + _assertPixelApprox(canvas, 95,45, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.align.end.ltr", function () { + // textAlign end with ltr is the right edge + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'end'; + ctx.fillText('DD', 100, 37.5); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + _assertPixelApprox(canvas, 5,45, 0,255,0,255); + _assertPixelApprox(canvas, 95,45, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.align.end.rtl", function () { + // textAlign end with rtl is the left edge + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'end'; + ctx.fillText('DD', 0, 37.5); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + _assertPixelApprox(canvas, 5,45, 0,255,0,255); + _assertPixelApprox(canvas, 95,45, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.align.center", function () { + // textAlign center is the center of the em squares (not the bounding box) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'center'; + ctx.fillText('DD', 50, 37.5); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + _assertPixelApprox(canvas, 5,45, 0,255,0,255); + _assertPixelApprox(canvas, 95,45, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.space.basic", function () { + // U+0020 is rendered the correct size (1em wide) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText('E EE', -100, 37.5); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.space.collapse.nonspace", function () { + // Non-space characters are not converted to U+0020 and collapsed + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText('E\x0b EE', -150, 37.5); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }), 500); + }); + + it("2d.text.measure.width.basic", function () { + // The width of character is same as font used + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + assert.strictEqual(ctx.measureText('A').width, 50, "ctx.measureText('A').width", "50") + assert.strictEqual(ctx.measureText('AA').width, 100, "ctx.measureText('AA').width", "100") + assert.strictEqual(ctx.measureText('ABCD').width, 200, "ctx.measureText('ABCD').width", "200") + + ctx.font = '100px CanvasTest'; + assert.strictEqual(ctx.measureText('A').width, 100, "ctx.measureText('A').width", "100") + }), 500); + }); + }); + + it("2d.text.measure.width.empty", function () { + // The empty string has zero width + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + assert.strictEqual(ctx.measureText("").width, 0, "ctx.measureText(\"\").width", "0") + }), 500); + }); + }); + + it("2d.text.measure.advances", function () { + // Testing width advances + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + ctx.direction = 'ltr'; + ctx.align = 'left' + // Some platforms may return '-0'. + assert.strictEqual(Math.abs(ctx.measureText('Hello').advances[0]), 0, "Math.abs(ctx.measureText('Hello').advances[\""+(0)+"\"])", "0") + // Different platforms may render text slightly different. + assert(ctx.measureText('Hello').advances[1] >= 36, "ctx.measureText('Hello').advances[\""+(1)+"\"] >= 36"); + assert(ctx.measureText('Hello').advances[2] >= 58, "ctx.measureText('Hello').advances[\""+(2)+"\"] >= 58"); + assert(ctx.measureText('Hello').advances[3] >= 70, "ctx.measureText('Hello').advances[\""+(3)+"\"] >= 70"); + assert(ctx.measureText('Hello').advances[4] >= 80, "ctx.measureText('Hello').advances[\""+(4)+"\"] >= 80"); + + var tm = ctx.measureText('Hello'); + assert.strictEqual(ctx.measureText('Hello').advances[0], tm.advances[0], "ctx.measureText('Hello').advances[\""+(0)+"\"]", "tm.advances[\""+(0)+"\"]") + assert.strictEqual(ctx.measureText('Hello').advances[1], tm.advances[1], "ctx.measureText('Hello').advances[\""+(1)+"\"]", "tm.advances[\""+(1)+"\"]") + assert.strictEqual(ctx.measureText('Hello').advances[2], tm.advances[2], "ctx.measureText('Hello').advances[\""+(2)+"\"]", "tm.advances[\""+(2)+"\"]") + assert.strictEqual(ctx.measureText('Hello').advances[3], tm.advances[3], "ctx.measureText('Hello').advances[\""+(3)+"\"]", "tm.advances[\""+(3)+"\"]") + assert.strictEqual(ctx.measureText('Hello').advances[4], tm.advances[4], "ctx.measureText('Hello').advances[\""+(4)+"\"]", "tm.advances[\""+(4)+"\"]") + }), 500); + }); + }); + + it("2d.text.measure.actualBoundingBox", function () { + // Testing actualBoundingBox + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + ctx.direction = 'ltr'; + ctx.align = 'left' + ctx.baseline = 'alphabetic' + // Different platforms may render text slightly different. + // Values that are nominally expected to be zero might actually vary by a pixel or so + // if the UA accounts for antialiasing at glyph edges, so we allow a slight deviation. + assert(Math.abs(ctx.measureText('A').actualBoundingBoxLeft) <= 1, "Math.abs(ctx.measureText('A').actualBoundingBoxLeft) <= 1"); + assert(ctx.measureText('A').actualBoundingBoxRight >= 50, "ctx.measureText('A').actualBoundingBoxRight >= 50"); + assert(ctx.measureText('A').actualBoundingBoxAscent >= 35, "ctx.measureText('A').actualBoundingBoxAscent >= 35"); + assert(Math.abs(ctx.measureText('A').actualBoundingBoxDescent) <= 1, "Math.abs(ctx.measureText('A').actualBoundingBoxDescent) <= 1"); + + assert(ctx.measureText('D').actualBoundingBoxLeft >= 48, "ctx.measureText('D').actualBoundingBoxLeft >= 48"); + assert(ctx.measureText('D').actualBoundingBoxLeft <= 52, "ctx.measureText('D').actualBoundingBoxLeft <= 52"); + assert(ctx.measureText('D').actualBoundingBoxRight >= 75, "ctx.measureText('D').actualBoundingBoxRight >= 75"); + assert(ctx.measureText('D').actualBoundingBoxRight <= 80, "ctx.measureText('D').actualBoundingBoxRight <= 80"); + assert(ctx.measureText('D').actualBoundingBoxAscent >= 35, "ctx.measureText('D').actualBoundingBoxAscent >= 35"); + assert(ctx.measureText('D').actualBoundingBoxAscent <= 40, "ctx.measureText('D').actualBoundingBoxAscent <= 40"); + assert(ctx.measureText('D').actualBoundingBoxDescent >= 12, "ctx.measureText('D').actualBoundingBoxDescent >= 12"); + assert(ctx.measureText('D').actualBoundingBoxDescent <= 15, "ctx.measureText('D').actualBoundingBoxDescent <= 15"); + + assert(Math.abs(ctx.measureText('ABCD').actualBoundingBoxLeft) <= 1, "Math.abs(ctx.measureText('ABCD').actualBoundingBoxLeft) <= 1"); + assert(ctx.measureText('ABCD').actualBoundingBoxRight >= 200, "ctx.measureText('ABCD').actualBoundingBoxRight >= 200"); + assert(ctx.measureText('ABCD').actualBoundingBoxAscent >= 85, "ctx.measureText('ABCD').actualBoundingBoxAscent >= 85"); + assert(ctx.measureText('ABCD').actualBoundingBoxDescent >= 37, "ctx.measureText('ABCD').actualBoundingBoxDescent >= 37"); + }), 500); + }); + }); + + it("2d.text.measure.fontBoundingBox", function () { + // Testing fontBoundingBox + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + ctx.direction = 'ltr'; + ctx.align = 'left' + assert.strictEqual(ctx.measureText('A').fontBoundingBoxAscent, 85, "ctx.measureText('A').fontBoundingBoxAscent", "85") + assert.strictEqual(ctx.measureText('A').fontBoundingBoxDescent, 39, "ctx.measureText('A').fontBoundingBoxDescent", "39") + + assert.strictEqual(ctx.measureText('ABCD').fontBoundingBoxAscent, 85, "ctx.measureText('ABCD').fontBoundingBoxAscent", "85") + assert.strictEqual(ctx.measureText('ABCD').fontBoundingBoxDescent, 39, "ctx.measureText('ABCD').fontBoundingBoxDescent", "39") + }), 500); + }); + }); + + it("2d.text.measure.fontBoundingBox.ahem", function () { + // Testing fontBoundingBox for font ahem + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + deferTest(); + var f = new FontFace("Ahem", "/fonts/Ahem.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px Ahem'; + ctx.direction = 'ltr'; + ctx.align = 'left' + assert.strictEqual(ctx.measureText('A').fontBoundingBoxAscent, 40, "ctx.measureText('A').fontBoundingBoxAscent", "40") + assert.strictEqual(ctx.measureText('A').fontBoundingBoxDescent, 10, "ctx.measureText('A').fontBoundingBoxDescent", "10") + + assert.strictEqual(ctx.measureText('ABCD').fontBoundingBoxAscent, 40, "ctx.measureText('ABCD').fontBoundingBoxAscent", "40") + assert.strictEqual(ctx.measureText('ABCD').fontBoundingBoxDescent, 10, "ctx.measureText('ABCD').fontBoundingBoxDescent", "10") + }), 500); + }); + }); + + it("2d.text.measure.emHeights", function () { + // Testing emHeights + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + ctx.direction = 'ltr'; + ctx.align = 'left' + assert.strictEqual(ctx.measureText('A').emHeightAscent, 37.5, "ctx.measureText('A').emHeightAscent", "37.5") + assert.strictEqual(ctx.measureText('A').emHeightDescent, 12.5, "ctx.measureText('A').emHeightDescent", "12.5") + assert.strictEqual(ctx.measureText('A').emHeightDescent + ctx.measureText('A').emHeightAscent, 50, "ctx.measureText('A').emHeightDescent + ctx.measureText('A').emHeightAscent", "50") + + assert.strictEqual(ctx.measureText('ABCD').emHeightAscent, 37.5, "ctx.measureText('ABCD').emHeightAscent", "37.5") + assert.strictEqual(ctx.measureText('ABCD').emHeightDescent, 12.5, "ctx.measureText('ABCD').emHeightDescent", "12.5") + assert.strictEqual(ctx.measureText('ABCD').emHeightDescent + ctx.measureText('ABCD').emHeightAscent, 50, "ctx.measureText('ABCD').emHeightDescent + ctx.measureText('ABCD').emHeightAscent", "50") + }), 500); + }); + }); + + it("2d.text.measure.baselines", function () { + // Testing baselines + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + ctx.direction = 'ltr'; + ctx.align = 'left' + assert.strictEqual(Math.abs(ctx.measureText('A').getBaselines().alphabetic), 0, "Math.abs(ctx.measureText('A').getBaselines().alphabetic)", "0") + assert.strictEqual(ctx.measureText('A').getBaselines().ideographic, -39, "ctx.measureText('A').getBaselines().ideographic", "-39") + assert.strictEqual(ctx.measureText('A').getBaselines().hanging, 68, "ctx.measureText('A').getBaselines().hanging", "68") + + assert.strictEqual(Math.abs(ctx.measureText('ABCD').getBaselines().alphabetic), 0, "Math.abs(ctx.measureText('ABCD').getBaselines().alphabetic)", "0") + assert.strictEqual(ctx.measureText('ABCD').getBaselines().ideographic, -39, "ctx.measureText('ABCD').getBaselines().ideographic", "-39") + assert.strictEqual(ctx.measureText('ABCD').getBaselines().hanging, 68, "ctx.measureText('ABCD').getBaselines().hanging", "68") + }), 500); + }); + }); + + it("2d.text.drawing.style.spacing", function () { + // Testing letter spacing and word spacing + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.letterSpacing, '0px', "ctx.letterSpacing", "'0px'") + assert.strictEqual(ctx.wordSpacing, '0px', "ctx.wordSpacing", "'0px'") + + ctx.letterSpacing = '3px'; + assert.strictEqual(ctx.letterSpacing, '3px', "ctx.letterSpacing", "'3px'") + assert.strictEqual(ctx.wordSpacing, '0px', "ctx.wordSpacing", "'0px'") + + ctx.wordSpacing = '5px'; + assert.strictEqual(ctx.letterSpacing, '3px', "ctx.letterSpacing", "'3px'") + assert.strictEqual(ctx.wordSpacing, '5px', "ctx.wordSpacing", "'5px'") + + ctx.letterSpacing = '-1px'; + ctx.wordSpacing = '-1px'; + assert.strictEqual(ctx.letterSpacing, '-1px', "ctx.letterSpacing", "'-1px'") + assert.strictEqual(ctx.wordSpacing, '-1px', "ctx.wordSpacing", "'-1px'") + + ctx.letterSpacing = '1PX'; + ctx.wordSpacing = '1EM'; + assert.strictEqual(ctx.letterSpacing, '1px', "ctx.letterSpacing", "'1px'") + assert.strictEqual(ctx.wordSpacing, '1em', "ctx.wordSpacing", "'1em'") + }); + + it("2d.text.drawing.style.nonfinite.spacing", function () { + // Testing letter spacing and word spacing with nonfinite inputs + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.letterSpacing, '0px', "ctx.letterSpacing", "'0px'") + assert.strictEqual(ctx.wordSpacing, '0px', "ctx.wordSpacing", "'0px'") + + function test_word_spacing(value) { + ctx.wordSpacing = value; + ctx.letterSpacing = value; + assert.strictEqual(ctx.wordSpacing, '0px', "ctx.wordSpacing", "'0px'") + assert.strictEqual(ctx.letterSpacing, '0px', "ctx.letterSpacing", "'0px'") + } + test_word_spacing(NaN); + test_word_spacing(Infinity); + test_word_spacing(-Infinity); + }); + + it("2d.text.drawing.style.invalid.spacing", function () { + // Testing letter spacing and word spacing with invalid units + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.letterSpacing, '0px', "ctx.letterSpacing", "'0px'") + assert.strictEqual(ctx.wordSpacing, '0px', "ctx.wordSpacing", "'0px'") + + function test_word_spacing(value) { + ctx.wordSpacing = value; + ctx.letterSpacing = value; + assert.strictEqual(ctx.wordSpacing, '0px', "ctx.wordSpacing", "'0px'") + assert.strictEqual(ctx.letterSpacing, '0px', "ctx.letterSpacing", "'0px'") + } + test_word_spacing('0s'); + test_word_spacing('1min'); + test_word_spacing('1deg'); + test_word_spacing('1pp'); + }); + + it("2d.text.drawing.style.letterSpacing.measure", function () { + // Testing letter spacing and word spacing + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.letterSpacing, '0px', "ctx.letterSpacing", "'0px'") + assert.strictEqual(ctx.wordSpacing, '0px', "ctx.wordSpacing", "'0px'") + var width_normal = ctx.measureText('Hello World').width; + + function test_letter_spacing(value, difference_spacing, epsilon) { + ctx.letterSpacing = value; + assert.strictEqual(ctx.letterSpacing, value, "ctx.letterSpacing", "value") + assert.strictEqual(ctx.wordSpacing, '0px', "ctx.wordSpacing", "'0px'") + width_with_letter_spacing = ctx.measureText('Hello World').width; + assert_approx_equals(width_with_letter_spacing, width_normal + difference_spacing, epsilon, "letter spacing doesn't work."); + } + + // The first value is the letter Spacing to be set, the second value the + // change in length of string 'Hello World', note that there are 11 letters + // in 'hello world', so the length difference is always letterSpacing * 11. + // and the third value is the acceptable differencee for the length change, + // note that unit such as 1cm/1mm doesn't map to an exact pixel value. + test_cases = [['3px', 33, 0], + ['5px', 55, 0], + ['-2px', -22, 0], + ['1em', 110, 0], + ['1in', 1056, 0], + ['-0.1cm', -41.65, 0.2], + ['-0.6mm', -24,95, 0.2]] + + for (const test_case of test_cases) { + test_letter_spacing(test_case[0], test_case[1], test_case[2]); + } + }); + + it("2d.text.drawing.style.wordSpacing.measure", function () { + // Testing if word spacing is working properly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.letterSpacing, '0px', "ctx.letterSpacing", "'0px'") + assert.strictEqual(ctx.wordSpacing, '0px', "ctx.wordSpacing", "'0px'") + var width_normal = ctx.measureText('Hello World, again').width; + + function test_word_spacing(value, difference_spacing, epsilon) { + ctx.wordSpacing = value; + assert.strictEqual(ctx.letterSpacing, '0px', "ctx.letterSpacing", "'0px'") + assert.strictEqual(ctx.wordSpacing, value, "ctx.wordSpacing", "value") + width_with_word_spacing = ctx.measureText('Hello World, again').width; + assert_approx_equals(width_with_word_spacing, width_normal + difference_spacing, epsilon, "word spacing doesn't work."); + } + + // The first value is the word Spacing to be set, the second value the + // change in length of string 'Hello World', note that there are 2 words + // in 'Hello World, again', so the length difference is always wordSpacing * 2. + // and the third value is the acceptable differencee for the length change, + // note that unit such as 1cm/1mm doesn't map to an exact pixel value. + test_cases = [['3px', 6, 0], + ['5px', 10, 0], + ['-2px', -4, 0], + ['1em', 20, 0], + ['1in', 192, 0], + ['-0.1cm', -7.57, 0.2], + ['-0.6mm', -4.54, 0.2]] + + for (const test_case of test_cases) { + test_word_spacing(test_case[0], test_case[1], test_case[2]); + } + }); + + it("2d.text.drawing.style.letterSpacing.change.font", function () { + // Set letter spacing and word spacing to font dependent value and verify it works after font change. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.letterSpacing, '0px', "ctx.letterSpacing", "'0px'") + assert.strictEqual(ctx.wordSpacing, '0px', "ctx.wordSpacing", "'0px'") + // Get the width for 'Hello World' at default size, 10px. + var width_normal = ctx.measureText('Hello World').width; + + ctx.letterSpacing = '1em'; + assert.strictEqual(ctx.letterSpacing, '1em', "ctx.letterSpacing", "'1em'") + // 1em = 10px. Add 10px after each letter in "Hello World", + // makes it 110px longer. + var width_with_spacing = ctx.measureText('Hello World').width; + assert.strictEqual(width_with_spacing, width_normal + 110, "width_with_spacing", "width_normal + 110") + + // Changing font to 20px. Without resetting the spacing, 1em letterSpacing + // is now 20px, so it's suppose to be 220px longer without any letterSpacing set. + ctx.font = '20px serif'; + width_with_spacing = ctx.measureText('Hello World').width; + // Now calculate the reference spacing for "Hello World" with no spacing. + ctx.letterSpacing = '0em'; + width_normal = ctx.measureText('Hello World').width; + assert.strictEqual(width_with_spacing, width_normal + 220, "width_with_spacing", "width_normal + 220") + }); + + it("2d.text.drawing.style.wordSpacing.change.font", function () { + // Set word spacing and word spacing to font dependent value and verify it works after font change. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.letterSpacing, '0px', "ctx.letterSpacing", "'0px'") + assert.strictEqual(ctx.wordSpacing, '0px', "ctx.wordSpacing", "'0px'") + // Get the width for 'Hello World, again' at default size, 10px. + var width_normal = ctx.measureText('Hello World, again').width; + + ctx.wordSpacing = '1em'; + assert.strictEqual(ctx.wordSpacing, '1em', "ctx.wordSpacing", "'1em'") + // 1em = 10px. Add 10px after each word in "Hello World, again", + // makes it 20px longer. + var width_with_spacing = ctx.measureText('Hello World, again').width; + assert.strictEqual(width_with_spacing, width_normal + 20, "width_with_spacing", "width_normal + 20") + + // Changing font to 20px. Without resetting the spacing, 1em wordSpacing + // is now 20px, so it's suppose to be 40px longer without any wordSpacing set. + ctx.font = '20px serif'; + width_with_spacing = ctx.measureText('Hello World, again').width; + // Now calculate the reference spacing for "Hello World, again" with no spacing. + ctx.wordSpacing = '0em'; + width_normal = ctx.measureText('Hello World, again').width; + assert.strictEqual(width_with_spacing, width_normal + 40, "width_with_spacing", "width_normal + 40") + }); + + it("2d.text.drawing.style.fontKerning", function () { + // Testing basic functionalities of fontKerning for canvas + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.fontKerning, "auto", "ctx.fontKerning", "\"auto\"") + ctx.fontKerning = "normal"; + assert.strictEqual(ctx.fontKerning, "normal", "ctx.fontKerning", "\"normal\"") + width_normal = ctx.measureText("TAWATAVA").width; + ctx.fontKerning = "none"; + assert.strictEqual(ctx.fontKerning, "none", "ctx.fontKerning", "\"none\"") + width_none = ctx.measureText("TAWATAVA").width; + assert(width_normal < width_none, "width_normal < width_none"); + }); + + it("2d.text.drawing.style.fontKerning.with.uppercase", function () { + // Testing basic functionalities of fontKerning for canvas + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.fontKerning, "auto", "ctx.fontKerning", "\"auto\"") + ctx.fontKerning = "Normal"; + assert.strictEqual(ctx.fontKerning, "normal", "ctx.fontKerning", "\"normal\"") + ctx.fontKerning = "Auto"; + ctx.fontKerning = "normal"; + assert.strictEqual(ctx.fontKerning, "normal", "ctx.fontKerning", "\"normal\"") + ctx.fontKerning = "Auto"; + ctx.fontKerning = "noRmal"; + assert.strictEqual(ctx.fontKerning, "normal", "ctx.fontKerning", "\"normal\"") + ctx.fontKerning = "Auto"; + ctx.fontKerning = "NoRMal"; + assert.strictEqual(ctx.fontKerning, "normal", "ctx.fontKerning", "\"normal\"") + ctx.fontKerning = "Auto"; + ctx.fontKerning = "NORMAL"; + assert.strictEqual(ctx.fontKerning, "normal", "ctx.fontKerning", "\"normal\"") + + ctx.fontKerning = "None"; + assert.strictEqual(ctx.fontKerning, "none", "ctx.fontKerning", "\"none\"") + ctx.fontKerning = "Auto"; + ctx.fontKerning = "none"; + assert.strictEqual(ctx.fontKerning, "none", "ctx.fontKerning", "\"none\"") + ctx.fontKerning = "Auto"; + ctx.fontKerning = "nOne"; + assert.strictEqual(ctx.fontKerning, "none", "ctx.fontKerning", "\"none\"") + ctx.fontKerning = "Auto"; + ctx.fontKerning = "nonE"; + assert.strictEqual(ctx.fontKerning, "none", "ctx.fontKerning", "\"none\"") + ctx.fontKerning = "Auto"; + ctx.fontKerning = "NONE"; + assert.strictEqual(ctx.fontKerning, "none", "ctx.fontKerning", "\"none\"") + }); + + it("2d.text.drawing.style.fontVariant.settings", function () { + // Testing basic functionalities of fontKerning for canvas + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + // Setting fontVariantCaps with lower cases + assert.strictEqual(ctx.fontVariantCaps, "normal", "ctx.fontVariantCaps", "\"normal\"") + + ctx.fontVariantCaps = "normal"; + assert.strictEqual(ctx.fontVariantCaps, "normal", "ctx.fontVariantCaps", "\"normal\"") + + ctx.fontVariantCaps = "small-caps"; + assert.strictEqual(ctx.fontVariantCaps, "small-caps", "ctx.fontVariantCaps", "\"small-caps\"") + + ctx.fontVariantCaps = "all-small-caps"; + assert.strictEqual(ctx.fontVariantCaps, "all-small-caps", "ctx.fontVariantCaps", "\"all-small-caps\"") + + ctx.fontVariantCaps = "petite-caps"; + assert.strictEqual(ctx.fontVariantCaps, "petite-caps", "ctx.fontVariantCaps", "\"petite-caps\"") + + ctx.fontVariantCaps = "all-petite-caps"; + assert.strictEqual(ctx.fontVariantCaps, "all-petite-caps", "ctx.fontVariantCaps", "\"all-petite-caps\"") + + ctx.fontVariantCaps = "unicase"; + assert.strictEqual(ctx.fontVariantCaps, "unicase", "ctx.fontVariantCaps", "\"unicase\"") + + ctx.fontVariantCaps = "titling-caps"; + assert.strictEqual(ctx.fontVariantCaps, "titling-caps", "ctx.fontVariantCaps", "\"titling-caps\"") + + // Setting fontVariantCaps with lower cases and upper cases word. + ctx.fontVariantCaps = "nORmal"; + assert.strictEqual(ctx.fontVariantCaps, "normal", "ctx.fontVariantCaps", "\"normal\"") + + ctx.fontVariantCaps = "smaLL-caps"; + assert.strictEqual(ctx.fontVariantCaps, "small-caps", "ctx.fontVariantCaps", "\"small-caps\"") + + ctx.fontVariantCaps = "all-small-CAPS"; + assert.strictEqual(ctx.fontVariantCaps, "all-small-caps", "ctx.fontVariantCaps", "\"all-small-caps\"") + + ctx.fontVariantCaps = "pEtitE-caps"; + assert.strictEqual(ctx.fontVariantCaps, "petite-caps", "ctx.fontVariantCaps", "\"petite-caps\"") + + ctx.fontVariantCaps = "All-Petite-Caps"; + assert.strictEqual(ctx.fontVariantCaps, "all-petite-caps", "ctx.fontVariantCaps", "\"all-petite-caps\"") + + ctx.fontVariantCaps = "uNIcase"; + assert.strictEqual(ctx.fontVariantCaps, "unicase", "ctx.fontVariantCaps", "\"unicase\"") + + ctx.fontVariantCaps = "titling-CAPS"; + assert.strictEqual(ctx.fontVariantCaps, "titling-caps", "ctx.fontVariantCaps", "\"titling-caps\"") + + // Setting fontVariantCaps with non-existing font variant. + ctx.fontVariantCaps = "abcd"; + assert.strictEqual(ctx.fontVariantCaps, "titling-caps", "ctx.fontVariantCaps", "\"titling-caps\"") + }); + + it("2d.text.drawing.style.textRendering.settings", function () { + // Testing basic functionalities of textRendering in Canvas + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + // Setting textRendering with lower cases + assert.strictEqual(ctx.textRendering, "auto", "ctx.textRendering", "\"auto\"") + + ctx.textRendering = "auto"; + assert.strictEqual(ctx.textRendering, "auto", "ctx.textRendering", "\"auto\"") + + ctx.textRendering = "optimizespeed"; + assert.strictEqual(ctx.textRendering, "optimizeSpeed", "ctx.textRendering", "\"optimizeSpeed\"") + + ctx.textRendering = "optimizelegibility"; + assert.strictEqual(ctx.textRendering, "optimizeLegibility", "ctx.textRendering", "\"optimizeLegibility\"") + + ctx.textRendering = "geometricprecision"; + assert.strictEqual(ctx.textRendering, "geometricPrecision", "ctx.textRendering", "\"geometricPrecision\"") + + // Setting textRendering with lower cases and upper cases word. + ctx.textRendering = "aUto"; + assert.strictEqual(ctx.textRendering, "auto", "ctx.textRendering", "\"auto\"") + + ctx.textRendering = "OPtimizeSpeed"; + assert.strictEqual(ctx.textRendering, "optimizeSpeed", "ctx.textRendering", "\"optimizeSpeed\"") + + ctx.textRendering = "OPtimizELEgibility"; + assert.strictEqual(ctx.textRendering, "optimizeLegibility", "ctx.textRendering", "\"optimizeLegibility\"") + + ctx.textRendering = "GeometricPrecision"; + assert.strictEqual(ctx.textRendering, "geometricPrecision", "ctx.textRendering", "\"geometricPrecision\"") + + // Setting textRendering with non-existing font variant. + ctx.textRendering = "abcd"; + assert.strictEqual(ctx.textRendering, "geometricPrecision", "ctx.textRendering", "\"geometricPrecision\"") + }); +}); diff --git a/test/wpt/generated/line-styles.js b/test/wpt/generated/line-styles.js new file mode 100644 index 000000000..815b3dc19 --- /dev/null +++ b/test/wpt/generated/line-styles.js @@ -0,0 +1,1136 @@ +// THIS FILE WAS AUTO-GENERATED. DO NOT EDIT BY HAND. + +const assert = require('assert'); +const path = require('path'); + +const { + createCanvas, + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + CanvasPattern, + CanvasGradient +} = require('../../..'); + +const window = { + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + Uint8ClampedArray, + CanvasPattern, + CanvasGradient +}; + +const document = { + createElement(type, ...args) { + if (type !== "canvas") + throw new Error(`createElement(${type}) not supported`); + return createCanvas(...args); + } +}; + +function _getPixel(canvas, x, y) { + const ctx = canvas.getContext('2d'); + const imgdata = ctx.getImageData(x, y, 1, 1); + return [ imgdata.data[0], imgdata.data[1], imgdata.data[2], imgdata.data[3] ]; +} + +function _assertApprox(actual, expected, epsilon=0, msg="") { + assert(typeof actual === "number", "actual should be a number but got a ${typeof type_actual}"); + + // The epsilon math below does not place nice with NaN and Infinity + // But in this case Infinity = Infinity and NaN = NaN + if (isFinite(actual) || isFinite(expected)) { + assert(Math.abs(actual - expected) <= epsilon, + `expected ${actual} to equal ${expected} +/- ${epsilon}. ${msg}`); + } else { + assert.strictEqual(actual, expected); + } +} + +function _assertPixel(canvas, x, y, r, g, b, a, pos, color) { + const c = _getPixel(canvas, x,y); + assert.strictEqual(c[0], r, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[1], g, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[2], b, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[3], a, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function _assertPixelApprox(canvas, x, y, r, g, b, a, pos, color, tolerance) { + const c = _getPixel(canvas, x,y); + _assertApprox(c[0], r, tolerance, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[1], g, tolerance, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[2], b, tolerance, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[3], a, tolerance, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function assert_throws_js(Type, fn) { + assert.throws(fn, Type); +} + +// Used by font tests to allow fonts to load. +function deferTest() {} + +class Test { + // Two cases of this in the tests, look unnecessary. + done() {} + // Used by font tests to allow fonts to load. + step_func_done(func) { func(); } + // Used for image onload callback. + step_func(func) { func(); } +} + +function step_timeout(result, time) { + // Nothing; code needs to be converted for this to work. +} + +describe("WPT: line-styles", function () { + + it("2d.line.defaults", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.lineWidth, 1, "ctx.lineWidth", "1") + assert.strictEqual(ctx.lineCap, 'butt', "ctx.lineCap", "'butt'") + assert.strictEqual(ctx.lineJoin, 'miter', "ctx.lineJoin", "'miter'") + assert.strictEqual(ctx.miterLimit, 10, "ctx.miterLimit", "10") + }); + + it("2d.line.width.basic", function () { + // lineWidth determines the width of line strokes + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 20; + // Draw a green line over a red box, to check the line is not too small + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + ctx.fillRect(15, 15, 20, 20); + ctx.beginPath(); + ctx.moveTo(25, 15); + ctx.lineTo(25, 35); + ctx.stroke(); + + // Draw a green box over a red line, to check the line is not too large + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(75, 15); + ctx.lineTo(75, 35); + ctx.stroke(); + ctx.fillRect(65, 15, 20, 20); + + _assertPixel(canvas, 14,25, 0,255,0,255); + _assertPixel(canvas, 15,25, 0,255,0,255); + _assertPixel(canvas, 16,25, 0,255,0,255); + _assertPixel(canvas, 25,25, 0,255,0,255); + _assertPixel(canvas, 34,25, 0,255,0,255); + _assertPixel(canvas, 35,25, 0,255,0,255); + _assertPixel(canvas, 36,25, 0,255,0,255); + + _assertPixel(canvas, 64,25, 0,255,0,255); + _assertPixel(canvas, 65,25, 0,255,0,255); + _assertPixel(canvas, 66,25, 0,255,0,255); + _assertPixel(canvas, 75,25, 0,255,0,255); + _assertPixel(canvas, 84,25, 0,255,0,255); + _assertPixel(canvas, 85,25, 0,255,0,255); + _assertPixel(canvas, 86,25, 0,255,0,255); + }); + + it("2d.line.width.transformed", function () { + // Line stroke widths are affected by scale transformations + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 4; + // Draw a green line over a red box, to check the line is not too small + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + ctx.fillRect(15, 15, 20, 20); + ctx.save(); + ctx.scale(5, 1); + ctx.beginPath(); + ctx.moveTo(5, 15); + ctx.lineTo(5, 35); + ctx.stroke(); + ctx.restore(); + + // Draw a green box over a red line, to check the line is not too large + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.save(); + ctx.scale(-5, 1); + ctx.beginPath(); + ctx.moveTo(-15, 15); + ctx.lineTo(-15, 35); + ctx.stroke(); + ctx.restore(); + ctx.fillRect(65, 15, 20, 20); + + _assertPixel(canvas, 14,25, 0,255,0,255); + _assertPixel(canvas, 15,25, 0,255,0,255); + _assertPixel(canvas, 16,25, 0,255,0,255); + _assertPixel(canvas, 25,25, 0,255,0,255); + _assertPixel(canvas, 34,25, 0,255,0,255); + _assertPixel(canvas, 35,25, 0,255,0,255); + _assertPixel(canvas, 36,25, 0,255,0,255); + + _assertPixel(canvas, 64,25, 0,255,0,255); + _assertPixel(canvas, 65,25, 0,255,0,255); + _assertPixel(canvas, 66,25, 0,255,0,255); + _assertPixel(canvas, 75,25, 0,255,0,255); + _assertPixel(canvas, 84,25, 0,255,0,255); + _assertPixel(canvas, 85,25, 0,255,0,255); + _assertPixel(canvas, 86,25, 0,255,0,255); + }); + + it("2d.line.width.scaledefault", function () { + // Default lineWidth strokes are affected by scale transformations + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.scale(50, 50); + ctx.strokeStyle = '#0f0'; + ctx.moveTo(0, 0.5); + ctx.lineTo(2, 0.5); + ctx.stroke(); + + _assertPixel(canvas, 25,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 75,25, 0,255,0,255); + _assertPixel(canvas, 50,5, 0,255,0,255); + _assertPixel(canvas, 50,45, 0,255,0,255); + }); + + it("2d.line.width.valid", function () { + // Setting lineWidth to valid values works + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.lineWidth = 1.5; + assert.strictEqual(ctx.lineWidth, 1.5, "ctx.lineWidth", "1.5") + + ctx.lineWidth = "1e1"; + assert.strictEqual(ctx.lineWidth, 10, "ctx.lineWidth", "10") + + ctx.lineWidth = 1/1024; + assert.strictEqual(ctx.lineWidth, 1/1024, "ctx.lineWidth", "1/1024") + + ctx.lineWidth = 1000; + assert.strictEqual(ctx.lineWidth, 1000, "ctx.lineWidth", "1000") + }); + + it("2d.line.width.invalid", function () { + // Setting lineWidth to invalid values is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.lineWidth = 1.5; + assert.strictEqual(ctx.lineWidth, 1.5, "ctx.lineWidth", "1.5") + + ctx.lineWidth = 1.5; + ctx.lineWidth = 0; + assert.strictEqual(ctx.lineWidth, 1.5, "ctx.lineWidth", "1.5") + + ctx.lineWidth = 1.5; + ctx.lineWidth = -1; + assert.strictEqual(ctx.lineWidth, 1.5, "ctx.lineWidth", "1.5") + + ctx.lineWidth = 1.5; + ctx.lineWidth = Infinity; + assert.strictEqual(ctx.lineWidth, 1.5, "ctx.lineWidth", "1.5") + + ctx.lineWidth = 1.5; + ctx.lineWidth = -Infinity; + assert.strictEqual(ctx.lineWidth, 1.5, "ctx.lineWidth", "1.5") + + ctx.lineWidth = 1.5; + ctx.lineWidth = NaN; + assert.strictEqual(ctx.lineWidth, 1.5, "ctx.lineWidth", "1.5") + + ctx.lineWidth = 1.5; + ctx.lineWidth = 'string'; + assert.strictEqual(ctx.lineWidth, 1.5, "ctx.lineWidth", "1.5") + + ctx.lineWidth = 1.5; + ctx.lineWidth = true; + assert.strictEqual(ctx.lineWidth, 1, "ctx.lineWidth", "1") + + ctx.lineWidth = 1.5; + ctx.lineWidth = false; + assert.strictEqual(ctx.lineWidth, 1.5, "ctx.lineWidth", "1.5") + }); + + it("2d.line.cap.butt", function () { + // lineCap 'butt' is rendered correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineCap = 'butt'; + ctx.lineWidth = 20; + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + ctx.fillRect(15, 15, 20, 20); + ctx.beginPath(); + ctx.moveTo(25, 15); + ctx.lineTo(25, 35); + ctx.stroke(); + + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(75, 15); + ctx.lineTo(75, 35); + ctx.stroke(); + ctx.fillRect(65, 15, 20, 20); + + _assertPixel(canvas, 25,14, 0,255,0,255); + _assertPixel(canvas, 25,15, 0,255,0,255); + _assertPixel(canvas, 25,16, 0,255,0,255); + _assertPixel(canvas, 25,34, 0,255,0,255); + _assertPixel(canvas, 25,35, 0,255,0,255); + _assertPixel(canvas, 25,36, 0,255,0,255); + + _assertPixel(canvas, 75,14, 0,255,0,255); + _assertPixel(canvas, 75,15, 0,255,0,255); + _assertPixel(canvas, 75,16, 0,255,0,255); + _assertPixel(canvas, 75,34, 0,255,0,255); + _assertPixel(canvas, 75,35, 0,255,0,255); + _assertPixel(canvas, 75,36, 0,255,0,255); + }); + + it("2d.line.cap.round", function () { + // lineCap 'round' is rendered correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var tol = 1; // tolerance to avoid antialiasing artifacts + + ctx.lineCap = 'round'; + ctx.lineWidth = 20; + + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + + ctx.beginPath(); + ctx.moveTo(35-tol, 15); + ctx.arc(25, 15, 10-tol, 0, Math.PI, true); + ctx.arc(25, 35, 10-tol, Math.PI, 0, true); + ctx.fill(); + + ctx.beginPath(); + ctx.moveTo(25, 15); + ctx.lineTo(25, 35); + ctx.stroke(); + + + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + + ctx.beginPath(); + ctx.moveTo(75, 15); + ctx.lineTo(75, 35); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(85+tol, 15); + ctx.arc(75, 15, 10+tol, 0, Math.PI, true); + ctx.arc(75, 35, 10+tol, Math.PI, 0, true); + ctx.fill(); + + _assertPixel(canvas, 17,6, 0,255,0,255); + _assertPixel(canvas, 25,6, 0,255,0,255); + _assertPixel(canvas, 32,6, 0,255,0,255); + _assertPixel(canvas, 17,43, 0,255,0,255); + _assertPixel(canvas, 25,43, 0,255,0,255); + _assertPixel(canvas, 32,43, 0,255,0,255); + + _assertPixel(canvas, 67,6, 0,255,0,255); + _assertPixel(canvas, 75,6, 0,255,0,255); + _assertPixel(canvas, 82,6, 0,255,0,255); + _assertPixel(canvas, 67,43, 0,255,0,255); + _assertPixel(canvas, 75,43, 0,255,0,255); + _assertPixel(canvas, 82,43, 0,255,0,255); + }); + + it("2d.line.cap.square", function () { + // lineCap 'square' is rendered correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineCap = 'square'; + ctx.lineWidth = 20; + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + ctx.fillRect(15, 5, 20, 40); + ctx.beginPath(); + ctx.moveTo(25, 15); + ctx.lineTo(25, 35); + ctx.stroke(); + + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(75, 15); + ctx.lineTo(75, 35); + ctx.stroke(); + ctx.fillRect(65, 5, 20, 40); + + _assertPixel(canvas, 25,4, 0,255,0,255); + _assertPixel(canvas, 25,5, 0,255,0,255); + _assertPixel(canvas, 25,6, 0,255,0,255); + _assertPixel(canvas, 25,44, 0,255,0,255); + _assertPixel(canvas, 25,45, 0,255,0,255); + _assertPixel(canvas, 25,46, 0,255,0,255); + + _assertPixel(canvas, 75,4, 0,255,0,255); + _assertPixel(canvas, 75,5, 0,255,0,255); + _assertPixel(canvas, 75,6, 0,255,0,255); + _assertPixel(canvas, 75,44, 0,255,0,255); + _assertPixel(canvas, 75,45, 0,255,0,255); + _assertPixel(canvas, 75,46, 0,255,0,255); + }); + + it("2d.line.cap.open", function () { + // Line caps are drawn at the corners of an unclosed rectangle + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineJoin = 'bevel'; + ctx.lineCap = 'square'; + ctx.lineWidth = 400; + + ctx.beginPath(); + ctx.moveTo(200, 200); + ctx.lineTo(200, 1000); + ctx.lineTo(1000, 1000); + ctx.lineTo(1000, 200); + ctx.lineTo(200, 200); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.line.cap.closed", function () { + // Line caps are not drawn at the corners of an unclosed rectangle + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineJoin = 'bevel'; + ctx.lineCap = 'square'; + ctx.lineWidth = 400; + + ctx.beginPath(); + ctx.moveTo(200, 200); + ctx.lineTo(200, 1000); + ctx.lineTo(1000, 1000); + ctx.lineTo(1000, 200); + ctx.closePath(); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.line.cap.valid", function () { + // Setting lineCap to valid values works + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.lineCap = 'butt' + assert.strictEqual(ctx.lineCap, 'butt', "ctx.lineCap", "'butt'") + + ctx.lineCap = 'round'; + assert.strictEqual(ctx.lineCap, 'round', "ctx.lineCap", "'round'") + + ctx.lineCap = 'square'; + assert.strictEqual(ctx.lineCap, 'square', "ctx.lineCap", "'square'") + }); + + it("2d.line.cap.invalid", function () { + // Setting lineCap to invalid values is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.lineCap = 'butt' + assert.strictEqual(ctx.lineCap, 'butt', "ctx.lineCap", "'butt'") + + ctx.lineCap = 'butt'; + ctx.lineCap = 'invalid'; + assert.strictEqual(ctx.lineCap, 'butt', "ctx.lineCap", "'butt'") + + ctx.lineCap = 'butt'; + ctx.lineCap = 'ROUND'; + assert.strictEqual(ctx.lineCap, 'butt', "ctx.lineCap", "'butt'") + + ctx.lineCap = 'butt'; + ctx.lineCap = 'round\0'; + assert.strictEqual(ctx.lineCap, 'butt', "ctx.lineCap", "'butt'") + + ctx.lineCap = 'butt'; + ctx.lineCap = 'round '; + assert.strictEqual(ctx.lineCap, 'butt', "ctx.lineCap", "'butt'") + + ctx.lineCap = 'butt'; + ctx.lineCap = ""; + assert.strictEqual(ctx.lineCap, 'butt', "ctx.lineCap", "'butt'") + + ctx.lineCap = 'butt'; + ctx.lineCap = 'bevel'; + assert.strictEqual(ctx.lineCap, 'butt', "ctx.lineCap", "'butt'") + }); + + it("2d.line.join.bevel", function () { + // lineJoin 'bevel' is rendered correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var tol = 1; // tolerance to avoid antialiasing artifacts + + ctx.lineJoin = 'bevel'; + ctx.lineWidth = 20; + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + + ctx.fillRect(10, 10, 20, 20); + ctx.fillRect(20, 20, 20, 20); + ctx.beginPath(); + ctx.moveTo(30, 20); + ctx.lineTo(40-tol, 20); + ctx.lineTo(30, 10+tol); + ctx.fill(); + + ctx.beginPath(); + ctx.moveTo(10, 20); + ctx.lineTo(30, 20); + ctx.lineTo(30, 40); + ctx.stroke(); + + + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + + ctx.beginPath(); + ctx.moveTo(60, 20); + ctx.lineTo(80, 20); + ctx.lineTo(80, 40); + ctx.stroke(); + + ctx.fillRect(60, 10, 20, 20); + ctx.fillRect(70, 20, 20, 20); + ctx.beginPath(); + ctx.moveTo(80, 20); + ctx.lineTo(90+tol, 20); + ctx.lineTo(80, 10-tol); + ctx.fill(); + + _assertPixel(canvas, 34,16, 0,255,0,255); + _assertPixel(canvas, 34,15, 0,255,0,255); + _assertPixel(canvas, 35,15, 0,255,0,255); + _assertPixel(canvas, 36,15, 0,255,0,255); + _assertPixel(canvas, 36,14, 0,255,0,255); + + _assertPixel(canvas, 84,16, 0,255,0,255); + _assertPixel(canvas, 84,15, 0,255,0,255); + _assertPixel(canvas, 85,15, 0,255,0,255); + _assertPixel(canvas, 86,15, 0,255,0,255); + _assertPixel(canvas, 86,14, 0,255,0,255); + }); + + it("2d.line.join.round", function () { + // lineJoin 'round' is rendered correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var tol = 1; // tolerance to avoid antialiasing artifacts + + ctx.lineJoin = 'round'; + ctx.lineWidth = 20; + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + + ctx.fillRect(10, 10, 20, 20); + ctx.fillRect(20, 20, 20, 20); + ctx.beginPath(); + ctx.moveTo(30, 20); + ctx.arc(30, 20, 10-tol, 0, 2*Math.PI, true); + ctx.fill(); + + ctx.beginPath(); + ctx.moveTo(10, 20); + ctx.lineTo(30, 20); + ctx.lineTo(30, 40); + ctx.stroke(); + + + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + + ctx.beginPath(); + ctx.moveTo(60, 20); + ctx.lineTo(80, 20); + ctx.lineTo(80, 40); + ctx.stroke(); + + ctx.fillRect(60, 10, 20, 20); + ctx.fillRect(70, 20, 20, 20); + ctx.beginPath(); + ctx.moveTo(80, 20); + ctx.arc(80, 20, 10+tol, 0, 2*Math.PI, true); + ctx.fill(); + + _assertPixel(canvas, 36,14, 0,255,0,255); + _assertPixel(canvas, 36,13, 0,255,0,255); + _assertPixel(canvas, 37,13, 0,255,0,255); + _assertPixel(canvas, 38,13, 0,255,0,255); + _assertPixel(canvas, 38,12, 0,255,0,255); + + _assertPixel(canvas, 86,14, 0,255,0,255); + _assertPixel(canvas, 86,13, 0,255,0,255); + _assertPixel(canvas, 87,13, 0,255,0,255); + _assertPixel(canvas, 88,13, 0,255,0,255); + _assertPixel(canvas, 88,12, 0,255,0,255); + }); + + it("2d.line.join.miter", function () { + // lineJoin 'miter' is rendered correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineJoin = 'miter'; + ctx.lineWidth = 20; + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + + ctx.fillRect(10, 10, 30, 20); + ctx.fillRect(20, 10, 20, 30); + + ctx.beginPath(); + ctx.moveTo(10, 20); + ctx.lineTo(30, 20); + ctx.lineTo(30, 40); + ctx.stroke(); + + + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + + ctx.beginPath(); + ctx.moveTo(60, 20); + ctx.lineTo(80, 20); + ctx.lineTo(80, 40); + ctx.stroke(); + + ctx.fillRect(60, 10, 30, 20); + ctx.fillRect(70, 10, 20, 30); + + _assertPixel(canvas, 38,12, 0,255,0,255); + _assertPixel(canvas, 39,11, 0,255,0,255); + _assertPixel(canvas, 40,10, 0,255,0,255); + _assertPixel(canvas, 41,9, 0,255,0,255); + _assertPixel(canvas, 42,8, 0,255,0,255); + + _assertPixel(canvas, 88,12, 0,255,0,255); + _assertPixel(canvas, 89,11, 0,255,0,255); + _assertPixel(canvas, 90,10, 0,255,0,255); + _assertPixel(canvas, 91,9, 0,255,0,255); + _assertPixel(canvas, 92,8, 0,255,0,255); + }); + + it("2d.line.join.open", function () { + // Line joins are not drawn at the corner of an unclosed rectangle + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineJoin = 'miter'; + ctx.lineWidth = 200; + + ctx.beginPath(); + ctx.moveTo(100, 50); + ctx.lineTo(100, 1000); + ctx.lineTo(1000, 1000); + ctx.lineTo(1000, 50); + ctx.lineTo(100, 50); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.line.join.closed", function () { + // Line joins are drawn at the corner of a closed rectangle + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineJoin = 'miter'; + ctx.lineWidth = 200; + + ctx.beginPath(); + ctx.moveTo(100, 50); + ctx.lineTo(100, 1000); + ctx.lineTo(1000, 1000); + ctx.lineTo(1000, 50); + ctx.closePath(); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.line.join.parallel", function () { + // Line joins are drawn at 180-degree joins + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 300; + ctx.lineJoin = 'round'; + ctx.beginPath(); + ctx.moveTo(-100, 25); + ctx.lineTo(0, 25); + ctx.lineTo(-100, 25); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.line.join.valid", function () { + // Setting lineJoin to valid values works + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.lineJoin = 'bevel' + assert.strictEqual(ctx.lineJoin, 'bevel', "ctx.lineJoin", "'bevel'") + + ctx.lineJoin = 'round'; + assert.strictEqual(ctx.lineJoin, 'round', "ctx.lineJoin", "'round'") + + ctx.lineJoin = 'miter'; + assert.strictEqual(ctx.lineJoin, 'miter', "ctx.lineJoin", "'miter'") + }); + + it("2d.line.join.invalid", function () { + // Setting lineJoin to invalid values is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.lineJoin = 'bevel' + assert.strictEqual(ctx.lineJoin, 'bevel', "ctx.lineJoin", "'bevel'") + + ctx.lineJoin = 'bevel'; + ctx.lineJoin = 'invalid'; + assert.strictEqual(ctx.lineJoin, 'bevel', "ctx.lineJoin", "'bevel'") + + ctx.lineJoin = 'bevel'; + ctx.lineJoin = 'ROUND'; + assert.strictEqual(ctx.lineJoin, 'bevel', "ctx.lineJoin", "'bevel'") + + ctx.lineJoin = 'bevel'; + ctx.lineJoin = 'round\0'; + assert.strictEqual(ctx.lineJoin, 'bevel', "ctx.lineJoin", "'bevel'") + + ctx.lineJoin = 'bevel'; + ctx.lineJoin = 'round '; + assert.strictEqual(ctx.lineJoin, 'bevel', "ctx.lineJoin", "'bevel'") + + ctx.lineJoin = 'bevel'; + ctx.lineJoin = ""; + assert.strictEqual(ctx.lineJoin, 'bevel', "ctx.lineJoin", "'bevel'") + + ctx.lineJoin = 'bevel'; + ctx.lineJoin = 'butt'; + assert.strictEqual(ctx.lineJoin, 'bevel', "ctx.lineJoin", "'bevel'") + }); + + it("2d.line.miter.exceeded", function () { + // Miter joins are not drawn when the miter limit is exceeded + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 400; + ctx.lineJoin = 'miter'; + + ctx.strokeStyle = '#f00'; + ctx.miterLimit = 1.414; + ctx.beginPath(); + ctx.moveTo(200, 1000); + ctx.lineTo(200, 200); + ctx.lineTo(1000, 201); // slightly non-right-angle to avoid being a special case + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.line.miter.acute", function () { + // Miter joins are drawn correctly with acute angles + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 200; + ctx.lineJoin = 'miter'; + + ctx.strokeStyle = '#0f0'; + ctx.miterLimit = 2.614; + ctx.beginPath(); + ctx.moveTo(100, 1000); + ctx.lineTo(100, 100); + ctx.lineTo(1000, 1000); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.miterLimit = 2.613; + ctx.beginPath(); + ctx.moveTo(100, 1000); + ctx.lineTo(100, 100); + ctx.lineTo(1000, 1000); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.line.miter.obtuse", function () { + // Miter joins are drawn correctly with obtuse angles + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 1600; + ctx.lineJoin = 'miter'; + + ctx.strokeStyle = '#0f0'; + ctx.miterLimit = 1.083; + ctx.beginPath(); + ctx.moveTo(800, 10000); + ctx.lineTo(800, 300); + ctx.lineTo(10000, -8900); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.miterLimit = 1.082; + ctx.beginPath(); + ctx.moveTo(800, 10000); + ctx.lineTo(800, 300); + ctx.lineTo(10000, -8900); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.line.miter.rightangle", function () { + // Miter joins are not drawn when the miter limit is exceeded, on exact right angles + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 400; + ctx.lineJoin = 'miter'; + + ctx.strokeStyle = '#f00'; + ctx.miterLimit = 1.414; + ctx.beginPath(); + ctx.moveTo(200, 1000); + ctx.lineTo(200, 200); + ctx.lineTo(1000, 200); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.line.miter.lineedge", function () { + // Miter joins are not drawn when the miter limit is exceeded at the corners of a zero-height rectangle + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 200; + ctx.lineJoin = 'miter'; + + ctx.strokeStyle = '#f00'; + ctx.miterLimit = 1.414; + ctx.beginPath(); + ctx.strokeRect(100, 25, 200, 0); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.line.miter.within", function () { + // Miter joins are drawn when the miter limit is not quite exceeded + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 400; + ctx.lineJoin = 'miter'; + + ctx.strokeStyle = '#0f0'; + ctx.miterLimit = 1.416; + ctx.beginPath(); + ctx.moveTo(200, 1000); + ctx.lineTo(200, 200); + ctx.lineTo(1000, 201); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.line.miter.valid", function () { + // Setting miterLimit to valid values works + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.miterLimit = 1.5; + assert.strictEqual(ctx.miterLimit, 1.5, "ctx.miterLimit", "1.5") + + ctx.miterLimit = "1e1"; + assert.strictEqual(ctx.miterLimit, 10, "ctx.miterLimit", "10") + + ctx.miterLimit = 1/1024; + assert.strictEqual(ctx.miterLimit, 1/1024, "ctx.miterLimit", "1/1024") + + ctx.miterLimit = 1000; + assert.strictEqual(ctx.miterLimit, 1000, "ctx.miterLimit", "1000") + }); + + it("2d.line.miter.invalid", function () { + // Setting miterLimit to invalid values is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.miterLimit = 1.5; + assert.strictEqual(ctx.miterLimit, 1.5, "ctx.miterLimit", "1.5") + + ctx.miterLimit = 1.5; + ctx.miterLimit = 0; + assert.strictEqual(ctx.miterLimit, 1.5, "ctx.miterLimit", "1.5") + + ctx.miterLimit = 1.5; + ctx.miterLimit = -1; + assert.strictEqual(ctx.miterLimit, 1.5, "ctx.miterLimit", "1.5") + + ctx.miterLimit = 1.5; + ctx.miterLimit = Infinity; + assert.strictEqual(ctx.miterLimit, 1.5, "ctx.miterLimit", "1.5") + + ctx.miterLimit = 1.5; + ctx.miterLimit = -Infinity; + assert.strictEqual(ctx.miterLimit, 1.5, "ctx.miterLimit", "1.5") + + ctx.miterLimit = 1.5; + ctx.miterLimit = NaN; + assert.strictEqual(ctx.miterLimit, 1.5, "ctx.miterLimit", "1.5") + + ctx.miterLimit = 1.5; + ctx.miterLimit = 'string'; + assert.strictEqual(ctx.miterLimit, 1.5, "ctx.miterLimit", "1.5") + + ctx.miterLimit = 1.5; + ctx.miterLimit = true; + assert.strictEqual(ctx.miterLimit, 1, "ctx.miterLimit", "1") + + ctx.miterLimit = 1.5; + ctx.miterLimit = false; + assert.strictEqual(ctx.miterLimit, 1.5, "ctx.miterLimit", "1.5") + }); + + it("2d.line.cross", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 200; + ctx.lineJoin = 'bevel'; + + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(110, 50); + ctx.lineTo(110, 60); + ctx.lineTo(100, 60); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.line.union", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 100; + ctx.lineCap = 'round'; + + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 24); + ctx.lineTo(100, 25); + ctx.lineTo(0, 26); + ctx.closePath(); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 25,1, 0,255,0,255); + _assertPixel(canvas, 48,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 25,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + }); + + it("2d.line.invalid.strokestyle", function () { + // Verify correct behavior of canvas on an invalid strokeStyle() + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.strokeStyle = 'rgb(0, 255, 0)'; + ctx.strokeStyle = 'nonsense'; + ctx.lineWidth = 200; + ctx.moveTo(0,100); + ctx.lineTo(200,100); + ctx.stroke(); + var imageData = ctx.getImageData(0, 0, 200, 200); + var imgdata = imageData.data; + assert(imgdata[4] == 0, "imgdata[\""+(4)+"\"] == 0"); + assert(imgdata[5] == 255, "imgdata[\""+(5)+"\"] == 255"); + assert(imgdata[6] == 0, "imgdata[\""+(6)+"\"] == 0"); + }); +}); diff --git a/test/wpt/generated/meta.js b/test/wpt/generated/meta.js new file mode 100644 index 000000000..9e15857b3 --- /dev/null +++ b/test/wpt/generated/meta.js @@ -0,0 +1,92 @@ +// THIS FILE WAS AUTO-GENERATED. DO NOT EDIT BY HAND. + +const assert = require('assert'); +const path = require('path'); + +const { + createCanvas, + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + CanvasPattern, + CanvasGradient +} = require('../../..'); + +const window = { + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + Uint8ClampedArray, + CanvasPattern, + CanvasGradient +}; + +const document = { + createElement(type, ...args) { + if (type !== "canvas") + throw new Error(`createElement(${type}) not supported`); + return createCanvas(...args); + } +}; + +function _getPixel(canvas, x, y) { + const ctx = canvas.getContext('2d'); + const imgdata = ctx.getImageData(x, y, 1, 1); + return [ imgdata.data[0], imgdata.data[1], imgdata.data[2], imgdata.data[3] ]; +} + +function _assertApprox(actual, expected, epsilon=0, msg="") { + assert(typeof actual === "number", "actual should be a number but got a ${typeof type_actual}"); + + // The epsilon math below does not place nice with NaN and Infinity + // But in this case Infinity = Infinity and NaN = NaN + if (isFinite(actual) || isFinite(expected)) { + assert(Math.abs(actual - expected) <= epsilon, + `expected ${actual} to equal ${expected} +/- ${epsilon}. ${msg}`); + } else { + assert.strictEqual(actual, expected); + } +} + +function _assertPixel(canvas, x, y, r, g, b, a, pos, color) { + const c = _getPixel(canvas, x,y); + assert.strictEqual(c[0], r, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[1], g, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[2], b, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[3], a, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function _assertPixelApprox(canvas, x, y, r, g, b, a, pos, color, tolerance) { + const c = _getPixel(canvas, x,y); + _assertApprox(c[0], r, tolerance, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[1], g, tolerance, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[2], b, tolerance, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[3], a, tolerance, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function assert_throws_js(Type, fn) { + assert.throws(fn, Type); +} + +// Used by font tests to allow fonts to load. +function deferTest() {} + +class Test { + // Two cases of this in the tests, look unnecessary. + done() {} + // Used by font tests to allow fonts to load. + step_func_done(func) { func(); } + // Used for image onload callback. + step_func(func) { func(); } +} + +function step_timeout(result, time) { + // Nothing; code needs to be converted for this to work. +} + +describe("WPT: meta", function () { +}); diff --git a/test/wpt/generated/path-objects.js b/test/wpt/generated/path-objects.js new file mode 100644 index 000000000..d01c89072 --- /dev/null +++ b/test/wpt/generated/path-objects.js @@ -0,0 +1,4352 @@ +// THIS FILE WAS AUTO-GENERATED. DO NOT EDIT BY HAND. + +const assert = require('assert'); +const path = require('path'); + +const { + createCanvas, + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + CanvasPattern, + CanvasGradient +} = require('../../..'); + +const window = { + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + Uint8ClampedArray, + CanvasPattern, + CanvasGradient +}; + +const document = { + createElement(type, ...args) { + if (type !== "canvas") + throw new Error(`createElement(${type}) not supported`); + return createCanvas(...args); + } +}; + +function _getPixel(canvas, x, y) { + const ctx = canvas.getContext('2d'); + const imgdata = ctx.getImageData(x, y, 1, 1); + return [ imgdata.data[0], imgdata.data[1], imgdata.data[2], imgdata.data[3] ]; +} + +function _assertApprox(actual, expected, epsilon=0, msg="") { + assert(typeof actual === "number", "actual should be a number but got a ${typeof type_actual}"); + + // The epsilon math below does not place nice with NaN and Infinity + // But in this case Infinity = Infinity and NaN = NaN + if (isFinite(actual) || isFinite(expected)) { + assert(Math.abs(actual - expected) <= epsilon, + `expected ${actual} to equal ${expected} +/- ${epsilon}. ${msg}`); + } else { + assert.strictEqual(actual, expected); + } +} + +function _assertPixel(canvas, x, y, r, g, b, a, pos, color) { + const c = _getPixel(canvas, x,y); + assert.strictEqual(c[0], r, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[1], g, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[2], b, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[3], a, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function _assertPixelApprox(canvas, x, y, r, g, b, a, pos, color, tolerance) { + const c = _getPixel(canvas, x,y); + _assertApprox(c[0], r, tolerance, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[1], g, tolerance, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[2], b, tolerance, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[3], a, tolerance, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function assert_throws_js(Type, fn) { + assert.throws(fn, Type); +} + +// Used by font tests to allow fonts to load. +function deferTest() {} + +class Test { + // Two cases of this in the tests, look unnecessary. + done() {} + // Used by font tests to allow fonts to load. + step_func_done(func) { func(); } + // Used for image onload callback. + step_func(func) { func(); } +} + +function step_timeout(result, time) { + // Nothing; code needs to be converted for this to work. +} + +describe("WPT: path-objects", function () { + + it("2d.path.initial", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.closePath(); + ctx.fillStyle = '#f00'; + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.beginPath", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.rect(0, 0, 100, 50); + ctx.beginPath(); + ctx.fillStyle = '#f00'; + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.moveTo.basic", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.rect(0, 0, 10, 50); + ctx.moveTo(100, 0); + ctx.lineTo(10, 0); + ctx.lineTo(10, 50); + ctx.lineTo(100, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 90,25, 0,255,0,255); + }); + + it("2d.path.moveTo.newsubpath", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.moveTo(100, 0); + ctx.moveTo(100, 50); + ctx.moveTo(0, 50); + ctx.fillStyle = '#f00'; + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.moveTo.multiple", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.moveTo(0, 25); + ctx.moveTo(100, 25); + ctx.moveTo(0, 25); + ctx.lineTo(100, 25); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.moveTo.nonfinite", function () { + // moveTo() with Infinity/NaN is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + ctx.moveTo(Infinity, 50); + ctx.moveTo(-Infinity, 50); + ctx.moveTo(NaN, 50); + ctx.moveTo(0, Infinity); + ctx.moveTo(0, -Infinity); + ctx.moveTo(0, NaN); + ctx.moveTo(Infinity, Infinity); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.closePath.empty", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.closePath(); + ctx.fillStyle = '#f00'; + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.closePath.newline", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.moveTo(-100, 25); + ctx.lineTo(-100, -100); + ctx.lineTo(200, -100); + ctx.lineTo(200, 25); + ctx.closePath(); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.closePath.nextpoint", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.moveTo(-100, 25); + ctx.lineTo(-100, -1000); + ctx.closePath(); + ctx.lineTo(1000, 25); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.lineTo.ensuresubpath.1", function () { + // If there is no subpath, the point is added and nothing is drawn + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.lineTo(100, 50); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.lineTo.ensuresubpath.2", function () { + // If there is no subpath, the point is added and used for subsequent drawing + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.lineTo(0, 25); + ctx.lineTo(100, 25); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.lineTo.basic", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.lineTo(100, 25); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.lineTo.nextpoint", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.moveTo(-100, -100); + ctx.lineTo(0, 25); + ctx.lineTo(100, 25); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.lineTo.nonfinite", function () { + // lineTo() with Infinity/NaN is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + ctx.lineTo(Infinity, 50); + ctx.lineTo(-Infinity, 50); + ctx.lineTo(NaN, 50); + ctx.lineTo(0, Infinity); + ctx.lineTo(0, -Infinity); + ctx.lineTo(0, NaN); + ctx.lineTo(Infinity, Infinity); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 90,45, 0,255,0,255); + }); + + it("2d.path.lineTo.nonfinite.details", function () { + // lineTo() with Infinity/NaN for first arg still converts the second arg + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + for (var arg1 of [Infinity, -Infinity, NaN]) { + var converted = false; + ctx.lineTo(arg1, { valueOf: function() { converted = true; return 0; } }); + assert(converted, "converted"); + } + }); + + it("2d.path.quadraticCurveTo.ensuresubpath.1", function () { + // If there is no subpath, the first control point is added (and nothing is drawn up to it) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.quadraticCurveTo(100, 50, 200, 50); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 95,45, 0,255,0,255); + }); + + it("2d.path.quadraticCurveTo.ensuresubpath.2", function () { + // If there is no subpath, the first control point is added + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.quadraticCurveTo(0, 25, 100, 25); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 5,45, 0,255,0,255); + }); + + it("2d.path.quadraticCurveTo.basic", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.quadraticCurveTo(100, 25, 100, 25); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.quadraticCurveTo.shape", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 55; + ctx.beginPath(); + ctx.moveTo(-1000, 1050); + ctx.quadraticCurveTo(0, -1000, 1200, 1050); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.quadraticCurveTo.scaled", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.scale(1000, 1000); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 0.055; + ctx.beginPath(); + ctx.moveTo(-1, 1.05); + ctx.quadraticCurveTo(0, -1, 1.2, 1.05); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.quadraticCurveTo.nonfinite", function () { + // quadraticCurveTo() with Infinity/NaN is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + ctx.quadraticCurveTo(Infinity, 50, 0, 50); + ctx.quadraticCurveTo(-Infinity, 50, 0, 50); + ctx.quadraticCurveTo(NaN, 50, 0, 50); + ctx.quadraticCurveTo(0, Infinity, 0, 50); + ctx.quadraticCurveTo(0, -Infinity, 0, 50); + ctx.quadraticCurveTo(0, NaN, 0, 50); + ctx.quadraticCurveTo(0, 50, Infinity, 50); + ctx.quadraticCurveTo(0, 50, -Infinity, 50); + ctx.quadraticCurveTo(0, 50, NaN, 50); + ctx.quadraticCurveTo(0, 50, 0, Infinity); + ctx.quadraticCurveTo(0, 50, 0, -Infinity); + ctx.quadraticCurveTo(0, 50, 0, NaN); + ctx.quadraticCurveTo(Infinity, Infinity, 0, 50); + ctx.quadraticCurveTo(Infinity, Infinity, Infinity, 50); + ctx.quadraticCurveTo(Infinity, Infinity, Infinity, Infinity); + ctx.quadraticCurveTo(Infinity, Infinity, 0, Infinity); + ctx.quadraticCurveTo(Infinity, 50, Infinity, 50); + ctx.quadraticCurveTo(Infinity, 50, Infinity, Infinity); + ctx.quadraticCurveTo(Infinity, 50, 0, Infinity); + ctx.quadraticCurveTo(0, Infinity, Infinity, 50); + ctx.quadraticCurveTo(0, Infinity, Infinity, Infinity); + ctx.quadraticCurveTo(0, Infinity, 0, Infinity); + ctx.quadraticCurveTo(0, 50, Infinity, Infinity); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 90,45, 0,255,0,255); + }); + + it("2d.path.bezierCurveTo.ensuresubpath.1", function () { + // If there is no subpath, the first control point is added (and nothing is drawn up to it) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.bezierCurveTo(100, 50, 200, 50, 200, 50); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 95,45, 0,255,0,255); + }); + + it("2d.path.bezierCurveTo.ensuresubpath.2", function () { + // If there is no subpath, the first control point is added + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.bezierCurveTo(0, 25, 100, 25, 100, 25); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 5,45, 0,255,0,255); + }); + + it("2d.path.bezierCurveTo.basic", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.bezierCurveTo(100, 25, 100, 25, 100, 25); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.bezierCurveTo.shape", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 55; + ctx.beginPath(); + ctx.moveTo(-2000, 3100); + ctx.bezierCurveTo(-2000, -1000, 2100, -1000, 2100, 3100); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.bezierCurveTo.scaled", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.scale(1000, 1000); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 0.055; + ctx.beginPath(); + ctx.moveTo(-2, 3.1); + ctx.bezierCurveTo(-2, -1, 2.1, -1, 2.1, 3.1); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.bezierCurveTo.nonfinite", function () { + // bezierCurveTo() with Infinity/NaN is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + ctx.bezierCurveTo(Infinity, 50, 0, 50, 0, 50); + ctx.bezierCurveTo(-Infinity, 50, 0, 50, 0, 50); + ctx.bezierCurveTo(NaN, 50, 0, 50, 0, 50); + ctx.bezierCurveTo(0, Infinity, 0, 50, 0, 50); + ctx.bezierCurveTo(0, -Infinity, 0, 50, 0, 50); + ctx.bezierCurveTo(0, NaN, 0, 50, 0, 50); + ctx.bezierCurveTo(0, 50, Infinity, 50, 0, 50); + ctx.bezierCurveTo(0, 50, -Infinity, 50, 0, 50); + ctx.bezierCurveTo(0, 50, NaN, 50, 0, 50); + ctx.bezierCurveTo(0, 50, 0, Infinity, 0, 50); + ctx.bezierCurveTo(0, 50, 0, -Infinity, 0, 50); + ctx.bezierCurveTo(0, 50, 0, NaN, 0, 50); + ctx.bezierCurveTo(0, 50, 0, 50, Infinity, 50); + ctx.bezierCurveTo(0, 50, 0, 50, -Infinity, 50); + ctx.bezierCurveTo(0, 50, 0, 50, NaN, 50); + ctx.bezierCurveTo(0, 50, 0, 50, 0, Infinity); + ctx.bezierCurveTo(0, 50, 0, 50, 0, -Infinity); + ctx.bezierCurveTo(0, 50, 0, 50, 0, NaN); + ctx.bezierCurveTo(Infinity, Infinity, 0, 50, 0, 50); + ctx.bezierCurveTo(Infinity, Infinity, Infinity, 50, 0, 50); + ctx.bezierCurveTo(Infinity, Infinity, Infinity, Infinity, 0, 50); + ctx.bezierCurveTo(Infinity, Infinity, Infinity, Infinity, Infinity, 50); + ctx.bezierCurveTo(Infinity, Infinity, Infinity, Infinity, Infinity, Infinity); + ctx.bezierCurveTo(Infinity, Infinity, Infinity, Infinity, 0, Infinity); + ctx.bezierCurveTo(Infinity, Infinity, Infinity, 50, Infinity, 50); + ctx.bezierCurveTo(Infinity, Infinity, Infinity, 50, Infinity, Infinity); + ctx.bezierCurveTo(Infinity, Infinity, Infinity, 50, 0, Infinity); + ctx.bezierCurveTo(Infinity, Infinity, 0, Infinity, 0, 50); + ctx.bezierCurveTo(Infinity, Infinity, 0, Infinity, Infinity, 50); + ctx.bezierCurveTo(Infinity, Infinity, 0, Infinity, Infinity, Infinity); + ctx.bezierCurveTo(Infinity, Infinity, 0, Infinity, 0, Infinity); + ctx.bezierCurveTo(Infinity, Infinity, 0, 50, Infinity, 50); + ctx.bezierCurveTo(Infinity, Infinity, 0, 50, Infinity, Infinity); + ctx.bezierCurveTo(Infinity, Infinity, 0, 50, 0, Infinity); + ctx.bezierCurveTo(Infinity, 50, Infinity, 50, 0, 50); + ctx.bezierCurveTo(Infinity, 50, Infinity, Infinity, 0, 50); + ctx.bezierCurveTo(Infinity, 50, Infinity, Infinity, Infinity, 50); + ctx.bezierCurveTo(Infinity, 50, Infinity, Infinity, Infinity, Infinity); + ctx.bezierCurveTo(Infinity, 50, Infinity, Infinity, 0, Infinity); + ctx.bezierCurveTo(Infinity, 50, Infinity, 50, Infinity, 50); + ctx.bezierCurveTo(Infinity, 50, Infinity, 50, Infinity, Infinity); + ctx.bezierCurveTo(Infinity, 50, Infinity, 50, 0, Infinity); + ctx.bezierCurveTo(Infinity, 50, 0, Infinity, 0, 50); + ctx.bezierCurveTo(Infinity, 50, 0, Infinity, Infinity, 50); + ctx.bezierCurveTo(Infinity, 50, 0, Infinity, Infinity, Infinity); + ctx.bezierCurveTo(Infinity, 50, 0, Infinity, 0, Infinity); + ctx.bezierCurveTo(Infinity, 50, 0, 50, Infinity, 50); + ctx.bezierCurveTo(Infinity, 50, 0, 50, Infinity, Infinity); + ctx.bezierCurveTo(Infinity, 50, 0, 50, 0, Infinity); + ctx.bezierCurveTo(0, Infinity, Infinity, 50, 0, 50); + ctx.bezierCurveTo(0, Infinity, Infinity, Infinity, 0, 50); + ctx.bezierCurveTo(0, Infinity, Infinity, Infinity, Infinity, 50); + ctx.bezierCurveTo(0, Infinity, Infinity, Infinity, Infinity, Infinity); + ctx.bezierCurveTo(0, Infinity, Infinity, Infinity, 0, Infinity); + ctx.bezierCurveTo(0, Infinity, Infinity, 50, Infinity, 50); + ctx.bezierCurveTo(0, Infinity, Infinity, 50, Infinity, Infinity); + ctx.bezierCurveTo(0, Infinity, Infinity, 50, 0, Infinity); + ctx.bezierCurveTo(0, Infinity, 0, Infinity, 0, 50); + ctx.bezierCurveTo(0, Infinity, 0, Infinity, Infinity, 50); + ctx.bezierCurveTo(0, Infinity, 0, Infinity, Infinity, Infinity); + ctx.bezierCurveTo(0, Infinity, 0, Infinity, 0, Infinity); + ctx.bezierCurveTo(0, Infinity, 0, 50, Infinity, 50); + ctx.bezierCurveTo(0, Infinity, 0, 50, Infinity, Infinity); + ctx.bezierCurveTo(0, Infinity, 0, 50, 0, Infinity); + ctx.bezierCurveTo(0, 50, Infinity, Infinity, 0, 50); + ctx.bezierCurveTo(0, 50, Infinity, Infinity, Infinity, 50); + ctx.bezierCurveTo(0, 50, Infinity, Infinity, Infinity, Infinity); + ctx.bezierCurveTo(0, 50, Infinity, Infinity, 0, Infinity); + ctx.bezierCurveTo(0, 50, Infinity, 50, Infinity, 50); + ctx.bezierCurveTo(0, 50, Infinity, 50, Infinity, Infinity); + ctx.bezierCurveTo(0, 50, Infinity, 50, 0, Infinity); + ctx.bezierCurveTo(0, 50, 0, Infinity, Infinity, 50); + ctx.bezierCurveTo(0, 50, 0, Infinity, Infinity, Infinity); + ctx.bezierCurveTo(0, 50, 0, Infinity, 0, Infinity); + ctx.bezierCurveTo(0, 50, 0, 50, Infinity, Infinity); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 90,45, 0,255,0,255); + }); + + it("2d.path.arcTo.ensuresubpath.1", function () { + // If there is no subpath, the first control point is added (and nothing is drawn up to it) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.arcTo(100, 50, 200, 50, 0.1); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arcTo.ensuresubpath.2", function () { + // If there is no subpath, the first control point is added + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.arcTo(0, 25, 50, 250, 0.1); // adds (x1,y1), draws nothing + ctx.lineTo(100, 25); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arcTo.coincide.1", function () { + // arcTo() has no effect if P0 = P1 + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(0, 25, 50, 1000, 1); + ctx.lineTo(100, 25); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.arcTo(50, 25, 100, 25, 1); + ctx.stroke(); + + _assertPixel(canvas, 50,1, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 50,48, 0,255,0,255); + }); + + it("2d.path.arcTo.coincide.2", function () { + // arcTo() draws a straight line to P1 if P1 = P2 + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(100, 25, 100, 25, 1); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arcTo.collinear.1", function () { + // arcTo() with all points on a line, and P1 between P0/P2, draws a straight line to P1 + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(100, 25, 200, 25, 1); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(-100, 25); + ctx.arcTo(0, 25, 100, 25, 1); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arcTo.collinear.2", function () { + // arcTo() with all points on a line, and P2 between P0/P1, draws a straight line to P1 + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(100, 25, 10, 25, 1); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 25); + ctx.arcTo(200, 25, 110, 25, 1); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arcTo.collinear.3", function () { + // arcTo() with all points on a line, and P0 between P1/P2, draws a straight line to P1 + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(100, 25, -100, 25, 1); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 25); + ctx.arcTo(200, 25, 0, 25, 1); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(-100, 25); + ctx.arcTo(0, 25, -200, 25, 1); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arcTo.shape.curve1", function () { + // arcTo() curves in the right kind of shape + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var tol = 1.5; // tolerance to avoid antialiasing artifacts + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 10; + ctx.beginPath(); + ctx.moveTo(10, 25); + ctx.arcTo(75, 25, 75, 60, 20); + ctx.stroke(); + + ctx.fillStyle = '#0f0'; + ctx.beginPath(); + ctx.rect(10, 20, 45, 10); + ctx.moveTo(80, 45); + ctx.arc(55, 45, 25+tol, 0, -Math.PI/2, true); + ctx.arc(55, 45, 15-tol, -Math.PI/2, 0, false); + ctx.fill(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 55,19, 0,255,0,255); + _assertPixel(canvas, 55,20, 0,255,0,255); + _assertPixel(canvas, 55,21, 0,255,0,255); + _assertPixel(canvas, 64,22, 0,255,0,255); + _assertPixel(canvas, 65,21, 0,255,0,255); + _assertPixel(canvas, 72,28, 0,255,0,255); + _assertPixel(canvas, 73,27, 0,255,0,255); + _assertPixel(canvas, 78,36, 0,255,0,255); + _assertPixel(canvas, 79,35, 0,255,0,255); + _assertPixel(canvas, 80,44, 0,255,0,255); + _assertPixel(canvas, 80,45, 0,255,0,255); + _assertPixel(canvas, 80,46, 0,255,0,255); + _assertPixel(canvas, 65,45, 0,255,0,255); + }); + + it("2d.path.arcTo.shape.curve2", function () { + // arcTo() curves in the right kind of shape + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var tol = 1.5; // tolerance to avoid antialiasing artifacts + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.rect(10, 20, 45, 10); + ctx.moveTo(80, 45); + ctx.arc(55, 45, 25-tol, 0, -Math.PI/2, true); + ctx.arc(55, 45, 15+tol, -Math.PI/2, 0, false); + ctx.fill(); + + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 10; + ctx.beginPath(); + ctx.moveTo(10, 25); + ctx.arcTo(75, 25, 75, 60, 20); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 55,19, 0,255,0,255); + _assertPixel(canvas, 55,20, 0,255,0,255); + _assertPixel(canvas, 55,21, 0,255,0,255); + _assertPixel(canvas, 64,22, 0,255,0,255); + _assertPixel(canvas, 65,21, 0,255,0,255); + _assertPixel(canvas, 72,28, 0,255,0,255); + _assertPixel(canvas, 73,27, 0,255,0,255); + _assertPixel(canvas, 78,36, 0,255,0,255); + _assertPixel(canvas, 79,35, 0,255,0,255); + _assertPixel(canvas, 80,44, 0,255,0,255); + _assertPixel(canvas, 80,45, 0,255,0,255); + _assertPixel(canvas, 80,46, 0,255,0,255); + }); + + it("2d.path.arcTo.shape.start", function () { + // arcTo() draws a straight line from P0 to P1 + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(200, 25, 200, 50, 10); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.arcTo.shape.end", function () { + // arcTo() does not draw anything from P1 to P2 + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.moveTo(-100, -100); + ctx.arcTo(-100, 25, 200, 25, 10); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.arcTo.negative", function () { + // arcTo() with negative radius throws an exception + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.throws(function() { ctx.arcTo(0, 0, 0, 0, -1); }, /INDEX_SIZE_ERR/); + var path = new Path2D(); + assert.throws(function() { path.arcTo(10, 10, 20, 20, -5); }, /INDEX_SIZE_ERR/); + }); + + it("2d.path.arcTo.zero.1", function () { + // arcTo() with zero radius draws a straight line from P0 to P1 + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(100, 25, 100, 100, 0); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(0, -25); + ctx.arcTo(50, -25, 50, 50, 0); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arcTo.zero.2", function () { + // arcTo() with zero radius draws a straight line from P0 to P1, even when all points are collinear + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(100, 25, -100, 25, 0); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 25); + ctx.arcTo(200, 25, 50, 25, 0); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arcTo.transformation", function () { + // arcTo joins up to the last subpath point correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 50); + ctx.translate(100, 0); + ctx.arcTo(50, 50, 50, 0, 50); + ctx.lineTo(-100, 0); + ctx.fill(); + + _assertPixel(canvas, 0,0, 0,255,0,255); + _assertPixel(canvas, 50,0, 0,255,0,255); + _assertPixel(canvas, 99,0, 0,255,0,255); + _assertPixel(canvas, 0,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 99,25, 0,255,0,255); + _assertPixel(canvas, 0,49, 0,255,0,255); + _assertPixel(canvas, 50,49, 0,255,0,255); + _assertPixel(canvas, 99,49, 0,255,0,255); + }); + + it("2d.path.arcTo.scale", function () { + // arcTo scales the curve, not just the control points + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 50); + ctx.translate(100, 0); + ctx.scale(0.1, 1); + ctx.arcTo(50, 50, 50, 0, 50); + ctx.lineTo(-1000, 0); + ctx.fill(); + + _assertPixel(canvas, 0,0, 0,255,0,255); + _assertPixel(canvas, 50,0, 0,255,0,255); + _assertPixel(canvas, 99,0, 0,255,0,255); + _assertPixel(canvas, 0,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 99,25, 0,255,0,255); + _assertPixel(canvas, 0,49, 0,255,0,255); + _assertPixel(canvas, 50,49, 0,255,0,255); + _assertPixel(canvas, 99,49, 0,255,0,255); + }); + + it("2d.path.arcTo.nonfinite", function () { + // arcTo() with Infinity/NaN is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + ctx.arcTo(Infinity, 50, 0, 50, 0); + ctx.arcTo(-Infinity, 50, 0, 50, 0); + ctx.arcTo(NaN, 50, 0, 50, 0); + ctx.arcTo(0, Infinity, 0, 50, 0); + ctx.arcTo(0, -Infinity, 0, 50, 0); + ctx.arcTo(0, NaN, 0, 50, 0); + ctx.arcTo(0, 50, Infinity, 50, 0); + ctx.arcTo(0, 50, -Infinity, 50, 0); + ctx.arcTo(0, 50, NaN, 50, 0); + ctx.arcTo(0, 50, 0, Infinity, 0); + ctx.arcTo(0, 50, 0, -Infinity, 0); + ctx.arcTo(0, 50, 0, NaN, 0); + ctx.arcTo(0, 50, 0, 50, Infinity); + ctx.arcTo(0, 50, 0, 50, -Infinity); + ctx.arcTo(0, 50, 0, 50, NaN); + ctx.arcTo(Infinity, Infinity, 0, 50, 0); + ctx.arcTo(Infinity, Infinity, Infinity, 50, 0); + ctx.arcTo(Infinity, Infinity, Infinity, Infinity, 0); + ctx.arcTo(Infinity, Infinity, Infinity, Infinity, Infinity); + ctx.arcTo(Infinity, Infinity, Infinity, 50, Infinity); + ctx.arcTo(Infinity, Infinity, 0, Infinity, 0); + ctx.arcTo(Infinity, Infinity, 0, Infinity, Infinity); + ctx.arcTo(Infinity, Infinity, 0, 50, Infinity); + ctx.arcTo(Infinity, 50, Infinity, 50, 0); + ctx.arcTo(Infinity, 50, Infinity, Infinity, 0); + ctx.arcTo(Infinity, 50, Infinity, Infinity, Infinity); + ctx.arcTo(Infinity, 50, Infinity, 50, Infinity); + ctx.arcTo(Infinity, 50, 0, Infinity, 0); + ctx.arcTo(Infinity, 50, 0, Infinity, Infinity); + ctx.arcTo(Infinity, 50, 0, 50, Infinity); + ctx.arcTo(0, Infinity, Infinity, 50, 0); + ctx.arcTo(0, Infinity, Infinity, Infinity, 0); + ctx.arcTo(0, Infinity, Infinity, Infinity, Infinity); + ctx.arcTo(0, Infinity, Infinity, 50, Infinity); + ctx.arcTo(0, Infinity, 0, Infinity, 0); + ctx.arcTo(0, Infinity, 0, Infinity, Infinity); + ctx.arcTo(0, Infinity, 0, 50, Infinity); + ctx.arcTo(0, 50, Infinity, Infinity, 0); + ctx.arcTo(0, 50, Infinity, Infinity, Infinity); + ctx.arcTo(0, 50, Infinity, 50, Infinity); + ctx.arcTo(0, 50, 0, Infinity, Infinity); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 90,45, 0,255,0,255); + }); + + it("2d.path.arc.empty", function () { + // arc() with an empty path does not draw a straight line to the start point + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.arc(200, 25, 5, 0, 2*Math.PI, true); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arc.nonempty", function () { + // arc() with a non-empty path does draw a straight line to the start point + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arc(200, 25, 5, 0, 2*Math.PI, true); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arc.end", function () { + // arc() adds the end point of the arc to the subpath + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(-100, 0); + ctx.arc(-100, 0, 25, -Math.PI/2, Math.PI/2, true); + ctx.lineTo(100, 25); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arc.default", function () { + // arc() with missing last argument defaults to clockwise + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 0); + ctx.arc(100, 0, 150, -Math.PI, Math.PI/2); + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arc.angle.1", function () { + // arc() draws pi/2 .. -pi anticlockwise correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 0); + ctx.arc(100, 0, 150, Math.PI/2, -Math.PI, true); + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arc.angle.2", function () { + // arc() draws -3pi/2 .. -pi anticlockwise correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 0); + ctx.arc(100, 0, 150, -3*Math.PI/2, -Math.PI, true); + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arc.angle.3", function () { + // arc() wraps angles mod 2pi when anticlockwise and end > start+2pi + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 0); + ctx.arc(100, 0, 150, (512+1/2)*Math.PI, (1024-1)*Math.PI, true); + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arc.angle.4", function () { + // arc() draws a full circle when clockwise and end > start+2pi + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.arc(50, 25, 60, (512+1/2)*Math.PI, (1024-1)*Math.PI, false); + ctx.fill(); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.arc.angle.5", function () { + // arc() wraps angles mod 2pi when clockwise and start > end+2pi + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 0); + ctx.arc(100, 0, 150, (1024-1)*Math.PI, (512+1/2)*Math.PI, false); + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arc.angle.6", function () { + // arc() draws a full circle when anticlockwise and start > end+2pi + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.arc(50, 25, 60, (1024-1)*Math.PI, (512+1/2)*Math.PI, true); + ctx.fill(); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.arc.zero.1", function () { + // arc() draws nothing when startAngle = endAngle and anticlockwise + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.arc(50, 25, 50, 0, 0, true); + ctx.stroke(); + _assertPixel(canvas, 50,20, 0,255,0,255); + }); + + it("2d.path.arc.zero.2", function () { + // arc() draws nothing when startAngle = endAngle and clockwise + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.arc(50, 25, 50, 0, 0, false); + ctx.stroke(); + _assertPixel(canvas, 50,20, 0,255,0,255); + }); + + it("2d.path.arc.twopie.1", function () { + // arc() draws nothing when end = start + 2pi-e and anticlockwise + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.arc(50, 25, 50, 0, 2*Math.PI - 1e-4, true); + ctx.stroke(); + _assertPixel(canvas, 50,20, 0,255,0,255); + }); + + it("2d.path.arc.twopie.2", function () { + // arc() draws a full circle when end = start + 2pi-e and clockwise + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.arc(50, 25, 50, 0, 2*Math.PI - 1e-4, false); + ctx.stroke(); + _assertPixel(canvas, 50,20, 0,255,0,255); + }); + + it("2d.path.arc.twopie.3", function () { + // arc() draws a full circle when end = start + 2pi+e and anticlockwise + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.arc(50, 25, 50, 0, 2*Math.PI + 1e-4, true); + ctx.stroke(); + _assertPixel(canvas, 50,20, 0,255,0,255); + }); + + it("2d.path.arc.twopie.4", function () { + // arc() draws nothing when end = start + 2pi+e and clockwise + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.arc(50, 25, 50, 0, 2*Math.PI + 1e-4, false); + ctx.stroke(); + _assertPixel(canvas, 50,20, 0,255,0,255); + }); + + it("2d.path.arc.shape.1", function () { + // arc() from 0 to pi does not draw anything in the wrong half + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.arc(50, 50, 50, 0, Math.PI, false); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 20,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.arc.shape.2", function () { + // arc() from 0 to pi draws stuff in the right half + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 100; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.arc(50, 50, 50, 0, Math.PI, true); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 20,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.arc.shape.3", function () { + // arc() from 0 to -pi/2 does not draw anything in the wrong quadrant + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 100; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.arc(0, 50, 50, 0, -Math.PI/2, false); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.arc.shape.4", function () { + // arc() from 0 to -pi/2 draws stuff in the right quadrant + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 150; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.arc(-50, 50, 100, 0, -Math.PI/2, true); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.arc.shape.5", function () { + // arc() from 0 to 5pi does not draw crazy things + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 200; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.arc(300, 0, 100, 0, 5*Math.PI, false); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.arc.selfintersect.1", function () { + // arc() with lineWidth > 2*radius is drawn sensibly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 200; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.arc(100, 50, 25, 0, -Math.PI/2, true); + ctx.stroke(); + ctx.beginPath(); + ctx.arc(0, 0, 25, 0, -Math.PI/2, true); + ctx.stroke(); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arc.selfintersect.2", function () { + // arc() with lineWidth > 2*radius is drawn sensibly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 180; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.arc(-50, 50, 25, 0, -Math.PI/2, true); + ctx.stroke(); + ctx.beginPath(); + ctx.arc(100, 0, 25, 0, -Math.PI/2, true); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 90,10, 0,255,0,255); + _assertPixel(canvas, 97,1, 0,255,0,255); + _assertPixel(canvas, 97,2, 0,255,0,255); + _assertPixel(canvas, 97,3, 0,255,0,255); + _assertPixel(canvas, 2,48, 0,255,0,255); + }); + + it("2d.path.arc.negative", function () { + // arc() with negative radius throws INDEX_SIZE_ERR + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.throws(function() { ctx.arc(0, 0, -1, 0, 0, true); }, /INDEX_SIZE_ERR/); + var path = new Path2D(); + assert.throws(function() { path.arc(10, 10, -5, 0, 1, false); }, /INDEX_SIZE_ERR/); + }); + + it("2d.path.arc.zeroradius", function () { + // arc() with zero radius draws a line to the start point + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00' + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arc(200, 25, 0, 0, Math.PI, true); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arc.scale.1", function () { + // Non-uniformly scaled arcs are the right shape + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.scale(2, 0.5); + ctx.fillStyle = '#0f0'; + ctx.beginPath(); + ctx.arc(25, 50, 56, 0, 2*Math.PI, false); + ctx.fill(); + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(-25, 50); + ctx.arc(-25, 50, 24, 0, 2*Math.PI, false); + ctx.moveTo(75, 50); + ctx.arc(75, 50, 24, 0, 2*Math.PI, false); + ctx.moveTo(25, -25); + ctx.arc(25, -25, 24, 0, 2*Math.PI, false); + ctx.moveTo(25, 125); + ctx.arc(25, 125, 24, 0, 2*Math.PI, false); + ctx.fill(); + + _assertPixel(canvas, 0,0, 0,255,0,255); + _assertPixel(canvas, 50,0, 0,255,0,255); + _assertPixel(canvas, 99,0, 0,255,0,255); + _assertPixel(canvas, 0,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 99,25, 0,255,0,255); + _assertPixel(canvas, 0,49, 0,255,0,255); + _assertPixel(canvas, 50,49, 0,255,0,255); + _assertPixel(canvas, 99,49, 0,255,0,255); + }); + + it("2d.path.arc.scale.2", function () { + // Highly scaled arcs are the right shape + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.scale(100, 100); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 1.2; + ctx.beginPath(); + ctx.arc(0, 0, 0.6, 0, Math.PI/2, false); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 50,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 98,25, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 50,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it.skip("2d.path.arc.nonfinite", function () { + // arc() with Infinity/NaN is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + ctx.arc(Infinity, 0, 50, 0, 2*Math.PI, true); + ctx.arc(-Infinity, 0, 50, 0, 2*Math.PI, true); + ctx.arc(NaN, 0, 50, 0, 2*Math.PI, true); + ctx.arc(0, Infinity, 50, 0, 2*Math.PI, true); + ctx.arc(0, -Infinity, 50, 0, 2*Math.PI, true); + ctx.arc(0, NaN, 50, 0, 2*Math.PI, true); + ctx.arc(0, 0, Infinity, 0, 2*Math.PI, true); + ctx.arc(0, 0, -Infinity, 0, 2*Math.PI, true); + ctx.arc(0, 0, NaN, 0, 2*Math.PI, true); + ctx.arc(0, 0, 50, Infinity, 2*Math.PI, true); + ctx.arc(0, 0, 50, -Infinity, 2*Math.PI, true); + ctx.arc(0, 0, 50, NaN, 2*Math.PI, true); + ctx.arc(0, 0, 50, 0, Infinity, true); + ctx.arc(0, 0, 50, 0, -Infinity, true); + ctx.arc(0, 0, 50, 0, NaN, true); + ctx.arc(Infinity, Infinity, 50, 0, 2*Math.PI, true); + ctx.arc(Infinity, Infinity, Infinity, 0, 2*Math.PI, true); + ctx.arc(Infinity, Infinity, Infinity, Infinity, 2*Math.PI, true); + ctx.arc(Infinity, Infinity, Infinity, Infinity, Infinity, true); + ctx.arc(Infinity, Infinity, Infinity, 0, Infinity, true); + ctx.arc(Infinity, Infinity, 50, Infinity, 2*Math.PI, true); + ctx.arc(Infinity, Infinity, 50, Infinity, Infinity, true); + ctx.arc(Infinity, Infinity, 50, 0, Infinity, true); + ctx.arc(Infinity, 0, Infinity, 0, 2*Math.PI, true); + ctx.arc(Infinity, 0, Infinity, Infinity, 2*Math.PI, true); + ctx.arc(Infinity, 0, Infinity, Infinity, Infinity, true); + ctx.arc(Infinity, 0, Infinity, 0, Infinity, true); + ctx.arc(Infinity, 0, 50, Infinity, 2*Math.PI, true); + ctx.arc(Infinity, 0, 50, Infinity, Infinity, true); + ctx.arc(Infinity, 0, 50, 0, Infinity, true); + ctx.arc(0, Infinity, Infinity, 0, 2*Math.PI, true); + ctx.arc(0, Infinity, Infinity, Infinity, 2*Math.PI, true); + ctx.arc(0, Infinity, Infinity, Infinity, Infinity, true); + ctx.arc(0, Infinity, Infinity, 0, Infinity, true); + ctx.arc(0, Infinity, 50, Infinity, 2*Math.PI, true); + ctx.arc(0, Infinity, 50, Infinity, Infinity, true); + ctx.arc(0, Infinity, 50, 0, Infinity, true); + ctx.arc(0, 0, Infinity, Infinity, 2*Math.PI, true); + ctx.arc(0, 0, Infinity, Infinity, Infinity, true); + ctx.arc(0, 0, Infinity, 0, Infinity, true); + ctx.arc(0, 0, 50, Infinity, Infinity, true); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 90,45, 0,255,0,255); + }); + + it("2d.path.rect.basic", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.rect(0, 0, 100, 50); + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.rect.newsubpath", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.beginPath(); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.moveTo(-100, 25); + ctx.lineTo(-50, 25); + ctx.rect(200, 25, 1, 1); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.rect.closed", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 200; + ctx.lineJoin = 'miter'; + ctx.rect(100, 50, 100, 100); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.rect.end.1", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.rect(200, 100, 400, 1000); + ctx.lineTo(-2000, -1000); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.rect.end.2", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 450; + ctx.lineCap = 'round'; + ctx.lineJoin = 'bevel'; + ctx.rect(150, 150, 2000, 2000); + ctx.lineTo(160, 160); + ctx.stroke(); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.rect.zero.1", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.rect(0, 50, 100, 0); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.rect.zero.2", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.rect(50, -100, 0, 250); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.rect.zero.3", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.rect(50, 25, 0, 0); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.rect.zero.4", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.rect(100, 25, 0, 0); + ctx.lineTo(0, 25); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.rect.zero.5", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.moveTo(0, 0); + ctx.rect(100, 25, 0, 0); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.rect.zero.6", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineJoin = 'miter'; + ctx.miterLimit = 1.5; + ctx.lineWidth = 200; + ctx.beginPath(); + ctx.rect(100, 25, 1000, 0); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.rect.negative", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.beginPath(); + ctx.fillStyle = '#0f0'; + ctx.rect(0, 0, 50, 25); + ctx.rect(100, 0, -50, 25); + ctx.rect(0, 50, 50, -25); + ctx.rect(100, 50, -50, -25); + ctx.fill(); + _assertPixel(canvas, 25,12, 0,255,0,255); + _assertPixel(canvas, 75,12, 0,255,0,255); + _assertPixel(canvas, 25,37, 0,255,0,255); + _assertPixel(canvas, 75,37, 0,255,0,255); + }); + + it("2d.path.rect.winding", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.beginPath(); + ctx.fillStyle = '#f00'; + ctx.rect(0, 0, 50, 50); + ctx.rect(100, 50, -50, -50); + ctx.rect(0, 25, 100, -25); + ctx.rect(100, 25, -100, 25); + ctx.fill(); + _assertPixel(canvas, 25,12, 0,255,0,255); + _assertPixel(canvas, 75,12, 0,255,0,255); + _assertPixel(canvas, 25,37, 0,255,0,255); + _assertPixel(canvas, 75,37, 0,255,0,255); + }); + + it("2d.path.rect.selfintersect", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 90; + ctx.beginPath(); + ctx.rect(45, 20, 10, 10); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.rect.nonfinite", function () { + // rect() with Infinity/NaN is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + ctx.rect(Infinity, 50, 1, 1); + ctx.rect(-Infinity, 50, 1, 1); + ctx.rect(NaN, 50, 1, 1); + ctx.rect(0, Infinity, 1, 1); + ctx.rect(0, -Infinity, 1, 1); + ctx.rect(0, NaN, 1, 1); + ctx.rect(0, 50, Infinity, 1); + ctx.rect(0, 50, -Infinity, 1); + ctx.rect(0, 50, NaN, 1); + ctx.rect(0, 50, 1, Infinity); + ctx.rect(0, 50, 1, -Infinity); + ctx.rect(0, 50, 1, NaN); + ctx.rect(Infinity, Infinity, 1, 1); + ctx.rect(Infinity, Infinity, Infinity, 1); + ctx.rect(Infinity, Infinity, Infinity, Infinity); + ctx.rect(Infinity, Infinity, 1, Infinity); + ctx.rect(Infinity, 50, Infinity, 1); + ctx.rect(Infinity, 50, Infinity, Infinity); + ctx.rect(Infinity, 50, 1, Infinity); + ctx.rect(0, Infinity, Infinity, 1); + ctx.rect(0, Infinity, Infinity, Infinity); + ctx.rect(0, Infinity, 1, Infinity); + ctx.rect(0, 50, Infinity, Infinity); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 90,45, 0,255,0,255); + }); + + it("2d.path.roundrect.newsubpath", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.beginPath(); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.moveTo(-100, 25); + ctx.lineTo(-50, 25); + ctx.roundRect(200, 25, 1, 1, [0]); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.roundrect.closed", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 200; + ctx.lineJoin = 'miter'; + ctx.roundRect(100, 50, 100, 100, [0]); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.roundrect.end.1", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.roundRect(200, 100, 400, 1000, [0]); + ctx.lineTo(-2000, -1000); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.roundrect.end.2", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 450; + ctx.lineCap = 'round'; + ctx.lineJoin = 'bevel'; + ctx.roundRect(150, 150, 2000, 2000, [0]); + ctx.lineTo(160, 160); + ctx.stroke(); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.roundrect.end.3", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.roundRect(101, 51, 2000, 2000, [500, 500, 500, 500]); + ctx.lineTo(-1, -1); + ctx.stroke(); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.roundrect.end.4", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 10; + ctx.roundRect(-1, -1, 2000, 2000, [1000, 1000, 1000, 1000]); + ctx.lineTo(-150, -150); + ctx.stroke(); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.roundrect.zero.1", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.roundRect(0, 50, 100, 0, [0]); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.roundrect.zero.2", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.roundRect(50, -100, 0, 250, [0]); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.roundrect.zero.3", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.roundRect(50, 25, 0, 0, [0]); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.roundrect.zero.4", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.roundRect(100, 25, 0, 0, [0]); + ctx.lineTo(0, 25); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.roundrect.zero.5", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.moveTo(0, 0); + ctx.roundRect(100, 25, 0, 0, [0]); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.roundrect.zero.6", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineJoin = 'miter'; + ctx.miterLimit = 1.5; + ctx.lineWidth = 200; + ctx.beginPath(); + ctx.roundRect(100, 25, 1000, 0, [0]); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.roundrect.negative", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.beginPath(); + ctx.fillStyle = '#0f0'; + ctx.roundRect(0, 0, 50, 25, [10, 0, 0, 0]); + ctx.roundRect(100, 0, -50, 25, [10, 0, 0, 0]); + ctx.roundRect(0, 50, 50, -25, [10, 0, 0, 0]); + ctx.roundRect(100, 50, -50, -25, [10, 0, 0, 0]); + ctx.fill(); + // All rects drawn + _assertPixel(canvas, 25,12, 0,255,0,255); + _assertPixel(canvas, 75,12, 0,255,0,255); + _assertPixel(canvas, 25,37, 0,255,0,255); + _assertPixel(canvas, 75,37, 0,255,0,255); + // Correct corners are rounded. + _assertPixel(canvas, 1,1, 255,0,0,255); + _assertPixel(canvas, 98,1, 255,0,0,255); + _assertPixel(canvas, 1,48, 255,0,0,255); + _assertPixel(canvas, 98,48, 255,0,0,255); + }); + + it("2d.path.roundrect.winding", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.beginPath(); + ctx.fillStyle = '#f00'; + ctx.roundRect(0, 0, 50, 50, [0]); + ctx.roundRect(100, 50, -50, -50, [0]); + ctx.roundRect(0, 25, 100, -25, [0]); + ctx.roundRect(100, 25, -100, 25, [0]); + ctx.fill(); + _assertPixel(canvas, 25,12, 0,255,0,255); + _assertPixel(canvas, 75,12, 0,255,0,255); + _assertPixel(canvas, 25,37, 0,255,0,255); + _assertPixel(canvas, 75,37, 0,255,0,255); + }); + + it("2d.path.roundrect.selfintersect", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.roundRect(0, 0, 100, 50, [0]); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 90; + ctx.beginPath(); + ctx.roundRect(45, 20, 10, 10, [0]); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.roundrect.nonfinite", function () { + // roundRect() with Infinity/NaN is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + ctx.roundRect(Infinity, 50, 1, 1, [0]); + ctx.roundRect(-Infinity, 50, 1, 1, [0]); + ctx.roundRect(NaN, 50, 1, 1, [0]); + ctx.roundRect(0, Infinity, 1, 1, [0]); + ctx.roundRect(0, -Infinity, 1, 1, [0]); + ctx.roundRect(0, NaN, 1, 1, [0]); + ctx.roundRect(0, 50, Infinity, 1, [0]); + ctx.roundRect(0, 50, -Infinity, 1, [0]); + ctx.roundRect(0, 50, NaN, 1, [0]); + ctx.roundRect(0, 50, 1, Infinity, [0]); + ctx.roundRect(0, 50, 1, -Infinity, [0]); + ctx.roundRect(0, 50, 1, NaN, [0]); + ctx.roundRect(0, 50, 1, 1, [Infinity]); + ctx.roundRect(0, 50, 1, 1, [-Infinity]); + ctx.roundRect(0, 50, 1, 1, [NaN]); + ctx.roundRect(0, 50, 1, 1, [Infinity,0]); + ctx.roundRect(0, 50, 1, 1, [-Infinity,0]); + ctx.roundRect(0, 50, 1, 1, [NaN,0]); + ctx.roundRect(0, 50, 1, 1, [0,Infinity]); + ctx.roundRect(0, 50, 1, 1, [0,-Infinity]); + ctx.roundRect(0, 50, 1, 1, [0,NaN]); + ctx.roundRect(0, 50, 1, 1, [Infinity,0,0]); + ctx.roundRect(0, 50, 1, 1, [-Infinity,0,0]); + ctx.roundRect(0, 50, 1, 1, [NaN,0,0]); + ctx.roundRect(0, 50, 1, 1, [0,Infinity,0]); + ctx.roundRect(0, 50, 1, 1, [0,-Infinity,0]); + ctx.roundRect(0, 50, 1, 1, [0,NaN,0]); + ctx.roundRect(0, 50, 1, 1, [0,0,Infinity]); + ctx.roundRect(0, 50, 1, 1, [0,0,-Infinity]); + ctx.roundRect(0, 50, 1, 1, [0,0,NaN]); + ctx.roundRect(0, 50, 1, 1, [Infinity,0,0,0]); + ctx.roundRect(0, 50, 1, 1, [-Infinity,0,0,0]); + ctx.roundRect(0, 50, 1, 1, [NaN,0,0,0]); + ctx.roundRect(0, 50, 1, 1, [0,Infinity,0,0]); + ctx.roundRect(0, 50, 1, 1, [0,-Infinity,0,0]); + ctx.roundRect(0, 50, 1, 1, [0,NaN,0,0]); + ctx.roundRect(0, 50, 1, 1, [0,0,Infinity,0]); + ctx.roundRect(0, 50, 1, 1, [0,0,-Infinity,0]); + ctx.roundRect(0, 50, 1, 1, [0,0,NaN,0]); + ctx.roundRect(0, 50, 1, 1, [0,0,0,Infinity]); + ctx.roundRect(0, 50, 1, 1, [0,0,0,-Infinity]); + ctx.roundRect(0, 50, 1, 1, [0,0,0,NaN]); + ctx.roundRect(Infinity, Infinity, 1, 1, [0]); + ctx.roundRect(Infinity, Infinity, Infinity, 1, [0]); + ctx.roundRect(Infinity, Infinity, Infinity, Infinity, [0]); + ctx.roundRect(Infinity, Infinity, Infinity, Infinity, [Infinity]); + ctx.roundRect(Infinity, Infinity, Infinity, 1, [Infinity]); + ctx.roundRect(Infinity, Infinity, 1, Infinity, [0]); + ctx.roundRect(Infinity, Infinity, 1, Infinity, [Infinity]); + ctx.roundRect(Infinity, Infinity, 1, 1, [Infinity]); + ctx.roundRect(Infinity, 50, Infinity, 1, [0]); + ctx.roundRect(Infinity, 50, Infinity, Infinity, [0]); + ctx.roundRect(Infinity, 50, Infinity, Infinity, [Infinity]); + ctx.roundRect(Infinity, 50, Infinity, 1, [Infinity]); + ctx.roundRect(Infinity, 50, 1, Infinity, [0]); + ctx.roundRect(Infinity, 50, 1, Infinity, [Infinity]); + ctx.roundRect(Infinity, 50, 1, 1, [Infinity]); + ctx.roundRect(0, Infinity, Infinity, 1, [0]); + ctx.roundRect(0, Infinity, Infinity, Infinity, [0]); + ctx.roundRect(0, Infinity, Infinity, Infinity, [Infinity]); + ctx.roundRect(0, Infinity, Infinity, 1, [Infinity]); + ctx.roundRect(0, Infinity, 1, Infinity, [0]); + ctx.roundRect(0, Infinity, 1, Infinity, [Infinity]); + ctx.roundRect(0, Infinity, 1, 1, [Infinity]); + ctx.roundRect(0, 50, Infinity, Infinity, [0]); + ctx.roundRect(0, 50, Infinity, Infinity, [Infinity]); + ctx.roundRect(0, 50, Infinity, 1, [Infinity]); + ctx.roundRect(0, 50, 1, Infinity, [Infinity]); + ctx.roundRect(0, 0, 100, 100, [new DOMPoint(10, Infinity)]); + ctx.roundRect(0, 0, 100, 100, [new DOMPoint(10, -Infinity)]); + ctx.roundRect(0, 0, 100, 100, [new DOMPoint(10, NaN)]); + ctx.roundRect(0, 0, 100, 100, [new DOMPoint(Infinity, 10)]); + ctx.roundRect(0, 0, 100, 100, [new DOMPoint(-Infinity, 10)]); + ctx.roundRect(0, 0, 100, 100, [new DOMPoint(NaN, 10)]); + ctx.roundRect(0, 0, 100, 100, [{x: 10, y: Infinity}]); + ctx.roundRect(0, 0, 100, 100, [{x: 10, y: -Infinity}]); + ctx.roundRect(0, 0, 100, 100, [{x: 10, y: NaN}]); + ctx.roundRect(0, 0, 100, 100, [{x: Infinity, y: 10}]); + ctx.roundRect(0, 0, 100, 100, [{x: -Infinity, y: 10}]); + ctx.roundRect(0, 0, 100, 100, [{x: NaN, y: 10}]); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 90,45, 0,255,0,255); + }); + + it("2d.path.roundrect.4.radii.1.double", function () { + // Verify that when four radii are given to roundRect(), the first radius, specified as a double, applies to the top-left corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [20, 0, 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 1,1, 255,0,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.4.radii.1.dompoint", function () { + // Verify that when four radii are given to roundRect(), the first radius, specified as a DOMPoint, applies to the top-left corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [new DOMPoint(40, 20), 0, 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + _assertPixel(canvas, 20,1, 255,0,0,255); + _assertPixel(canvas, 41,1, 0,255,0,255); + _assertPixel(canvas, 1,10, 255,0,0,255); + _assertPixel(canvas, 1,21, 0,255,0,255); + + // other corners + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.4.radii.1.dompointinit", function () { + // Verify that when four radii are given to roundRect(), the first radius, specified as a DOMPointInit, applies to the top-left corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [{x: 40, y: 20}, 0, 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + _assertPixel(canvas, 20,1, 255,0,0,255); + _assertPixel(canvas, 41,1, 0,255,0,255); + _assertPixel(canvas, 1,10, 255,0,0,255); + _assertPixel(canvas, 1,21, 0,255,0,255); + + // other corners + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.4.radii.2.double", function () { + // Verify that when four radii are given to roundRect(), the second radius, specified as a double, applies to the top-right corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 20, 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 255,0,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.4.radii.2.dompoint", function () { + // Verify that when four radii are given to roundRect(), the second radius, specified as a DOMPoint, applies to the top-right corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, new DOMPoint(40, 20), 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-right corner + _assertPixel(canvas, 79,1, 255,0,0,255); + _assertPixel(canvas, 58,1, 0,255,0,255); + _assertPixel(canvas, 98,10, 255,0,0,255); + _assertPixel(canvas, 98,21, 0,255,0,255); + + // other corners + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.4.radii.2.dompointinit", function () { + // Verify that when four radii are given to roundRect(), the second radius, specified as a DOMPointInit, applies to the top-right corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, {x: 40, y: 20}, 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-right corner + _assertPixel(canvas, 79,1, 255,0,0,255); + _assertPixel(canvas, 58,1, 0,255,0,255); + _assertPixel(canvas, 98,10, 255,0,0,255); + _assertPixel(canvas, 98,21, 0,255,0,255); + + // other corners + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.4.radii.3.double", function () { + // Verify that when four radii are given to roundRect(), the third radius, specified as a double, applies to the bottom-right corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, 20, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 255,0,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.4.radii.3.dompoint", function () { + // Verify that when four radii are given to roundRect(), the third radius, specified as a DOMPoint, applies to the bottom-right corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, new DOMPoint(40, 20), 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // bottom-right corner + _assertPixel(canvas, 79,48, 255,0,0,255); + _assertPixel(canvas, 58,48, 0,255,0,255); + _assertPixel(canvas, 98,39, 255,0,0,255); + _assertPixel(canvas, 98,28, 0,255,0,255); + + // other corners + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.4.radii.3.dompointinit", function () { + // Verify that when four radii are given to roundRect(), the third radius, specified as a DOMPointInit, applies to the bottom-right corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, {x: 40, y: 20}, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // bottom-right corner + _assertPixel(canvas, 79,48, 255,0,0,255); + _assertPixel(canvas, 58,48, 0,255,0,255); + _assertPixel(canvas, 98,39, 255,0,0,255); + _assertPixel(canvas, 98,28, 0,255,0,255); + + // other corners + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.4.radii.4.double", function () { + // Verify that when four radii are given to roundRect(), the fourth radius, specified as a double, applies to the bottom-left corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, 0, 20]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 255,0,0,255); + }); + + it("2d.path.roundrect.4.radii.4.dompoint", function () { + // Verify that when four radii are given to roundRect(), the fourth radius, specified as a DOMPoint, applies to the bottom-left corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, 0, new DOMPoint(40, 20)]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // bottom-left corner + _assertPixel(canvas, 20,48, 255,0,0,255); + _assertPixel(canvas, 41,48, 0,255,0,255); + _assertPixel(canvas, 1,39, 255,0,0,255); + _assertPixel(canvas, 1,28, 0,255,0,255); + + // other corners + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.roundrect.4.radii.4.dompointinit", function () { + // Verify that when four radii are given to roundRect(), the fourth radius, specified as a DOMPointInit, applies to the bottom-left corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, 0, {x: 40, y: 20}]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // bottom-left corner + _assertPixel(canvas, 20,48, 255,0,0,255); + _assertPixel(canvas, 41,48, 0,255,0,255); + _assertPixel(canvas, 1,39, 255,0,0,255); + _assertPixel(canvas, 1,28, 0,255,0,255); + + // other corners + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.roundrect.3.radii.1.double", function () { + // Verify that when three radii are given to roundRect(), the first radius, specified as a double, applies to the top-left corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [20, 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 1,1, 255,0,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.3.radii.1.dompoint", function () { + // Verify that when three radii are given to roundRect(), the first radius, specified as a DOMPoint, applies to the top-left corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [new DOMPoint(40, 20), 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + _assertPixel(canvas, 20,1, 255,0,0,255); + _assertPixel(canvas, 41,1, 0,255,0,255); + _assertPixel(canvas, 1,10, 255,0,0,255); + _assertPixel(canvas, 1,21, 0,255,0,255); + + // other corners + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.3.radii.1.dompointinit", function () { + // Verify that when three radii are given to roundRect(), the first radius, specified as a DOMPointInit, applies to the top-left corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [{x: 40, y: 20}, 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + _assertPixel(canvas, 20,1, 255,0,0,255); + _assertPixel(canvas, 41,1, 0,255,0,255); + _assertPixel(canvas, 1,10, 255,0,0,255); + _assertPixel(canvas, 1,21, 0,255,0,255); + + // other corners + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.3.radii.2.double", function () { + // Verify that when three radii are given to roundRect(), the second radius, specified as a double, applies to the top-right and bottom-left corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 20, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 255,0,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 255,0,0,255); + }); + + it("2d.path.roundrect.3.radii.2.dompoint", function () { + // Verify that when three radii are given to roundRect(), the second radius, specified as a DOMPoint, applies to the top-right and bottom-left corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, new DOMPoint(40, 20), 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-right corner + _assertPixel(canvas, 79,1, 255,0,0,255); + _assertPixel(canvas, 58,1, 0,255,0,255); + _assertPixel(canvas, 98,10, 255,0,0,255); + _assertPixel(canvas, 98,21, 0,255,0,255); + + // bottom-left corner + _assertPixel(canvas, 20,48, 255,0,0,255); + _assertPixel(canvas, 41,48, 0,255,0,255); + _assertPixel(canvas, 1,39, 255,0,0,255); + _assertPixel(canvas, 1,28, 0,255,0,255); + + // other corners + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.roundrect.3.radii.2.dompointinit", function () { + // Verify that when three radii are given to roundRect(), the second radius, specified as a DOMPoint, applies to the top-right and bottom-left corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, {x: 40, y: 20}, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-right corner + _assertPixel(canvas, 79,1, 255,0,0,255); + _assertPixel(canvas, 58,1, 0,255,0,255); + _assertPixel(canvas, 98,10, 255,0,0,255); + _assertPixel(canvas, 98,21, 0,255,0,255); + + // bottom-left corner + _assertPixel(canvas, 20,48, 255,0,0,255); + _assertPixel(canvas, 41,48, 0,255,0,255); + _assertPixel(canvas, 1,39, 255,0,0,255); + _assertPixel(canvas, 1,28, 0,255,0,255); + + // other corners + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.roundrect.3.radii.3.double", function () { + // Verify that when three radii are given to roundRect(), the third radius, specified as a double, applies to the bottom-right corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, 20]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 255,0,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.3.radii.3.dompoint", function () { + // Verify that when three radii are given to roundRect(), the third radius, specified as a DOMPoint, applies to the bottom-right corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, new DOMPoint(40, 20)]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // bottom-right corner + _assertPixel(canvas, 79,48, 255,0,0,255); + _assertPixel(canvas, 58,48, 0,255,0,255); + _assertPixel(canvas, 98,39, 255,0,0,255); + _assertPixel(canvas, 98,28, 0,255,0,255); + + // other corners + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.3.radii.3.dompointinit", function () { + // Verify that when three radii are given to roundRect(), the third radius, specified as a DOMPointInit, applies to the bottom-right corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, {x: 40, y: 20}]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // bottom-right corner + _assertPixel(canvas, 79,48, 255,0,0,255); + _assertPixel(canvas, 58,48, 0,255,0,255); + _assertPixel(canvas, 98,39, 255,0,0,255); + _assertPixel(canvas, 98,28, 0,255,0,255); + + // other corners + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.2.radii.1.double", function () { + // Verify that when two radii are given to roundRect(), the first radius, specified as a double, applies to the top-left and bottom-right corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [20, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 1,1, 255,0,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 255,0,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.2.radii.1.dompoint", function () { + // Verify that when two radii are given to roundRect(), the first radius, specified as a DOMPoint, applies to the top-left and bottom-right corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [new DOMPoint(40, 20), 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + _assertPixel(canvas, 20,1, 255,0,0,255); + _assertPixel(canvas, 41,1, 0,255,0,255); + _assertPixel(canvas, 1,10, 255,0,0,255); + _assertPixel(canvas, 1,21, 0,255,0,255); + + // bottom-right corner + _assertPixel(canvas, 79,48, 255,0,0,255); + _assertPixel(canvas, 58,48, 0,255,0,255); + _assertPixel(canvas, 98,39, 255,0,0,255); + _assertPixel(canvas, 98,28, 0,255,0,255); + + // other corners + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.2.radii.1.dompointinit", function () { + // Verify that when two radii are given to roundRect(), the first radius, specified as a DOMPointInit, applies to the top-left and bottom-right corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [{x: 40, y: 20}, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + _assertPixel(canvas, 20,1, 255,0,0,255); + _assertPixel(canvas, 41,1, 0,255,0,255); + _assertPixel(canvas, 1,10, 255,0,0,255); + _assertPixel(canvas, 1,21, 0,255,0,255); + + // bottom-right corner + _assertPixel(canvas, 79,48, 255,0,0,255); + _assertPixel(canvas, 58,48, 0,255,0,255); + _assertPixel(canvas, 98,39, 255,0,0,255); + _assertPixel(canvas, 98,28, 0,255,0,255); + + // other corners + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.2.radii.2.double", function () { + // Verify that when two radii are given to roundRect(), the second radius, specified as a double, applies to the top-right and bottom-left corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 20]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 255,0,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 255,0,0,255); + }); + + it("2d.path.roundrect.2.radii.2.dompoint", function () { + // Verify that when two radii are given to roundRect(), the second radius, specified as a DOMPoint, applies to the top-right and bottom-left corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, new DOMPoint(40, 20)]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-right corner + _assertPixel(canvas, 79,1, 255,0,0,255); + _assertPixel(canvas, 58,1, 0,255,0,255); + _assertPixel(canvas, 98,10, 255,0,0,255); + _assertPixel(canvas, 98,21, 0,255,0,255); + + // bottom-left corner + _assertPixel(canvas, 20,48, 255,0,0,255); + _assertPixel(canvas, 41,48, 0,255,0,255); + _assertPixel(canvas, 1,39, 255,0,0,255); + _assertPixel(canvas, 1,28, 0,255,0,255); + + // other corners + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.roundrect.2.radii.2.dompointinit", function () { + // Verify that when two radii are given to roundRect(), the second radius, specified as a DOMPointInit, applies to the top-right and bottom-left corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, {x: 40, y: 20}]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-right corner + _assertPixel(canvas, 79,1, 255,0,0,255); + _assertPixel(canvas, 58,1, 0,255,0,255); + _assertPixel(canvas, 98,10, 255,0,0,255); + _assertPixel(canvas, 98,21, 0,255,0,255); + + // bottom-left corner + _assertPixel(canvas, 20,48, 255,0,0,255); + _assertPixel(canvas, 41,48, 0,255,0,255); + _assertPixel(canvas, 1,39, 255,0,0,255); + _assertPixel(canvas, 1,28, 0,255,0,255); + + // other corners + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.roundrect.1.radius.double", function () { + // Verify that when one radius is given to roundRect(), specified as a double, it applies to all corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [20]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 1,1, 255,0,0,255); + _assertPixel(canvas, 98,1, 255,0,0,255); + _assertPixel(canvas, 98,48, 255,0,0,255); + _assertPixel(canvas, 1,48, 255,0,0,255); + }); + + it("2d.path.roundrect.1.radius.double.single.argument", function () { + // Verify that when one radius is given to roundRect() as a non-array argument, specified as a double, it applies to all corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, 20); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 1,1, 255,0,0,255); + _assertPixel(canvas, 98,1, 255,0,0,255); + _assertPixel(canvas, 98,48, 255,0,0,255); + _assertPixel(canvas, 1,48, 255,0,0,255); + }); + + it("2d.path.roundrect.1.radius.dompoint", function () { + // Verify that when one radius is given to roundRect(), specified as a DOMPoint, it applies to all corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [new DOMPoint(40, 20)]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + _assertPixel(canvas, 20,1, 255,0,0,255); + _assertPixel(canvas, 41,1, 0,255,0,255); + _assertPixel(canvas, 1,10, 255,0,0,255); + _assertPixel(canvas, 1,21, 0,255,0,255); + + // top-right corner + _assertPixel(canvas, 79,1, 255,0,0,255); + _assertPixel(canvas, 58,1, 0,255,0,255); + _assertPixel(canvas, 98,10, 255,0,0,255); + _assertPixel(canvas, 98,21, 0,255,0,255); + + // bottom-right corner + _assertPixel(canvas, 79,48, 255,0,0,255); + _assertPixel(canvas, 58,48, 0,255,0,255); + _assertPixel(canvas, 98,39, 255,0,0,255); + _assertPixel(canvas, 98,28, 0,255,0,255); + + // bottom-left corner + _assertPixel(canvas, 20,48, 255,0,0,255); + _assertPixel(canvas, 41,48, 0,255,0,255); + _assertPixel(canvas, 1,39, 255,0,0,255); + _assertPixel(canvas, 1,28, 0,255,0,255); + }); + + it("2d.path.roundrect.1.radius.dompoint.single argument", function () { + // Verify that when one radius is given to roundRect() as a non-array argument, specified as a DOMPoint, it applies to all corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, new DOMPoint(40, 20)); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + _assertPixel(canvas, 20,1, 255,0,0,255); + _assertPixel(canvas, 41,1, 0,255,0,255); + _assertPixel(canvas, 1,10, 255,0,0,255); + _assertPixel(canvas, 1,21, 0,255,0,255); + + // top-right corner + _assertPixel(canvas, 79,1, 255,0,0,255); + _assertPixel(canvas, 58,1, 0,255,0,255); + _assertPixel(canvas, 98,10, 255,0,0,255); + _assertPixel(canvas, 98,21, 0,255,0,255); + + // bottom-right corner + _assertPixel(canvas, 79,48, 255,0,0,255); + _assertPixel(canvas, 58,48, 0,255,0,255); + _assertPixel(canvas, 98,39, 255,0,0,255); + _assertPixel(canvas, 98,28, 0,255,0,255); + + // bottom-left corner + _assertPixel(canvas, 20,48, 255,0,0,255); + _assertPixel(canvas, 41,48, 0,255,0,255); + _assertPixel(canvas, 1,39, 255,0,0,255); + _assertPixel(canvas, 1,28, 0,255,0,255); + }); + + it("2d.path.roundrect.1.radius.dompointinit", function () { + // Verify that when one radius is given to roundRect(), specified as a DOMPointInit, applies to all corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [{x: 40, y: 20}]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + _assertPixel(canvas, 20,1, 255,0,0,255); + _assertPixel(canvas, 41,1, 0,255,0,255); + _assertPixel(canvas, 1,10, 255,0,0,255); + _assertPixel(canvas, 1,21, 0,255,0,255); + + // top-right corner + _assertPixel(canvas, 79,1, 255,0,0,255); + _assertPixel(canvas, 58,1, 0,255,0,255); + _assertPixel(canvas, 98,10, 255,0,0,255); + _assertPixel(canvas, 98,21, 0,255,0,255); + + // bottom-right corner + _assertPixel(canvas, 79,48, 255,0,0,255); + _assertPixel(canvas, 58,48, 0,255,0,255); + _assertPixel(canvas, 98,39, 255,0,0,255); + _assertPixel(canvas, 98,28, 0,255,0,255); + + // bottom-left corner + _assertPixel(canvas, 20,48, 255,0,0,255); + _assertPixel(canvas, 41,48, 0,255,0,255); + _assertPixel(canvas, 1,39, 255,0,0,255); + _assertPixel(canvas, 1,28, 0,255,0,255); + }); + + it("2d.path.roundrect.1.radius.dompointinit.single.argument", function () { + // Verify that when one radius is given to roundRect() as a non-array argument, specified as a DOMPointInit, applies to all corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, {x: 40, y: 20}); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + _assertPixel(canvas, 20,1, 255,0,0,255); + _assertPixel(canvas, 41,1, 0,255,0,255); + _assertPixel(canvas, 1,10, 255,0,0,255); + _assertPixel(canvas, 1,21, 0,255,0,255); + + // top-right corner + _assertPixel(canvas, 79,1, 255,0,0,255); + _assertPixel(canvas, 58,1, 0,255,0,255); + _assertPixel(canvas, 98,10, 255,0,0,255); + _assertPixel(canvas, 98,21, 0,255,0,255); + + // bottom-right corner + _assertPixel(canvas, 79,48, 255,0,0,255); + _assertPixel(canvas, 58,48, 0,255,0,255); + _assertPixel(canvas, 98,39, 255,0,0,255); + _assertPixel(canvas, 98,28, 0,255,0,255); + + // bottom-left corner + _assertPixel(canvas, 20,48, 255,0,0,255); + _assertPixel(canvas, 41,48, 0,255,0,255); + _assertPixel(canvas, 1,39, 255,0,0,255); + _assertPixel(canvas, 1,28, 0,255,0,255); + }); + + it("2d.path.roundrect.radius.intersecting.1", function () { + // Check that roundRects with intersecting corner arcs are rendered correctly. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [40, 40, 40, 40]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 2,25, 0,255,0,255); + _assertPixel(canvas, 50,1, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 50,48, 0,255,0,255); + _assertPixel(canvas, 97,25, 0,255,0,255); + _assertPixel(canvas, 1,1, 255,0,0,255); + _assertPixel(canvas, 98,1, 255,0,0,255); + _assertPixel(canvas, 1,48, 255,0,0,255); + _assertPixel(canvas, 98,48, 255,0,0,255); + }); + + it("2d.path.roundrect.radius.intersecting.2", function () { + // Check that roundRects with intersecting corner arcs are rendered correctly. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [1000, 1000, 1000, 1000]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 2,25, 0,255,0,255); + _assertPixel(canvas, 50,1, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 50,48, 0,255,0,255); + _assertPixel(canvas, 97,25, 0,255,0,255); + _assertPixel(canvas, 1,1, 255,0,0,255); + _assertPixel(canvas, 98,1, 255,0,0,255); + _assertPixel(canvas, 1,48, 255,0,0,255); + _assertPixel(canvas, 98,48, 255,0,0,255); + }); + + it("2d.path.roundrect.radius.none", function () { + // Check that roundRect throws an RangeError if radii is an empty array. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 100, 50, [])}); + }); + + it("2d.path.roundrect.radius.noargument", function () { + // Check that roundRect draws a rectangle when no radii are provided. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(10, 10, 80, 30); + ctx.fillStyle = '#0f0'; + ctx.fill(); + // upper left corner (10, 10) + _assertPixel(canvas, 10,9, 255,0,0,255); + _assertPixel(canvas, 9,10, 255,0,0,255); + _assertPixel(canvas, 10,10, 0,255,0,255); + + // upper right corner (89, 10) + _assertPixel(canvas, 90,10, 255,0,0,255); + _assertPixel(canvas, 89,9, 255,0,0,255); + _assertPixel(canvas, 89,10, 0,255,0,255); + + // lower right corner (89, 39) + _assertPixel(canvas, 89,40, 255,0,0,255); + _assertPixel(canvas, 90,39, 255,0,0,255); + _assertPixel(canvas, 89,39, 0,255,0,255); + + // lower left corner (10, 30) + _assertPixel(canvas, 9,39, 255,0,0,255); + _assertPixel(canvas, 10,40, 255,0,0,255); + _assertPixel(canvas, 10,39, 0,255,0,255); + }); + + it("2d.path.roundrect.radius.toomany", function () { + // Check that roundRect throws an IndeSizeError if radii has more than four items. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 100, 50, [0, 0, 0, 0, 0])}); + }); + + it("2d.path.roundrect.radius.negative", function () { + // roundRect() with negative radius throws an exception + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 0, 0, [-1])}); + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 0, 0, [1, -1])}); + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 0, 0, [new DOMPoint(-1, 1), 1])}); + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 0, 0, [new DOMPoint(1, -1)])}); + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 0, 0, [{x: -1, y: 1}, 1])}); + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 0, 0, [{x: 1, y: -1}])}); + }); + + it("2d.path.ellipse.basics", function () { + // Verify canvas throws error when drawing ellipse with negative radii. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.ellipse(10, 10, 10, 5, 0, 0, 1, false); + ctx.ellipse(10, 10, 10, 0, 0, 0, 1, false); + ctx.ellipse(10, 10, -0, 5, 0, 0, 1, false); + assert.throws(function() { ctx.ellipse(10, 10, -2, 5, 0, 0, 1, false); }, /INDEX_SIZE_ERR/); + assert.throws(function() { ctx.ellipse(10, 10, 0, -1.5, 0, 0, 1, false); }, /INDEX_SIZE_ERR/); + assert.throws(function() { ctx.ellipse(10, 10, -2, -5, 0, 0, 1, false); }, /INDEX_SIZE_ERR/); + ctx.ellipse(80, 0, 10, 4294967277, Math.PI / -84, -Math.PI / 2147483436, false); + }); + + it("2d.path.fill.overlap", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = 'rgba(0, 255, 0, 0.5)'; + ctx.rect(0, 0, 100, 50); + ctx.closePath(); + ctx.rect(10, 10, 80, 30); + ctx.fill(); + + _assertPixelApprox(canvas, 50,25, 0,127,0,255, 1); + }); + + it("2d.path.fill.winding.add", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.moveTo(-10, -10); + ctx.lineTo(110, -10); + ctx.lineTo(110, 60); + ctx.lineTo(-10, 60); + ctx.lineTo(-10, -10); + ctx.lineTo(0, 0); + ctx.lineTo(100, 0); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fill(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.fill.winding.subtract.1", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#f00'; + ctx.moveTo(-10, -10); + ctx.lineTo(110, -10); + ctx.lineTo(110, 60); + ctx.lineTo(-10, 60); + ctx.lineTo(-10, -10); + ctx.lineTo(0, 0); + ctx.lineTo(0, 50); + ctx.lineTo(100, 50); + ctx.lineTo(100, 0); + ctx.fill(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.fill.winding.subtract.2", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#f00'; + ctx.moveTo(-10, -10); + ctx.lineTo(110, -10); + ctx.lineTo(110, 60); + ctx.lineTo(-10, 60); + ctx.moveTo(0, 0); + ctx.lineTo(0, 50); + ctx.lineTo(100, 50); + ctx.lineTo(100, 0); + ctx.fill(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.fill.winding.subtract.3", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.moveTo(-10, -10); + ctx.lineTo(110, -10); + ctx.lineTo(110, 60); + ctx.lineTo(-10, 60); + ctx.lineTo(-10, -10); + ctx.lineTo(-20, -20); + ctx.lineTo(120, -20); + ctx.lineTo(120, 70); + ctx.lineTo(-20, 70); + ctx.lineTo(-20, -20); + ctx.lineTo(0, 0); + ctx.lineTo(0, 50); + ctx.lineTo(100, 50); + ctx.lineTo(100, 0); + ctx.fill(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.fill.closed.basic", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fill(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.fill.closed.unaffected", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#00f'; + ctx.fillRect(0, 0, 100, 50); + + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + ctx.lineTo(100, 50); + ctx.fillStyle = '#f00'; + ctx.fill(); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + _assertPixel(canvas, 90,10, 0,255,0,255); + _assertPixel(canvas, 10,40, 0,255,0,255); + }); + + it("2d.path.stroke.overlap", function () { + // Stroked subpaths are combined before being drawn + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = 'rgba(0, 255, 0, 0.5)'; + ctx.lineWidth = 50; + ctx.moveTo(0, 20); + ctx.lineTo(100, 20); + ctx.moveTo(0, 30); + ctx.lineTo(100, 30); + ctx.stroke(); + + _assertPixelApprox(canvas, 50,25, 0,127,0,255, 1); + }); + + it("2d.path.stroke.union", function () { + // Strokes in opposite directions are unioned, not subtracted + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 40; + ctx.moveTo(0, 10); + ctx.lineTo(100, 10); + ctx.moveTo(100, 40); + ctx.lineTo(0, 40); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.stroke.unaffected", function () { + // Stroking does not start a new path or subpath + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 50; + ctx.moveTo(-100, 25); + ctx.lineTo(-100, -100); + ctx.lineTo(200, -100); + ctx.lineTo(200, 25); + ctx.strokeStyle = '#f00'; + ctx.stroke(); + + ctx.closePath(); + ctx.strokeStyle = '#0f0'; + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.stroke.scale1", function () { + // Stroke line widths are scaled by the current transformation matrix + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.rect(25, 12.5, 50, 25); + ctx.save(); + ctx.scale(50, 25); + ctx.strokeStyle = '#0f0'; + ctx.stroke(); + ctx.restore(); + + ctx.beginPath(); + ctx.rect(-25, -12.5, 150, 75); + ctx.save(); + ctx.scale(50, 25); + ctx.strokeStyle = '#f00'; + ctx.stroke(); + ctx.restore(); + + _assertPixel(canvas, 0,0, 0,255,0,255); + _assertPixel(canvas, 50,0, 0,255,0,255); + _assertPixel(canvas, 99,0, 0,255,0,255); + _assertPixel(canvas, 0,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 99,25, 0,255,0,255); + _assertPixel(canvas, 0,49, 0,255,0,255); + _assertPixel(canvas, 50,49, 0,255,0,255); + _assertPixel(canvas, 99,49, 0,255,0,255); + }); + + it("2d.path.stroke.scale2", function () { + // Stroke line widths are scaled by the current transformation matrix + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.rect(25, 12.5, 50, 25); + ctx.save(); + ctx.rotate(Math.PI/2); + ctx.scale(25, 50); + ctx.strokeStyle = '#0f0'; + ctx.stroke(); + ctx.restore(); + + ctx.beginPath(); + ctx.rect(-25, -12.5, 150, 75); + ctx.save(); + ctx.rotate(Math.PI/2); + ctx.scale(25, 50); + ctx.strokeStyle = '#f00'; + ctx.stroke(); + ctx.restore(); + + _assertPixel(canvas, 0,0, 0,255,0,255); + _assertPixel(canvas, 50,0, 0,255,0,255); + _assertPixel(canvas, 99,0, 0,255,0,255); + _assertPixel(canvas, 0,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 99,25, 0,255,0,255); + _assertPixel(canvas, 0,49, 0,255,0,255); + _assertPixel(canvas, 50,49, 0,255,0,255); + _assertPixel(canvas, 99,49, 0,255,0,255); + }); + + it("2d.path.stroke.skew", function () { + // Strokes lines are skewed by the current transformation matrix + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.save(); + ctx.beginPath(); + ctx.moveTo(49, -50); + ctx.lineTo(201, -50); + ctx.rotate(Math.PI/4); + ctx.scale(1, 283); + ctx.strokeStyle = '#0f0'; + ctx.stroke(); + ctx.restore(); + + ctx.save(); + ctx.beginPath(); + ctx.translate(-150, 0); + ctx.moveTo(49, -50); + ctx.lineTo(199, -50); + ctx.rotate(Math.PI/4); + ctx.scale(1, 142); + ctx.strokeStyle = '#f00'; + ctx.stroke(); + ctx.restore(); + + ctx.save(); + ctx.beginPath(); + ctx.translate(-150, 0); + ctx.moveTo(49, -50); + ctx.lineTo(199, -50); + ctx.rotate(Math.PI/4); + ctx.scale(1, 142); + ctx.strokeStyle = '#f00'; + ctx.stroke(); + ctx.restore(); + + _assertPixel(canvas, 0,0, 0,255,0,255); + _assertPixel(canvas, 50,0, 0,255,0,255); + _assertPixel(canvas, 99,0, 0,255,0,255); + _assertPixel(canvas, 0,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 99,25, 0,255,0,255); + _assertPixel(canvas, 0,49, 0,255,0,255); + _assertPixel(canvas, 50,49, 0,255,0,255); + _assertPixel(canvas, 99,49, 0,255,0,255); + }); + + it("2d.path.stroke.empty", function () { + // Empty subpaths are not stroked + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + ctx.beginPath(); + ctx.moveTo(40, 25); + ctx.moveTo(60, 25); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.stroke.prune.line", function () { + // Zero-length line segments from lineTo are removed before stroking + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.lineTo(50, 25); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.stroke.prune.closed", function () { + // Zero-length line segments from closed paths are removed before stroking + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.lineTo(50, 25); + ctx.closePath(); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.stroke.prune.curve", function () { + // Zero-length line segments from quadraticCurveTo and bezierCurveTo are removed before stroking + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.quadraticCurveTo(50, 25, 50, 25); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.bezierCurveTo(50, 25, 50, 25, 50, 25); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.stroke.prune.arc", function () { + // Zero-length line segments from arcTo and arc are removed before stroking + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.arcTo(50, 25, 150, 25, 10); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(60, 25); + ctx.arc(50, 25, 10, 0, 0, false); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.stroke.prune.rect", function () { + // Zero-length line segments from rect and strokeRect are removed before stroking + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + ctx.beginPath(); + ctx.rect(50, 25, 0, 0); + ctx.stroke(); + + ctx.strokeRect(50, 25, 0, 0); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.stroke.prune.corner", function () { + // Zero-length line segments are removed before stroking with miters + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 400; + ctx.lineJoin = 'miter'; + ctx.miterLimit = 1.4; + + ctx.beginPath(); + ctx.moveTo(-1000, 200); + ctx.lineTo(-100, 200); + ctx.lineTo(-100, 200); + ctx.lineTo(-100, 200); + ctx.lineTo(-100, 1000); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.transformation.basic", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(-100, 0); + ctx.rect(100, 0, 100, 50); + ctx.translate(0, -100); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.transformation.multiple", function () { + // Transformations are applied while building paths, not when drawing + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#f00'; + ctx.translate(-100, 0); + ctx.rect(0, 0, 100, 50); + ctx.fill(); + ctx.translate(100, 0); + ctx.fill(); + + ctx.beginPath(); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.translate(0, -50); + ctx.moveTo(0, 25); + ctx.lineTo(100, 25); + ctx.stroke(); + ctx.translate(0, 50); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.transformation.changing", function () { + // Transformations are applied while building paths, not when drawing + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.moveTo(0, 0); + ctx.translate(100, 0); + ctx.lineTo(0, 0); + ctx.translate(0, 50); + ctx.lineTo(0, 0); + ctx.translate(-100, 0); + ctx.lineTo(0, 0); + ctx.translate(1000, 1000); + ctx.rotate(Math.PI/2); + ctx.scale(0.1, 0.1); + ctx.fill(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.clip.empty", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.clip(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.clip.basic.1", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.rect(0, 0, 100, 50); + ctx.clip(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.clip.basic.2", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.rect(-100, 0, 100, 50); + ctx.clip(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.clip.intersect", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.rect(0, 0, 50, 50); + ctx.clip(); + ctx.beginPath(); + ctx.rect(50, 0, 50, 50) + ctx.clip(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.clip.winding.1", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.moveTo(-10, -10); + ctx.lineTo(110, -10); + ctx.lineTo(110, 60); + ctx.lineTo(-10, 60); + ctx.lineTo(-10, -10); + ctx.lineTo(0, 0); + ctx.lineTo(0, 50); + ctx.lineTo(100, 50); + ctx.lineTo(100, 0); + ctx.clip(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.clip.winding.2", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.moveTo(-10, -10); + ctx.lineTo(110, -10); + ctx.lineTo(110, 60); + ctx.lineTo(-10, 60); + ctx.lineTo(-10, -10); + ctx.clip(); + + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(0, 50); + ctx.lineTo(100, 50); + ctx.lineTo(100, 0); + ctx.lineTo(0, 0); + ctx.clip(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.clip.unaffected", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(0, 50); + ctx.lineTo(100, 50); + ctx.lineTo(100, 0); + ctx.clip(); + + ctx.lineTo(0, 0); + ctx.fill(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.isPointInPath.basic.1", function () { + // isPointInPath() detects whether the point is inside the path + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.rect(0, 0, 20, 20); + assert.strictEqual(ctx.isPointInPath(10, 10), true, "ctx.isPointInPath(10, 10)", "true") + assert.strictEqual(ctx.isPointInPath(30, 10), false, "ctx.isPointInPath(30, 10)", "false") + }); + + it("2d.path.isPointInPath.basic.2", function () { + // isPointInPath() detects whether the point is inside the path + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.rect(20, 0, 20, 20); + assert.strictEqual(ctx.isPointInPath(10, 10), false, "ctx.isPointInPath(10, 10)", "false") + assert.strictEqual(ctx.isPointInPath(30, 10), true, "ctx.isPointInPath(30, 10)", "true") + }); + + it("2d.path.isPointInPath.edge", function () { + // isPointInPath() counts points on the path as being inside + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.rect(0, 0, 20, 20); + assert.strictEqual(ctx.isPointInPath(0, 0), true, "ctx.isPointInPath(0, 0)", "true") + assert.strictEqual(ctx.isPointInPath(10, 0), true, "ctx.isPointInPath(10, 0)", "true") + assert.strictEqual(ctx.isPointInPath(20, 0), true, "ctx.isPointInPath(20, 0)", "true") + assert.strictEqual(ctx.isPointInPath(20, 10), true, "ctx.isPointInPath(20, 10)", "true") + assert.strictEqual(ctx.isPointInPath(20, 20), true, "ctx.isPointInPath(20, 20)", "true") + assert.strictEqual(ctx.isPointInPath(10, 20), true, "ctx.isPointInPath(10, 20)", "true") + assert.strictEqual(ctx.isPointInPath(0, 20), true, "ctx.isPointInPath(0, 20)", "true") + assert.strictEqual(ctx.isPointInPath(0, 10), true, "ctx.isPointInPath(0, 10)", "true") + assert.strictEqual(ctx.isPointInPath(10, -0.01), false, "ctx.isPointInPath(10, -0.01)", "false") + assert.strictEqual(ctx.isPointInPath(10, 20.01), false, "ctx.isPointInPath(10, 20.01)", "false") + assert.strictEqual(ctx.isPointInPath(-0.01, 10), false, "ctx.isPointInPath(-0.01, 10)", "false") + assert.strictEqual(ctx.isPointInPath(20.01, 10), false, "ctx.isPointInPath(20.01, 10)", "false") + }); + + it("2d.path.isPointInPath.empty", function () { + // isPointInPath() works when there is no path + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.isPointInPath(0, 0), false, "ctx.isPointInPath(0, 0)", "false") + }); + + it("2d.path.isPointInPath.subpath", function () { + // isPointInPath() uses the current path, not just the subpath + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.rect(0, 0, 20, 20); + ctx.beginPath(); + ctx.rect(20, 0, 20, 20); + ctx.closePath(); + ctx.rect(40, 0, 20, 20); + assert.strictEqual(ctx.isPointInPath(10, 10), false, "ctx.isPointInPath(10, 10)", "false") + assert.strictEqual(ctx.isPointInPath(30, 10), true, "ctx.isPointInPath(30, 10)", "true") + assert.strictEqual(ctx.isPointInPath(50, 10), true, "ctx.isPointInPath(50, 10)", "true") + }); + + it("2d.path.isPointInPath.outside", function () { + // isPointInPath() works on paths outside the canvas + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.rect(0, -100, 20, 20); + ctx.rect(20, -10, 20, 20); + assert.strictEqual(ctx.isPointInPath(10, -110), false, "ctx.isPointInPath(10, -110)", "false") + assert.strictEqual(ctx.isPointInPath(10, -90), true, "ctx.isPointInPath(10, -90)", "true") + assert.strictEqual(ctx.isPointInPath(10, -70), false, "ctx.isPointInPath(10, -70)", "false") + assert.strictEqual(ctx.isPointInPath(30, -20), false, "ctx.isPointInPath(30, -20)", "false") + assert.strictEqual(ctx.isPointInPath(30, 0), true, "ctx.isPointInPath(30, 0)", "true") + assert.strictEqual(ctx.isPointInPath(30, 20), false, "ctx.isPointInPath(30, 20)", "false") + }); + + it("2d.path.isPointInPath.unclosed", function () { + // isPointInPath() works on unclosed subpaths + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.moveTo(0, 0); + ctx.lineTo(20, 0); + ctx.lineTo(20, 20); + ctx.lineTo(0, 20); + assert.strictEqual(ctx.isPointInPath(10, 10), true, "ctx.isPointInPath(10, 10)", "true") + assert.strictEqual(ctx.isPointInPath(30, 10), false, "ctx.isPointInPath(30, 10)", "false") + }); + + it("2d.path.isPointInPath.arc", function () { + // isPointInPath() works on arcs + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.arc(50, 25, 10, 0, Math.PI, false); + assert.strictEqual(ctx.isPointInPath(50, 10), false, "ctx.isPointInPath(50, 10)", "false") + assert.strictEqual(ctx.isPointInPath(50, 20), false, "ctx.isPointInPath(50, 20)", "false") + assert.strictEqual(ctx.isPointInPath(50, 30), true, "ctx.isPointInPath(50, 30)", "true") + assert.strictEqual(ctx.isPointInPath(50, 40), false, "ctx.isPointInPath(50, 40)", "false") + assert.strictEqual(ctx.isPointInPath(30, 20), false, "ctx.isPointInPath(30, 20)", "false") + assert.strictEqual(ctx.isPointInPath(70, 20), false, "ctx.isPointInPath(70, 20)", "false") + assert.strictEqual(ctx.isPointInPath(30, 30), false, "ctx.isPointInPath(30, 30)", "false") + assert.strictEqual(ctx.isPointInPath(70, 30), false, "ctx.isPointInPath(70, 30)", "false") + }); + + it("2d.path.isPointInPath.bigarc", function () { + // isPointInPath() works on unclosed arcs larger than 2pi + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.arc(50, 25, 10, 0, 7, false); + assert.strictEqual(ctx.isPointInPath(50, 10), false, "ctx.isPointInPath(50, 10)", "false") + assert.strictEqual(ctx.isPointInPath(50, 20), true, "ctx.isPointInPath(50, 20)", "true") + assert.strictEqual(ctx.isPointInPath(50, 30), true, "ctx.isPointInPath(50, 30)", "true") + assert.strictEqual(ctx.isPointInPath(50, 40), false, "ctx.isPointInPath(50, 40)", "false") + assert.strictEqual(ctx.isPointInPath(30, 20), false, "ctx.isPointInPath(30, 20)", "false") + assert.strictEqual(ctx.isPointInPath(70, 20), false, "ctx.isPointInPath(70, 20)", "false") + assert.strictEqual(ctx.isPointInPath(30, 30), false, "ctx.isPointInPath(30, 30)", "false") + assert.strictEqual(ctx.isPointInPath(70, 30), false, "ctx.isPointInPath(70, 30)", "false") + }); + + it("2d.path.isPointInPath.bezier", function () { + // isPointInPath() works on Bezier curves + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.moveTo(25, 25); + ctx.bezierCurveTo(50, -50, 50, 100, 75, 25); + assert.strictEqual(ctx.isPointInPath(25, 20), false, "ctx.isPointInPath(25, 20)", "false") + assert.strictEqual(ctx.isPointInPath(25, 30), false, "ctx.isPointInPath(25, 30)", "false") + assert.strictEqual(ctx.isPointInPath(30, 20), true, "ctx.isPointInPath(30, 20)", "true") + assert.strictEqual(ctx.isPointInPath(30, 30), false, "ctx.isPointInPath(30, 30)", "false") + assert.strictEqual(ctx.isPointInPath(40, 2), false, "ctx.isPointInPath(40, 2)", "false") + assert.strictEqual(ctx.isPointInPath(40, 20), true, "ctx.isPointInPath(40, 20)", "true") + assert.strictEqual(ctx.isPointInPath(40, 30), false, "ctx.isPointInPath(40, 30)", "false") + assert.strictEqual(ctx.isPointInPath(40, 47), false, "ctx.isPointInPath(40, 47)", "false") + assert.strictEqual(ctx.isPointInPath(45, 20), true, "ctx.isPointInPath(45, 20)", "true") + assert.strictEqual(ctx.isPointInPath(45, 30), false, "ctx.isPointInPath(45, 30)", "false") + assert.strictEqual(ctx.isPointInPath(55, 20), false, "ctx.isPointInPath(55, 20)", "false") + assert.strictEqual(ctx.isPointInPath(55, 30), true, "ctx.isPointInPath(55, 30)", "true") + assert.strictEqual(ctx.isPointInPath(60, 2), false, "ctx.isPointInPath(60, 2)", "false") + assert.strictEqual(ctx.isPointInPath(60, 20), false, "ctx.isPointInPath(60, 20)", "false") + assert.strictEqual(ctx.isPointInPath(60, 30), true, "ctx.isPointInPath(60, 30)", "true") + assert.strictEqual(ctx.isPointInPath(60, 47), false, "ctx.isPointInPath(60, 47)", "false") + assert.strictEqual(ctx.isPointInPath(70, 20), false, "ctx.isPointInPath(70, 20)", "false") + assert.strictEqual(ctx.isPointInPath(70, 30), true, "ctx.isPointInPath(70, 30)", "true") + assert.strictEqual(ctx.isPointInPath(75, 20), false, "ctx.isPointInPath(75, 20)", "false") + assert.strictEqual(ctx.isPointInPath(75, 30), false, "ctx.isPointInPath(75, 30)", "false") + }); + + it("2d.path.isPointInPath.winding", function () { + // isPointInPath() uses the non-zero winding number rule + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + // Create a square ring, using opposite windings to make a hole in the centre + ctx.moveTo(0, 0); + ctx.lineTo(50, 0); + ctx.lineTo(50, 50); + ctx.lineTo(0, 50); + ctx.lineTo(0, 0); + ctx.lineTo(10, 10); + ctx.lineTo(10, 40); + ctx.lineTo(40, 40); + ctx.lineTo(40, 10); + ctx.lineTo(10, 10); + + assert.strictEqual(ctx.isPointInPath(5, 5), true, "ctx.isPointInPath(5, 5)", "true") + assert.strictEqual(ctx.isPointInPath(25, 5), true, "ctx.isPointInPath(25, 5)", "true") + assert.strictEqual(ctx.isPointInPath(45, 5), true, "ctx.isPointInPath(45, 5)", "true") + assert.strictEqual(ctx.isPointInPath(5, 25), true, "ctx.isPointInPath(5, 25)", "true") + assert.strictEqual(ctx.isPointInPath(25, 25), false, "ctx.isPointInPath(25, 25)", "false") + assert.strictEqual(ctx.isPointInPath(45, 25), true, "ctx.isPointInPath(45, 25)", "true") + assert.strictEqual(ctx.isPointInPath(5, 45), true, "ctx.isPointInPath(5, 45)", "true") + assert.strictEqual(ctx.isPointInPath(25, 45), true, "ctx.isPointInPath(25, 45)", "true") + assert.strictEqual(ctx.isPointInPath(45, 45), true, "ctx.isPointInPath(45, 45)", "true") + }); + + it("2d.path.isPointInPath.transform.1", function () { + // isPointInPath() handles transformations correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.translate(50, 0); + ctx.rect(0, 0, 20, 20); + assert.strictEqual(ctx.isPointInPath(-40, 10), false, "ctx.isPointInPath(-40, 10)", "false") + assert.strictEqual(ctx.isPointInPath(10, 10), false, "ctx.isPointInPath(10, 10)", "false") + assert.strictEqual(ctx.isPointInPath(49, 10), false, "ctx.isPointInPath(49, 10)", "false") + assert.strictEqual(ctx.isPointInPath(51, 10), true, "ctx.isPointInPath(51, 10)", "true") + assert.strictEqual(ctx.isPointInPath(69, 10), true, "ctx.isPointInPath(69, 10)", "true") + assert.strictEqual(ctx.isPointInPath(71, 10), false, "ctx.isPointInPath(71, 10)", "false") + }); + + it("2d.path.isPointInPath.transform.2", function () { + // isPointInPath() handles transformations correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.rect(50, 0, 20, 20); + ctx.translate(50, 0); + assert.strictEqual(ctx.isPointInPath(-40, 10), false, "ctx.isPointInPath(-40, 10)", "false") + assert.strictEqual(ctx.isPointInPath(10, 10), false, "ctx.isPointInPath(10, 10)", "false") + assert.strictEqual(ctx.isPointInPath(49, 10), false, "ctx.isPointInPath(49, 10)", "false") + assert.strictEqual(ctx.isPointInPath(51, 10), true, "ctx.isPointInPath(51, 10)", "true") + assert.strictEqual(ctx.isPointInPath(69, 10), true, "ctx.isPointInPath(69, 10)", "true") + assert.strictEqual(ctx.isPointInPath(71, 10), false, "ctx.isPointInPath(71, 10)", "false") + }); + + it("2d.path.isPointInPath.transform.3", function () { + // isPointInPath() handles transformations correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.scale(-1, 1); + ctx.rect(-70, 0, 20, 20); + assert.strictEqual(ctx.isPointInPath(-40, 10), false, "ctx.isPointInPath(-40, 10)", "false") + assert.strictEqual(ctx.isPointInPath(10, 10), false, "ctx.isPointInPath(10, 10)", "false") + assert.strictEqual(ctx.isPointInPath(49, 10), false, "ctx.isPointInPath(49, 10)", "false") + assert.strictEqual(ctx.isPointInPath(51, 10), true, "ctx.isPointInPath(51, 10)", "true") + assert.strictEqual(ctx.isPointInPath(69, 10), true, "ctx.isPointInPath(69, 10)", "true") + assert.strictEqual(ctx.isPointInPath(71, 10), false, "ctx.isPointInPath(71, 10)", "false") + }); + + it("2d.path.isPointInPath.transform.4", function () { + // isPointInPath() handles transformations correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.translate(50, 0); + ctx.rect(50, 0, 20, 20); + ctx.translate(0, 50); + assert.strictEqual(ctx.isPointInPath(60, 10), false, "ctx.isPointInPath(60, 10)", "false") + assert.strictEqual(ctx.isPointInPath(110, 10), true, "ctx.isPointInPath(110, 10)", "true") + assert.strictEqual(ctx.isPointInPath(110, 60), false, "ctx.isPointInPath(110, 60)", "false") + }); + + it("2d.path.isPointInPath.nonfinite", function () { + // isPointInPath() returns false for non-finite arguments + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.rect(-100, -50, 200, 100); + assert.strictEqual(ctx.isPointInPath(Infinity, 0), false, "ctx.isPointInPath(Infinity, 0)", "false") + assert.strictEqual(ctx.isPointInPath(-Infinity, 0), false, "ctx.isPointInPath(-Infinity, 0)", "false") + assert.strictEqual(ctx.isPointInPath(NaN, 0), false, "ctx.isPointInPath(NaN, 0)", "false") + assert.strictEqual(ctx.isPointInPath(0, Infinity), false, "ctx.isPointInPath(0, Infinity)", "false") + assert.strictEqual(ctx.isPointInPath(0, -Infinity), false, "ctx.isPointInPath(0, -Infinity)", "false") + assert.strictEqual(ctx.isPointInPath(0, NaN), false, "ctx.isPointInPath(0, NaN)", "false") + assert.strictEqual(ctx.isPointInPath(NaN, NaN), false, "ctx.isPointInPath(NaN, NaN)", "false") + }); + + it("2d.path.isPointInStroke.scaleddashes", function () { + // isPointInStroke() should return correct results on dashed paths at high scale factors + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var scale = 20; + ctx.setLineDash([10, 21.4159]); // dash from t=0 to t=10 along the circle + ctx.scale(scale, scale); + ctx.ellipse(6, 10, 5, 5, 0, 2*Math.PI, false); + ctx.stroke(); + + // hit-test the beginning of the dash (t=0) + assert.strictEqual(ctx.isPointInStroke(11*scale, 10*scale), true, "ctx.isPointInStroke(11*scale, 10*scale)", "true") + // hit-test the middle of the dash (t=5) + assert.strictEqual(ctx.isPointInStroke(8.70*scale, 14.21*scale), true, "ctx.isPointInStroke(8.70*scale, 14.21*scale)", "true") + // hit-test the end of the dash (t=9.8) + assert.strictEqual(ctx.isPointInStroke(4.10*scale, 14.63*scale), true, "ctx.isPointInStroke(4.10*scale, 14.63*scale)", "true") + // hit-test past the end of the dash (t=10.2) + assert.strictEqual(ctx.isPointInStroke(3.74*scale, 14.46*scale), false, "ctx.isPointInStroke(3.74*scale, 14.46*scale)", "false") + }); + + it("2d.path.isPointInPath.basic", function () { + // Verify the winding rule in isPointInPath works for for rect path. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + canvas.width = 200; + canvas.height = 200; + + // Testing default isPointInPath + ctx.beginPath(); + ctx.rect(0, 0, 100, 100); + ctx.rect(25, 25, 50, 50); + assert.strictEqual(ctx.isPointInPath(50, 50), true, "ctx.isPointInPath(50, 50)", "true") + assert.strictEqual(ctx.isPointInPath(NaN, 50), false, "ctx.isPointInPath(NaN, 50)", "false") + assert.strictEqual(ctx.isPointInPath(50, NaN), false, "ctx.isPointInPath(50, NaN)", "false") + + // Testing nonzero isPointInPath + ctx.beginPath(); + ctx.rect(0, 0, 100, 100); + ctx.rect(25, 25, 50, 50); + assert.strictEqual(ctx.isPointInPath(50, 50, 'nonzero'), true, "ctx.isPointInPath(50, 50, 'nonzero')", "true") + + // Testing evenodd isPointInPath + ctx.beginPath(); + ctx.rect(0, 0, 100, 100); + ctx.rect(25, 25, 50, 50); + assert.strictEqual(ctx.isPointInPath(50, 50, 'evenodd'), false, "ctx.isPointInPath(50, 50, 'evenodd')", "false") + + // Testing extremely large scale + ctx.save(); + ctx.scale(Number.MAX_VALUE, Number.MAX_VALUE); + ctx.beginPath(); + ctx.rect(-10, -10, 20, 20); + assert.strictEqual(ctx.isPointInPath(0, 0, 'nonzero'), true, "ctx.isPointInPath(0, 0, 'nonzero')", "true") + assert.strictEqual(ctx.isPointInPath(0, 0, 'evenodd'), true, "ctx.isPointInPath(0, 0, 'evenodd')", "true") + ctx.restore(); + + // Check with non-invertible ctm. + ctx.save(); + ctx.scale(0, 0); + ctx.beginPath(); + ctx.rect(-10, -10, 20, 20); + assert.strictEqual(ctx.isPointInPath(0, 0, 'nonzero'), false, "ctx.isPointInPath(0, 0, 'nonzero')", "false") + assert.strictEqual(ctx.isPointInPath(0, 0, 'evenodd'), false, "ctx.isPointInPath(0, 0, 'evenodd')", "false") + ctx.restore(); + }); + + it("2d.path.isPointInpath.multi.path", function () { + // Verify the winding rule in isPointInPath works for path object. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + canvas.width = 200; + canvas.height = 200; + + // Testing default isPointInPath with Path object'); + path = new Path2D(); + path.rect(0, 0, 100, 100); + path.rect(25, 25, 50, 50); + assert.strictEqual(ctx.isPointInPath(path, 50, 50), true, "ctx.isPointInPath(path, 50, 50)", "true") + assert.strictEqual(ctx.isPointInPath(path, 50, 50, undefined), true, "ctx.isPointInPath(path, 50, 50, undefined)", "true") + assert.strictEqual(ctx.isPointInPath(path, NaN, 50), false, "ctx.isPointInPath(path, NaN, 50)", "false") + assert.strictEqual(ctx.isPointInPath(path, 50, NaN), false, "ctx.isPointInPath(path, 50, NaN)", "false") + + // Testing nonzero isPointInPath with Path object'); + path = new Path2D(); + path.rect(0, 0, 100, 100); + path.rect(25, 25, 50, 50); + assert.strictEqual(ctx.isPointInPath(path, 50, 50, 'nonzero'), true, "ctx.isPointInPath(path, 50, 50, 'nonzero')", "true") + + // Testing evenodd isPointInPath with Path object'); + path = new Path2D(); + path.rect(0, 0, 100, 100); + path.rect(25, 25, 50, 50); + assert_false(ctx.isPointInPath(path, 50, 50, 'evenodd')); + }); + + it("2d.path.isPointInpath.invalid", function () { + // Verify isPointInPath throws exceptions with invalid inputs. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + canvas.width = 200; + canvas.height = 200; + path = new Path2D(); + path.rect(0, 0, 100, 100); + path.rect(25, 25, 50, 50); + // Testing invalid enumeration isPointInPath (w/ and w/o Path object'); + assert.throws(function() { ctx.isPointInPath(path, 50, 50, 'gazonk'); }, TypeError); + assert.throws(function() { ctx.isPointInPath(50, 50, 'gazonk'); }, TypeError); + + // Testing invalid type isPointInPath with Path object'); + assert.throws(function() { ctx.isPointInPath(null, 50, 50); }, TypeError); + assert.throws(function() { ctx.isPointInPath(null, 50, 50, 'nonzero'); }, TypeError); + assert.throws(function() { ctx.isPointInPath(null, 50, 50, 'evenodd'); }, TypeError); + assert.throws(function() { ctx.isPointInPath(null, 50, 50, null); }, TypeError); + assert.throws(function() { ctx.isPointInPath(path, 50, 50, null); }, TypeError); + assert.throws(function() { ctx.isPointInPath(undefined, 50, 50); }, TypeError); + assert.throws(function() { ctx.isPointInPath(undefined, 50, 50, 'nonzero'); }, TypeError); + assert.throws(function() { ctx.isPointInPath(undefined, 50, 50, 'evenodd'); }, TypeError); + assert.throws(function() { ctx.isPointInPath(undefined, 50, 50, undefined); }, TypeError); + assert.throws(function() { ctx.isPointInPath([], 50, 50); }, TypeError); + assert.throws(function() { ctx.isPointInPath([], 50, 50, 'nonzero'); }, TypeError); + assert.throws(function() { ctx.isPointInPath([], 50, 50, 'evenodd'); }, TypeError); + assert.throws(function() { ctx.isPointInPath({}, 50, 50); }, TypeError); + assert.throws(function() { ctx.isPointInPath({}, 50, 50, 'nonzero'); }, TypeError); + assert.throws(function() { ctx.isPointInPath({}, 50, 50, 'evenodd'); }, TypeError); + }); +}); diff --git a/test/wpt/generated/pixel-manipulation.js b/test/wpt/generated/pixel-manipulation.js new file mode 100644 index 000000000..453572d0c --- /dev/null +++ b/test/wpt/generated/pixel-manipulation.js @@ -0,0 +1,1448 @@ +// THIS FILE WAS AUTO-GENERATED. DO NOT EDIT BY HAND. + +const assert = require('assert'); +const path = require('path'); + +const { + createCanvas, + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + CanvasPattern, + CanvasGradient +} = require('../../..'); + +const window = { + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + Uint8ClampedArray, + CanvasPattern, + CanvasGradient +}; + +const document = { + createElement(type, ...args) { + if (type !== "canvas") + throw new Error(`createElement(${type}) not supported`); + return createCanvas(...args); + } +}; + +function _getPixel(canvas, x, y) { + const ctx = canvas.getContext('2d'); + const imgdata = ctx.getImageData(x, y, 1, 1); + return [ imgdata.data[0], imgdata.data[1], imgdata.data[2], imgdata.data[3] ]; +} + +function _assertApprox(actual, expected, epsilon=0, msg="") { + assert(typeof actual === "number", "actual should be a number but got a ${typeof type_actual}"); + + // The epsilon math below does not place nice with NaN and Infinity + // But in this case Infinity = Infinity and NaN = NaN + if (isFinite(actual) || isFinite(expected)) { + assert(Math.abs(actual - expected) <= epsilon, + `expected ${actual} to equal ${expected} +/- ${epsilon}. ${msg}`); + } else { + assert.strictEqual(actual, expected); + } +} + +function _assertPixel(canvas, x, y, r, g, b, a, pos, color) { + const c = _getPixel(canvas, x,y); + assert.strictEqual(c[0], r, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[1], g, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[2], b, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[3], a, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function _assertPixelApprox(canvas, x, y, r, g, b, a, pos, color, tolerance) { + const c = _getPixel(canvas, x,y); + _assertApprox(c[0], r, tolerance, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[1], g, tolerance, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[2], b, tolerance, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[3], a, tolerance, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function assert_throws_js(Type, fn) { + assert.throws(fn, Type); +} + +// Used by font tests to allow fonts to load. +function deferTest() {} + +class Test { + // Two cases of this in the tests, look unnecessary. + done() {} + // Used by font tests to allow fonts to load. + step_func_done(func) { func(); } + // Used for image onload callback. + step_func(func) { func(); } +} + +function step_timeout(result, time) { + // Nothing; code needs to be converted for this to work. +} + +describe("WPT: pixel-manipulation", function () { + + it("2d.imageData.create2.basic", function () { + // createImageData(sw, sh) exists and returns something + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.notStrictEqual(ctx.createImageData(1, 1), null, "ctx.createImageData(1, 1)", "null"); + }); + + it("2d.imageData.create1.basic", function () { + // createImageData(imgdata) exists and returns something + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.notStrictEqual(ctx.createImageData(ctx.createImageData(1, 1)), null, "ctx.createImageData(ctx.createImageData(1, 1))", "null"); + }); + + it("2d.imageData.create2.type", function () { + // createImageData(sw, sh) returns an ImageData object containing a Uint8ClampedArray object + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.notStrictEqual(window.ImageData, undefined, "window.ImageData", "undefined"); + assert.notStrictEqual(window.Uint8ClampedArray, undefined, "window.Uint8ClampedArray", "undefined"); + window.ImageData.prototype.thisImplementsImageData = true; + window.Uint8ClampedArray.prototype.thisImplementsUint8ClampedArray = true; + var imgdata = ctx.createImageData(1, 1); + assert(imgdata.thisImplementsImageData, "imgdata.thisImplementsImageData"); + assert(imgdata.data.thisImplementsUint8ClampedArray, "imgdata.data.thisImplementsUint8ClampedArray"); + }); + + it("2d.imageData.create1.type", function () { + // createImageData(imgdata) returns an ImageData object containing a Uint8ClampedArray object + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.notStrictEqual(window.ImageData, undefined, "window.ImageData", "undefined"); + assert.notStrictEqual(window.Uint8ClampedArray, undefined, "window.Uint8ClampedArray", "undefined"); + window.ImageData.prototype.thisImplementsImageData = true; + window.Uint8ClampedArray.prototype.thisImplementsUint8ClampedArray = true; + var imgdata = ctx.createImageData(ctx.createImageData(1, 1)); + assert(imgdata.thisImplementsImageData, "imgdata.thisImplementsImageData"); + assert(imgdata.data.thisImplementsUint8ClampedArray, "imgdata.data.thisImplementsUint8ClampedArray"); + }); + + it("2d.imageData.create2.this", function () { + // createImageData(sw, sh) should throw when called with the wrong |this| + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.throws(function() { CanvasRenderingContext2D.prototype.createImageData.call(null, 1, 1); }, TypeError); + assert.throws(function() { CanvasRenderingContext2D.prototype.createImageData.call(undefined, 1, 1); }, TypeError); + assert.throws(function() { CanvasRenderingContext2D.prototype.createImageData.call({}, 1, 1); }, TypeError); + }); + + it("2d.imageData.create1.this", function () { + // createImageData(imgdata) should throw when called with the wrong |this| + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = ctx.createImageData(1, 1); + assert.throws(function() { CanvasRenderingContext2D.prototype.createImageData.call(null, imgdata); }, TypeError); + assert.throws(function() { CanvasRenderingContext2D.prototype.createImageData.call(undefined, imgdata); }, TypeError); + assert.throws(function() { CanvasRenderingContext2D.prototype.createImageData.call({}, imgdata); }, TypeError); + }); + + it("2d.imageData.create2.initial", function () { + // createImageData(sw, sh) returns transparent black data of the right size + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = ctx.createImageData(10, 20); + assert.strictEqual(imgdata.data.length, imgdata.width*imgdata.height*4, "imgdata.data.length", "imgdata.width*imgdata.height*4") + assert(imgdata.width < imgdata.height, "imgdata.width < imgdata.height"); + assert(imgdata.width > 0, "imgdata.width > 0"); + var isTransparentBlack = true; + for (var i = 0; i < imgdata.data.length; ++i) + if (imgdata.data[i] !== 0) + isTransparentBlack = false; + assert(isTransparentBlack, "isTransparentBlack"); + }); + + it("2d.imageData.create1.initial", function () { + // createImageData(imgdata) returns transparent black data of the right size + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + var imgdata1 = ctx.getImageData(0, 0, 10, 20); + var imgdata2 = ctx.createImageData(imgdata1); + assert.strictEqual(imgdata2.data.length, imgdata1.data.length, "imgdata2.data.length", "imgdata1.data.length") + assert.strictEqual(imgdata2.width, imgdata1.width, "imgdata2.width", "imgdata1.width") + assert.strictEqual(imgdata2.height, imgdata1.height, "imgdata2.height", "imgdata1.height") + var isTransparentBlack = true; + for (var i = 0; i < imgdata2.data.length; ++i) + if (imgdata2.data[i] !== 0) + isTransparentBlack = false; + assert(isTransparentBlack, "isTransparentBlack"); + }); + + it("2d.imageData.create2.large", function () { + // createImageData(sw, sh) works for sizes much larger than the canvas + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = ctx.createImageData(1000, 2000); + assert.strictEqual(imgdata.data.length, imgdata.width*imgdata.height*4, "imgdata.data.length", "imgdata.width*imgdata.height*4") + assert(imgdata.width < imgdata.height, "imgdata.width < imgdata.height"); + assert(imgdata.width > 0, "imgdata.width > 0"); + var isTransparentBlack = true; + for (var i = 0; i < imgdata.data.length; i += 7813) // check ~1024 points (assuming normal scaling) + if (imgdata.data[i] !== 0) + isTransparentBlack = false; + assert(isTransparentBlack, "isTransparentBlack"); + }); + + it.skip("2d.imageData.create2.negative", function () { + // createImageData(sw, sh) takes the absolute magnitude of the size arguments + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata1 = ctx.createImageData(10, 20); + var imgdata2 = ctx.createImageData(-10, 20); + var imgdata3 = ctx.createImageData(10, -20); + var imgdata4 = ctx.createImageData(-10, -20); + assert.strictEqual(imgdata1.data.length, imgdata2.data.length, "imgdata1.data.length", "imgdata2.data.length") + assert.strictEqual(imgdata2.data.length, imgdata3.data.length, "imgdata2.data.length", "imgdata3.data.length") + assert.strictEqual(imgdata3.data.length, imgdata4.data.length, "imgdata3.data.length", "imgdata4.data.length") + }); + + it.skip("2d.imageData.create2.zero", function () { + // createImageData(sw, sh) throws INDEX_SIZE_ERR if size is zero + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.throws(function() { ctx.createImageData(10, 0); }, /INDEX_SIZE_ERR/); + assert.throws(function() { ctx.createImageData(0, 10); }, /INDEX_SIZE_ERR/); + assert.throws(function() { ctx.createImageData(0, 0); }, /INDEX_SIZE_ERR/); + assert.throws(function() { ctx.createImageData(0.99, 10); }, /INDEX_SIZE_ERR/); + assert.throws(function() { ctx.createImageData(10, 0.1); }, /INDEX_SIZE_ERR/); + }); + + it.skip("2d.imageData.create2.nonfinite", function () { + // createImageData() throws TypeError if arguments are not finite + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.throws(function() { ctx.createImageData(Infinity, 10); }, TypeError); + assert.throws(function() { ctx.createImageData(-Infinity, 10); }, TypeError); + assert.throws(function() { ctx.createImageData(NaN, 10); }, TypeError); + assert.throws(function() { ctx.createImageData(10, Infinity); }, TypeError); + assert.throws(function() { ctx.createImageData(10, -Infinity); }, TypeError); + assert.throws(function() { ctx.createImageData(10, NaN); }, TypeError); + assert.throws(function() { ctx.createImageData(Infinity, Infinity); }, TypeError); + var posinfobj = { valueOf: function() { return Infinity; } }, + neginfobj = { valueOf: function() { return -Infinity; } }, + nanobj = { valueOf: function() { return -Infinity; } }; + assert.throws(function() { ctx.createImageData(posinfobj, 10); }, TypeError); + assert.throws(function() { ctx.createImageData(neginfobj, 10); }, TypeError); + assert.throws(function() { ctx.createImageData(nanobj, 10); }, TypeError); + assert.throws(function() { ctx.createImageData(10, posinfobj); }, TypeError); + assert.throws(function() { ctx.createImageData(10, neginfobj); }, TypeError); + assert.throws(function() { ctx.createImageData(10, nanobj); }, TypeError); + assert.throws(function() { ctx.createImageData(posinfobj, posinfobj); }, TypeError); + }); + + it.skip("2d.imageData.create1.zero", function () { + // createImageData(null) throws TypeError + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.throws(function() { ctx.createImageData(null); }, TypeError); + }); + + it.skip("2d.imageData.create2.double", function () { + // createImageData(w, h) double is converted to long + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata1 = ctx.createImageData(10.01, 10.99); + var imgdata2 = ctx.createImageData(-10.01, -10.99); + assert.strictEqual(imgdata1.width, 10, "imgdata1.width", "10") + assert.strictEqual(imgdata1.height, 10, "imgdata1.height", "10") + assert.strictEqual(imgdata2.width, 10, "imgdata2.width", "10") + assert.strictEqual(imgdata2.height, 10, "imgdata2.height", "10") + }); + + it("2d.imageData.create.and.resize", function () { + // Verify no crash when resizing an image bitmap to zero. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var image = new Image(); + image.onload = t.step_func(function() { + var options = { resizeHeight: 0 }; + var p1 = createImageBitmap(image, options); + p1.catch(function(error){}); + t.done(); + }); + image.src = 'red.png'; + }); + + it("2d.imageData.get.basic", function () { + // getImageData() exists and returns something + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.notStrictEqual(ctx.getImageData(0, 0, 100, 50), null, "ctx.getImageData(0, 0, 100, 50)", "null"); + }); + + it("2d.imageData.get.type", function () { + // getImageData() returns an ImageData object containing a Uint8ClampedArray object + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.notStrictEqual(window.ImageData, undefined, "window.ImageData", "undefined"); + assert.notStrictEqual(window.Uint8ClampedArray, undefined, "window.Uint8ClampedArray", "undefined"); + window.ImageData.prototype.thisImplementsImageData = true; + window.Uint8ClampedArray.prototype.thisImplementsUint8ClampedArray = true; + var imgdata = ctx.getImageData(0, 0, 1, 1); + assert(imgdata.thisImplementsImageData, "imgdata.thisImplementsImageData"); + assert(imgdata.data.thisImplementsUint8ClampedArray, "imgdata.data.thisImplementsUint8ClampedArray"); + }); + + it("2d.imageData.get.zero", function () { + // getImageData() throws INDEX_SIZE_ERR if size is zero + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.throws(function() { ctx.getImageData(1, 1, 10, 0); }, /INDEX_SIZE_ERR/); + assert.throws(function() { ctx.getImageData(1, 1, 0, 10); }, /INDEX_SIZE_ERR/); + assert.throws(function() { ctx.getImageData(1, 1, 0, 0); }, /INDEX_SIZE_ERR/); + assert.throws(function() { ctx.getImageData(1, 1, 0.1, 10); }, /INDEX_SIZE_ERR/); + assert.throws(function() { ctx.getImageData(1, 1, 10, 0.99); }, /INDEX_SIZE_ERR/); + assert.throws(function() { ctx.getImageData(1, 1, -0.1, 10); }, /INDEX_SIZE_ERR/); + assert.throws(function() { ctx.getImageData(1, 1, 10, -0.99); }, /INDEX_SIZE_ERR/); + }); + + it("2d.imageData.get.nonfinite", function () { + // getImageData() throws TypeError if arguments are not finite + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.throws(function() { ctx.getImageData(Infinity, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(-Infinity, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(NaN, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, Infinity, 10, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, -Infinity, 10, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, NaN, 10, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, 10, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, 10, -Infinity, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, 10, NaN, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, 10, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.getImageData(10, 10, 10, -Infinity); }, TypeError); + assert.throws(function() { ctx.getImageData(10, 10, 10, NaN); }, TypeError); + assert.throws(function() { ctx.getImageData(Infinity, Infinity, 10, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(Infinity, Infinity, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(Infinity, Infinity, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.getImageData(Infinity, Infinity, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.getImageData(Infinity, 10, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(Infinity, 10, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.getImageData(Infinity, 10, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.getImageData(10, Infinity, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, Infinity, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.getImageData(10, Infinity, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.getImageData(10, 10, Infinity, Infinity); }, TypeError); + var posinfobj = { valueOf: function() { return Infinity; } }, + neginfobj = { valueOf: function() { return -Infinity; } }, + nanobj = { valueOf: function() { return -Infinity; } }; + assert.throws(function() { ctx.getImageData(posinfobj, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(neginfobj, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(nanobj, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, posinfobj, 10, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, neginfobj, 10, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, nanobj, 10, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, 10, posinfobj, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, 10, neginfobj, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, 10, nanobj, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, 10, 10, posinfobj); }, TypeError); + assert.throws(function() { ctx.getImageData(10, 10, 10, neginfobj); }, TypeError); + assert.throws(function() { ctx.getImageData(10, 10, 10, nanobj); }, TypeError); + assert.throws(function() { ctx.getImageData(posinfobj, posinfobj, 10, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(posinfobj, posinfobj, posinfobj, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(posinfobj, posinfobj, posinfobj, posinfobj); }, TypeError); + assert.throws(function() { ctx.getImageData(posinfobj, posinfobj, 10, posinfobj); }, TypeError); + assert.throws(function() { ctx.getImageData(posinfobj, 10, posinfobj, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(posinfobj, 10, posinfobj, posinfobj); }, TypeError); + assert.throws(function() { ctx.getImageData(posinfobj, 10, 10, posinfobj); }, TypeError); + assert.throws(function() { ctx.getImageData(10, posinfobj, posinfobj, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, posinfobj, posinfobj, posinfobj); }, TypeError); + assert.throws(function() { ctx.getImageData(10, posinfobj, 10, posinfobj); }, TypeError); + assert.throws(function() { ctx.getImageData(10, 10, posinfobj, posinfobj); }, TypeError); + }); + + it.skip("2d.imageData.get.source.outside", function () { + // getImageData() returns transparent black outside the canvas + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#08f'; + ctx.fillRect(0, 0, 100, 50); + + var imgdata1 = ctx.getImageData(-10, 5, 1, 1); + assert.strictEqual(imgdata1.data[0], 0, "imgdata1.data[\""+(0)+"\"]", "0") + assert.strictEqual(imgdata1.data[1], 0, "imgdata1.data[\""+(1)+"\"]", "0") + assert.strictEqual(imgdata1.data[2], 0, "imgdata1.data[\""+(2)+"\"]", "0") + assert.strictEqual(imgdata1.data[3], 0, "imgdata1.data[\""+(3)+"\"]", "0") + + var imgdata2 = ctx.getImageData(10, -5, 1, 1); + assert.strictEqual(imgdata2.data[0], 0, "imgdata2.data[\""+(0)+"\"]", "0") + assert.strictEqual(imgdata2.data[1], 0, "imgdata2.data[\""+(1)+"\"]", "0") + assert.strictEqual(imgdata2.data[2], 0, "imgdata2.data[\""+(2)+"\"]", "0") + assert.strictEqual(imgdata2.data[3], 0, "imgdata2.data[\""+(3)+"\"]", "0") + + var imgdata3 = ctx.getImageData(200, 5, 1, 1); + assert.strictEqual(imgdata3.data[0], 0, "imgdata3.data[\""+(0)+"\"]", "0") + assert.strictEqual(imgdata3.data[1], 0, "imgdata3.data[\""+(1)+"\"]", "0") + assert.strictEqual(imgdata3.data[2], 0, "imgdata3.data[\""+(2)+"\"]", "0") + assert.strictEqual(imgdata3.data[3], 0, "imgdata3.data[\""+(3)+"\"]", "0") + + var imgdata4 = ctx.getImageData(10, 60, 1, 1); + assert.strictEqual(imgdata4.data[0], 0, "imgdata4.data[\""+(0)+"\"]", "0") + assert.strictEqual(imgdata4.data[1], 0, "imgdata4.data[\""+(1)+"\"]", "0") + assert.strictEqual(imgdata4.data[2], 0, "imgdata4.data[\""+(2)+"\"]", "0") + assert.strictEqual(imgdata4.data[3], 0, "imgdata4.data[\""+(3)+"\"]", "0") + + var imgdata5 = ctx.getImageData(100, 10, 1, 1); + assert.strictEqual(imgdata5.data[0], 0, "imgdata5.data[\""+(0)+"\"]", "0") + assert.strictEqual(imgdata5.data[1], 0, "imgdata5.data[\""+(1)+"\"]", "0") + assert.strictEqual(imgdata5.data[2], 0, "imgdata5.data[\""+(2)+"\"]", "0") + assert.strictEqual(imgdata5.data[3], 0, "imgdata5.data[\""+(3)+"\"]", "0") + + var imgdata6 = ctx.getImageData(0, 10, 1, 1); + assert.strictEqual(imgdata6.data[0], 0, "imgdata6.data[\""+(0)+"\"]", "0") + assert.strictEqual(imgdata6.data[1], 136, "imgdata6.data[\""+(1)+"\"]", "136") + assert.strictEqual(imgdata6.data[2], 255, "imgdata6.data[\""+(2)+"\"]", "255") + assert.strictEqual(imgdata6.data[3], 255, "imgdata6.data[\""+(3)+"\"]", "255") + + var imgdata7 = ctx.getImageData(-10, 10, 20, 20); + assert.strictEqual(imgdata7.data[ 0*4+0], 0, "imgdata7.data[ 0*4+0]", "0") + assert.strictEqual(imgdata7.data[ 0*4+1], 0, "imgdata7.data[ 0*4+1]", "0") + assert.strictEqual(imgdata7.data[ 0*4+2], 0, "imgdata7.data[ 0*4+2]", "0") + assert.strictEqual(imgdata7.data[ 0*4+3], 0, "imgdata7.data[ 0*4+3]", "0") + assert.strictEqual(imgdata7.data[ 9*4+0], 0, "imgdata7.data[ 9*4+0]", "0") + assert.strictEqual(imgdata7.data[ 9*4+1], 0, "imgdata7.data[ 9*4+1]", "0") + assert.strictEqual(imgdata7.data[ 9*4+2], 0, "imgdata7.data[ 9*4+2]", "0") + assert.strictEqual(imgdata7.data[ 9*4+3], 0, "imgdata7.data[ 9*4+3]", "0") + assert.strictEqual(imgdata7.data[10*4+0], 0, "imgdata7.data[10*4+0]", "0") + assert.strictEqual(imgdata7.data[10*4+1], 136, "imgdata7.data[10*4+1]", "136") + assert.strictEqual(imgdata7.data[10*4+2], 255, "imgdata7.data[10*4+2]", "255") + assert.strictEqual(imgdata7.data[10*4+3], 255, "imgdata7.data[10*4+3]", "255") + assert.strictEqual(imgdata7.data[19*4+0], 0, "imgdata7.data[19*4+0]", "0") + assert.strictEqual(imgdata7.data[19*4+1], 136, "imgdata7.data[19*4+1]", "136") + assert.strictEqual(imgdata7.data[19*4+2], 255, "imgdata7.data[19*4+2]", "255") + assert.strictEqual(imgdata7.data[19*4+3], 255, "imgdata7.data[19*4+3]", "255") + assert.strictEqual(imgdata7.data[20*4+0], 0, "imgdata7.data[20*4+0]", "0") + assert.strictEqual(imgdata7.data[20*4+1], 0, "imgdata7.data[20*4+1]", "0") + assert.strictEqual(imgdata7.data[20*4+2], 0, "imgdata7.data[20*4+2]", "0") + assert.strictEqual(imgdata7.data[20*4+3], 0, "imgdata7.data[20*4+3]", "0") + }); + + it.skip("2d.imageData.get.source.negative", function () { + // getImageData() works with negative width and height, and returns top-to-bottom left-to-right + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#fff'; + ctx.fillRect(20, 10, 60, 10); + + var imgdata1 = ctx.getImageData(85, 25, -10, -10); + assert.strictEqual(imgdata1.data[0], 255, "imgdata1.data[\""+(0)+"\"]", "255") + assert.strictEqual(imgdata1.data[1], 255, "imgdata1.data[\""+(1)+"\"]", "255") + assert.strictEqual(imgdata1.data[2], 255, "imgdata1.data[\""+(2)+"\"]", "255") + assert.strictEqual(imgdata1.data[3], 255, "imgdata1.data[\""+(3)+"\"]", "255") + assert.strictEqual(imgdata1.data[imgdata1.data.length-4+0], 0, "imgdata1.data[imgdata1.data.length-4+0]", "0") + assert.strictEqual(imgdata1.data[imgdata1.data.length-4+1], 0, "imgdata1.data[imgdata1.data.length-4+1]", "0") + assert.strictEqual(imgdata1.data[imgdata1.data.length-4+2], 0, "imgdata1.data[imgdata1.data.length-4+2]", "0") + assert.strictEqual(imgdata1.data[imgdata1.data.length-4+3], 255, "imgdata1.data[imgdata1.data.length-4+3]", "255") + + var imgdata2 = ctx.getImageData(0, 0, -1, -1); + assert.strictEqual(imgdata2.data[0], 0, "imgdata2.data[\""+(0)+"\"]", "0") + assert.strictEqual(imgdata2.data[1], 0, "imgdata2.data[\""+(1)+"\"]", "0") + assert.strictEqual(imgdata2.data[2], 0, "imgdata2.data[\""+(2)+"\"]", "0") + assert.strictEqual(imgdata2.data[3], 0, "imgdata2.data[\""+(3)+"\"]", "0") + }); + + it("2d.imageData.get.source.size", function () { + // getImageData() returns bigger ImageData for bigger source rectangle + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata1 = ctx.getImageData(0, 0, 10, 10); + var imgdata2 = ctx.getImageData(0, 0, 20, 20); + assert(imgdata2.width > imgdata1.width, "imgdata2.width > imgdata1.width"); + assert(imgdata2.height > imgdata1.height, "imgdata2.height > imgdata1.height"); + }); + + it.skip("2d.imageData.get.double", function () { + // createImageData(w, h) double is converted to long + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata1 = ctx.getImageData(0, 0, 10.01, 10.99); + var imgdata2 = ctx.getImageData(0, 0, -10.01, -10.99); + assert.strictEqual(imgdata1.width, 10, "imgdata1.width", "10") + assert.strictEqual(imgdata1.height, 10, "imgdata1.height", "10") + assert.strictEqual(imgdata2.width, 10, "imgdata2.width", "10") + assert.strictEqual(imgdata2.height, 10, "imgdata2.height", "10") + }); + + it("2d.imageData.get.nonpremul", function () { + // getImageData() returns non-premultiplied colors + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; + ctx.fillRect(0, 0, 100, 50); + var imgdata = ctx.getImageData(10, 10, 10, 10); + assert(imgdata.data[0] > 200, "imgdata.data[\""+(0)+"\"] > 200"); + assert(imgdata.data[1] > 200, "imgdata.data[\""+(1)+"\"] > 200"); + assert(imgdata.data[2] > 200, "imgdata.data[\""+(2)+"\"] > 200"); + assert(imgdata.data[3] > 100, "imgdata.data[\""+(3)+"\"] > 100"); + assert(imgdata.data[3] < 200, "imgdata.data[\""+(3)+"\"] < 200"); + }); + + it("2d.imageData.get.range", function () { + // getImageData() returns values in the range [0, 255] + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#fff'; + ctx.fillRect(20, 10, 60, 10); + var imgdata1 = ctx.getImageData(10, 5, 1, 1); + assert.strictEqual(imgdata1.data[0], 0, "imgdata1.data[\""+(0)+"\"]", "0") + var imgdata2 = ctx.getImageData(30, 15, 1, 1); + assert.strictEqual(imgdata2.data[0], 255, "imgdata2.data[\""+(0)+"\"]", "255") + }); + + it("2d.imageData.get.clamp", function () { + // getImageData() clamps colors to the range [0, 255] + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = 'rgb(-100, -200, -300)'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = 'rgb(256, 300, 400)'; + ctx.fillRect(20, 10, 60, 10); + var imgdata1 = ctx.getImageData(10, 5, 1, 1); + assert.strictEqual(imgdata1.data[0], 0, "imgdata1.data[\""+(0)+"\"]", "0") + assert.strictEqual(imgdata1.data[1], 0, "imgdata1.data[\""+(1)+"\"]", "0") + assert.strictEqual(imgdata1.data[2], 0, "imgdata1.data[\""+(2)+"\"]", "0") + var imgdata2 = ctx.getImageData(30, 15, 1, 1); + assert.strictEqual(imgdata2.data[0], 255, "imgdata2.data[\""+(0)+"\"]", "255") + assert.strictEqual(imgdata2.data[1], 255, "imgdata2.data[\""+(1)+"\"]", "255") + assert.strictEqual(imgdata2.data[2], 255, "imgdata2.data[\""+(2)+"\"]", "255") + }); + + it("2d.imageData.get.length", function () { + // getImageData() returns a correctly-sized Uint8ClampedArray + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = ctx.getImageData(0, 0, 10, 10); + assert.strictEqual(imgdata.data.length, imgdata.width*imgdata.height*4, "imgdata.data.length", "imgdata.width*imgdata.height*4") + }); + + it("2d.imageData.get.order.cols", function () { + // getImageData() returns leftmost columns first + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#fff'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 2, 50); + var imgdata = ctx.getImageData(0, 0, 10, 10); + assert.strictEqual(imgdata.data[0], 0, "imgdata.data[\""+(0)+"\"]", "0") + assert.strictEqual(imgdata.data[Math.round(imgdata.width/2*4)], 255, "imgdata.data[Math.round(imgdata.width/2*4)]", "255") + assert.strictEqual(imgdata.data[Math.round((imgdata.height/2)*imgdata.width*4)], 0, "imgdata.data[Math.round((imgdata.height/2)*imgdata.width*4)]", "0") + }); + + it("2d.imageData.get.order.rows", function () { + // getImageData() returns topmost rows first + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#fff'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 2); + var imgdata = ctx.getImageData(0, 0, 10, 10); + assert.strictEqual(imgdata.data[0], 0, "imgdata.data[\""+(0)+"\"]", "0") + assert.strictEqual(imgdata.data[Math.floor(imgdata.width/2*4)], 0, "imgdata.data[Math.floor(imgdata.width/2*4)]", "0") + assert.strictEqual(imgdata.data[(imgdata.height/2)*imgdata.width*4], 255, "imgdata.data[(imgdata.height/2)*imgdata.width*4]", "255") + }); + + it("2d.imageData.get.order.rgb", function () { + // getImageData() returns R then G then B + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#48c'; + ctx.fillRect(0, 0, 100, 50); + var imgdata = ctx.getImageData(0, 0, 10, 10); + assert.strictEqual(imgdata.data[0], 0x44, "imgdata.data[\""+(0)+"\"]", "0x44") + assert.strictEqual(imgdata.data[1], 0x88, "imgdata.data[\""+(1)+"\"]", "0x88") + assert.strictEqual(imgdata.data[2], 0xCC, "imgdata.data[\""+(2)+"\"]", "0xCC") + assert.strictEqual(imgdata.data[3], 255, "imgdata.data[\""+(3)+"\"]", "255") + assert.strictEqual(imgdata.data[4], 0x44, "imgdata.data[\""+(4)+"\"]", "0x44") + assert.strictEqual(imgdata.data[5], 0x88, "imgdata.data[\""+(5)+"\"]", "0x88") + assert.strictEqual(imgdata.data[6], 0xCC, "imgdata.data[\""+(6)+"\"]", "0xCC") + assert.strictEqual(imgdata.data[7], 255, "imgdata.data[\""+(7)+"\"]", "255") + }); + + it("2d.imageData.get.order.alpha", function () { + // getImageData() returns A in the fourth component + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; + ctx.fillRect(0, 0, 100, 50); + var imgdata = ctx.getImageData(0, 0, 10, 10); + assert(imgdata.data[3] < 200, "imgdata.data[\""+(3)+"\"] < 200"); + assert(imgdata.data[3] > 100, "imgdata.data[\""+(3)+"\"] > 100"); + }); + + it("2d.imageData.get.unaffected", function () { + // getImageData() is not affected by context state + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 50) + ctx.fillStyle = '#f00'; + ctx.fillRect(50, 0, 50, 50) + ctx.save(); + ctx.translate(50, 0); + ctx.globalAlpha = 0.1; + ctx.globalCompositeOperation = 'destination-atop'; + ctx.shadowColor = '#f00'; + ctx.rect(0, 0, 5, 5); + ctx.clip(); + var imgdata = ctx.getImageData(0, 0, 50, 50); + ctx.restore(); + ctx.putImageData(imgdata, 50, 0); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }); + + it.skip("2d.imageData.get.large.crash", function () { + // Test that canvas crash when image data cannot be allocated. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.throws(function() { ctx.getImageData(10, 0xffffffff, 2147483647, 10); }, TypeError); + }); + + it("2d.imageData.get.rounding", function () { + // Test the handling of non-integer source coordinates in getImageData(). + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + function testDimensions(sx, sy, sw, sh, width, height) + { + imageData = ctx.getImageData(sx, sy, sw, sh); + assert(imageData.width == width, "imageData.width == width"); + assert(imageData.height == height, "imageData.height == height"); + } + + testDimensions(0, 0, 20, 10, 20, 10); + + testDimensions(.1, .2, 20, 10, 20, 10); + testDimensions(.9, .8, 20, 10, 20, 10); + + testDimensions(0, 0, 20.9, 10.9, 20, 10); + testDimensions(0, 0, 20.1, 10.1, 20, 10); + + testDimensions(-1, -1, 20, 10, 20, 10); + + testDimensions(-1.1, 0, 20, 10, 20, 10); + testDimensions(-1.9, 0, 20, 10, 20, 10); + }); + + it("2d.imageData.get.invalid", function () { + // Verify getImageData() behavior in invalid cases. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + imageData = ctx.getImageData(0,0,2,2); + var testValues = [NaN, true, false, "\"garbage\"", "-1", + "0", "1", "2", Infinity, -Infinity, + -5, -0.5, 0, 0.5, 5, + 5.4, 255, 256, null, undefined]; + var testResults = [0, 1, 0, 0, 0, + 0, 1, 2, 255, 0, + 0, 0, 0, 0, 5, + 5, 255, 255, 0, 0]; + for (var i = 0; i < testValues.length; i++) { + imageData.data[0] = testValues[i]; + assert(imageData.data[0] == testResults[i], "imageData.data[\""+(0)+"\"] == testResults[\""+(i)+"\"]"); + } + imageData.data['foo']='garbage'; + assert(imageData.data['foo'] == 'garbage', "imageData.data['foo'] == 'garbage'"); + imageData.data[-1]='garbage'; + assert(imageData.data[-1] == undefined, "imageData.data[-1] == undefined"); + imageData.data[17]='garbage'; + assert(imageData.data[17] == undefined, "imageData.data[\""+(17)+"\"] == undefined"); + }); + + it("2d.imageData.object.properties", function () { + // ImageData objects have the right properties + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = ctx.getImageData(0, 0, 10, 10); + assert.strictEqual(typeof(imgdata.width), 'number', "typeof(imgdata.width)", "'number'") + assert.strictEqual(typeof(imgdata.height), 'number', "typeof(imgdata.height)", "'number'") + assert.strictEqual(typeof(imgdata.data), 'object', "typeof(imgdata.data)", "'object'") + }); + + it("2d.imageData.object.readonly", function () { + // ImageData objects properties are read-only + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = ctx.getImageData(0, 0, 10, 10); + var w = imgdata.width; + var h = imgdata.height; + var d = imgdata.data; + imgdata.width = 123; + imgdata.height = 123; + imgdata.data = [100,100,100,100]; + assert.strictEqual(imgdata.width, w, "imgdata.width", "w") + assert.strictEqual(imgdata.height, h, "imgdata.height", "h") + assert.strictEqual(imgdata.data, d, "imgdata.data", "d") + assert.strictEqual(imgdata.data[0], 0, "imgdata.data[\""+(0)+"\"]", "0") + assert.strictEqual(imgdata.data[1], 0, "imgdata.data[\""+(1)+"\"]", "0") + assert.strictEqual(imgdata.data[2], 0, "imgdata.data[\""+(2)+"\"]", "0") + assert.strictEqual(imgdata.data[3], 0, "imgdata.data[\""+(3)+"\"]", "0") + }); + + it("2d.imageData.object.ctor.size", function () { + // ImageData has a usable constructor + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.notStrictEqual(window.ImageData, undefined, "window.ImageData", "undefined"); + + var imgdata = new window.ImageData(2, 3); + assert.strictEqual(imgdata.width, 2, "imgdata.width", "2") + assert.strictEqual(imgdata.height, 3, "imgdata.height", "3") + assert.strictEqual(imgdata.data.length, 2 * 3 * 4, "imgdata.data.length", "2 * 3 * 4") + for (var i = 0; i < imgdata.data.length; ++i) { + assert.strictEqual(imgdata.data[i], 0, "imgdata.data[\""+(i)+"\"]", "0") + } + }); + + it("2d.imageData.object.ctor.basics", function () { + // Testing different type of ImageData constructor + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + function setRGBA(imageData, i, rgba) + { + var s = i * 4; + imageData[s] = rgba[0]; + imageData[s + 1] = rgba[1]; + imageData[s + 2] = rgba[2]; + imageData[s + 3] = rgba[3]; + } + + function getRGBA(imageData, i) + { + var result = []; + var s = i * 4; + for (var j = 0; j < 4; j++) { + result[j] = imageData[s + j]; + } + return result; + } + + function assertArrayEquals(actual, expected) + { + assert.strictEqual(typeof actual, "object", "typeof actual", "\"object\"") + assert.notStrictEqual(actual, null, "actual", "null"); + assert.strictEqual("length" in actual, true, "\"length\" in actual", "true") + assert.strictEqual(actual.length, expected.length, "actual.length", "expected.length") + for (var i = 0; i < actual.length; i++) { + assert.strictEqual(actual.hasOwnProperty(i), expected.hasOwnProperty(i), "actual.hasOwnProperty(i)", "expected.hasOwnProperty(i)") + assert.strictEqual(actual[i], expected[i], "actual[\""+(i)+"\"]", "expected[\""+(i)+"\"]") + } + } + + assert.notStrictEqual(ImageData, undefined, "ImageData", "undefined"); + imageData = new ImageData(100, 50); + + assert.notStrictEqual(imageData, null, "imageData", "null"); + assert.notStrictEqual(imageData.data, null, "imageData.data", "null"); + assert.strictEqual(imageData.width, 100, "imageData.width", "100") + assert.strictEqual(imageData.height, 50, "imageData.height", "50") + assertArrayEquals(getRGBA(imageData.data, 4), [0, 0, 0, 0]); + + var testColor = [0, 255, 255, 128]; + setRGBA(imageData.data, 4, testColor); + assertArrayEquals(getRGBA(imageData.data, 4), testColor); + + assert.throws(function() { new ImageData(10); }, TypeError); + assert.throws(function() { new ImageData(0, 10); }, /INDEX_SIZE_ERR/); + assert.throws(function() { new ImageData(10, 0); }, /INDEX_SIZE_ERR/); + assert.throws(function() { new ImageData('width', 'height'); }, /INDEX_SIZE_ERR/); + assert.throws(function() { new ImageData(1 << 31, 1 << 31); }, /INDEX_SIZE_ERR/); + assert.throws(function() { new ImageData(new Uint8ClampedArray(0)); }, TypeError); + assert.throws(function() { new ImageData(new Uint8Array(100), 25); }, /INDEX_SIZE_ERR/); + assert.throws(function() { new ImageData(new Uint8ClampedArray(27), 2); }, /INVALID_STATE_ERR/); + assert.throws(function() { new ImageData(new Uint8ClampedArray(28), 7, 0); }, /INDEX_SIZE_ERR/); + assert.throws(function() { new ImageData(new Uint8ClampedArray(104), 14); }, /INDEX_SIZE_ERR/); + assert.throws(function() { new ImageData(new Uint8ClampedArray([12, 34, 168, 65328]), 1, 151); }, /INDEX_SIZE_ERR/); + assert.throws(function() { new ImageData(self, 4, 4); }, TypeError); + assert.throws(function() { new ImageData(null, 4, 4); }, TypeError); + assert.throws(function() { new ImageData(imageData.data, 0); }, /INDEX_SIZE_ERR/); + assert.throws(function() { new ImageData(imageData.data, 13); }, /INDEX_SIZE_ERR/); + assert.throws(function() { new ImageData(imageData.data, 1 << 31); }, /INDEX_SIZE_ERR/); + assert.throws(function() { new ImageData(imageData.data, 'biggish'); }, /INDEX_SIZE_ERR/); + assert.throws(function() { new ImageData(imageData.data, 1 << 24, 1 << 31); }, /INDEX_SIZE_ERR/); + assert.strictEqual(new ImageData(new Uint8ClampedArray(28), 7).height, 1, "new ImageData(new Uint8ClampedArray(28), 7).height", "1") + + imageDataFromData = new ImageData(imageData.data, 100); + assert.strictEqual(imageDataFromData.width, 100, "imageDataFromData.width", "100") + assert.strictEqual(imageDataFromData.height, 50, "imageDataFromData.height", "50") + assert.strictEqual(imageDataFromData.data, imageData.data, "imageDataFromData.data", "imageData.data") + assertArrayEquals(getRGBA(imageDataFromData.data, 10), getRGBA(imageData.data, 10)); + setRGBA(imageData.data, 10, testColor); + assertArrayEquals(getRGBA(imageDataFromData.data, 10), getRGBA(imageData.data, 10)); + + var data = new Uint8ClampedArray(400); + data[22] = 129; + imageDataFromData = new ImageData(data, 20, 5); + assert.strictEqual(imageDataFromData.width, 20, "imageDataFromData.width", "20") + assert.strictEqual(imageDataFromData.height, 5, "imageDataFromData.height", "5") + assert.strictEqual(imageDataFromData.data, data, "imageDataFromData.data", "data") + assertArrayEquals(getRGBA(imageDataFromData.data, 2), getRGBA(data, 2)); + setRGBA(imageDataFromData.data, 2, testColor); + assertArrayEquals(getRGBA(imageDataFromData.data, 2), getRGBA(data, 2)); + + if (window.SharedArrayBuffer) { + assert.throws(function() { new ImageData(new Uint16Array(new SharedArrayBuffer(32)), 4, 2); }, TypeError); + } + }); + + it("2d.imageData.object.ctor.array", function () { + // ImageData has a usable constructor + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.notStrictEqual(window.ImageData, undefined, "window.ImageData", "undefined"); + + var array = new Uint8ClampedArray(8); + var imgdata = new window.ImageData(array, 1, 2); + assert.strictEqual(imgdata.width, 1, "imgdata.width", "1") + assert.strictEqual(imgdata.height, 2, "imgdata.height", "2") + assert.strictEqual(imgdata.data, array, "imgdata.data", "array") + }); + + it("2d.imageData.object.ctor.array.bounds", function () { + // ImageData has a usable constructor + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.notStrictEqual(window.ImageData, undefined, "window.ImageData", "undefined"); + + assert.throws(function() { new ImageData(new Uint8ClampedArray(0), 1); }, /INVALID_STATE_ERR/); + assert.throws(function() { new ImageData(new Uint8ClampedArray(3), 1); }, /INVALID_STATE_ERR/); + assert.throws(function() { new ImageData(new Uint8ClampedArray(4), 0); }, /INDEX_SIZE_ERR/); + assert.throws(function() { new ImageData(new Uint8ClampedArray(4), 1, 2); }, /INDEX_SIZE_ERR/); + assert.throws(function() { new ImageData(new Uint8Array(8), 1, 2); }, TypeError); + assert.throws(function() { new ImageData(new Int8Array(8), 1, 2); }, TypeError); + }); + + it("2d.imageData.object.set", function () { + // ImageData.data can be modified + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = ctx.getImageData(0, 0, 10, 10); + imgdata.data[0] = 100; + assert.strictEqual(imgdata.data[0], 100, "imgdata.data[\""+(0)+"\"]", "100") + imgdata.data[0] = 200; + assert.strictEqual(imgdata.data[0], 200, "imgdata.data[\""+(0)+"\"]", "200") + }); + + it("2d.imageData.object.undefined", function () { + // ImageData.data converts undefined to 0 + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = ctx.getImageData(0, 0, 10, 10); + imgdata.data[0] = 100; + imgdata.data[0] = undefined; + assert.strictEqual(imgdata.data[0], 0, "imgdata.data[\""+(0)+"\"]", "0") + }); + + it("2d.imageData.object.nan", function () { + // ImageData.data converts NaN to 0 + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = ctx.getImageData(0, 0, 10, 10); + imgdata.data[0] = 100; + imgdata.data[0] = NaN; + assert.strictEqual(imgdata.data[0], 0, "imgdata.data[\""+(0)+"\"]", "0") + imgdata.data[0] = 100; + imgdata.data[0] = "cheese"; + assert.strictEqual(imgdata.data[0], 0, "imgdata.data[\""+(0)+"\"]", "0") + }); + + it("2d.imageData.object.string", function () { + // ImageData.data converts strings to numbers with ToNumber + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = ctx.getImageData(0, 0, 10, 10); + imgdata.data[0] = 100; + imgdata.data[0] = "110"; + assert.strictEqual(imgdata.data[0], 110, "imgdata.data[\""+(0)+"\"]", "110") + imgdata.data[0] = 100; + imgdata.data[0] = "0x78"; + assert.strictEqual(imgdata.data[0], 120, "imgdata.data[\""+(0)+"\"]", "120") + imgdata.data[0] = 100; + imgdata.data[0] = " +130e0 "; + assert.strictEqual(imgdata.data[0], 130, "imgdata.data[\""+(0)+"\"]", "130") + }); + + it("2d.imageData.object.clamp", function () { + // ImageData.data clamps numbers to [0, 255] + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = ctx.getImageData(0, 0, 10, 10); + + imgdata.data[0] = 100; + imgdata.data[0] = 300; + assert.strictEqual(imgdata.data[0], 255, "imgdata.data[\""+(0)+"\"]", "255") + imgdata.data[0] = 100; + imgdata.data[0] = -100; + assert.strictEqual(imgdata.data[0], 0, "imgdata.data[\""+(0)+"\"]", "0") + + imgdata.data[0] = 100; + imgdata.data[0] = 200+Math.pow(2, 32); + assert.strictEqual(imgdata.data[0], 255, "imgdata.data[\""+(0)+"\"]", "255") + imgdata.data[0] = 100; + imgdata.data[0] = -200-Math.pow(2, 32); + assert.strictEqual(imgdata.data[0], 0, "imgdata.data[\""+(0)+"\"]", "0") + + imgdata.data[0] = 100; + imgdata.data[0] = Math.pow(10, 39); + assert.strictEqual(imgdata.data[0], 255, "imgdata.data[\""+(0)+"\"]", "255") + imgdata.data[0] = 100; + imgdata.data[0] = -Math.pow(10, 39); + assert.strictEqual(imgdata.data[0], 0, "imgdata.data[\""+(0)+"\"]", "0") + + imgdata.data[0] = 100; + imgdata.data[0] = -Infinity; + assert.strictEqual(imgdata.data[0], 0, "imgdata.data[\""+(0)+"\"]", "0") + imgdata.data[0] = 100; + imgdata.data[0] = Infinity; + assert.strictEqual(imgdata.data[0], 255, "imgdata.data[\""+(0)+"\"]", "255") + }); + + it("2d.imageData.object.round", function () { + // ImageData.data rounds numbers with round-to-zero + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = ctx.getImageData(0, 0, 10, 10); + imgdata.data[0] = 0.499; + assert.strictEqual(imgdata.data[0], 0, "imgdata.data[\""+(0)+"\"]", "0") + imgdata.data[0] = 0.5; + assert.strictEqual(imgdata.data[0], 0, "imgdata.data[\""+(0)+"\"]", "0") + imgdata.data[0] = 0.501; + assert.strictEqual(imgdata.data[0], 1, "imgdata.data[\""+(0)+"\"]", "1") + imgdata.data[0] = 1.499; + assert.strictEqual(imgdata.data[0], 1, "imgdata.data[\""+(0)+"\"]", "1") + imgdata.data[0] = 1.5; + assert.strictEqual(imgdata.data[0], 2, "imgdata.data[\""+(0)+"\"]", "2") + imgdata.data[0] = 1.501; + assert.strictEqual(imgdata.data[0], 2, "imgdata.data[\""+(0)+"\"]", "2") + imgdata.data[0] = 2.5; + assert.strictEqual(imgdata.data[0], 2, "imgdata.data[\""+(0)+"\"]", "2") + imgdata.data[0] = 3.5; + assert.strictEqual(imgdata.data[0], 4, "imgdata.data[\""+(0)+"\"]", "4") + imgdata.data[0] = 252.5; + assert.strictEqual(imgdata.data[0], 252, "imgdata.data[\""+(0)+"\"]", "252") + imgdata.data[0] = 253.5; + assert.strictEqual(imgdata.data[0], 254, "imgdata.data[\""+(0)+"\"]", "254") + imgdata.data[0] = 254.5; + assert.strictEqual(imgdata.data[0], 254, "imgdata.data[\""+(0)+"\"]", "254") + imgdata.data[0] = 256.5; + assert.strictEqual(imgdata.data[0], 255, "imgdata.data[\""+(0)+"\"]", "255") + imgdata.data[0] = -0.5; + assert.strictEqual(imgdata.data[0], 0, "imgdata.data[\""+(0)+"\"]", "0") + imgdata.data[0] = -1.5; + assert.strictEqual(imgdata.data[0], 0, "imgdata.data[\""+(0)+"\"]", "0") + }); + + it("2d.imageData.put.null", function () { + // putImageData() with null imagedata throws TypeError + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.throws(function() { ctx.putImageData(null, 0, 0); }, TypeError); + }); + + it("2d.imageData.put.nonfinite", function () { + // putImageData() throws TypeError if arguments are not finite + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = ctx.getImageData(0, 0, 10, 10); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, -Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, NaN, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, -Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, NaN); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, 10, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, -Infinity, 10, 10, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, NaN, 10, 10, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, 10, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, -Infinity, 10, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, NaN, 10, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, Infinity, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, -Infinity, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, NaN, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, 10, Infinity, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, 10, -Infinity, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, 10, NaN, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, 10, 10, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, 10, 10, -Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, 10, 10, NaN, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, 10, 10, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, 10, 10, 10, -Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, 10, 10, 10, NaN); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, 10, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, Infinity, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, Infinity, Infinity, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, Infinity, Infinity, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, Infinity, Infinity, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, Infinity, Infinity, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, Infinity, 10, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, Infinity, 10, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, Infinity, 10, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, 10, Infinity, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, 10, Infinity, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, 10, Infinity, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, 10, Infinity, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, 10, 10, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, 10, 10, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, 10, 10, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, Infinity, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, Infinity, Infinity, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, Infinity, Infinity, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, Infinity, Infinity, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, Infinity, Infinity, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, Infinity, 10, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, Infinity, 10, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, Infinity, 10, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, 10, Infinity, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, 10, Infinity, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, 10, Infinity, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, 10, Infinity, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, 10, 10, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, 10, 10, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, 10, 10, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, Infinity, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, Infinity, Infinity, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, Infinity, Infinity, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, Infinity, Infinity, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, Infinity, Infinity, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, Infinity, 10, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, Infinity, 10, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, Infinity, 10, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, 10, Infinity, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, 10, Infinity, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, 10, Infinity, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, 10, Infinity, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, 10, 10, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, 10, 10, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, 10, 10, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, Infinity, Infinity, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, Infinity, Infinity, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, Infinity, Infinity, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, Infinity, Infinity, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, Infinity, 10, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, Infinity, 10, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, Infinity, 10, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, 10, Infinity, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, 10, Infinity, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, 10, Infinity, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, 10, 10, Infinity, Infinity); }, TypeError); + }); + + it("2d.imageData.put.basic", function () { + // putImageData() puts image data from getImageData() onto the canvas + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + var imgdata = ctx.getImageData(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.putImageData(imgdata, 0, 0); + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + }); + + it("2d.imageData.put.created", function () { + // putImageData() puts image data from createImageData() onto the canvas + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = ctx.createImageData(100, 50); + for (var i = 0; i < imgdata.data.length; i += 4) { + imgdata.data[i] = 0; + imgdata.data[i+1] = 255; + imgdata.data[i+2] = 0; + imgdata.data[i+3] = 255; + } + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.putImageData(imgdata, 0, 0); + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + }); + + it("2d.imageData.put.wrongtype", function () { + // putImageData() does not accept non-ImageData objects + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = { width: 1, height: 1, data: [255, 0, 0, 255] }; + assert.throws(function() { ctx.putImageData(imgdata, 0, 0); }, TypeError); + assert.throws(function() { ctx.putImageData("cheese", 0, 0); }, TypeError); + assert.throws(function() { ctx.putImageData(42, 0, 0); }, TypeError); + }); + + it("2d.imageData.put.cross", function () { + // putImageData() accepts image data got from a different canvas + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var canvas2 = document.createElement('canvas'); + var ctx2 = canvas2.getContext('2d'); + ctx2.fillStyle = '#0f0'; + ctx2.fillRect(0, 0, 100, 50) + var imgdata = ctx2.getImageData(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.putImageData(imgdata, 0, 0); + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + }); + + it("2d.imageData.put.alpha", function () { + // putImageData() puts non-solid image data correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = 'rgba(0, 255, 0, 0.25)'; + ctx.fillRect(0, 0, 100, 50) + var imgdata = ctx.getImageData(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.putImageData(imgdata, 0, 0); + _assertPixelApprox(canvas, 50,25, 0,255,0,64); + }); + + it("2d.imageData.put.modified", function () { + // putImageData() puts modified image data correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + ctx.fillStyle = '#f00'; + ctx.fillRect(45, 20, 10, 10) + var imgdata = ctx.getImageData(45, 20, 10, 10); + for (var i = 0, len = imgdata.width*imgdata.height*4; i < len; i += 4) + { + imgdata.data[i] = 0; + imgdata.data[i+1] = 255; + } + ctx.putImageData(imgdata, 45, 20); + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + }); + + it("2d.imageData.put.dirty.zero", function () { + // putImageData() with zero-sized dirty rectangle puts nothing + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + var imgdata = ctx.getImageData(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + ctx.putImageData(imgdata, 0, 0, 0, 0, 0, 0); + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + }); + + it("2d.imageData.put.dirty.rect1", function () { + // putImageData() only modifies areas inside the dirty rectangle, using width and height + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 20, 20) + + var imgdata = ctx.getImageData(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + ctx.fillStyle = '#f00'; + ctx.fillRect(40, 20, 20, 20) + ctx.putImageData(imgdata, 40, 20, 0, 0, 20, 20); + + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + _assertPixelApprox(canvas, 35,25, 0,255,0,255); + _assertPixelApprox(canvas, 65,25, 0,255,0,255); + _assertPixelApprox(canvas, 50,15, 0,255,0,255); + _assertPixelApprox(canvas, 50,45, 0,255,0,255); + }); + + it("2d.imageData.put.dirty.rect2", function () { + // putImageData() only modifies areas inside the dirty rectangle, using x and y + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.fillStyle = '#0f0'; + ctx.fillRect(60, 30, 20, 20) + + var imgdata = ctx.getImageData(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + ctx.fillStyle = '#f00'; + ctx.fillRect(40, 20, 20, 20) + ctx.putImageData(imgdata, -20, -10, 60, 30, 20, 20); + + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + _assertPixelApprox(canvas, 35,25, 0,255,0,255); + _assertPixelApprox(canvas, 65,25, 0,255,0,255); + _assertPixelApprox(canvas, 50,15, 0,255,0,255); + _assertPixelApprox(canvas, 50,45, 0,255,0,255); + }); + + it("2d.imageData.put.dirty.negative", function () { + // putImageData() handles negative-sized dirty rectangles correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 20, 20) + + var imgdata = ctx.getImageData(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + ctx.fillStyle = '#f00'; + ctx.fillRect(40, 20, 20, 20) + ctx.putImageData(imgdata, 40, 20, 20, 20, -20, -20); + + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + _assertPixelApprox(canvas, 35,25, 0,255,0,255); + _assertPixelApprox(canvas, 65,25, 0,255,0,255); + _assertPixelApprox(canvas, 50,15, 0,255,0,255); + _assertPixelApprox(canvas, 50,45, 0,255,0,255); + }); + + it("2d.imageData.put.dirty.outside", function () { + // putImageData() handles dirty rectangles outside the canvas correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + + var imgdata = ctx.getImageData(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + + ctx.putImageData(imgdata, 100, 20, 20, 20, -20, -20); + ctx.putImageData(imgdata, 200, 200, 0, 0, 100, 50); + ctx.putImageData(imgdata, 40, 20, -30, -20, 30, 20); + ctx.putImageData(imgdata, -30, 20, 0, 0, 30, 20); + + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + _assertPixelApprox(canvas, 98,15, 0,255,0,255); + _assertPixelApprox(canvas, 98,25, 0,255,0,255); + _assertPixelApprox(canvas, 98,45, 0,255,0,255); + _assertPixelApprox(canvas, 1,5, 0,255,0,255); + _assertPixelApprox(canvas, 1,25, 0,255,0,255); + _assertPixelApprox(canvas, 1,45, 0,255,0,255); + }); + + it("2d.imageData.put.unchanged", function () { + // putImageData(getImageData(...), ...) has no effect + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var i = 0; + for (var y = 0; y < 16; ++y) { + for (var x = 0; x < 16; ++x, ++i) { + ctx.fillStyle = 'rgba(' + i + ',' + (Math.floor(i*1.5) % 256) + ',' + (Math.floor(i*23.3) % 256) + ',' + (i/256) + ')'; + ctx.fillRect(x, y, 1, 1); + } + } + var imgdata1 = ctx.getImageData(0.1, 0.2, 15.8, 15.9); + var olddata = []; + for (var i = 0; i < imgdata1.data.length; ++i) + olddata[i] = imgdata1.data[i]; + + ctx.putImageData(imgdata1, 0.1, 0.2); + + var imgdata2 = ctx.getImageData(0.1, 0.2, 15.8, 15.9); + for (var i = 0; i < imgdata2.data.length; ++i) { + assert.strictEqual(olddata[i], imgdata2.data[i], "olddata[\""+(i)+"\"]", "imgdata2.data[\""+(i)+"\"]") + } + }); + + it("2d.imageData.put.unaffected", function () { + // putImageData() is not affected by context state + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + var imgdata = ctx.getImageData(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.globalAlpha = 0.1; + ctx.globalCompositeOperation = 'destination-atop'; + ctx.shadowColor = '#f00'; + ctx.shadowBlur = 1; + ctx.translate(100, 50); + ctx.scale(0.1, 0.1); + ctx.putImageData(imgdata, 0, 0); + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + }); + + it("2d.imageData.put.clip", function () { + // putImageData() is not affected by clipping regions + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + var imgdata = ctx.getImageData(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.beginPath(); + ctx.rect(0, 0, 50, 50); + ctx.clip(); + ctx.putImageData(imgdata, 0, 0); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }); + + it("2d.imageData.put.path", function () { + // putImageData() does not affect the current path + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.rect(0, 0, 100, 50); + var imgdata = ctx.getImageData(0, 0, 100, 50); + ctx.putImageData(imgdata, 0, 0); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + }); +}); diff --git a/test/wpt/generated/shadows.js b/test/wpt/generated/shadows.js new file mode 100644 index 000000000..91a138519 --- /dev/null +++ b/test/wpt/generated/shadows.js @@ -0,0 +1,1203 @@ +// THIS FILE WAS AUTO-GENERATED. DO NOT EDIT BY HAND. + +const assert = require('assert'); +const path = require('path'); + +const { + createCanvas, + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + CanvasPattern, + CanvasGradient +} = require('../../..'); + +const window = { + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + Uint8ClampedArray, + CanvasPattern, + CanvasGradient +}; + +const document = { + createElement(type, ...args) { + if (type !== "canvas") + throw new Error(`createElement(${type}) not supported`); + return createCanvas(...args); + } +}; + +function _getPixel(canvas, x, y) { + const ctx = canvas.getContext('2d'); + const imgdata = ctx.getImageData(x, y, 1, 1); + return [ imgdata.data[0], imgdata.data[1], imgdata.data[2], imgdata.data[3] ]; +} + +function _assertApprox(actual, expected, epsilon=0, msg="") { + assert(typeof actual === "number", "actual should be a number but got a ${typeof type_actual}"); + + // The epsilon math below does not place nice with NaN and Infinity + // But in this case Infinity = Infinity and NaN = NaN + if (isFinite(actual) || isFinite(expected)) { + assert(Math.abs(actual - expected) <= epsilon, + `expected ${actual} to equal ${expected} +/- ${epsilon}. ${msg}`); + } else { + assert.strictEqual(actual, expected); + } +} + +function _assertPixel(canvas, x, y, r, g, b, a, pos, color) { + const c = _getPixel(canvas, x,y); + assert.strictEqual(c[0], r, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[1], g, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[2], b, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[3], a, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function _assertPixelApprox(canvas, x, y, r, g, b, a, pos, color, tolerance) { + const c = _getPixel(canvas, x,y); + _assertApprox(c[0], r, tolerance, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[1], g, tolerance, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[2], b, tolerance, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[3], a, tolerance, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function assert_throws_js(Type, fn) { + assert.throws(fn, Type); +} + +// Used by font tests to allow fonts to load. +function deferTest() {} + +class Test { + // Two cases of this in the tests, look unnecessary. + done() {} + // Used by font tests to allow fonts to load. + step_func_done(func) { func(); } + // Used for image onload callback. + step_func(func) { func(); } +} + +function step_timeout(result, time) { + // Nothing; code needs to be converted for this to work. +} + +describe("WPT: shadows", function () { + + it("2d.shadow.attributes.shadowBlur.initial", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.shadowBlur, 0, "ctx.shadowBlur", "0") + }); + + it("2d.shadow.attributes.shadowBlur.valid", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.shadowBlur = 1; + assert.strictEqual(ctx.shadowBlur, 1, "ctx.shadowBlur", "1") + + ctx.shadowBlur = 0.5; + assert.strictEqual(ctx.shadowBlur, 0.5, "ctx.shadowBlur", "0.5") + + ctx.shadowBlur = 1e6; + assert.strictEqual(ctx.shadowBlur, 1e6, "ctx.shadowBlur", "1e6") + + ctx.shadowBlur = 0; + assert.strictEqual(ctx.shadowBlur, 0, "ctx.shadowBlur", "0") + }); + + it("2d.shadow.attributes.shadowBlur.invalid", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.shadowBlur = 1; + ctx.shadowBlur = -2; + assert.strictEqual(ctx.shadowBlur, 1, "ctx.shadowBlur", "1") + + ctx.shadowBlur = 1; + ctx.shadowBlur = Infinity; + assert.strictEqual(ctx.shadowBlur, 1, "ctx.shadowBlur", "1") + + ctx.shadowBlur = 1; + ctx.shadowBlur = -Infinity; + assert.strictEqual(ctx.shadowBlur, 1, "ctx.shadowBlur", "1") + + ctx.shadowBlur = 1; + ctx.shadowBlur = NaN; + assert.strictEqual(ctx.shadowBlur, 1, "ctx.shadowBlur", "1") + + ctx.shadowBlur = 1; + ctx.shadowBlur = 'string'; + assert.strictEqual(ctx.shadowBlur, 1, "ctx.shadowBlur", "1") + + ctx.shadowBlur = 1; + ctx.shadowBlur = true; + assert.strictEqual(ctx.shadowBlur, 1, "ctx.shadowBlur", "1") + + ctx.shadowBlur = 1; + ctx.shadowBlur = false; + assert.strictEqual(ctx.shadowBlur, 0, "ctx.shadowBlur", "0") + }); + + it("2d.shadow.attributes.shadowOffset.initial", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.shadowOffsetX, 0, "ctx.shadowOffsetX", "0") + assert.strictEqual(ctx.shadowOffsetY, 0, "ctx.shadowOffsetY", "0") + }); + + it("2d.shadow.attributes.shadowOffset.valid", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 2; + assert.strictEqual(ctx.shadowOffsetX, 1, "ctx.shadowOffsetX", "1") + assert.strictEqual(ctx.shadowOffsetY, 2, "ctx.shadowOffsetY", "2") + + ctx.shadowOffsetX = 0.5; + ctx.shadowOffsetY = 0.25; + assert.strictEqual(ctx.shadowOffsetX, 0.5, "ctx.shadowOffsetX", "0.5") + assert.strictEqual(ctx.shadowOffsetY, 0.25, "ctx.shadowOffsetY", "0.25") + + ctx.shadowOffsetX = -0.5; + ctx.shadowOffsetY = -0.25; + assert.strictEqual(ctx.shadowOffsetX, -0.5, "ctx.shadowOffsetX", "-0.5") + assert.strictEqual(ctx.shadowOffsetY, -0.25, "ctx.shadowOffsetY", "-0.25") + + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + assert.strictEqual(ctx.shadowOffsetX, 0, "ctx.shadowOffsetX", "0") + assert.strictEqual(ctx.shadowOffsetY, 0, "ctx.shadowOffsetY", "0") + + ctx.shadowOffsetX = 1e6; + ctx.shadowOffsetY = 1e6; + assert.strictEqual(ctx.shadowOffsetX, 1e6, "ctx.shadowOffsetX", "1e6") + assert.strictEqual(ctx.shadowOffsetY, 1e6, "ctx.shadowOffsetY", "1e6") + }); + + it("2d.shadow.attributes.shadowOffset.invalid", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 2; + ctx.shadowOffsetX = Infinity; + ctx.shadowOffsetY = Infinity; + assert.strictEqual(ctx.shadowOffsetX, 1, "ctx.shadowOffsetX", "1") + assert.strictEqual(ctx.shadowOffsetY, 2, "ctx.shadowOffsetY", "2") + + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 2; + ctx.shadowOffsetX = -Infinity; + ctx.shadowOffsetY = -Infinity; + assert.strictEqual(ctx.shadowOffsetX, 1, "ctx.shadowOffsetX", "1") + assert.strictEqual(ctx.shadowOffsetY, 2, "ctx.shadowOffsetY", "2") + + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 2; + ctx.shadowOffsetX = NaN; + ctx.shadowOffsetY = NaN; + assert.strictEqual(ctx.shadowOffsetX, 1, "ctx.shadowOffsetX", "1") + assert.strictEqual(ctx.shadowOffsetY, 2, "ctx.shadowOffsetY", "2") + + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 2; + ctx.shadowOffsetX = 'string'; + ctx.shadowOffsetY = 'string'; + assert.strictEqual(ctx.shadowOffsetX, 1, "ctx.shadowOffsetX", "1") + assert.strictEqual(ctx.shadowOffsetY, 2, "ctx.shadowOffsetY", "2") + + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 2; + ctx.shadowOffsetX = true; + ctx.shadowOffsetY = true; + assert.strictEqual(ctx.shadowOffsetX, 1, "ctx.shadowOffsetX", "1") + assert.strictEqual(ctx.shadowOffsetY, 1, "ctx.shadowOffsetY", "1") + + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 2; + ctx.shadowOffsetX = false; + ctx.shadowOffsetY = false; + assert.strictEqual(ctx.shadowOffsetX, 0, "ctx.shadowOffsetX", "0") + assert.strictEqual(ctx.shadowOffsetY, 0, "ctx.shadowOffsetY", "0") + }); + + it("2d.shadow.attributes.shadowColor.initial", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.shadowColor, 'rgba(0, 0, 0, 0)', "ctx.shadowColor", "'rgba(0, 0, 0, 0)'") + }); + + it("2d.shadow.attributes.shadowColor.valid", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.shadowColor = 'lime'; + assert.strictEqual(ctx.shadowColor, '#00ff00', "ctx.shadowColor", "'#00ff00'") + + ctx.shadowColor = 'RGBA(0,255, 0,0)'; + assert.strictEqual(ctx.shadowColor, 'rgba(0, 255, 0, 0)', "ctx.shadowColor", "'rgba(0, 255, 0, 0)'") + }); + + it("2d.shadow.attributes.shadowColor.invalid", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.shadowColor = '#00ff00'; + ctx.shadowColor = 'bogus'; + assert.strictEqual(ctx.shadowColor, '#00ff00', "ctx.shadowColor", "'#00ff00'") + + ctx.shadowColor = '#00ff00'; + ctx.shadowColor = 'red bogus'; + assert.strictEqual(ctx.shadowColor, '#00ff00', "ctx.shadowColor", "'#00ff00'") + + ctx.shadowColor = '#00ff00'; + ctx.shadowColor = ctx; + assert.strictEqual(ctx.shadowColor, '#00ff00', "ctx.shadowColor", "'#00ff00'") + + ctx.shadowColor = '#00ff00'; + ctx.shadowColor = undefined; + assert.strictEqual(ctx.shadowColor, '#00ff00', "ctx.shadowColor", "'#00ff00'") + }); + + it("2d.shadow.enable.off.1", function () { + // Shadows are not drawn when only shadowColor is set + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.shadowColor = '#f00'; + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.enable.off.2", function () { + // Shadows are not drawn when only shadowColor is set + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.globalCompositeOperation = 'destination-atop'; + ctx.shadowColor = '#f00'; + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.enable.blur", function () { + // Shadows are drawn if shadowBlur is set + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.globalCompositeOperation = 'destination-atop'; + ctx.shadowColor = '#0f0'; + ctx.shadowBlur = 0.1; + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.enable.x", function () { + // Shadows are drawn if shadowOffsetX is set + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.globalCompositeOperation = 'destination-atop'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetX = 0.1; + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.enable.y", function () { + // Shadows are drawn if shadowOffsetY is set + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.globalCompositeOperation = 'destination-atop'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 0.1; + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.offset.positiveX", function () { + // Shadows can be offset with positive x + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetX = 50; + ctx.fillRect(0, 0, 50, 50); + _assertPixel(canvas, 25,25, 0,255,0,255); + _assertPixel(canvas, 75,25, 0,255,0,255); + }); + + it("2d.shadow.offset.negativeX", function () { + // Shadows can be offset with negative x + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetX = -50; + ctx.fillRect(50, 0, 50, 50); + _assertPixel(canvas, 25,25, 0,255,0,255); + _assertPixel(canvas, 75,25, 0,255,0,255); + }); + + it("2d.shadow.offset.positiveY", function () { + // Shadows can be offset with positive y + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 25; + ctx.fillRect(0, 0, 100, 25); + _assertPixel(canvas, 50,12, 0,255,0,255); + _assertPixel(canvas, 50,37, 0,255,0,255); + }); + + it("2d.shadow.offset.negativeY", function () { + // Shadows can be offset with negative y + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = -25; + ctx.fillRect(0, 25, 100, 25); + _assertPixel(canvas, 50,12, 0,255,0,255); + _assertPixel(canvas, 50,37, 0,255,0,255); + }); + + it("2d.shadow.outside", function () { + // Shadows of shapes outside the visible area can be offset onto the visible area + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetX = 100; + ctx.fillRect(-100, 0, 25, 50); + ctx.shadowOffsetX = -100; + ctx.fillRect(175, 0, 25, 50); + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 100; + ctx.fillRect(25, -100, 50, 25); + ctx.shadowOffsetY = -100; + ctx.fillRect(25, 125, 50, 25); + _assertPixel(canvas, 12,25, 0,255,0,255); + _assertPixel(canvas, 87,25, 0,255,0,255); + _assertPixel(canvas, 50,12, 0,255,0,255); + _assertPixel(canvas, 50,37, 0,255,0,255); + }); + + it("2d.shadow.clip.1", function () { + // Shadows of clipped shapes are still drawn within the clipping region + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(50, 0, 50, 50); + + ctx.save(); + ctx.beginPath(); + ctx.rect(50, 0, 50, 50); + ctx.clip(); + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetX = 50; + ctx.fillRect(0, 0, 50, 50); + ctx.restore(); + + _assertPixel(canvas, 25,25, 0,255,0,255); + _assertPixel(canvas, 75,25, 0,255,0,255); + }); + + it("2d.shadow.clip.2", function () { + // Shadows are not drawn outside the clipping region + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(50, 0, 50, 50); + + ctx.save(); + ctx.beginPath(); + ctx.rect(0, 0, 50, 50); + ctx.clip(); + ctx.shadowColor = '#f00'; + ctx.shadowOffsetX = 50; + ctx.fillRect(0, 0, 50, 50); + ctx.restore(); + + _assertPixel(canvas, 25,25, 0,255,0,255); + _assertPixel(canvas, 75,25, 0,255,0,255); + }); + + it("2d.shadow.clip.3", function () { + // Shadows of clipped shapes are still drawn within the clipping region + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(50, 0, 50, 50); + + ctx.save(); + ctx.beginPath(); + ctx.rect(0, 0, 50, 50); + ctx.clip(); + ctx.fillStyle = '#f00'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetX = 50; + ctx.fillRect(-50, 0, 50, 50); + ctx.restore(); + + _assertPixel(canvas, 25,25, 0,255,0,255); + _assertPixel(canvas, 75,25, 0,255,0,255); + }); + + it("2d.shadow.stroke.basic", function () { + // Shadows are drawn for strokes + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 50; + ctx.beginPath(); + ctx.lineWidth = 50; + ctx.moveTo(0, -25); + ctx.lineTo(100, -25); + ctx.stroke(); + + _assertPixel(canvas, 1,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 98,25, 0,255,0,255); + }); + + it("2d.shadow.stroke.cap.1", function () { + // Shadows are not drawn for areas outside stroke caps + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.shadowColor = '#f00'; + ctx.shadowOffsetY = 50; + ctx.beginPath(); + ctx.lineWidth = 50; + ctx.lineCap = 'butt'; + ctx.moveTo(-50, -25); + ctx.lineTo(0, -25); + ctx.moveTo(100, -25); + ctx.lineTo(150, -25); + ctx.stroke(); + + _assertPixel(canvas, 1,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 98,25, 0,255,0,255); + }); + + it("2d.shadow.stroke.cap.2", function () { + // Shadows are drawn for stroke caps + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 50; + ctx.beginPath(); + ctx.lineWidth = 50; + ctx.lineCap = 'square'; + ctx.moveTo(25, -25); + ctx.lineTo(75, -25); + ctx.stroke(); + + _assertPixel(canvas, 1,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 98,25, 0,255,0,255); + }); + + it("2d.shadow.stroke.join.1", function () { + // Shadows are not drawn for areas outside stroke joins + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.shadowColor = '#f00'; + ctx.shadowOffsetX = 100; + ctx.lineWidth = 200; + ctx.lineJoin = 'bevel'; + ctx.beginPath(); + ctx.moveTo(-200, -50); + ctx.lineTo(-150, -50); + ctx.lineTo(-151, -100); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.shadow.stroke.join.2", function () { + // Shadows are drawn for stroke joins + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(50, 0, 50, 50); + ctx.strokeStyle = '#f00'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetX = 100; + ctx.lineWidth = 200; + ctx.lineJoin = 'miter'; + ctx.beginPath(); + ctx.moveTo(-200, -50); + ctx.lineTo(-150, -50); + ctx.lineTo(-151, -100); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.shadow.stroke.join.3", function () { + // Shadows are drawn for stroke joins respecting miter limit + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.shadowColor = '#f00'; + ctx.shadowOffsetX = 100; + ctx.lineWidth = 200; + ctx.lineJoin = 'miter'; + ctx.miterLimit = 0.1; + ctx.beginPath(); + ctx.moveTo(-200, -50); + ctx.lineTo(-150, -50); + ctx.lineTo(-151, -100); // (not an exact right angle, to avoid some other bug in Firefox 3) + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.shadow.image.basic", function () { + // Shadows are drawn for images + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 50; + ctx.drawImage(document.getElementById('red.png'), 0, -50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.image.transparent.1", function () { + // Shadows are not drawn for transparent images + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#f00'; + ctx.shadowOffsetY = 50; + ctx.drawImage(document.getElementById('transparent.png'), 0, -50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.image.transparent.2", function () { + // Shadows are not drawn for transparent parts of images + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(50, 0, 50, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#0f0'; + ctx.drawImage(document.getElementById('redtransparent.png'), 50, -50); + ctx.shadowColor = '#f00'; + ctx.drawImage(document.getElementById('redtransparent.png'), -50, -50); + + _assertPixel(canvas, 25,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 75,25, 0,255,0,255); + }); + + it("2d.shadow.image.alpha", function () { + // Shadows are drawn correctly for partially-transparent images + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#00f'; + ctx.drawImage(document.getElementById('transparent50.png'), 0, -50); + + _assertPixelApprox(canvas, 50,25, 127,0,127,255); + }); + + it("2d.shadow.image.section", function () { + // Shadows are not drawn for areas outside image source rectangles + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#f00'; + ctx.drawImage(document.getElementById('redtransparent.png'), 50, 0, 50, 50, 0, -50, 50, 50); + + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }); + + it("2d.shadow.image.scale", function () { + // Shadows are drawn correctly for scaled images + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#0f0'; + ctx.drawImage(document.getElementById('redtransparent.png'), 0, 0, 100, 50, -10, -50, 240, 50); + + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }); + + it("2d.shadow.canvas.basic", function () { + // Shadows are drawn for canvases + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var ctx2 = canvas2.getContext('2d'); + ctx2.fillStyle = '#f00'; + ctx2.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 50; + ctx.drawImage(canvas2, 0, -50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.canvas.transparent.1", function () { + // Shadows are not drawn for transparent canvases + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var ctx2 = canvas2.getContext('2d'); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#f00'; + ctx.shadowOffsetY = 50; + ctx.drawImage(canvas2, 0, -50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.canvas.transparent.2", function () { + // Shadows are not drawn for transparent parts of canvases + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var ctx2 = canvas2.getContext('2d'); + ctx2.fillStyle = '#f00'; + ctx2.fillRect(0, 0, 50, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(50, 0, 50, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#0f0'; + ctx.drawImage(canvas2, 50, -50); + ctx.shadowColor = '#f00'; + ctx.drawImage(canvas2, -50, -50); + + _assertPixel(canvas, 25,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 75,25, 0,255,0,255); + }); + + it("2d.shadow.canvas.alpha", function () { + // Shadows are drawn correctly for partially-transparent canvases + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var ctx2 = canvas2.getContext('2d'); + ctx2.fillStyle = 'rgba(255, 0, 0, 0.5)'; + ctx2.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#00f'; + ctx.drawImage(canvas2, 0, -50); + + _assertPixelApprox(canvas, 50,25, 127,0,127,255); + }); + + it("2d.shadow.pattern.basic", function () { + // Shadows are drawn for fill patterns + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var pattern = ctx.createPattern(document.getElementById('red.png'), 'repeat'); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 50; + ctx.fillStyle = pattern; + ctx.fillRect(0, -50, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.pattern.transparent.1", function () { + // Shadows are not drawn for transparent fill patterns + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var pattern = ctx.createPattern(document.getElementById('transparent.png'), 'repeat'); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#f00'; + ctx.shadowOffsetY = 50; + ctx.fillStyle = pattern; + ctx.fillRect(0, -50, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.pattern.transparent.2", function () { + // Shadows are not drawn for transparent parts of fill patterns + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var pattern = ctx.createPattern(document.getElementById('redtransparent.png'), 'repeat'); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(50, 0, 50, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#0f0'; + ctx.fillStyle = pattern; + ctx.fillRect(0, -50, 100, 50); + + _assertPixel(canvas, 25,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 75,25, 0,255,0,255); + }); + + it("2d.shadow.pattern.alpha", function () { + // Shadows are drawn correctly for partially-transparent fill patterns + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var pattern = ctx.createPattern(document.getElementById('transparent50.png'), 'repeat'); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#00f'; + ctx.fillStyle = pattern; + ctx.fillRect(0, -50, 100, 50); + + _assertPixelApprox(canvas, 50,25, 127,0,127,255); + }); + + it("2d.shadow.gradient.basic", function () { + // Shadows are drawn for gradient fills + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var gradient = ctx.createLinearGradient(0, 0, 100, 0); + gradient.addColorStop(0, '#f00'); + gradient.addColorStop(1, '#f00'); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 50; + ctx.fillStyle = gradient; + ctx.fillRect(0, -50, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.gradient.transparent.1", function () { + // Shadows are not drawn for transparent gradient fills + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var gradient = ctx.createLinearGradient(0, 0, 100, 0); + gradient.addColorStop(0, 'rgba(0,0,0,0)'); + gradient.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#f00'; + ctx.shadowOffsetY = 50; + ctx.fillStyle = gradient; + ctx.fillRect(0, -50, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.gradient.transparent.2", function () { + // Shadows are not drawn for transparent parts of gradient fills + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var gradient = ctx.createLinearGradient(0, 0, 100, 0); + gradient.addColorStop(0, '#f00'); + gradient.addColorStop(0.499, '#f00'); + gradient.addColorStop(0.5, 'rgba(0,0,0,0)'); + gradient.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(50, 0, 50, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#0f0'; + ctx.fillStyle = gradient; + ctx.fillRect(0, -50, 100, 50); + + _assertPixel(canvas, 25,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 75,25, 0,255,0,255); + }); + + it("2d.shadow.gradient.alpha", function () { + // Shadows are drawn correctly for partially-transparent gradient fills + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var gradient = ctx.createLinearGradient(0, 0, 100, 0); + gradient.addColorStop(0, 'rgba(255,0,0,0.5)'); + gradient.addColorStop(1, 'rgba(255,0,0,0.5)'); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#00f'; + ctx.fillStyle = gradient; + ctx.fillRect(0, -50, 100, 50); + + _assertPixelApprox(canvas, 50,25, 127,0,127,255); + }); + + it("2d.shadow.transform.1", function () { + // Shadows take account of transformations + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#0f0'; + ctx.translate(100, 100); + ctx.fillRect(-100, -150, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.transform.2", function () { + // Shadow offsets are not affected by transformations + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#0f0'; + ctx.rotate(Math.PI) + ctx.fillRect(-100, 0, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.blur.low", function () { + // Shadows look correct for small blurs + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#ff0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#00f'; + ctx.shadowOffsetY = 25; + for (var x = 0; x < 100; ++x) { + ctx.save(); + ctx.beginPath(); + ctx.rect(x, 0, 1, 50); + ctx.clip(); + ctx.shadowBlur = x; + ctx.fillRect(-200, -200, 500, 200); + ctx.restore(); + } + }); + + it("2d.shadow.blur.high", function () { + // Shadows look correct for large blurs + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#ff0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#00f'; + ctx.shadowOffsetY = 0; + ctx.shadowBlur = 100; + ctx.fillRect(-200, -200, 200, 400); + }); + + it("2d.shadow.alpha.1", function () { + // Shadow color alpha components are used + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = 'rgba(255, 0, 0, 0.01)'; + ctx.shadowOffsetY = 50; + ctx.fillRect(0, -50, 100, 50); + + _assertPixelApprox(canvas, 50,25, 0,255,0,255, 4); + }); + + it("2d.shadow.alpha.2", function () { + // Shadow color alpha components are used + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = 'rgba(0, 0, 255, 0.5)'; + ctx.shadowOffsetY = 50; + ctx.fillRect(0, -50, 100, 50); + + _assertPixelApprox(canvas, 50,25, 127,0,127,255); + }); + + it("2d.shadow.alpha.3", function () { + // Shadows are affected by globalAlpha + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; // (work around broken Firefox globalAlpha caching) + ctx.shadowColor = '#00f'; + ctx.shadowOffsetY = 50; + ctx.globalAlpha = 0.5; + ctx.fillRect(0, -50, 100, 50); + + _assertPixelApprox(canvas, 50,25, 127,0,127,255); + }); + + it("2d.shadow.alpha.4", function () { + // Shadows with alpha components are correctly affected by globalAlpha + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; // (work around broken Firefox globalAlpha caching) + ctx.shadowColor = 'rgba(0, 0, 255, 0.707)'; + ctx.shadowOffsetY = 50; + ctx.globalAlpha = 0.707; + ctx.fillRect(0, -50, 100, 50); + + _assertPixelApprox(canvas, 50,25, 127,0,127,255); + }); + + it("2d.shadow.alpha.5", function () { + // Shadows of shapes with alpha components are drawn correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = 'rgba(64, 0, 0, 0.5)'; + ctx.shadowColor = '#00f'; + ctx.shadowOffsetY = 50; + ctx.fillRect(0, -50, 100, 50); + + _assertPixelApprox(canvas, 50,25, 127,0,127,255); + }); + + it("2d.shadow.composite.1", function () { + // Shadows are drawn using globalCompositeOperation + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.globalCompositeOperation = 'xor'; + ctx.shadowColor = '#f00'; + ctx.shadowOffsetX = 100; + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, 0, 200, 50); + + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.composite.2", function () { + // Shadows are drawn using globalCompositeOperation + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.globalCompositeOperation = 'xor'; + ctx.shadowColor = '#f00'; + ctx.shadowBlur = 1; + ctx.fillStyle = '#0f0'; + ctx.fillRect(-10, -10, 120, 70); + + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.composite.3", function () { + // Areas outside shadows are drawn correctly with destination-out + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.globalCompositeOperation = 'destination-out'; + ctx.shadowColor = '#f00'; + ctx.shadowBlur = 10; + ctx.fillStyle = '#f00'; + ctx.fillRect(200, 0, 100, 50); + + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + }); +}); diff --git a/test/wpt/generated/text-styles.js b/test/wpt/generated/text-styles.js new file mode 100644 index 000000000..3c227841e --- /dev/null +++ b/test/wpt/generated/text-styles.js @@ -0,0 +1,614 @@ +// THIS FILE WAS AUTO-GENERATED. DO NOT EDIT BY HAND. + +const assert = require('assert'); +const path = require('path'); + +const { + createCanvas, + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + CanvasPattern, + CanvasGradient +} = require('../../..'); + +const window = { + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + Uint8ClampedArray, + CanvasPattern, + CanvasGradient +}; + +const document = { + createElement(type, ...args) { + if (type !== "canvas") + throw new Error(`createElement(${type}) not supported`); + return createCanvas(...args); + } +}; + +function _getPixel(canvas, x, y) { + const ctx = canvas.getContext('2d'); + const imgdata = ctx.getImageData(x, y, 1, 1); + return [ imgdata.data[0], imgdata.data[1], imgdata.data[2], imgdata.data[3] ]; +} + +function _assertApprox(actual, expected, epsilon=0, msg="") { + assert(typeof actual === "number", "actual should be a number but got a ${typeof type_actual}"); + + // The epsilon math below does not place nice with NaN and Infinity + // But in this case Infinity = Infinity and NaN = NaN + if (isFinite(actual) || isFinite(expected)) { + assert(Math.abs(actual - expected) <= epsilon, + `expected ${actual} to equal ${expected} +/- ${epsilon}. ${msg}`); + } else { + assert.strictEqual(actual, expected); + } +} + +function _assertPixel(canvas, x, y, r, g, b, a, pos, color) { + const c = _getPixel(canvas, x,y); + assert.strictEqual(c[0], r, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[1], g, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[2], b, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[3], a, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function _assertPixelApprox(canvas, x, y, r, g, b, a, pos, color, tolerance) { + const c = _getPixel(canvas, x,y); + _assertApprox(c[0], r, tolerance, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[1], g, tolerance, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[2], b, tolerance, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[3], a, tolerance, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function assert_throws_js(Type, fn) { + assert.throws(fn, Type); +} + +// Used by font tests to allow fonts to load. +function deferTest() {} + +class Test { + // Two cases of this in the tests, look unnecessary. + done() {} + // Used by font tests to allow fonts to load. + step_func_done(func) { func(); } + // Used for image onload callback. + step_func(func) { func(); } +} + +function step_timeout(result, time) { + // Nothing; code needs to be converted for this to work. +} + +describe("WPT: text-styles", function () { + + it("2d.text.font.parse.basic", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '20px serif'; + assert.strictEqual(ctx.font, '20px serif', "ctx.font", "'20px serif'") + + ctx.font = '20PX SERIF'; + assert.strictEqual(ctx.font, '20px serif', "ctx.font", "'20px serif'") + }); + + it("2d.text.font.parse.tiny", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '1px sans-serif'; + assert.strictEqual(ctx.font, '1px sans-serif', "ctx.font", "'1px sans-serif'") + }); + + it("2d.text.font.parse.complex", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = 'small-caps italic 400 12px/2 Unknown Font, sans-serif'; + assert.strictEqual(ctx.font, 'italic small-caps 12px "Unknown Font", sans-serif', "ctx.font", "'italic small-caps 12px \"Unknown Font\", sans-serif'") + }); + + it("2d.text.font.parse.family", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '20px cursive,fantasy,monospace,sans-serif,serif,UnquotedFont,"QuotedFont\\\\\\","'; + assert.strictEqual(ctx.font, '20px cursive, fantasy, monospace, sans-serif, serif, UnquotedFont, "QuotedFont\\\\\\","', "ctx.font", "'20px cursive, fantasy, monospace, sans-serif, serif, UnquotedFont, \"QuotedFont\\\\\\\\\\\\\",\"'") + }); + + it("2d.text.font.parse.size.percentage", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50% serif'; + assert.strictEqual(ctx.font, '72px serif', "ctx.font", "'72px serif'") + canvas.setAttribute('style', 'font-size: 100px'); + assert.strictEqual(ctx.font, '72px serif', "ctx.font", "'72px serif'") + }); + + it("2d.text.font.parse.size.percentage.default", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var canvas2 = document.createElement('canvas'); + var ctx2 = canvas2.getContext('2d'); + ctx2.font = '1000% serif'; + assert.strictEqual(ctx2.font, '100px serif', "ctx2.font", "'100px serif'") + }); + + it("2d.text.font.parse.system", function () { + // System fonts must be computed to explicit values + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = 'message-box'; + assert.notStrictEqual(ctx.font, 'message-box', "ctx.font", "'message-box'"); + }); + + it("2d.text.font.parse.invalid", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '20px serif'; + assert.strictEqual(ctx.font, '20px serif', "ctx.font", "'20px serif'") + + ctx.font = '20px serif'; + ctx.font = ''; + assert.strictEqual(ctx.font, '20px serif', "ctx.font", "'20px serif'") + + ctx.font = '20px serif'; + ctx.font = 'bogus'; + assert.strictEqual(ctx.font, '20px serif', "ctx.font", "'20px serif'") + + ctx.font = '20px serif'; + ctx.font = 'inherit'; + assert.strictEqual(ctx.font, '20px serif', "ctx.font", "'20px serif'") + + ctx.font = '20px serif'; + ctx.font = '10px {bogus}'; + assert.strictEqual(ctx.font, '20px serif', "ctx.font", "'20px serif'") + + ctx.font = '20px serif'; + ctx.font = '10px initial'; + assert.strictEqual(ctx.font, '20px serif', "ctx.font", "'20px serif'") + + ctx.font = '20px serif'; + ctx.font = '10px default'; + assert.strictEqual(ctx.font, '20px serif', "ctx.font", "'20px serif'") + + ctx.font = '20px serif'; + ctx.font = '10px inherit'; + assert.strictEqual(ctx.font, '20px serif', "ctx.font", "'20px serif'") + + ctx.font = '20px serif'; + ctx.font = '10px revert'; + assert.strictEqual(ctx.font, '20px serif', "ctx.font", "'20px serif'") + + ctx.font = '20px serif'; + ctx.font = 'var(--x)'; + assert.strictEqual(ctx.font, '20px serif', "ctx.font", "'20px serif'") + + ctx.font = '20px serif'; + ctx.font = 'var(--x, 10px serif)'; + assert.strictEqual(ctx.font, '20px serif', "ctx.font", "'20px serif'") + + ctx.font = '20px serif'; + ctx.font = '1em serif; background: green; margin: 10px'; + assert.strictEqual(ctx.font, '20px serif', "ctx.font", "'20px serif'") + }); + + it("2d.text.font.default", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.font, '10px sans-serif', "ctx.font", "'10px sans-serif'") + }); + + it("2d.text.font.relative_size", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var canvas2 = document.createElement('canvas'); + var ctx2 = canvas2.getContext('2d'); + ctx2.font = '1em sans-serif'; + assert.strictEqual(ctx2.font, '10px sans-serif', "ctx2.font", "'10px sans-serif'") + }); + + it("2d.text.align.valid", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.textAlign = 'start'; + assert.strictEqual(ctx.textAlign, 'start', "ctx.textAlign", "'start'") + + ctx.textAlign = 'end'; + assert.strictEqual(ctx.textAlign, 'end', "ctx.textAlign", "'end'") + + ctx.textAlign = 'left'; + assert.strictEqual(ctx.textAlign, 'left', "ctx.textAlign", "'left'") + + ctx.textAlign = 'right'; + assert.strictEqual(ctx.textAlign, 'right', "ctx.textAlign", "'right'") + + ctx.textAlign = 'center'; + assert.strictEqual(ctx.textAlign, 'center', "ctx.textAlign", "'center'") + }); + + it("2d.text.align.invalid", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.textAlign = 'start'; + ctx.textAlign = 'bogus'; + assert.strictEqual(ctx.textAlign, 'start', "ctx.textAlign", "'start'") + + ctx.textAlign = 'start'; + ctx.textAlign = 'END'; + assert.strictEqual(ctx.textAlign, 'start', "ctx.textAlign", "'start'") + + ctx.textAlign = 'start'; + ctx.textAlign = 'end '; + assert.strictEqual(ctx.textAlign, 'start', "ctx.textAlign", "'start'") + + ctx.textAlign = 'start'; + ctx.textAlign = 'end\0'; + assert.strictEqual(ctx.textAlign, 'start', "ctx.textAlign", "'start'") + }); + + it("2d.text.align.default", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.textAlign, 'start', "ctx.textAlign", "'start'") + }); + + it("2d.text.baseline.valid", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.textBaseline = 'top'; + assert.strictEqual(ctx.textBaseline, 'top', "ctx.textBaseline", "'top'") + + ctx.textBaseline = 'hanging'; + assert.strictEqual(ctx.textBaseline, 'hanging', "ctx.textBaseline", "'hanging'") + + ctx.textBaseline = 'middle'; + assert.strictEqual(ctx.textBaseline, 'middle', "ctx.textBaseline", "'middle'") + + ctx.textBaseline = 'alphabetic'; + assert.strictEqual(ctx.textBaseline, 'alphabetic', "ctx.textBaseline", "'alphabetic'") + + ctx.textBaseline = 'ideographic'; + assert.strictEqual(ctx.textBaseline, 'ideographic', "ctx.textBaseline", "'ideographic'") + + ctx.textBaseline = 'bottom'; + assert.strictEqual(ctx.textBaseline, 'bottom', "ctx.textBaseline", "'bottom'") + }); + + it("2d.text.baseline.invalid", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.textBaseline = 'top'; + ctx.textBaseline = 'bogus'; + assert.strictEqual(ctx.textBaseline, 'top', "ctx.textBaseline", "'top'") + + ctx.textBaseline = 'top'; + ctx.textBaseline = 'MIDDLE'; + assert.strictEqual(ctx.textBaseline, 'top', "ctx.textBaseline", "'top'") + + ctx.textBaseline = 'top'; + ctx.textBaseline = 'middle '; + assert.strictEqual(ctx.textBaseline, 'top', "ctx.textBaseline", "'top'") + + ctx.textBaseline = 'top'; + ctx.textBaseline = 'middle\0'; + assert.strictEqual(ctx.textBaseline, 'top', "ctx.textBaseline", "'top'") + }); + + it("2d.text.baseline.default", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.textBaseline, 'alphabetic', "ctx.textBaseline", "'alphabetic'") + }); + + it("2d.text.draw.baseline.top", function () { + // textBaseline top is the top of the em square (not the bounding box) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textBaseline = 'top'; + ctx.fillText('CC', 0, 0); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + _assertPixelApprox(canvas, 5,45, 0,255,0,255); + _assertPixelApprox(canvas, 95,45, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.baseline.bottom", function () { + // textBaseline bottom is the bottom of the em square (not the bounding box) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textBaseline = 'bottom'; + ctx.fillText('CC', 0, 50); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + _assertPixelApprox(canvas, 5,45, 0,255,0,255); + _assertPixelApprox(canvas, 95,45, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.baseline.middle", function () { + // textBaseline middle is the middle of the em square (not the bounding box) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textBaseline = 'middle'; + ctx.fillText('CC', 0, 25); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + _assertPixelApprox(canvas, 5,45, 0,255,0,255); + _assertPixelApprox(canvas, 95,45, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.baseline.alphabetic", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textBaseline = 'alphabetic'; + ctx.fillText('CC', 0, 37.5); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + _assertPixelApprox(canvas, 5,45, 0,255,0,255); + _assertPixelApprox(canvas, 95,45, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.baseline.ideographic", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textBaseline = 'ideographic'; + ctx.fillText('CC', 0, 31.25); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + _assertPixelApprox(canvas, 5,45, 0,255,0,255); + _assertPixelApprox(canvas, 95,45, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.baseline.hanging", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textBaseline = 'hanging'; + ctx.fillText('CC', 0, 12.5); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + _assertPixelApprox(canvas, 5,45, 0,255,0,255); + _assertPixelApprox(canvas, 95,45, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.space.collapse.space", function () { + // Space characters are converted to U+0020, and collapsed (per CSS) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText('E EE', -100, 37.5); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.space.collapse.other", function () { + // Space characters are converted to U+0020, and collapsed (per CSS) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText('E \x09\x0a\x0c\x0d \x09\x0a\x0c\x0dEE', -100, 37.5); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.space.collapse.start", function () { + // Space characters at the start of a line are collapsed (per CSS) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText(' EE', 0, 37.5); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.space.collapse.end", function () { + // Space characters at the end of a line are collapsed (per CSS) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'right'; + ctx.fillText('EE ', 100, 37.5); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }), 500); + }); + + it("2d.text.measure.width.space", function () { + // Space characters are converted to U+0020 and collapsed (per CSS) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + assert.strictEqual(ctx.measureText('A B').width, 150, "ctx.measureText('A B').width", "150") + assert.strictEqual(ctx.measureText('A B').width, 200, "ctx.measureText('A B').width", "200") + assert.strictEqual(ctx.measureText('A \x09\x0a\x0c\x0d \x09\x0a\x0c\x0dB').width, 150, "ctx.measureText('A \\x09\\x0a\\x0c\\x0d \\x09\\x0a\\x0c\\x0dB').width", "150") + assert(ctx.measureText('A \x0b B').width >= 200, "ctx.measureText('A \\x0b B').width >= 200"); + + assert.strictEqual(ctx.measureText(' AB').width, 100, "ctx.measureText(' AB').width", "100") + assert.strictEqual(ctx.measureText('AB ').width, 100, "ctx.measureText('AB ').width", "100") + }), 500); + }); + }); + + it("2d.text.measure.rtl.text", function () { + // Measurement should follow canvas direction instead text direction + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + metrics = ctx.measureText('اَلْعَرَبِيَّةُ'); + assert(metrics.actualBoundingBoxLeft < metrics.actualBoundingBoxRight, "metrics.actualBoundingBoxLeft < metrics.actualBoundingBoxRight"); + + metrics = ctx.measureText('hello'); + assert(metrics.actualBoundingBoxLeft < metrics.actualBoundingBoxRight, "metrics.actualBoundingBoxLeft < metrics.actualBoundingBoxRight"); + }); + + it("2d.text.measure.boundingBox.textAlign", function () { + // Measurement should be related to textAlignment + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.textAlign = "right"; + metrics = ctx.measureText('hello'); + assert(metrics.actualBoundingBoxLeft > metrics.actualBoundingBoxRight, "metrics.actualBoundingBoxLeft > metrics.actualBoundingBoxRight"); + + ctx.textAlign = "left" + metrics = ctx.measureText('hello'); + assert(metrics.actualBoundingBoxLeft < metrics.actualBoundingBoxRight, "metrics.actualBoundingBoxLeft < metrics.actualBoundingBoxRight"); + }); + + it("2d.text.measure.boundingBox.direction", function () { + // Measurement should follow text direction + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.direction = "ltr"; + metrics = ctx.measureText('hello'); + assert(metrics.actualBoundingBoxLeft < metrics.actualBoundingBoxRight, "metrics.actualBoundingBoxLeft < metrics.actualBoundingBoxRight"); + + ctx.direction = "rtl"; + metrics = ctx.measureText('hello'); + assert(metrics.actualBoundingBoxLeft > metrics.actualBoundingBoxRight, "metrics.actualBoundingBoxLeft > metrics.actualBoundingBoxRight"); + }); +}); diff --git a/test/wpt/generated/the-canvas-element.js b/test/wpt/generated/the-canvas-element.js new file mode 100644 index 000000000..8b1a6817e --- /dev/null +++ b/test/wpt/generated/the-canvas-element.js @@ -0,0 +1,273 @@ +// THIS FILE WAS AUTO-GENERATED. DO NOT EDIT BY HAND. + +const assert = require('assert'); +const path = require('path'); + +const { + createCanvas, + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + CanvasPattern, + CanvasGradient +} = require('../../..'); + +const window = { + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + Uint8ClampedArray, + CanvasPattern, + CanvasGradient +}; + +const document = { + createElement(type, ...args) { + if (type !== "canvas") + throw new Error(`createElement(${type}) not supported`); + return createCanvas(...args); + } +}; + +function _getPixel(canvas, x, y) { + const ctx = canvas.getContext('2d'); + const imgdata = ctx.getImageData(x, y, 1, 1); + return [ imgdata.data[0], imgdata.data[1], imgdata.data[2], imgdata.data[3] ]; +} + +function _assertApprox(actual, expected, epsilon=0, msg="") { + assert(typeof actual === "number", "actual should be a number but got a ${typeof type_actual}"); + + // The epsilon math below does not place nice with NaN and Infinity + // But in this case Infinity = Infinity and NaN = NaN + if (isFinite(actual) || isFinite(expected)) { + assert(Math.abs(actual - expected) <= epsilon, + `expected ${actual} to equal ${expected} +/- ${epsilon}. ${msg}`); + } else { + assert.strictEqual(actual, expected); + } +} + +function _assertPixel(canvas, x, y, r, g, b, a, pos, color) { + const c = _getPixel(canvas, x,y); + assert.strictEqual(c[0], r, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[1], g, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[2], b, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[3], a, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function _assertPixelApprox(canvas, x, y, r, g, b, a, pos, color, tolerance) { + const c = _getPixel(canvas, x,y); + _assertApprox(c[0], r, tolerance, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[1], g, tolerance, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[2], b, tolerance, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[3], a, tolerance, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function assert_throws_js(Type, fn) { + assert.throws(fn, Type); +} + +// Used by font tests to allow fonts to load. +function deferTest() {} + +class Test { + // Two cases of this in the tests, look unnecessary. + done() {} + // Used by font tests to allow fonts to load. + step_func_done(func) { func(); } + // Used for image onload callback. + step_func(func) { func(); } +} + +function step_timeout(result, time) { + // Nothing; code needs to be converted for this to work. +} + +describe("WPT: the-canvas-element", function () { + + it("2d.getcontext.exists", function () { + // The 2D context is implemented + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.notStrictEqual(canvas.getContext('2d'), null, "canvas.getContext('2d')", "null"); + }); + + it("2d.getcontext.invalid.args", function () { + // Calling getContext with invalid arguments. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(canvas.getContext(''), null, "canvas.getContext('')", "null") + assert.strictEqual(canvas.getContext('2d#'), null, "canvas.getContext('2d#')", "null") + assert.strictEqual(canvas.getContext('This is clearly not a valid context name.'), null, "canvas.getContext('This is clearly not a valid context name.')", "null") + assert.strictEqual(canvas.getContext('2d\0'), null, "canvas.getContext('2d\\0')", "null") + assert.strictEqual(canvas.getContext('2\uFF44'), null, "canvas.getContext('2\\uFF44')", "null") + assert.strictEqual(canvas.getContext('2D'), null, "canvas.getContext('2D')", "null") + assert.throws(function() { canvas.getContext(); }, TypeError); + assert.strictEqual(canvas.getContext('null'), null, "canvas.getContext('null')", "null") + assert.strictEqual(canvas.getContext('undefined'), null, "canvas.getContext('undefined')", "null") + }); + + it("2d.getcontext.extraargs.create", function () { + // The 2D context doesn't throw with extra getContext arguments (new context) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.notStrictEqual(document.createElement("canvas").getContext('2d', false, {}, [], 1, "2"), null, "document.createElement(\"canvas\").getContext('2d', false, {}, [], 1, \"2\")", "null"); + assert.notStrictEqual(document.createElement("canvas").getContext('2d', 123), null, "document.createElement(\"canvas\").getContext('2d', 123)", "null"); + assert.notStrictEqual(document.createElement("canvas").getContext('2d', "test"), null, "document.createElement(\"canvas\").getContext('2d', \"test\")", "null"); + assert.notStrictEqual(document.createElement("canvas").getContext('2d', undefined), null, "document.createElement(\"canvas\").getContext('2d', undefined)", "null"); + assert.notStrictEqual(document.createElement("canvas").getContext('2d', null), null, "document.createElement(\"canvas\").getContext('2d', null)", "null"); + assert.notStrictEqual(document.createElement("canvas").getContext('2d', Symbol.hasInstance), null, "document.createElement(\"canvas\").getContext('2d', Symbol.hasInstance)", "null"); + }); + + it("2d.getcontext.extraargs.cache", function () { + // The 2D context doesn't throw with extra getContext arguments (cached) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.notStrictEqual(canvas.getContext('2d', false, {}, [], 1, "2"), null, "canvas.getContext('2d', false, {}, [], 1, \"2\")", "null"); + assert.notStrictEqual(canvas.getContext('2d', 123), null, "canvas.getContext('2d', 123)", "null"); + assert.notStrictEqual(canvas.getContext('2d', "test"), null, "canvas.getContext('2d', \"test\")", "null"); + assert.notStrictEqual(canvas.getContext('2d', undefined), null, "canvas.getContext('2d', undefined)", "null"); + assert.notStrictEqual(canvas.getContext('2d', null), null, "canvas.getContext('2d', null)", "null"); + assert.notStrictEqual(canvas.getContext('2d', Symbol.hasInstance), null, "canvas.getContext('2d', Symbol.hasInstance)", "null"); + }); + + it("2d.type.exists", function () { + // The 2D context interface is a property of 'window' + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert(window.CanvasRenderingContext2D, "window.CanvasRenderingContext2D"); + }); + + it("2d.type.prototype", function () { + // window.CanvasRenderingContext2D.prototype are not [[Writable]] and not [[Configurable]], and its methods are [[Configurable]]. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert(window.CanvasRenderingContext2D.prototype, "window.CanvasRenderingContext2D.prototype"); + assert(window.CanvasRenderingContext2D.prototype.fill, "window.CanvasRenderingContext2D.prototype.fill"); + window.CanvasRenderingContext2D.prototype = null; + assert(window.CanvasRenderingContext2D.prototype, "window.CanvasRenderingContext2D.prototype"); + delete window.CanvasRenderingContext2D.prototype; + assert(window.CanvasRenderingContext2D.prototype, "window.CanvasRenderingContext2D.prototype"); + window.CanvasRenderingContext2D.prototype.fill = 1; + assert.strictEqual(window.CanvasRenderingContext2D.prototype.fill, 1, "window.CanvasRenderingContext2D.prototype.fill", "1") + delete window.CanvasRenderingContext2D.prototype.fill; + assert.strictEqual(window.CanvasRenderingContext2D.prototype.fill, undefined, "window.CanvasRenderingContext2D.prototype.fill", "undefined") + }); + + it("2d.type.replace", function () { + // Interface methods can be overridden + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var fillRect = window.CanvasRenderingContext2D.prototype.fillRect; + window.CanvasRenderingContext2D.prototype.fillRect = function (x, y, w, h) + { + this.fillStyle = '#0f0'; + fillRect.call(this, x, y, w, h); + }; + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.type.extend", function () { + // Interface methods can be added + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + window.CanvasRenderingContext2D.prototype.fillRectGreen = function (x, y, w, h) + { + this.fillStyle = '#0f0'; + this.fillRect(x, y, w, h); + }; + ctx.fillStyle = '#f00'; + ctx.fillRectGreen(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.getcontext.unique", function () { + // getContext('2d') returns the same object + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(canvas.getContext('2d'), canvas.getContext('2d'), "canvas.getContext('2d')", "canvas.getContext('2d')") + }); + + it("2d.getcontext.shared", function () { + // getContext('2d') returns objects which share canvas state + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var ctx2 = canvas.getContext('2d'); + ctx.fillStyle = '#f00'; + ctx2.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.scaled", function () { + // CSS-scaled canvases get drawn correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#00f'; + ctx.fillRect(0, 0, 50, 25); + ctx.fillStyle = '#0ff'; + ctx.fillRect(0, 0, 25, 10); + }); + + it("2d.canvas.reference", function () { + // CanvasRenderingContext2D.canvas refers back to its canvas + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.canvas, canvas, "ctx.canvas", "canvas") + }); + + it("2d.canvas.readonly", function () { + // CanvasRenderingContext2D.canvas is readonly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var c = document.createElement('canvas'); + var d = ctx.canvas; + assert.notStrictEqual(c, d, "c", "d"); + ctx.canvas = c; + assert.strictEqual(ctx.canvas, d, "ctx.canvas", "d") + }); + + it("2d.canvas.context", function () { + // checks CanvasRenderingContext2D prototype + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(Object.getPrototypeOf(CanvasRenderingContext2D.prototype), Object.prototype, "Object.getPrototypeOf(CanvasRenderingContext2D.prototype)", "Object.prototype") + assert.strictEqual(Object.getPrototypeOf(ctx), CanvasRenderingContext2D.prototype, "Object.getPrototypeOf(ctx)", "CanvasRenderingContext2D.prototype") + t.done(); + }); +}); diff --git a/test/wpt/generated/the-canvas-state.js b/test/wpt/generated/the-canvas-state.js new file mode 100644 index 000000000..393bc6cd2 --- /dev/null +++ b/test/wpt/generated/the-canvas-state.js @@ -0,0 +1,206 @@ +// THIS FILE WAS AUTO-GENERATED. DO NOT EDIT BY HAND. + +const assert = require('assert'); +const path = require('path'); + +const { + createCanvas, + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + CanvasPattern, + CanvasGradient +} = require('../../..'); + +const window = { + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + Uint8ClampedArray, + CanvasPattern, + CanvasGradient +}; + +const document = { + createElement(type, ...args) { + if (type !== "canvas") + throw new Error(`createElement(${type}) not supported`); + return createCanvas(...args); + } +}; + +function _getPixel(canvas, x, y) { + const ctx = canvas.getContext('2d'); + const imgdata = ctx.getImageData(x, y, 1, 1); + return [ imgdata.data[0], imgdata.data[1], imgdata.data[2], imgdata.data[3] ]; +} + +function _assertApprox(actual, expected, epsilon=0, msg="") { + assert(typeof actual === "number", "actual should be a number but got a ${typeof type_actual}"); + + // The epsilon math below does not place nice with NaN and Infinity + // But in this case Infinity = Infinity and NaN = NaN + if (isFinite(actual) || isFinite(expected)) { + assert(Math.abs(actual - expected) <= epsilon, + `expected ${actual} to equal ${expected} +/- ${epsilon}. ${msg}`); + } else { + assert.strictEqual(actual, expected); + } +} + +function _assertPixel(canvas, x, y, r, g, b, a, pos, color) { + const c = _getPixel(canvas, x,y); + assert.strictEqual(c[0], r, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[1], g, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[2], b, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[3], a, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function _assertPixelApprox(canvas, x, y, r, g, b, a, pos, color, tolerance) { + const c = _getPixel(canvas, x,y); + _assertApprox(c[0], r, tolerance, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[1], g, tolerance, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[2], b, tolerance, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[3], a, tolerance, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function assert_throws_js(Type, fn) { + assert.throws(fn, Type); +} + +// Used by font tests to allow fonts to load. +function deferTest() {} + +class Test { + // Two cases of this in the tests, look unnecessary. + done() {} + // Used by font tests to allow fonts to load. + step_func_done(func) { func(); } + // Used for image onload callback. + step_func(func) { func(); } +} + +function step_timeout(result, time) { + // Nothing; code needs to be converted for this to work. +} + +describe("WPT: the-canvas-state", function () { + + it("2d.state.saverestore.transformation", function () { + // save()/restore() affects the current transformation matrix + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.save(); + ctx.translate(200, 0); + ctx.restore(); + ctx.fillStyle = '#f00'; + ctx.fillRect(-200, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.state.saverestore.clip", function () { + // save()/restore() affects the clipping path + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.save(); + ctx.rect(0, 0, 1, 1); + ctx.clip(); + ctx.restore(); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.state.saverestore.path", function () { + // save()/restore() does not affect the current path + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.save(); + ctx.rect(0, 0, 100, 50); + ctx.restore(); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.state.saverestore.bitmap", function () { + // save()/restore() does not affect the current bitmap + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.save(); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.restore(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.state.saverestore.stack", function () { + // save()/restore() can be nested as a stack + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.lineWidth = 1; + ctx.save(); + ctx.lineWidth = 2; + ctx.save(); + ctx.lineWidth = 3; + assert.strictEqual(ctx.lineWidth, 3, "ctx.lineWidth", "3") + ctx.restore(); + assert.strictEqual(ctx.lineWidth, 2, "ctx.lineWidth", "2") + ctx.restore(); + assert.strictEqual(ctx.lineWidth, 1, "ctx.lineWidth", "1") + }); + + it("2d.state.saverestore.stackdepth", function () { + // save()/restore() stack depth is not unreasonably limited + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var limit = 512; + for (var i = 1; i < limit; ++i) + { + ctx.save(); + ctx.lineWidth = i; + } + for (var i = limit-1; i > 0; --i) + { + assert.strictEqual(ctx.lineWidth, i, "ctx.lineWidth", "i") + ctx.restore(); + } + }); + + it("2d.state.saverestore.underflow", function () { + // restore() with an empty stack has no effect + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + for (var i = 0; i < 16; ++i) + ctx.restore(); + ctx.lineWidth = 0.5; + ctx.restore(); + assert.strictEqual(ctx.lineWidth, 0.5, "ctx.lineWidth", "0.5") + }); +}); diff --git a/test/wpt/generated/transformations.js b/test/wpt/generated/transformations.js new file mode 100644 index 000000000..a4b5f2fb6 --- /dev/null +++ b/test/wpt/generated/transformations.js @@ -0,0 +1,675 @@ +// THIS FILE WAS AUTO-GENERATED. DO NOT EDIT BY HAND. + +const assert = require('assert'); +const path = require('path'); + +const { + createCanvas, + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + CanvasPattern, + CanvasGradient +} = require('../../..'); + +const window = { + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + Uint8ClampedArray, + CanvasPattern, + CanvasGradient +}; + +const document = { + createElement(type, ...args) { + if (type !== "canvas") + throw new Error(`createElement(${type}) not supported`); + return createCanvas(...args); + } +}; + +function _getPixel(canvas, x, y) { + const ctx = canvas.getContext('2d'); + const imgdata = ctx.getImageData(x, y, 1, 1); + return [ imgdata.data[0], imgdata.data[1], imgdata.data[2], imgdata.data[3] ]; +} + +function _assertApprox(actual, expected, epsilon=0, msg="") { + assert(typeof actual === "number", "actual should be a number but got a ${typeof type_actual}"); + + // The epsilon math below does not place nice with NaN and Infinity + // But in this case Infinity = Infinity and NaN = NaN + if (isFinite(actual) || isFinite(expected)) { + assert(Math.abs(actual - expected) <= epsilon, + `expected ${actual} to equal ${expected} +/- ${epsilon}. ${msg}`); + } else { + assert.strictEqual(actual, expected); + } +} + +function _assertPixel(canvas, x, y, r, g, b, a, pos, color) { + const c = _getPixel(canvas, x,y); + assert.strictEqual(c[0], r, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[1], g, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[2], b, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[3], a, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function _assertPixelApprox(canvas, x, y, r, g, b, a, pos, color, tolerance) { + const c = _getPixel(canvas, x,y); + _assertApprox(c[0], r, tolerance, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[1], g, tolerance, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[2], b, tolerance, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[3], a, tolerance, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function assert_throws_js(Type, fn) { + assert.throws(fn, Type); +} + +// Used by font tests to allow fonts to load. +function deferTest() {} + +class Test { + // Two cases of this in the tests, look unnecessary. + done() {} + // Used by font tests to allow fonts to load. + step_func_done(func) { func(); } + // Used for image onload callback. + step_func(func) { func(); } +} + +function step_timeout(result, time) { + // Nothing; code needs to be converted for this to work. +} + +describe("WPT: transformations", function () { + + it("2d.transformation.order", function () { + // Transformations are applied in the right order + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.scale(2, 1); + ctx.rotate(Math.PI / 2); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, -50, 50, 50); + _assertPixel(canvas, 75,25, 0,255,0,255); + }); + + it("2d.transformation.scale.basic", function () { + // scale() works + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.scale(2, 4); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 12.5); + _assertPixel(canvas, 90,40, 0,255,0,255); + }); + + it("2d.transformation.scale.zero", function () { + // scale() with a scale factor of zero works + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.save(); + ctx.translate(50, 0); + ctx.scale(0, 1); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.restore(); + + ctx.save(); + ctx.translate(0, 25); + ctx.scale(1, 0); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.restore(); + + canvas.toDataURL(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.transformation.scale.negative", function () { + // scale() with negative scale factors works + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.save(); + ctx.scale(-1, 1); + ctx.fillStyle = '#0f0'; + ctx.fillRect(-50, 0, 50, 50); + ctx.restore(); + + ctx.save(); + ctx.scale(1, -1); + ctx.fillStyle = '#0f0'; + ctx.fillRect(50, -50, 50, 50); + ctx.restore(); + _assertPixel(canvas, 25,25, 0,255,0,255); + _assertPixel(canvas, 75,25, 0,255,0,255); + }); + + it("2d.transformation.scale.large", function () { + // scale() with large scale factors works + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.scale(1e5, 1e5); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 1, 1); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.transformation.scale.nonfinite", function () { + // scale() with Infinity/NaN is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(100, 10); + ctx.scale(Infinity, 0.1); + ctx.scale(-Infinity, 0.1); + ctx.scale(NaN, 0.1); + ctx.scale(0.1, Infinity); + ctx.scale(0.1, -Infinity); + ctx.scale(0.1, NaN); + ctx.scale(Infinity, Infinity); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -10, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.transformation.scale.multiple", function () { + // Multiple scale()s combine + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.scale(Math.sqrt(2), Math.sqrt(2)); + ctx.scale(Math.sqrt(2), Math.sqrt(2)); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 25); + _assertPixel(canvas, 90,40, 0,255,0,255); + }); + + it("2d.transformation.rotate.zero", function () { + // rotate() by 0 does nothing + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.rotate(0); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.transformation.rotate.radians", function () { + // rotate() uses radians + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.rotate(Math.PI); // should fail obviously if this is 3.1 degrees + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -50, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.transformation.rotate.direction", function () { + // rotate() is clockwise + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.rotate(Math.PI / 2); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, -100, 50, 100); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.transformation.rotate.wrap", function () { + // rotate() wraps large positive values correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.rotate(Math.PI * (1 + 4096)); // == pi (mod 2*pi) + // We need about pi +/- 0.001 in order to get correct-looking results + // 32-bit floats can store pi*4097 with precision 2^-10, so that should + // be safe enough on reasonable implementations + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -50, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 98,2, 0,255,0,255); + _assertPixel(canvas, 98,47, 0,255,0,255); + }); + + it("2d.transformation.rotate.wrapnegative", function () { + // rotate() wraps large negative values correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.rotate(-Math.PI * (1 + 4096)); + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -50, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 98,2, 0,255,0,255); + _assertPixel(canvas, 98,47, 0,255,0,255); + }); + + it("2d.transformation.rotate.nonfinite", function () { + // rotate() with Infinity/NaN is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(100, 10); + ctx.rotate(Infinity); + ctx.rotate(-Infinity); + ctx.rotate(NaN); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -10, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.transformation.translate.basic", function () { + // translate() works + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -50, 100, 50); + _assertPixel(canvas, 90,40, 0,255,0,255); + }); + + it("2d.transformation.translate.nonfinite", function () { + // translate() with Infinity/NaN is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(100, 10); + ctx.translate(Infinity, 0.1); + ctx.translate(-Infinity, 0.1); + ctx.translate(NaN, 0.1); + ctx.translate(0.1, Infinity); + ctx.translate(0.1, -Infinity); + ctx.translate(0.1, NaN); + ctx.translate(Infinity, Infinity); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -10, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.transformation.transform.identity", function () { + // transform() with the identity matrix does nothing + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.transform(1,0, 0,1, 0,0); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.transformation.transform.skewed", function () { + // transform() with skewy matrix transforms correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + // Create green with a red square ring inside it + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(20, 10, 60, 30); + ctx.fillStyle = '#0f0'; + ctx.fillRect(40, 20, 20, 10); + + // Draw a skewed shape to fill that gap, to make sure it is aligned correctly + ctx.transform(1,4, 2,3, 5,6); + // Post-transform coordinates: + // [[20,10],[80,10],[80,40],[20,40],[20,10],[40,20],[40,30],[60,30],[60,20],[40,20],[20,10]]; + // Hence pre-transform coordinates: + var pts=[[-7.4,11.2],[-43.4,59.2],[-31.4,53.2],[4.6,5.2],[-7.4,11.2], + [-15.4,25.2],[-11.4,23.2],[-23.4,39.2],[-27.4,41.2],[-15.4,25.2], + [-7.4,11.2]]; + ctx.beginPath(); + ctx.moveTo(pts[0][0], pts[0][1]); + for (var i = 0; i < pts.length; ++i) + ctx.lineTo(pts[i][0], pts[i][1]); + ctx.fill(); + _assertPixel(canvas, 21,11, 0,255,0,255); + _assertPixel(canvas, 79,11, 0,255,0,255); + _assertPixel(canvas, 21,39, 0,255,0,255); + _assertPixel(canvas, 79,39, 0,255,0,255); + _assertPixel(canvas, 39,19, 0,255,0,255); + _assertPixel(canvas, 61,19, 0,255,0,255); + _assertPixel(canvas, 39,31, 0,255,0,255); + _assertPixel(canvas, 61,31, 0,255,0,255); + }); + + it("2d.transformation.transform.multiply", function () { + // transform() multiplies the CTM + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.transform(1,2, 3,4, 5,6); + ctx.transform(-2,1, 3/2,-1/2, 1,-2); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.transformation.transform.nonfinite", function () { + // transform() with Infinity/NaN is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(100, 10); + ctx.transform(Infinity, 0, 0, 0, 0, 0); + ctx.transform(-Infinity, 0, 0, 0, 0, 0); + ctx.transform(NaN, 0, 0, 0, 0, 0); + ctx.transform(0, Infinity, 0, 0, 0, 0); + ctx.transform(0, -Infinity, 0, 0, 0, 0); + ctx.transform(0, NaN, 0, 0, 0, 0); + ctx.transform(0, 0, Infinity, 0, 0, 0); + ctx.transform(0, 0, -Infinity, 0, 0, 0); + ctx.transform(0, 0, NaN, 0, 0, 0); + ctx.transform(0, 0, 0, Infinity, 0, 0); + ctx.transform(0, 0, 0, -Infinity, 0, 0); + ctx.transform(0, 0, 0, NaN, 0, 0); + ctx.transform(0, 0, 0, 0, Infinity, 0); + ctx.transform(0, 0, 0, 0, -Infinity, 0); + ctx.transform(0, 0, 0, 0, NaN, 0); + ctx.transform(0, 0, 0, 0, 0, Infinity); + ctx.transform(0, 0, 0, 0, 0, -Infinity); + ctx.transform(0, 0, 0, 0, 0, NaN); + ctx.transform(Infinity, Infinity, 0, 0, 0, 0); + ctx.transform(Infinity, Infinity, Infinity, 0, 0, 0); + ctx.transform(Infinity, Infinity, Infinity, Infinity, 0, 0); + ctx.transform(Infinity, Infinity, Infinity, Infinity, Infinity, 0); + ctx.transform(Infinity, Infinity, Infinity, Infinity, Infinity, Infinity); + ctx.transform(Infinity, Infinity, Infinity, Infinity, 0, Infinity); + ctx.transform(Infinity, Infinity, Infinity, 0, Infinity, 0); + ctx.transform(Infinity, Infinity, Infinity, 0, Infinity, Infinity); + ctx.transform(Infinity, Infinity, Infinity, 0, 0, Infinity); + ctx.transform(Infinity, Infinity, 0, Infinity, 0, 0); + ctx.transform(Infinity, Infinity, 0, Infinity, Infinity, 0); + ctx.transform(Infinity, Infinity, 0, Infinity, Infinity, Infinity); + ctx.transform(Infinity, Infinity, 0, Infinity, 0, Infinity); + ctx.transform(Infinity, Infinity, 0, 0, Infinity, 0); + ctx.transform(Infinity, Infinity, 0, 0, Infinity, Infinity); + ctx.transform(Infinity, Infinity, 0, 0, 0, Infinity); + ctx.transform(Infinity, 0, Infinity, 0, 0, 0); + ctx.transform(Infinity, 0, Infinity, Infinity, 0, 0); + ctx.transform(Infinity, 0, Infinity, Infinity, Infinity, 0); + ctx.transform(Infinity, 0, Infinity, Infinity, Infinity, Infinity); + ctx.transform(Infinity, 0, Infinity, Infinity, 0, Infinity); + ctx.transform(Infinity, 0, Infinity, 0, Infinity, 0); + ctx.transform(Infinity, 0, Infinity, 0, Infinity, Infinity); + ctx.transform(Infinity, 0, Infinity, 0, 0, Infinity); + ctx.transform(Infinity, 0, 0, Infinity, 0, 0); + ctx.transform(Infinity, 0, 0, Infinity, Infinity, 0); + ctx.transform(Infinity, 0, 0, Infinity, Infinity, Infinity); + ctx.transform(Infinity, 0, 0, Infinity, 0, Infinity); + ctx.transform(Infinity, 0, 0, 0, Infinity, 0); + ctx.transform(Infinity, 0, 0, 0, Infinity, Infinity); + ctx.transform(Infinity, 0, 0, 0, 0, Infinity); + ctx.transform(0, Infinity, Infinity, 0, 0, 0); + ctx.transform(0, Infinity, Infinity, Infinity, 0, 0); + ctx.transform(0, Infinity, Infinity, Infinity, Infinity, 0); + ctx.transform(0, Infinity, Infinity, Infinity, Infinity, Infinity); + ctx.transform(0, Infinity, Infinity, Infinity, 0, Infinity); + ctx.transform(0, Infinity, Infinity, 0, Infinity, 0); + ctx.transform(0, Infinity, Infinity, 0, Infinity, Infinity); + ctx.transform(0, Infinity, Infinity, 0, 0, Infinity); + ctx.transform(0, Infinity, 0, Infinity, 0, 0); + ctx.transform(0, Infinity, 0, Infinity, Infinity, 0); + ctx.transform(0, Infinity, 0, Infinity, Infinity, Infinity); + ctx.transform(0, Infinity, 0, Infinity, 0, Infinity); + ctx.transform(0, Infinity, 0, 0, Infinity, 0); + ctx.transform(0, Infinity, 0, 0, Infinity, Infinity); + ctx.transform(0, Infinity, 0, 0, 0, Infinity); + ctx.transform(0, 0, Infinity, Infinity, 0, 0); + ctx.transform(0, 0, Infinity, Infinity, Infinity, 0); + ctx.transform(0, 0, Infinity, Infinity, Infinity, Infinity); + ctx.transform(0, 0, Infinity, Infinity, 0, Infinity); + ctx.transform(0, 0, Infinity, 0, Infinity, 0); + ctx.transform(0, 0, Infinity, 0, Infinity, Infinity); + ctx.transform(0, 0, Infinity, 0, 0, Infinity); + ctx.transform(0, 0, 0, Infinity, Infinity, 0); + ctx.transform(0, 0, 0, Infinity, Infinity, Infinity); + ctx.transform(0, 0, 0, Infinity, 0, Infinity); + ctx.transform(0, 0, 0, 0, Infinity, Infinity); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -10, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.transformation.setTransform.skewed", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + // Create green with a red square ring inside it + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(20, 10, 60, 30); + ctx.fillStyle = '#0f0'; + ctx.fillRect(40, 20, 20, 10); + + // Draw a skewed shape to fill that gap, to make sure it is aligned correctly + ctx.setTransform(1,4, 2,3, 5,6); + // Post-transform coordinates: + // [[20,10],[80,10],[80,40],[20,40],[20,10],[40,20],[40,30],[60,30],[60,20],[40,20],[20,10]]; + // Hence pre-transform coordinates: + var pts=[[-7.4,11.2],[-43.4,59.2],[-31.4,53.2],[4.6,5.2],[-7.4,11.2], + [-15.4,25.2],[-11.4,23.2],[-23.4,39.2],[-27.4,41.2],[-15.4,25.2], + [-7.4,11.2]]; + ctx.beginPath(); + ctx.moveTo(pts[0][0], pts[0][1]); + for (var i = 0; i < pts.length; ++i) + ctx.lineTo(pts[i][0], pts[i][1]); + ctx.fill(); + _assertPixel(canvas, 21,11, 0,255,0,255); + _assertPixel(canvas, 79,11, 0,255,0,255); + _assertPixel(canvas, 21,39, 0,255,0,255); + _assertPixel(canvas, 79,39, 0,255,0,255); + _assertPixel(canvas, 39,19, 0,255,0,255); + _assertPixel(canvas, 61,19, 0,255,0,255); + _assertPixel(canvas, 39,31, 0,255,0,255); + _assertPixel(canvas, 61,31, 0,255,0,255); + }); + + it("2d.transformation.setTransform.multiple", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.setTransform(1/2,0, 0,1/2, 0,0); + ctx.setTransform(); + ctx.setTransform(2,0, 0,2, 0,0); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 25); + _assertPixel(canvas, 75,35, 0,255,0,255); + }); + + it("2d.transformation.setTransform.nonfinite", function () { + // setTransform() with Infinity/NaN is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(100, 10); + ctx.setTransform(Infinity, 0, 0, 0, 0, 0); + ctx.setTransform(-Infinity, 0, 0, 0, 0, 0); + ctx.setTransform(NaN, 0, 0, 0, 0, 0); + ctx.setTransform(0, Infinity, 0, 0, 0, 0); + ctx.setTransform(0, -Infinity, 0, 0, 0, 0); + ctx.setTransform(0, NaN, 0, 0, 0, 0); + ctx.setTransform(0, 0, Infinity, 0, 0, 0); + ctx.setTransform(0, 0, -Infinity, 0, 0, 0); + ctx.setTransform(0, 0, NaN, 0, 0, 0); + ctx.setTransform(0, 0, 0, Infinity, 0, 0); + ctx.setTransform(0, 0, 0, -Infinity, 0, 0); + ctx.setTransform(0, 0, 0, NaN, 0, 0); + ctx.setTransform(0, 0, 0, 0, Infinity, 0); + ctx.setTransform(0, 0, 0, 0, -Infinity, 0); + ctx.setTransform(0, 0, 0, 0, NaN, 0); + ctx.setTransform(0, 0, 0, 0, 0, Infinity); + ctx.setTransform(0, 0, 0, 0, 0, -Infinity); + ctx.setTransform(0, 0, 0, 0, 0, NaN); + ctx.setTransform(Infinity, Infinity, 0, 0, 0, 0); + ctx.setTransform(Infinity, Infinity, Infinity, 0, 0, 0); + ctx.setTransform(Infinity, Infinity, Infinity, Infinity, 0, 0); + ctx.setTransform(Infinity, Infinity, Infinity, Infinity, Infinity, 0); + ctx.setTransform(Infinity, Infinity, Infinity, Infinity, Infinity, Infinity); + ctx.setTransform(Infinity, Infinity, Infinity, Infinity, 0, Infinity); + ctx.setTransform(Infinity, Infinity, Infinity, 0, Infinity, 0); + ctx.setTransform(Infinity, Infinity, Infinity, 0, Infinity, Infinity); + ctx.setTransform(Infinity, Infinity, Infinity, 0, 0, Infinity); + ctx.setTransform(Infinity, Infinity, 0, Infinity, 0, 0); + ctx.setTransform(Infinity, Infinity, 0, Infinity, Infinity, 0); + ctx.setTransform(Infinity, Infinity, 0, Infinity, Infinity, Infinity); + ctx.setTransform(Infinity, Infinity, 0, Infinity, 0, Infinity); + ctx.setTransform(Infinity, Infinity, 0, 0, Infinity, 0); + ctx.setTransform(Infinity, Infinity, 0, 0, Infinity, Infinity); + ctx.setTransform(Infinity, Infinity, 0, 0, 0, Infinity); + ctx.setTransform(Infinity, 0, Infinity, 0, 0, 0); + ctx.setTransform(Infinity, 0, Infinity, Infinity, 0, 0); + ctx.setTransform(Infinity, 0, Infinity, Infinity, Infinity, 0); + ctx.setTransform(Infinity, 0, Infinity, Infinity, Infinity, Infinity); + ctx.setTransform(Infinity, 0, Infinity, Infinity, 0, Infinity); + ctx.setTransform(Infinity, 0, Infinity, 0, Infinity, 0); + ctx.setTransform(Infinity, 0, Infinity, 0, Infinity, Infinity); + ctx.setTransform(Infinity, 0, Infinity, 0, 0, Infinity); + ctx.setTransform(Infinity, 0, 0, Infinity, 0, 0); + ctx.setTransform(Infinity, 0, 0, Infinity, Infinity, 0); + ctx.setTransform(Infinity, 0, 0, Infinity, Infinity, Infinity); + ctx.setTransform(Infinity, 0, 0, Infinity, 0, Infinity); + ctx.setTransform(Infinity, 0, 0, 0, Infinity, 0); + ctx.setTransform(Infinity, 0, 0, 0, Infinity, Infinity); + ctx.setTransform(Infinity, 0, 0, 0, 0, Infinity); + ctx.setTransform(0, Infinity, Infinity, 0, 0, 0); + ctx.setTransform(0, Infinity, Infinity, Infinity, 0, 0); + ctx.setTransform(0, Infinity, Infinity, Infinity, Infinity, 0); + ctx.setTransform(0, Infinity, Infinity, Infinity, Infinity, Infinity); + ctx.setTransform(0, Infinity, Infinity, Infinity, 0, Infinity); + ctx.setTransform(0, Infinity, Infinity, 0, Infinity, 0); + ctx.setTransform(0, Infinity, Infinity, 0, Infinity, Infinity); + ctx.setTransform(0, Infinity, Infinity, 0, 0, Infinity); + ctx.setTransform(0, Infinity, 0, Infinity, 0, 0); + ctx.setTransform(0, Infinity, 0, Infinity, Infinity, 0); + ctx.setTransform(0, Infinity, 0, Infinity, Infinity, Infinity); + ctx.setTransform(0, Infinity, 0, Infinity, 0, Infinity); + ctx.setTransform(0, Infinity, 0, 0, Infinity, 0); + ctx.setTransform(0, Infinity, 0, 0, Infinity, Infinity); + ctx.setTransform(0, Infinity, 0, 0, 0, Infinity); + ctx.setTransform(0, 0, Infinity, Infinity, 0, 0); + ctx.setTransform(0, 0, Infinity, Infinity, Infinity, 0); + ctx.setTransform(0, 0, Infinity, Infinity, Infinity, Infinity); + ctx.setTransform(0, 0, Infinity, Infinity, 0, Infinity); + ctx.setTransform(0, 0, Infinity, 0, Infinity, 0); + ctx.setTransform(0, 0, Infinity, 0, Infinity, Infinity); + ctx.setTransform(0, 0, Infinity, 0, 0, Infinity); + ctx.setTransform(0, 0, 0, Infinity, Infinity, 0); + ctx.setTransform(0, 0, 0, Infinity, Infinity, Infinity); + ctx.setTransform(0, 0, 0, Infinity, 0, Infinity); + ctx.setTransform(0, 0, 0, 0, Infinity, Infinity); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -10, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); +}); diff --git a/test/wpt/line-styles.yaml b/test/wpt/line-styles.yaml new file mode 100644 index 000000000..e6dc3205e --- /dev/null +++ b/test/wpt/line-styles.yaml @@ -0,0 +1,1017 @@ +- name: 2d.line.defaults + testing: + - 2d.lineWidth.default + - 2d.lineCap.default + - 2d.lineJoin.default + - 2d.miterLimit.default + code: | + @assert ctx.lineWidth === 1; + @assert ctx.lineCap === 'butt'; + @assert ctx.lineJoin === 'miter'; + @assert ctx.miterLimit === 10; + +- name: 2d.line.width.basic + desc: lineWidth determines the width of line strokes + testing: + - 2d.lineWidth + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 20; + // Draw a green line over a red box, to check the line is not too small + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + ctx.fillRect(15, 15, 20, 20); + ctx.beginPath(); + ctx.moveTo(25, 15); + ctx.lineTo(25, 35); + ctx.stroke(); + + // Draw a green box over a red line, to check the line is not too large + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(75, 15); + ctx.lineTo(75, 35); + ctx.stroke(); + ctx.fillRect(65, 15, 20, 20); + + @assert pixel 14,25 == 0,255,0,255; + @assert pixel 15,25 == 0,255,0,255; + @assert pixel 16,25 == 0,255,0,255; + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 34,25 == 0,255,0,255; + @assert pixel 35,25 == 0,255,0,255; + @assert pixel 36,25 == 0,255,0,255; + + @assert pixel 64,25 == 0,255,0,255; + @assert pixel 65,25 == 0,255,0,255; + @assert pixel 66,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + @assert pixel 84,25 == 0,255,0,255; + @assert pixel 85,25 == 0,255,0,255; + @assert pixel 86,25 == 0,255,0,255; + expected: green + +- name: 2d.line.width.transformed + desc: Line stroke widths are affected by scale transformations + testing: + - 2d.lineWidth + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 4; + // Draw a green line over a red box, to check the line is not too small + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + ctx.fillRect(15, 15, 20, 20); + ctx.save(); + ctx.scale(5, 1); + ctx.beginPath(); + ctx.moveTo(5, 15); + ctx.lineTo(5, 35); + ctx.stroke(); + ctx.restore(); + + // Draw a green box over a red line, to check the line is not too large + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.save(); + ctx.scale(-5, 1); + ctx.beginPath(); + ctx.moveTo(-15, 15); + ctx.lineTo(-15, 35); + ctx.stroke(); + ctx.restore(); + ctx.fillRect(65, 15, 20, 20); + + @assert pixel 14,25 == 0,255,0,255; + @assert pixel 15,25 == 0,255,0,255; + @assert pixel 16,25 == 0,255,0,255; + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 34,25 == 0,255,0,255; + @assert pixel 35,25 == 0,255,0,255; + @assert pixel 36,25 == 0,255,0,255; + + @assert pixel 64,25 == 0,255,0,255; + @assert pixel 65,25 == 0,255,0,255; + @assert pixel 66,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + @assert pixel 84,25 == 0,255,0,255; + @assert pixel 85,25 == 0,255,0,255; + @assert pixel 86,25 == 0,255,0,255; + expected: green + +- name: 2d.line.width.scaledefault + desc: Default lineWidth strokes are affected by scale transformations + testing: + - 2d.lineWidth + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.scale(50, 50); + ctx.strokeStyle = '#0f0'; + ctx.moveTo(0, 0.5); + ctx.lineTo(2, 0.5); + ctx.stroke(); + + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + @assert pixel 50,5 == 0,255,0,255; + @assert pixel 50,45 == 0,255,0,255; + expected: green + +- name: 2d.line.width.valid + desc: Setting lineWidth to valid values works + testing: + - 2d.lineWidth.set + - 2d.lineWidth.get + code: | + ctx.lineWidth = 1.5; + @assert ctx.lineWidth === 1.5; + + ctx.lineWidth = "1e1"; + @assert ctx.lineWidth === 10; + + ctx.lineWidth = 1/1024; + @assert ctx.lineWidth === 1/1024; + + ctx.lineWidth = 1000; + @assert ctx.lineWidth === 1000; + +- name: 2d.line.width.invalid + desc: Setting lineWidth to invalid values is ignored + testing: + - 2d.lineWidth.invalid + code: | + ctx.lineWidth = 1.5; + @assert ctx.lineWidth === 1.5; + + ctx.lineWidth = 1.5; + ctx.lineWidth = 0; + @assert ctx.lineWidth === 1.5; + + ctx.lineWidth = 1.5; + ctx.lineWidth = -1; + @assert ctx.lineWidth === 1.5; + + ctx.lineWidth = 1.5; + ctx.lineWidth = Infinity; + @assert ctx.lineWidth === 1.5; + + ctx.lineWidth = 1.5; + ctx.lineWidth = -Infinity; + @assert ctx.lineWidth === 1.5; + + ctx.lineWidth = 1.5; + ctx.lineWidth = NaN; + @assert ctx.lineWidth === 1.5; + + ctx.lineWidth = 1.5; + ctx.lineWidth = 'string'; + @assert ctx.lineWidth === 1.5; + + ctx.lineWidth = 1.5; + ctx.lineWidth = true; + @assert ctx.lineWidth === 1; + + ctx.lineWidth = 1.5; + ctx.lineWidth = false; + @assert ctx.lineWidth === 1.5; + +- name: 2d.line.cap.butt + desc: lineCap 'butt' is rendered correctly + testing: + - 2d.lineCap.butt + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineCap = 'butt'; + ctx.lineWidth = 20; + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + ctx.fillRect(15, 15, 20, 20); + ctx.beginPath(); + ctx.moveTo(25, 15); + ctx.lineTo(25, 35); + ctx.stroke(); + + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(75, 15); + ctx.lineTo(75, 35); + ctx.stroke(); + ctx.fillRect(65, 15, 20, 20); + + @assert pixel 25,14 == 0,255,0,255; + @assert pixel 25,15 == 0,255,0,255; + @assert pixel 25,16 == 0,255,0,255; + @assert pixel 25,34 == 0,255,0,255; + @assert pixel 25,35 == 0,255,0,255; + @assert pixel 25,36 == 0,255,0,255; + + @assert pixel 75,14 == 0,255,0,255; + @assert pixel 75,15 == 0,255,0,255; + @assert pixel 75,16 == 0,255,0,255; + @assert pixel 75,34 == 0,255,0,255; + @assert pixel 75,35 == 0,255,0,255; + @assert pixel 75,36 == 0,255,0,255; + expected: green + +- name: 2d.line.cap.round + desc: lineCap 'round' is rendered correctly + testing: + - 2d.lineCap.round + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var tol = 1; // tolerance to avoid antialiasing artifacts + + ctx.lineCap = 'round'; + ctx.lineWidth = 20; + + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + + ctx.beginPath(); + ctx.moveTo(35-tol, 15); + ctx.arc(25, 15, 10-tol, 0, Math.PI, true); + ctx.arc(25, 35, 10-tol, Math.PI, 0, true); + ctx.fill(); + + ctx.beginPath(); + ctx.moveTo(25, 15); + ctx.lineTo(25, 35); + ctx.stroke(); + + + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + + ctx.beginPath(); + ctx.moveTo(75, 15); + ctx.lineTo(75, 35); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(85+tol, 15); + ctx.arc(75, 15, 10+tol, 0, Math.PI, true); + ctx.arc(75, 35, 10+tol, Math.PI, 0, true); + ctx.fill(); + + @assert pixel 17,6 == 0,255,0,255; + @assert pixel 25,6 == 0,255,0,255; + @assert pixel 32,6 == 0,255,0,255; + @assert pixel 17,43 == 0,255,0,255; + @assert pixel 25,43 == 0,255,0,255; + @assert pixel 32,43 == 0,255,0,255; + + @assert pixel 67,6 == 0,255,0,255; + @assert pixel 75,6 == 0,255,0,255; + @assert pixel 82,6 == 0,255,0,255; + @assert pixel 67,43 == 0,255,0,255; + @assert pixel 75,43 == 0,255,0,255; + @assert pixel 82,43 == 0,255,0,255; + expected: green + +- name: 2d.line.cap.square + desc: lineCap 'square' is rendered correctly + testing: + - 2d.lineCap.square + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineCap = 'square'; + ctx.lineWidth = 20; + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + ctx.fillRect(15, 5, 20, 40); + ctx.beginPath(); + ctx.moveTo(25, 15); + ctx.lineTo(25, 35); + ctx.stroke(); + + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(75, 15); + ctx.lineTo(75, 35); + ctx.stroke(); + ctx.fillRect(65, 5, 20, 40); + + @assert pixel 25,4 == 0,255,0,255; + @assert pixel 25,5 == 0,255,0,255; + @assert pixel 25,6 == 0,255,0,255; + @assert pixel 25,44 == 0,255,0,255; + @assert pixel 25,45 == 0,255,0,255; + @assert pixel 25,46 == 0,255,0,255; + + @assert pixel 75,4 == 0,255,0,255; + @assert pixel 75,5 == 0,255,0,255; + @assert pixel 75,6 == 0,255,0,255; + @assert pixel 75,44 == 0,255,0,255; + @assert pixel 75,45 == 0,255,0,255; + @assert pixel 75,46 == 0,255,0,255; + expected: green + +- name: 2d.line.cap.open + desc: Line caps are drawn at the corners of an unclosed rectangle + testing: + - 2d.lineCap.end + code: | + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineJoin = 'bevel'; + ctx.lineCap = 'square'; + ctx.lineWidth = 400; + + ctx.beginPath(); + ctx.moveTo(200, 200); + ctx.lineTo(200, 1000); + ctx.lineTo(1000, 1000); + ctx.lineTo(1000, 200); + ctx.lineTo(200, 200); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + expected: green + +- name: 2d.line.cap.closed + desc: Line caps are not drawn at the corners of an unclosed rectangle + testing: + - 2d.lineCap.end + code: | + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineJoin = 'bevel'; + ctx.lineCap = 'square'; + ctx.lineWidth = 400; + + ctx.beginPath(); + ctx.moveTo(200, 200); + ctx.lineTo(200, 1000); + ctx.lineTo(1000, 1000); + ctx.lineTo(1000, 200); + ctx.closePath(); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + expected: green + +- name: 2d.line.cap.valid + desc: Setting lineCap to valid values works + testing: + - 2d.lineCap.set + - 2d.lineCap.get + code: | + ctx.lineCap = 'butt' + @assert ctx.lineCap === 'butt'; + + ctx.lineCap = 'round'; + @assert ctx.lineCap === 'round'; + + ctx.lineCap = 'square'; + @assert ctx.lineCap === 'square'; + +- name: 2d.line.cap.invalid + desc: Setting lineCap to invalid values is ignored + testing: + - 2d.lineCap.invalid + code: | + ctx.lineCap = 'butt' + @assert ctx.lineCap === 'butt'; + + ctx.lineCap = 'butt'; + ctx.lineCap = 'invalid'; + @assert ctx.lineCap === 'butt'; + + ctx.lineCap = 'butt'; + ctx.lineCap = 'ROUND'; + @assert ctx.lineCap === 'butt'; + + ctx.lineCap = 'butt'; + ctx.lineCap = 'round\0'; + @assert ctx.lineCap === 'butt'; + + ctx.lineCap = 'butt'; + ctx.lineCap = 'round '; + @assert ctx.lineCap === 'butt'; + + ctx.lineCap = 'butt'; + ctx.lineCap = ""; + @assert ctx.lineCap === 'butt'; + + ctx.lineCap = 'butt'; + ctx.lineCap = 'bevel'; + @assert ctx.lineCap === 'butt'; + +- name: 2d.line.join.bevel + desc: lineJoin 'bevel' is rendered correctly + testing: + - 2d.lineJoin.common + - 2d.lineJoin.bevel + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var tol = 1; // tolerance to avoid antialiasing artifacts + + ctx.lineJoin = 'bevel'; + ctx.lineWidth = 20; + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + + ctx.fillRect(10, 10, 20, 20); + ctx.fillRect(20, 20, 20, 20); + ctx.beginPath(); + ctx.moveTo(30, 20); + ctx.lineTo(40-tol, 20); + ctx.lineTo(30, 10+tol); + ctx.fill(); + + ctx.beginPath(); + ctx.moveTo(10, 20); + ctx.lineTo(30, 20); + ctx.lineTo(30, 40); + ctx.stroke(); + + + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + + ctx.beginPath(); + ctx.moveTo(60, 20); + ctx.lineTo(80, 20); + ctx.lineTo(80, 40); + ctx.stroke(); + + ctx.fillRect(60, 10, 20, 20); + ctx.fillRect(70, 20, 20, 20); + ctx.beginPath(); + ctx.moveTo(80, 20); + ctx.lineTo(90+tol, 20); + ctx.lineTo(80, 10-tol); + ctx.fill(); + + @assert pixel 34,16 == 0,255,0,255; + @assert pixel 34,15 == 0,255,0,255; + @assert pixel 35,15 == 0,255,0,255; + @assert pixel 36,15 == 0,255,0,255; + @assert pixel 36,14 == 0,255,0,255; + + @assert pixel 84,16 == 0,255,0,255; + @assert pixel 84,15 == 0,255,0,255; + @assert pixel 85,15 == 0,255,0,255; + @assert pixel 86,15 == 0,255,0,255; + @assert pixel 86,14 == 0,255,0,255; + expected: green + +- name: 2d.line.join.round + desc: lineJoin 'round' is rendered correctly + testing: + - 2d.lineJoin.round + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var tol = 1; // tolerance to avoid antialiasing artifacts + + ctx.lineJoin = 'round'; + ctx.lineWidth = 20; + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + + ctx.fillRect(10, 10, 20, 20); + ctx.fillRect(20, 20, 20, 20); + ctx.beginPath(); + ctx.moveTo(30, 20); + ctx.arc(30, 20, 10-tol, 0, 2*Math.PI, true); + ctx.fill(); + + ctx.beginPath(); + ctx.moveTo(10, 20); + ctx.lineTo(30, 20); + ctx.lineTo(30, 40); + ctx.stroke(); + + + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + + ctx.beginPath(); + ctx.moveTo(60, 20); + ctx.lineTo(80, 20); + ctx.lineTo(80, 40); + ctx.stroke(); + + ctx.fillRect(60, 10, 20, 20); + ctx.fillRect(70, 20, 20, 20); + ctx.beginPath(); + ctx.moveTo(80, 20); + ctx.arc(80, 20, 10+tol, 0, 2*Math.PI, true); + ctx.fill(); + + @assert pixel 36,14 == 0,255,0,255; + @assert pixel 36,13 == 0,255,0,255; + @assert pixel 37,13 == 0,255,0,255; + @assert pixel 38,13 == 0,255,0,255; + @assert pixel 38,12 == 0,255,0,255; + + @assert pixel 86,14 == 0,255,0,255; + @assert pixel 86,13 == 0,255,0,255; + @assert pixel 87,13 == 0,255,0,255; + @assert pixel 88,13 == 0,255,0,255; + @assert pixel 88,12 == 0,255,0,255; + expected: green + +- name: 2d.line.join.miter + desc: lineJoin 'miter' is rendered correctly + testing: + - 2d.lineJoin.miter + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineJoin = 'miter'; + ctx.lineWidth = 20; + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + + ctx.fillRect(10, 10, 30, 20); + ctx.fillRect(20, 10, 20, 30); + + ctx.beginPath(); + ctx.moveTo(10, 20); + ctx.lineTo(30, 20); + ctx.lineTo(30, 40); + ctx.stroke(); + + + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + + ctx.beginPath(); + ctx.moveTo(60, 20); + ctx.lineTo(80, 20); + ctx.lineTo(80, 40); + ctx.stroke(); + + ctx.fillRect(60, 10, 30, 20); + ctx.fillRect(70, 10, 20, 30); + + @assert pixel 38,12 == 0,255,0,255; + @assert pixel 39,11 == 0,255,0,255; + @assert pixel 40,10 == 0,255,0,255; + @assert pixel 41,9 == 0,255,0,255; + @assert pixel 42,8 == 0,255,0,255; + + @assert pixel 88,12 == 0,255,0,255; + @assert pixel 89,11 == 0,255,0,255; + @assert pixel 90,10 == 0,255,0,255; + @assert pixel 91,9 == 0,255,0,255; + @assert pixel 92,8 == 0,255,0,255; + expected: green + +- name: 2d.line.join.open + desc: Line joins are not drawn at the corner of an unclosed rectangle + testing: + - 2d.lineJoin.joins + code: | + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineJoin = 'miter'; + ctx.lineWidth = 200; + + ctx.beginPath(); + ctx.moveTo(100, 50); + ctx.lineTo(100, 1000); + ctx.lineTo(1000, 1000); + ctx.lineTo(1000, 50); + ctx.lineTo(100, 50); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + expected: green + +- name: 2d.line.join.closed + desc: Line joins are drawn at the corner of a closed rectangle + testing: + - 2d.lineJoin.joinclosed + code: | + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineJoin = 'miter'; + ctx.lineWidth = 200; + + ctx.beginPath(); + ctx.moveTo(100, 50); + ctx.lineTo(100, 1000); + ctx.lineTo(1000, 1000); + ctx.lineTo(1000, 50); + ctx.closePath(); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + expected: green + +- name: 2d.line.join.parallel + desc: Line joins are drawn at 180-degree joins + testing: + - 2d.lineJoin.joins + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 300; + ctx.lineJoin = 'round'; + ctx.beginPath(); + ctx.moveTo(-100, 25); + ctx.lineTo(0, 25); + ctx.lineTo(-100, 25); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + expected: green + +- name: 2d.line.join.valid + desc: Setting lineJoin to valid values works + testing: + - 2d.lineJoin.set + - 2d.lineJoin.get + code: | + ctx.lineJoin = 'bevel' + @assert ctx.lineJoin === 'bevel'; + + ctx.lineJoin = 'round'; + @assert ctx.lineJoin === 'round'; + + ctx.lineJoin = 'miter'; + @assert ctx.lineJoin === 'miter'; + +- name: 2d.line.join.invalid + desc: Setting lineJoin to invalid values is ignored + testing: + - 2d.lineJoin.invalid + code: | + ctx.lineJoin = 'bevel' + @assert ctx.lineJoin === 'bevel'; + + ctx.lineJoin = 'bevel'; + ctx.lineJoin = 'invalid'; + @assert ctx.lineJoin === 'bevel'; + + ctx.lineJoin = 'bevel'; + ctx.lineJoin = 'ROUND'; + @assert ctx.lineJoin === 'bevel'; + + ctx.lineJoin = 'bevel'; + ctx.lineJoin = 'round\0'; + @assert ctx.lineJoin === 'bevel'; + + ctx.lineJoin = 'bevel'; + ctx.lineJoin = 'round '; + @assert ctx.lineJoin === 'bevel'; + + ctx.lineJoin = 'bevel'; + ctx.lineJoin = ""; + @assert ctx.lineJoin === 'bevel'; + + ctx.lineJoin = 'bevel'; + ctx.lineJoin = 'butt'; + @assert ctx.lineJoin === 'bevel'; + +- name: 2d.line.miter.exceeded + desc: Miter joins are not drawn when the miter limit is exceeded + testing: + - 2d.lineJoin.miterLimit + - 2d.lineJoin.miter + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 400; + ctx.lineJoin = 'miter'; + + ctx.strokeStyle = '#f00'; + ctx.miterLimit = 1.414; + ctx.beginPath(); + ctx.moveTo(200, 1000); + ctx.lineTo(200, 200); + ctx.lineTo(1000, 201); // slightly non-right-angle to avoid being a special case + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + expected: green + +- name: 2d.line.miter.acute + desc: Miter joins are drawn correctly with acute angles + testing: + - 2d.lineJoin.miterLimit + - 2d.lineJoin.miter + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 200; + ctx.lineJoin = 'miter'; + + ctx.strokeStyle = '#0f0'; + ctx.miterLimit = 2.614; + ctx.beginPath(); + ctx.moveTo(100, 1000); + ctx.lineTo(100, 100); + ctx.lineTo(1000, 1000); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.miterLimit = 2.613; + ctx.beginPath(); + ctx.moveTo(100, 1000); + ctx.lineTo(100, 100); + ctx.lineTo(1000, 1000); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + expected: green + +- name: 2d.line.miter.obtuse + desc: Miter joins are drawn correctly with obtuse angles + testing: + - 2d.lineJoin.miterLimit + - 2d.lineJoin.miter + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 1600; + ctx.lineJoin = 'miter'; + + ctx.strokeStyle = '#0f0'; + ctx.miterLimit = 1.083; + ctx.beginPath(); + ctx.moveTo(800, 10000); + ctx.lineTo(800, 300); + ctx.lineTo(10000, -8900); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.miterLimit = 1.082; + ctx.beginPath(); + ctx.moveTo(800, 10000); + ctx.lineTo(800, 300); + ctx.lineTo(10000, -8900); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + expected: green + +- name: 2d.line.miter.rightangle + desc: Miter joins are not drawn when the miter limit is exceeded, on exact right + angles + testing: + - 2d.lineJoin.miter + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 400; + ctx.lineJoin = 'miter'; + + ctx.strokeStyle = '#f00'; + ctx.miterLimit = 1.414; + ctx.beginPath(); + ctx.moveTo(200, 1000); + ctx.lineTo(200, 200); + ctx.lineTo(1000, 200); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + expected: green + +- name: 2d.line.miter.lineedge + desc: Miter joins are not drawn when the miter limit is exceeded at the corners + of a zero-height rectangle + testing: + - 2d.lineJoin.miter + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 200; + ctx.lineJoin = 'miter'; + + ctx.strokeStyle = '#f00'; + ctx.miterLimit = 1.414; + ctx.beginPath(); + ctx.strokeRect(100, 25, 200, 0); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + expected: green + +- name: 2d.line.miter.within + desc: Miter joins are drawn when the miter limit is not quite exceeded + testing: + - 2d.lineJoin.miter + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 400; + ctx.lineJoin = 'miter'; + + ctx.strokeStyle = '#0f0'; + ctx.miterLimit = 1.416; + ctx.beginPath(); + ctx.moveTo(200, 1000); + ctx.lineTo(200, 200); + ctx.lineTo(1000, 201); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + expected: green + +- name: 2d.line.miter.valid + desc: Setting miterLimit to valid values works + testing: + - 2d.miterLimit.set + - 2d.miterLimit.get + code: | + ctx.miterLimit = 1.5; + @assert ctx.miterLimit === 1.5; + + ctx.miterLimit = "1e1"; + @assert ctx.miterLimit === 10; + + ctx.miterLimit = 1/1024; + @assert ctx.miterLimit === 1/1024; + + ctx.miterLimit = 1000; + @assert ctx.miterLimit === 1000; + +- name: 2d.line.miter.invalid + desc: Setting miterLimit to invalid values is ignored + testing: + - 2d.miterLimit.invalid + code: | + ctx.miterLimit = 1.5; + @assert ctx.miterLimit === 1.5; + + ctx.miterLimit = 1.5; + ctx.miterLimit = 0; + @assert ctx.miterLimit === 1.5; + + ctx.miterLimit = 1.5; + ctx.miterLimit = -1; + @assert ctx.miterLimit === 1.5; + + ctx.miterLimit = 1.5; + ctx.miterLimit = Infinity; + @assert ctx.miterLimit === 1.5; + + ctx.miterLimit = 1.5; + ctx.miterLimit = -Infinity; + @assert ctx.miterLimit === 1.5; + + ctx.miterLimit = 1.5; + ctx.miterLimit = NaN; + @assert ctx.miterLimit === 1.5; + + ctx.miterLimit = 1.5; + ctx.miterLimit = 'string'; + @assert ctx.miterLimit === 1.5; + + ctx.miterLimit = 1.5; + ctx.miterLimit = true; + @assert ctx.miterLimit === 1; + + ctx.miterLimit = 1.5; + ctx.miterLimit = false; + @assert ctx.miterLimit === 1.5; + +- name: 2d.line.cross + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 200; + ctx.lineJoin = 'bevel'; + + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(110, 50); + ctx.lineTo(110, 60); + ctx.lineTo(100, 60); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + expected: green + +- name: 2d.line.union + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 100; + ctx.lineCap = 'round'; + + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 24); + ctx.lineTo(100, 25); + ctx.lineTo(0, 26); + ctx.closePath(); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 25,1 == 0,255,0,255; + @assert pixel 48,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 25,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + expected: green + + + + + + + +- name: 2d.line.invalid.strokestyle + desc: Verify correct behavior of canvas on an invalid strokeStyle() + testing: + - 2d.strokestyle.invalid + code: | + ctx.strokeStyle = 'rgb(0, 255, 0)'; + ctx.strokeStyle = 'nonsense'; + ctx.lineWidth = 200; + ctx.moveTo(0,100); + ctx.lineTo(200,100); + ctx.stroke(); + var imageData = ctx.getImageData(0, 0, 200, 200); + var imgdata = imageData.data; + @assert imgdata[4] == 0; + @assert imgdata[5] == 255; + @assert imgdata[6] == 0; + diff --git a/test/wpt/meta.yaml b/test/wpt/meta.yaml new file mode 100644 index 000000000..f6902d078 --- /dev/null +++ b/test/wpt/meta.yaml @@ -0,0 +1,555 @@ +- meta: | + cases = [ + ("zero", "0", 0), + ("empty", "", None), + ("onlyspace", " ", None), + ("space", " 100", 100), + ("whitespace", "\r\n\t\f100", 100), + ("plus", "+100", 100), + ("minus", "-100", None), + ("octal", "0100", 100), + ("hex", "0x100", 0), + ("exp", "100e1", 100), + ("decimal", "100.999", 100), + ("percent", "100%", 100), + ("em", "100em", 100), + ("junk", "#!?", None), + ("trailingjunk", "100#!?", 100), + ] + def gen(name, string, exp, code): + testing = ["size.nonnegativeinteger"] + if exp is None: + testing.append("size.error") + code += "@assert canvas.width === 300;\n@assert canvas.height === 150;\n" + expected = "size 300 150" + else: + code += "@assert canvas.width === %s;\n@assert canvas.height === %s;\n" % (exp, exp) + expected = "size %s %s" % (exp, exp) + + # With "100%", Opera gets canvas.width = 100 but renders at 100% of the frame width, + # so check the CSS display width + code += '@assert window.getComputedStyle(canvas, null).getPropertyValue("width") === "%spx";\n' % (exp, ) + + code += "@assert canvas.getAttribute('width') === %r;\n" % string + code += "@assert canvas.getAttribute('height') === %r;\n" % string + + if exp == 0: + expected = None # can't generate zero-sized PNGs for the expected image + + return code, testing, expected + + for name, string, exp in cases: + code = "" + code, testing, expected = gen(name, string, exp, code) + # We need to replace \r with because \r\n gets converted to \n in the HTML parser. + htmlString = string.replace('\r', ' ') + tests.append( { + "name": "size.attributes.parse.%s" % name, + "desc": "Parsing of non-negative integers", + "testing": testing, + "canvas": 'width="%s" height="%s"' % (htmlString, htmlString), + "code": code, + "expected": expected + } ) + + for name, string, exp in cases: + code = "canvas.setAttribute('width', %r);\ncanvas.setAttribute('height', %r);\n" % (string, string) + code, testing, expected = gen(name, string, exp, code) + tests.append( { + "name": "size.attributes.setAttribute.%s" % name, + "desc": "Parsing of non-negative integers in setAttribute", + "testing": testing, + "canvas": 'width="50" height="50"', + "code": code, + "expected": expected + } ) + +- meta: | + state = [ # some non-default values to test with + ('strokeStyle', '"#ff0000"'), + ('fillStyle', '"#ff0000"'), + ('globalAlpha', 0.5), + ('lineWidth', 0.5), + ('lineCap', '"round"'), + ('lineJoin', '"round"'), + ('miterLimit', 0.5), + ('shadowOffsetX', 5), + ('shadowOffsetY', 5), + ('shadowBlur', 5), + ('shadowColor', '"#ff0000"'), + ('globalCompositeOperation', '"copy"'), + ('font', '"25px serif"'), + ('textAlign', '"center"'), + ('textBaseline', '"bottom"'), + ] + for key,value in state: + tests.append( { + 'name': '2d.state.saverestore.%s' % key, + 'desc': 'save()/restore() works for %s' % key, + 'testing': [ '2d.state.%s' % key ], + 'code': + """// Test that restore() undoes any modifications + var old = ctx.%(key)s; + ctx.save(); + ctx.%(key)s = %(value)s; + ctx.restore(); + @assert ctx.%(key)s === old; + + // Also test that save() doesn't modify the values + ctx.%(key)s = %(value)s; + old = ctx.%(key)s; + // we're not interested in failures caused by get(set(x)) != x (e.g. + // from rounding), so compare against 'old' instead of against %(value)s + ctx.save(); + @assert ctx.%(key)s === old; + ctx.restore(); + """ % { 'key':key, 'value':value } + } ) + + tests.append( { + 'name': 'initial.reset.2dstate', + 'desc': 'Resetting the canvas state resets 2D state variables', + 'testing': [ 'initial.reset' ], + 'code': + """canvas.width = 100; + var default_val; + """ + "".join( + """ + default_val = ctx.%(key)s; + ctx.%(key)s = %(value)s; + canvas.width = 100; + @assert ctx.%(key)s === default_val; + """ % { 'key':key, 'value':value } + for key,value in state), + } ) + +- meta: | + # Composite operation tests + # + ops = [ + # name FA FB + ('source-over', '1', '1-aA'), + ('destination-over', '1-aB', '1'), + ('source-in', 'aB', '0'), + ('destination-in', '0', 'aA'), + ('source-out', '1-aB', '0'), + ('destination-out', '0', '1-aA'), + ('source-atop', 'aB', '1-aA'), + ('destination-atop', '1-aB', 'aA'), + ('xor', '1-aB', '1-aA'), + ('copy', '1', '0'), + ('lighter', '1', '1'), + ] + + # The ones that change the output when src = (0,0,0,0): + ops_trans = [ 'source-in', 'destination-in', 'source-out', 'destination-atop', 'copy' ]; + + def calc_output(A, B, FA_code, FB_code): + (RA, GA, BA, aA) = A + (RB, GB, BB, aB) = B + rA, gA, bA = RA*aA, GA*aA, BA*aA + rB, gB, bB = RB*aB, GB*aB, BB*aB + + FA = eval(FA_code) + FB = eval(FB_code) + + rO = rA*FA + rB*FB + gO = gA*FA + gB*FB + bO = bA*FA + bB*FB + aO = aA*FA + aB*FB + + rO = min(255, rO) + gO = min(255, gO) + bO = min(255, bO) + aO = min(1, aO) + + if aO: + RO = rO / aO + GO = gO / aO + BO = bO / aO + else: RO = GO = BO = 0 + + return (RO, GO, BO, aO) + + def to_test(color): + r, g, b, a = color + return '%d,%d,%d,%d' % (round(r), round(g), round(b), round(a*255)) + def to_cairo(color): + r, g, b, a = color + return '%f,%f,%f,%f' % (r/255., g/255., b/255., a) + + for (name, src, dest) in [ + ('solid', (255, 255, 0, 1.0), (0, 255, 255, 1.0)), + ('transparent', (0, 0, 255, 0.75), (0, 255, 0, 0.5)), + # catches the atop, xor and lighter bugs in Opera 9.10 + ]: + for op, FA_code, FB_code in ops: + expected = calc_output(src, dest, FA_code, FB_code) + tests.append( { + 'name': '2d.composite.%s.%s' % (name, op), + 'testing': [ '2d.composite.%s' % op ], + 'code': """ + ctx.fillStyle = 'rgba%s'; + ctx.fillRect(0, 0, 100, 50); + ctx.globalCompositeOperation = '%s'; + ctx.fillStyle = 'rgba%s'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 ==~ %s +/- 5; + """ % (dest, op, src, to_test(expected)), + 'expected': """size 100 50 + cr.set_source_rgba(%s) + cr.rectangle(0, 0, 100, 50) + cr.fill() + """ % to_cairo(expected), + } ) + + for (name, src, dest) in [ ('image', (255, 255, 0, 0.75), (0, 255, 255, 0.5)) ]: + for op, FA_code, FB_code in ops: + expected = calc_output(src, dest, FA_code, FB_code) + tests.append( { + 'name': '2d.composite.%s.%s' % (name, op), + 'testing': [ '2d.composite.%s' % op ], + 'images': [ 'yellow75.png' ], + 'code': """ + ctx.fillStyle = 'rgba%s'; + ctx.fillRect(0, 0, 100, 50); + ctx.globalCompositeOperation = '%s'; + ctx.drawImage(document.getElementById('yellow75.png'), 0, 0); + @assert pixel 50,25 ==~ %s +/- 5; + """ % (dest, op, to_test(expected)), + 'expected': """size 100 50 + cr.set_source_rgba(%s) + cr.rectangle(0, 0, 100, 50) + cr.fill() + """ % to_cairo(expected), + } ) + + for (name, src, dest) in [ ('canvas', (255, 255, 0, 0.75), (0, 255, 255, 0.5)) ]: + for op, FA_code, FB_code in ops: + expected = calc_output(src, dest, FA_code, FB_code) + tests.append( { + 'name': '2d.composite.%s.%s' % (name, op), + 'testing': [ '2d.composite.%s' % op ], + 'images': [ 'yellow75.png' ], + 'code': """ + var canvas2 = document.createElement('canvas'); + canvas2.width = canvas.width; + canvas2.height = canvas.height; + var ctx2 = canvas2.getContext('2d'); + ctx2.drawImage(document.getElementById('yellow75.png'), 0, 0); + ctx.fillStyle = 'rgba%s'; + ctx.fillRect(0, 0, 100, 50); + ctx.globalCompositeOperation = '%s'; + ctx.drawImage(canvas2, 0, 0); + @assert pixel 50,25 ==~ %s +/- 5; + """ % (dest, op, to_test(expected)), + 'expected': """size 100 50 + cr.set_source_rgba(%s) + cr.rectangle(0, 0, 100, 50) + cr.fill() + """ % to_cairo(expected), + } ) + + + for (name, src, dest) in [ ('uncovered.fill', (0, 0, 255, 0.75), (0, 255, 0, 0.5)) ]: + for op, FA_code, FB_code in ops: + if op not in ops_trans: continue + expected0 = calc_output((0,0,0,0.0), dest, FA_code, FB_code) + tests.append( { + 'name': '2d.composite.%s.%s' % (name, op), + 'desc': 'fill() draws pixels not covered by the source object as (0,0,0,0), and does not leave the pixels unchanged.', + 'testing': [ '2d.composite.%s' % op ], + 'code': """ + ctx.fillStyle = 'rgba%s'; + ctx.fillRect(0, 0, 100, 50); + ctx.globalCompositeOperation = '%s'; + ctx.fillStyle = 'rgba%s'; + ctx.translate(0, 25); + ctx.fillRect(0, 50, 100, 50); + @assert pixel 50,25 ==~ %s +/- 5; + """ % (dest, op, src, to_test(expected0)), + 'expected': """size 100 50 + cr.set_source_rgba(%s) + cr.rectangle(0, 0, 100, 50) + cr.fill() + """ % (to_cairo(expected0)), + } ) + + for (name, src, dest) in [ ('uncovered.image', (255, 255, 0, 1.0), (0, 255, 255, 0.5)) ]: + for op, FA_code, FB_code in ops: + if op not in ops_trans: continue + expected0 = calc_output((0,0,0,0.0), dest, FA_code, FB_code) + tests.append( { + 'name': '2d.composite.%s.%s' % (name, op), + 'desc': 'drawImage() draws pixels not covered by the source object as (0,0,0,0), and does not leave the pixels unchanged.', + 'testing': [ '2d.composite.%s' % op ], + 'images': [ 'yellow.png' ], + 'code': """ + ctx.fillStyle = 'rgba%s'; + ctx.fillRect(0, 0, 100, 50); + ctx.globalCompositeOperation = '%s'; + ctx.drawImage(document.getElementById('yellow.png'), 40, 40, 10, 10, 40, 50, 10, 10); + @assert pixel 15,15 ==~ %s +/- 5; + @assert pixel 50,25 ==~ %s +/- 5; + """ % (dest, op, to_test(expected0), to_test(expected0)), + 'expected': """size 100 50 + cr.set_source_rgba(%s) + cr.rectangle(0, 0, 100, 50) + cr.fill() + """ % (to_cairo(expected0)), + } ) + + for (name, src, dest) in [ ('uncovered.nocontext', (255, 255, 0, 1.0), (0, 255, 255, 0.5)) ]: + for op, FA_code, FB_code in ops: + if op not in ops_trans: continue + expected0 = calc_output((0,0,0,0.0), dest, FA_code, FB_code) + tests.append( { + 'name': '2d.composite.%s.%s' % (name, op), + 'desc': 'drawImage() of a canvas with no context draws pixels as (0,0,0,0), and does not leave the pixels unchanged.', + 'testing': [ '2d.composite.%s' % op ], + 'code': """ + ctx.fillStyle = 'rgba%s'; + ctx.fillRect(0, 0, 100, 50); + ctx.globalCompositeOperation = '%s'; + var canvas2 = document.createElement('canvas'); + ctx.drawImage(canvas2, 0, 0); + @assert pixel 50,25 ==~ %s +/- 5; + """ % (dest, op, to_test(expected0)), + 'expected': """size 100 50 + cr.set_source_rgba(%s) + cr.rectangle(0, 0, 100, 50) + cr.fill() + """ % (to_cairo(expected0)), + } ) + + for (name, src, dest) in [ ('uncovered.pattern', (255, 255, 0, 1.0), (0, 255, 255, 0.5)) ]: + for op, FA_code, FB_code in ops: + if op not in ops_trans: continue + expected0 = calc_output((0,0,0,0.0), dest, FA_code, FB_code) + tests.append( { + 'name': '2d.composite.%s.%s' % (name, op), + 'desc': 'Pattern fill() draws pixels not covered by the source object as (0,0,0,0), and does not leave the pixels unchanged.', + 'testing': [ '2d.composite.%s' % op ], + 'images': [ 'yellow.png' ], + 'code': """ + ctx.fillStyle = 'rgba%s'; + ctx.fillRect(0, 0, 100, 50); + ctx.globalCompositeOperation = '%s'; + ctx.fillStyle = ctx.createPattern(document.getElementById('yellow.png'), 'no-repeat'); + ctx.fillRect(0, 50, 100, 50); + @assert pixel 50,25 ==~ %s +/- 5; + """ % (dest, op, to_test(expected0)), + 'expected': """size 100 50 + cr.set_source_rgba(%s) + cr.rectangle(0, 0, 100, 50) + cr.fill() + """ % (to_cairo(expected0)), + } ) + + for op, FA_code, FB_code in ops: + tests.append( { + 'name': '2d.composite.clip.%s' % (op), + 'desc': 'fill() does not affect pixels outside the clip region.', + 'testing': [ '2d.composite.%s' % op ], + 'code': """ + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.globalCompositeOperation = '%s'; + ctx.rect(-20, -20, 10, 10); + ctx.clip(); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 50, 50); + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + """ % (op), + 'expected': 'green' + } ) + +- meta: | + # Color parsing tests + + # Try most of the CSS3 Color values - http://www.w3.org/TR/css3-color/#colorunits + big_float = '1' + ('0' * 39) + big_double = '1' + ('0' * 310) + for name, string, r,g,b,a, notes in [ + ('html4', 'limE', 0,255,0,255, ""), + ('hex3', '#0f0', 0,255,0,255, ""), + ('hex4', '#0f0f', 0,255,0,255, ""), + ('hex6', '#00fF00', 0,255,0,255, ""), + ('hex8', '#00ff00ff', 0,255,0,255, ""), + ('rgb-num', 'rgb(0,255,0)', 0,255,0,255, ""), + ('rgb-clamp-1', 'rgb(-1000, 1000, -1000)', 0,255,0,255, 'Assumes colors are clamped to [0,255].'), + ('rgb-clamp-2', 'rgb(-200%, 200%, -200%)', 0,255,0,255, 'Assumes colors are clamped to [0,255].'), + ('rgb-clamp-3', 'rgb(-2147483649, 4294967298, -18446744073709551619)', 0,255,0,255, 'Assumes colors are clamped to [0,255].'), + ('rgb-clamp-4', 'rgb(-'+big_float+', '+big_float+', -'+big_float+')', 0,255,0,255, 'Assumes colors are clamped to [0,255].'), + ('rgb-clamp-5', 'rgb(-'+big_double+', '+big_double+', -'+big_double+')', 0,255,0,255, 'Assumes colors are clamped to [0,255].'), + ('rgb-percent', 'rgb(0% ,100% ,0%)', 0,255,0,255, 'CSS3 Color says "The integer value 255 corresponds to 100%". (In particular, it is not 254...)'), + ('rgb-eof', 'rgb(0, 255, 0', 0,255,0,255, ""), # see CSS2.1 4.2 "Unexpected end of style sheet" + ('rgba-solid-1', 'rgba( 0 , 255 , 0 , 1 )', 0,255,0,255, ""), + ('rgba-solid-2', 'rgba( 0 , 255 , 0 , 1.0 )', 0,255,0,255, ""), + ('rgba-solid-3', 'rgba( 0 , 255 , 0 , +1 )', 0,255,0,255, ""), + ('rgba-solid-4', 'rgba( -0 , 255 , +0 , 1 )', 0,255,0,255, ""), + ('rgba-num-1', 'rgba( 0 , 255 , 0 , .499 )', 0,255,0,127, ""), + ('rgba-num-2', 'rgba( 0 , 255 , 0 , 0.499 )', 0,255,0,127, ""), + ('rgba-percent', 'rgba(0%,100%,0%,0.499)', 0,255,0,127, ""), # 0.499*255 rounds to 127, both down and nearest, so it should be safe + ('rgba-clamp-1', 'rgba(0, 255, 0, -2)', 0,0,0,0, ""), + ('rgba-clamp-2', 'rgba(0, 255, 0, 2)', 0,255,0,255, ""), + ('rgba-eof', 'rgba(0, 255, 0, 1', 0,255,0,255, ""), + ('transparent-1', 'transparent', 0,0,0,0, ""), + ('transparent-2', 'TrAnSpArEnT', 0,0,0,0, ""), + ('hsl-1', 'hsl(120, 100%, 50%)', 0,255,0,255, ""), + ('hsl-2', 'hsl( -240 , 100% , 50% )', 0,255,0,255, ""), + ('hsl-3', 'hsl(360120, 100%, 50%)', 0,255,0,255, ""), + ('hsl-4', 'hsl(-360240, 100%, 50%)', 0,255,0,255, ""), + ('hsl-5', 'hsl(120.0, 100.0%, 50.0%)', 0,255,0,255, ""), + ('hsl-6', 'hsl(+120, +100%, +50%)', 0,255,0,255, ""), + ('hsl-clamp-1', 'hsl(120, 200%, 50%)', 0,255,0,255, ""), + ('hsl-clamp-2', 'hsl(120, -200%, 49.9%)', 127,127,127,255, ""), + ('hsl-clamp-3', 'hsl(120, 100%, 200%)', 255,255,255,255, ""), + ('hsl-clamp-4', 'hsl(120, 100%, -200%)', 0,0,0,255, ""), + ('hsla-1', 'hsla(120, 100%, 50%, 0.499)', 0,255,0,127, ""), + ('hsla-2', 'hsla( 120.0 , 100.0% , 50.0% , 1 )', 0,255,0,255, ""), + ('hsla-clamp-1', 'hsla(120, 200%, 50%, 1)', 0,255,0,255, ""), + ('hsla-clamp-2', 'hsla(120, -200%, 49.9%, 1)', 127,127,127,255, ""), + ('hsla-clamp-3', 'hsla(120, 100%, 200%, 1)', 255,255,255,255, ""), + ('hsla-clamp-4', 'hsla(120, 100%, -200%, 1)', 0,0,0,255, ""), + ('hsla-clamp-5', 'hsla(120, 100%, 50%, 2)', 0,255,0,255, ""), + ('hsla-clamp-6', 'hsla(120, 100%, 0%, -2)', 0,0,0,0, ""), + ('svg-1', 'gray', 128,128,128,255, ""), + ('svg-2', 'grey', 128,128,128,255, ""), + # css-color-4 rgb() color function + # https://drafts.csswg.org/css-color/#numeric-rgb + ('css-color-4-rgb-1', 'rgb(0, 255.0, 0)', 0,255,0,255, ""), + ('css-color-4-rgb-2', 'rgb(0, 255, 0, 0.2)', 0,255,0,51, ""), + ('css-color-4-rgb-3', 'rgb(0, 255, 0, 20%)', 0,255,0,51, ""), + ('css-color-4-rgb-4', 'rgb(0 255 0)', 0,255,0,255, ""), + ('css-color-4-rgb-5', 'rgb(0 255 0 / 0.2)', 0,255,0,51, ""), + ('css-color-4-rgb-6', 'rgb(0 255 0 / 20%)', 0,255,0,51, ""), + ('css-color-4-rgba-1', 'rgba(0, 255.0, 0)', 0,255,0,255, ""), + ('css-color-4-rgba-2', 'rgba(0, 255, 0, 0.2)', 0,255,0,51, ""), + ('css-color-4-rgba-3', 'rgba(0, 255, 0, 20%)', 0,255,0,51, ""), + ('css-color-4-rgba-4', 'rgba(0 255 0)', 0,255,0,255, ""), + ('css-color-4-rgba-5', 'rgba(0 255 0 / 0.2)', 0,255,0,51, ""), + ('css-color-4-rgba-6', 'rgba(0 255 0 / 20%)', 0,255,0,51, ""), + # css-color-4 hsl() color function + # https://drafts.csswg.org/css-color/#the-hsl-notation + ('css-color-4-hsl-1', 'hsl(120 100.0% 50.0%)', 0,255,0,255, ""), + ('css-color-4-hsl-2', 'hsl(120 100.0% 50.0% / 0.2)', 0,255,0,51, ""), + ('css-color-4-hsl-3', 'hsl(120.0, 100.0%, 50.0%, 0.2)', 0,255,0,51, ""), + ('css-color-4-hsl-4', 'hsl(120.0, 100.0%, 50.0%, 20%)', 0,255,0,51, ""), + ('css-color-4-hsl-5', 'hsl(120deg, 100.0%, 50.0%, 0.2)', 0,255,0,51, ""), + ('css-color-4-hsl-6', 'hsl(120deg, 100.0%, 50.0%)', 0,255,0,255, ""), + ('css-color-4-hsl-7', 'hsl(133.33333333grad, 100.0%, 50.0%)', 0,255,0,255, ""), + ('css-color-4-hsl-8', 'hsl(2.0943951024rad, 100.0%, 50.0%)', 0,255,0,255, ""), + ('css-color-4-hsl-9', 'hsl(0.3333333333turn, 100.0%, 50.0%)', 0,255,0,255, ""), + ('css-color-4-hsla-1', 'hsl(120 100.0% 50.0%)', 0,255,0,255, ""), + ('css-color-4-hsla-2', 'hsl(120 100.0% 50.0% / 0.2)', 0,255,0,51, ""), + ('css-color-4-hsla-3', 'hsl(120.0, 100.0%, 50.0%, 0.2)', 0,255,0,51, ""), + ('css-color-4-hsla-4', 'hsl(120.0, 100.0%, 50.0%, 20%)', 0,255,0,51, ""), + ('css-color-4-hsla-5', 'hsl(120deg, 100.0%, 50.0%, 0.2)', 0,255,0,51, ""), + ('css-color-4-hsla-6', 'hsl(120deg, 100.0%, 50.0%)', 0,255,0,255, ""), + ('css-color-4-hsla-7', 'hsl(133.33333333grad, 100.0%, 50.0%)', 0,255,0,255, ""), + ('css-color-4-hsla-8', 'hsl(2.0943951024rad, 100.0%, 50.0%)', 0,255,0,255, ""), + ('css-color-4-hsla-9', 'hsl(0.3333333333turn, 100.0%, 50.0%)', 0,255,0,255, ""), + # currentColor is handled later + ]: + # TODO: test by retrieving fillStyle, instead of actually drawing? + # TODO: test strokeStyle, shadowColor in the same way + test = { + 'name': '2d.fillStyle.parse.%s' % name, + 'testing': [ '2d.colors.parse' ], + 'notes': notes, + 'code': """ + ctx.fillStyle = '#f00'; + ctx.fillStyle = '%s'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == %d,%d,%d,%d; + """ % (string, r,g,b,a), + 'expected': """size 100 50 + cr.set_source_rgba(%f, %f, %f, %f) + cr.rectangle(0, 0, 100, 50) + cr.fill() + """ % (r/255., g/255., b/255., a/255.), + } + tests.append(test) + + # Also test that invalid colors are ignored + for name, string in [ + ('hex1', '#f'), + ('hex2', '#f0'), + ('hex3', '#g00'), + ('hex4', '#fg00'), + ('hex5', '#ff000'), + ('hex6', '#fg0000'), + ('hex7', '#ff0000f'), + ('hex8', '#fg0000ff'), + ('rgb-1', 'rgb(255.0, 0, 0,)'), + ('rgb-2', 'rgb(100%, 0, 0)'), + ('rgb-3', 'rgb(255, - 1, 0)'), + ('rgba-1', 'rgba(100%, 0, 0, 1)'), + ('rgba-2', 'rgba(255, 0, 0, 1. 0)'), + ('rgba-3', 'rgba(255, 0, 0, 1.)'), + ('rgba-4', 'rgba(255, 0, 0, '), + ('rgba-5', 'rgba(255, 0, 0, 1,)'), + ('hsl-1', 'hsl(0%, 100%, 50%)'), + ('hsl-2', 'hsl(z, 100%, 50%)'), + ('hsl-3', 'hsl(0, 0, 50%)'), + ('hsl-4', 'hsl(0, 100%, 0)'), + ('hsl-5', 'hsl(0, 100.%, 50%)'), + ('hsl-6', 'hsl(0, 100%, 50%,)'), + ('hsla-1', 'hsla(0%, 100%, 50%, 1)'), + ('hsla-2', 'hsla(0, 0, 50%, 1)'), + ('hsla-3', 'hsla(0, 0, 50%, 1,)'), + ('name-1', 'darkbrown'), + ('name-2', 'firebrick1'), + ('name-3', 'red blue'), + ('name-4', '"red"'), + ('name-5', '"red'), + # css-color-4 color function + # comma and comma-less expressions should not mix together. + ('css-color-4-rgb-1', 'rgb(255, 0, 0 / 1)'), + ('css-color-4-rgb-2', 'rgb(255 0 0, 1)'), + ('css-color-4-rgb-3', 'rgb(255, 0 0)'), + ('css-color-4-rgba-1', 'rgba(255, 0, 0 / 1)'), + ('css-color-4-rgba-2', 'rgba(255 0 0, 1)'), + ('css-color-4-rgba-3', 'rgba(255, 0 0)'), + ('css-color-4-hsl-1', 'hsl(0, 100%, 50% / 1)'), + ('css-color-4-hsl-2', 'hsl(0 100% 50%, 1)'), + ('css-color-4-hsl-3', 'hsl(0, 100% 50%)'), + ('css-color-4-hsla-1', 'hsla(0, 100%, 50% / 1)'), + ('css-color-4-hsla-2', 'hsla(0 100% 50%, 1)'), + ('css-color-4-hsla-3', 'hsla(0, 100% 50%)'), + # trailing slash + ('css-color-4-rgb-4', 'rgb(0 0 0 /)'), + ('css-color-4-rgb-5', 'rgb(0, 0, 0 /)'), + ('css-color-4-hsl-4', 'hsl(0 100% 50% /)'), + ('css-color-4-hsl-5', 'hsl(0, 100%, 50% /)'), + ]: + test = { + 'name': '2d.fillStyle.parse.invalid.%s' % name, + 'testing': [ '2d.colors.parse' ], + 'code': """ + ctx.fillStyle = '#0f0'; + try { ctx.fillStyle = '%s'; } catch (e) { } // this shouldn't throw, but it shouldn't matter here if it does + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + """ % string, + 'expected': 'green' + } + tests.append(test) + + # Some can't have positive tests, only negative tests, because we don't know what color they're meant to be + for name, string in [ + ('system', 'ThreeDDarkShadow'), + #('flavor', 'flavor'), # removed from latest CSS3 Color drafts + ]: + test = { + 'name': '2d.fillStyle.parse.%s' % name, + 'testing': [ '2d.colors.parse' ], + 'code': """ + ctx.fillStyle = '#f00'; + ctx.fillStyle = '%s'; + @assert ctx.fillStyle =~ /^#(?!(FF0000|ff0000|f00)$)/; // test that it's not red + """ % (string,), + } + tests.append(test) diff --git a/test/wpt/path-objects.yaml b/test/wpt/path-objects.yaml new file mode 100644 index 000000000..0ca97e762 --- /dev/null +++ b/test/wpt/path-objects.yaml @@ -0,0 +1,3646 @@ +- name: 2d.path.initial + testing: + - 2d.path.initial + #mozilla: { bug: TODO } + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.closePath(); + ctx.fillStyle = '#f00'; + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.beginPath + testing: + - 2d.path.beginPath + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.rect(0, 0, 100, 50); + ctx.beginPath(); + ctx.fillStyle = '#f00'; + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.moveTo.basic + testing: + - 2d.path.moveTo + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.rect(0, 0, 10, 50); + ctx.moveTo(100, 0); + ctx.lineTo(10, 0); + ctx.lineTo(10, 50); + ctx.lineTo(100, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 90,25 == 0,255,0,255; + expected: green + +- name: 2d.path.moveTo.newsubpath + testing: + - 2d.path.moveTo + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.moveTo(100, 0); + ctx.moveTo(100, 50); + ctx.moveTo(0, 50); + ctx.fillStyle = '#f00'; + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.moveTo.multiple + testing: + - 2d.path.moveTo + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.moveTo(0, 25); + ctx.moveTo(100, 25); + ctx.moveTo(0, 25); + ctx.lineTo(100, 25); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.moveTo.nonfinite + desc: moveTo() with Infinity/NaN is ignored + testing: + - 2d.nonfinite + code: | + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + @nonfinite ctx.moveTo(<0 Infinity -Infinity NaN>, <50 Infinity -Infinity NaN>); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.closePath.empty + testing: + - 2d.path.closePath.empty + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.closePath(); + ctx.fillStyle = '#f00'; + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.closePath.newline + testing: + - 2d.path.closePath.nonempty + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.moveTo(-100, 25); + ctx.lineTo(-100, -100); + ctx.lineTo(200, -100); + ctx.lineTo(200, 25); + ctx.closePath(); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.closePath.nextpoint + testing: + - 2d.path.closePath.nonempty + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.moveTo(-100, 25); + ctx.lineTo(-100, -1000); + ctx.closePath(); + ctx.lineTo(1000, 25); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.lineTo.ensuresubpath.1 + desc: If there is no subpath, the point is added and nothing is drawn + testing: + - 2d.path.lineTo.empty + - 2d.path.ensure + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.lineTo(100, 50); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.lineTo.ensuresubpath.2 + desc: If there is no subpath, the point is added and used for subsequent drawing + testing: + - 2d.path.lineTo.empty + - 2d.path.ensure + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.lineTo(0, 25); + ctx.lineTo(100, 25); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.lineTo.basic + testing: + - 2d.path.lineTo.nonempty + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.lineTo(100, 25); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.lineTo.nextpoint + testing: + - 2d.path.lineTo.nonempty + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.moveTo(-100, -100); + ctx.lineTo(0, 25); + ctx.lineTo(100, 25); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.lineTo.nonfinite + desc: lineTo() with Infinity/NaN is ignored + testing: + - 2d.nonfinite + code: | + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + @nonfinite ctx.lineTo(<0 Infinity -Infinity NaN>, <50 Infinity -Infinity NaN>); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 90,45 == 0,255,0,255; + expected: green + +- name: 2d.path.lineTo.nonfinite.details + desc: lineTo() with Infinity/NaN for first arg still converts the second arg + testing: + - 2d.nonfinite + code: | + for (var arg1 of [Infinity, -Infinity, NaN]) { + var converted = false; + ctx.lineTo(arg1, { valueOf: function() { converted = true; return 0; } }); + @assert converted; + } + expected: clear + +- name: 2d.path.quadraticCurveTo.ensuresubpath.1 + desc: If there is no subpath, the first control point is added (and nothing is drawn + up to it) + testing: + - 2d.path.quadratic.empty + - 2d.path.ensure + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.quadraticCurveTo(100, 50, 200, 50); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 95,45 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.path.quadraticCurveTo.ensuresubpath.2 + desc: If there is no subpath, the first control point is added + testing: + - 2d.path.quadratic.empty + - 2d.path.ensure + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.quadraticCurveTo(0, 25, 100, 25); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 5,45 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.path.quadraticCurveTo.basic + testing: + - 2d.path.quadratic.nonempty + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.quadraticCurveTo(100, 25, 100, 25); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.quadraticCurveTo.shape + testing: + - 2d.path.quadratic.nonempty + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 55; + ctx.beginPath(); + ctx.moveTo(-1000, 1050); + ctx.quadraticCurveTo(0, -1000, 1200, 1050); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.quadraticCurveTo.scaled + testing: + - 2d.path.quadratic.nonempty + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.scale(1000, 1000); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 0.055; + ctx.beginPath(); + ctx.moveTo(-1, 1.05); + ctx.quadraticCurveTo(0, -1, 1.2, 1.05); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.quadraticCurveTo.nonfinite + desc: quadraticCurveTo() with Infinity/NaN is ignored + testing: + - 2d.nonfinite + code: | + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + @nonfinite ctx.quadraticCurveTo(<0 Infinity -Infinity NaN>, <50 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <50 Infinity -Infinity NaN>); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 90,45 == 0,255,0,255; + expected: green + +- name: 2d.path.bezierCurveTo.ensuresubpath.1 + desc: If there is no subpath, the first control point is added (and nothing is drawn + up to it) + testing: + - 2d.path.bezier.empty + - 2d.path.ensure + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.bezierCurveTo(100, 50, 200, 50, 200, 50); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 95,45 == 0,255,0,255; + expected: green + +- name: 2d.path.bezierCurveTo.ensuresubpath.2 + desc: If there is no subpath, the first control point is added + testing: + - 2d.path.bezier.empty + - 2d.path.ensure + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.bezierCurveTo(0, 25, 100, 25, 100, 25); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 5,45 == 0,255,0,255; + expected: green + +- name: 2d.path.bezierCurveTo.basic + testing: + - 2d.path.bezier.nonempty + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.bezierCurveTo(100, 25, 100, 25, 100, 25); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.bezierCurveTo.shape + testing: + - 2d.path.bezier.nonempty + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 55; + ctx.beginPath(); + ctx.moveTo(-2000, 3100); + ctx.bezierCurveTo(-2000, -1000, 2100, -1000, 2100, 3100); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.bezierCurveTo.scaled + testing: + - 2d.path.bezier.nonempty + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.scale(1000, 1000); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 0.055; + ctx.beginPath(); + ctx.moveTo(-2, 3.1); + ctx.bezierCurveTo(-2, -1, 2.1, -1, 2.1, 3.1); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.bezierCurveTo.nonfinite + desc: bezierCurveTo() with Infinity/NaN is ignored + testing: + - 2d.nonfinite + code: | + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + @nonfinite ctx.bezierCurveTo(<0 Infinity -Infinity NaN>, <50 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <50 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <50 Infinity -Infinity NaN>); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 90,45 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.ensuresubpath.1 + desc: If there is no subpath, the first control point is added (and nothing is drawn + up to it) + testing: + - 2d.path.arcTo.empty + - 2d.path.ensure + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.arcTo(100, 50, 200, 50, 0.1); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.ensuresubpath.2 + desc: If there is no subpath, the first control point is added + testing: + - 2d.path.arcTo.empty + - 2d.path.ensure + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.arcTo(0, 25, 50, 250, 0.1); // adds (x1,y1), draws nothing + ctx.lineTo(100, 25); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.coincide.1 + desc: arcTo() has no effect if P0 = P1 + testing: + - 2d.path.arcTo.coincide.01 + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(0, 25, 50, 1000, 1); + ctx.lineTo(100, 25); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.arcTo(50, 25, 100, 25, 1); + ctx.stroke(); + + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.coincide.2 + desc: arcTo() draws a straight line to P1 if P1 = P2 + testing: + - 2d.path.arcTo.coincide.12 + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(100, 25, 100, 25, 1); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.collinear.1 + desc: arcTo() with all points on a line, and P1 between P0/P2, draws a straight + line to P1 + testing: + - 2d.path.arcTo.collinear + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(100, 25, 200, 25, 1); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(-100, 25); + ctx.arcTo(0, 25, 100, 25, 1); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.collinear.2 + desc: arcTo() with all points on a line, and P2 between P0/P1, draws a straight + line to P1 + testing: + - 2d.path.arcTo.collinear + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(100, 25, 10, 25, 1); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 25); + ctx.arcTo(200, 25, 110, 25, 1); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.collinear.3 + desc: arcTo() with all points on a line, and P0 between P1/P2, draws a straight + line to P1 + testing: + - 2d.path.arcTo.collinear + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(100, 25, -100, 25, 1); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 25); + ctx.arcTo(200, 25, 0, 25, 1); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(-100, 25); + ctx.arcTo(0, 25, -200, 25, 1); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.shape.curve1 + desc: arcTo() curves in the right kind of shape + testing: + - 2d.path.arcTo.shape + code: | + var tol = 1.5; // tolerance to avoid antialiasing artifacts + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 10; + ctx.beginPath(); + ctx.moveTo(10, 25); + ctx.arcTo(75, 25, 75, 60, 20); + ctx.stroke(); + + ctx.fillStyle = '#0f0'; + ctx.beginPath(); + ctx.rect(10, 20, 45, 10); + ctx.moveTo(80, 45); + ctx.arc(55, 45, 25+tol, 0, -Math.PI/2, true); + ctx.arc(55, 45, 15-tol, -Math.PI/2, 0, false); + ctx.fill(); + + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 55,19 == 0,255,0,255; + @assert pixel 55,20 == 0,255,0,255; + @assert pixel 55,21 == 0,255,0,255; + @assert pixel 64,22 == 0,255,0,255; + @assert pixel 65,21 == 0,255,0,255; + @assert pixel 72,28 == 0,255,0,255; + @assert pixel 73,27 == 0,255,0,255; + @assert pixel 78,36 == 0,255,0,255; + @assert pixel 79,35 == 0,255,0,255; + @assert pixel 80,44 == 0,255,0,255; + @assert pixel 80,45 == 0,255,0,255; + @assert pixel 80,46 == 0,255,0,255; + @assert pixel 65,45 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.shape.curve2 + desc: arcTo() curves in the right kind of shape + testing: + - 2d.path.arcTo.shape + code: | + var tol = 1.5; // tolerance to avoid antialiasing artifacts + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.rect(10, 20, 45, 10); + ctx.moveTo(80, 45); + ctx.arc(55, 45, 25-tol, 0, -Math.PI/2, true); + ctx.arc(55, 45, 15+tol, -Math.PI/2, 0, false); + ctx.fill(); + + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 10; + ctx.beginPath(); + ctx.moveTo(10, 25); + ctx.arcTo(75, 25, 75, 60, 20); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 55,19 == 0,255,0,255; + @assert pixel 55,20 == 0,255,0,255; + @assert pixel 55,21 == 0,255,0,255; + @assert pixel 64,22 == 0,255,0,255; + @assert pixel 65,21 == 0,255,0,255; + @assert pixel 72,28 == 0,255,0,255; + @assert pixel 73,27 == 0,255,0,255; + @assert pixel 78,36 == 0,255,0,255; + @assert pixel 79,35 == 0,255,0,255; + @assert pixel 80,44 == 0,255,0,255; + @assert pixel 80,45 == 0,255,0,255; + @assert pixel 80,46 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.shape.start + desc: arcTo() draws a straight line from P0 to P1 + testing: + - 2d.path.arcTo.shape + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(200, 25, 200, 50, 10); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.shape.end + desc: arcTo() does not draw anything from P1 to P2 + testing: + - 2d.path.arcTo.shape + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.moveTo(-100, -100); + ctx.arcTo(-100, 25, 200, 25, 10); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.negative + desc: arcTo() with negative radius throws an exception + testing: + - 2d.path.arcTo.negative + code: | + @assert throws INDEX_SIZE_ERR ctx.arcTo(0, 0, 0, 0, -1); + var path = new Path2D(); + @assert throws INDEX_SIZE_ERR path.arcTo(10, 10, 20, 20, -5); + +- name: 2d.path.arcTo.zero.1 + desc: arcTo() with zero radius draws a straight line from P0 to P1 + testing: + - 2d.path.arcTo.zeroradius + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(100, 25, 100, 100, 0); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(0, -25); + ctx.arcTo(50, -25, 50, 50, 0); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.zero.2 + desc: arcTo() with zero radius draws a straight line from P0 to P1, even when all + points are collinear + testing: + - 2d.path.arcTo.zeroradius + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(100, 25, -100, 25, 0); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 25); + ctx.arcTo(200, 25, 50, 25, 0); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.transformation + desc: arcTo joins up to the last subpath point correctly + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 50); + ctx.translate(100, 0); + ctx.arcTo(50, 50, 50, 0, 50); + ctx.lineTo(-100, 0); + ctx.fill(); + + @assert pixel 0,0 == 0,255,0,255; + @assert pixel 50,0 == 0,255,0,255; + @assert pixel 99,0 == 0,255,0,255; + @assert pixel 0,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 99,25 == 0,255,0,255; + @assert pixel 0,49 == 0,255,0,255; + @assert pixel 50,49 == 0,255,0,255; + @assert pixel 99,49 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.scale + desc: arcTo scales the curve, not just the control points + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 50); + ctx.translate(100, 0); + ctx.scale(0.1, 1); + ctx.arcTo(50, 50, 50, 0, 50); + ctx.lineTo(-1000, 0); + ctx.fill(); + + @assert pixel 0,0 == 0,255,0,255; + @assert pixel 50,0 == 0,255,0,255; + @assert pixel 99,0 == 0,255,0,255; + @assert pixel 0,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 99,25 == 0,255,0,255; + @assert pixel 0,49 == 0,255,0,255; + @assert pixel 50,49 == 0,255,0,255; + @assert pixel 99,49 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.nonfinite + desc: arcTo() with Infinity/NaN is ignored + testing: + - 2d.nonfinite + code: | + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + @nonfinite ctx.arcTo(<0 Infinity -Infinity NaN>, <50 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <50 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 90,45 == 0,255,0,255; + expected: green + + +- name: 2d.path.arc.empty + desc: arc() with an empty path does not draw a straight line to the start point + testing: + - 2d.path.arc.nonempty + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.arc(200, 25, 5, 0, 2*Math.PI, true); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.nonempty + desc: arc() with a non-empty path does draw a straight line to the start point + testing: + - 2d.path.arc.nonempty + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arc(200, 25, 5, 0, 2*Math.PI, true); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.end + desc: arc() adds the end point of the arc to the subpath + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(-100, 0); + ctx.arc(-100, 0, 25, -Math.PI/2, Math.PI/2, true); + ctx.lineTo(100, 25); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.default + desc: arc() with missing last argument defaults to clockwise + testing: + - 2d.path.arc.omitted + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 0); + ctx.arc(100, 0, 150, -Math.PI, Math.PI/2); + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.angle.1 + desc: arc() draws pi/2 .. -pi anticlockwise correctly + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 0); + ctx.arc(100, 0, 150, Math.PI/2, -Math.PI, true); + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.angle.2 + desc: arc() draws -3pi/2 .. -pi anticlockwise correctly + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 0); + ctx.arc(100, 0, 150, -3*Math.PI/2, -Math.PI, true); + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.angle.3 + desc: arc() wraps angles mod 2pi when anticlockwise and end > start+2pi + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 0); + ctx.arc(100, 0, 150, (512+1/2)*Math.PI, (1024-1)*Math.PI, true); + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.angle.4 + desc: arc() draws a full circle when clockwise and end > start+2pi + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.arc(50, 25, 60, (512+1/2)*Math.PI, (1024-1)*Math.PI, false); + ctx.fill(); + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.angle.5 + desc: arc() wraps angles mod 2pi when clockwise and start > end+2pi + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 0); + ctx.arc(100, 0, 150, (1024-1)*Math.PI, (512+1/2)*Math.PI, false); + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.angle.6 + desc: arc() draws a full circle when anticlockwise and start > end+2pi + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.arc(50, 25, 60, (1024-1)*Math.PI, (512+1/2)*Math.PI, true); + ctx.fill(); + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.zero.1 + desc: arc() draws nothing when startAngle = endAngle and anticlockwise + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.arc(50, 25, 50, 0, 0, true); + ctx.stroke(); + @assert pixel 50,20 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.zero.2 + desc: arc() draws nothing when startAngle = endAngle and clockwise + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.arc(50, 25, 50, 0, 0, false); + ctx.stroke(); + @assert pixel 50,20 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.twopie.1 + desc: arc() draws nothing when end = start + 2pi-e and anticlockwise + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.arc(50, 25, 50, 0, 2*Math.PI - 1e-4, true); + ctx.stroke(); + @assert pixel 50,20 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.twopie.2 + desc: arc() draws a full circle when end = start + 2pi-e and clockwise + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.arc(50, 25, 50, 0, 2*Math.PI - 1e-4, false); + ctx.stroke(); + @assert pixel 50,20 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.twopie.3 + desc: arc() draws a full circle when end = start + 2pi+e and anticlockwise + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.arc(50, 25, 50, 0, 2*Math.PI + 1e-4, true); + ctx.stroke(); + @assert pixel 50,20 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.twopie.4 + desc: arc() draws nothing when end = start + 2pi+e and clockwise + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.arc(50, 25, 50, 0, 2*Math.PI + 1e-4, false); + ctx.stroke(); + @assert pixel 50,20 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.shape.1 + desc: arc() from 0 to pi does not draw anything in the wrong half + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.arc(50, 50, 50, 0, Math.PI, false); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 20,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.shape.2 + desc: arc() from 0 to pi draws stuff in the right half + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 100; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.arc(50, 50, 50, 0, Math.PI, true); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 20,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.shape.3 + desc: arc() from 0 to -pi/2 does not draw anything in the wrong quadrant + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 100; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.arc(0, 50, 50, 0, -Math.PI/2, false); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; @moz-todo + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.shape.4 + desc: arc() from 0 to -pi/2 draws stuff in the right quadrant + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 150; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.arc(-50, 50, 100, 0, -Math.PI/2, true); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.shape.5 + desc: arc() from 0 to 5pi does not draw crazy things + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 200; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.arc(300, 0, 100, 0, 5*Math.PI, false); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.selfintersect.1 + desc: arc() with lineWidth > 2*radius is drawn sensibly + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 200; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.arc(100, 50, 25, 0, -Math.PI/2, true); + ctx.stroke(); + ctx.beginPath(); + ctx.arc(0, 0, 25, 0, -Math.PI/2, true); + ctx.stroke(); + @assert pixel 1,1 == 0,255,0,255; @moz-todo + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.selfintersect.2 + desc: arc() with lineWidth > 2*radius is drawn sensibly + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 180; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.arc(-50, 50, 25, 0, -Math.PI/2, true); + ctx.stroke(); + ctx.beginPath(); + ctx.arc(100, 0, 25, 0, -Math.PI/2, true); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 90,10 == 0,255,0,255; + @assert pixel 97,1 == 0,255,0,255; + @assert pixel 97,2 == 0,255,0,255; + @assert pixel 97,3 == 0,255,0,255; + @assert pixel 2,48 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.negative + desc: arc() with negative radius throws INDEX_SIZE_ERR + testing: + - 2d.path.arc.negative + code: | + @assert throws INDEX_SIZE_ERR ctx.arc(0, 0, -1, 0, 0, true); + var path = new Path2D(); + @assert throws INDEX_SIZE_ERR path.arc(10, 10, -5, 0, 1, false); + +- name: 2d.path.arc.zeroradius + desc: arc() with zero radius draws a line to the start point + testing: + - 2d.path.arc.zero + code: | + ctx.fillStyle = '#f00' + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arc(200, 25, 0, 0, Math.PI, true); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.scale.1 + desc: Non-uniformly scaled arcs are the right shape + testing: + - 2d.path.transformation + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.scale(2, 0.5); + ctx.fillStyle = '#0f0'; + ctx.beginPath(); + ctx.arc(25, 50, 56, 0, 2*Math.PI, false); + ctx.fill(); + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(-25, 50); + ctx.arc(-25, 50, 24, 0, 2*Math.PI, false); + ctx.moveTo(75, 50); + ctx.arc(75, 50, 24, 0, 2*Math.PI, false); + ctx.moveTo(25, -25); + ctx.arc(25, -25, 24, 0, 2*Math.PI, false); + ctx.moveTo(25, 125); + ctx.arc(25, 125, 24, 0, 2*Math.PI, false); + ctx.fill(); + + @assert pixel 0,0 == 0,255,0,255; + @assert pixel 50,0 == 0,255,0,255; + @assert pixel 99,0 == 0,255,0,255; + @assert pixel 0,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 99,25 == 0,255,0,255; + @assert pixel 0,49 == 0,255,0,255; + @assert pixel 50,49 == 0,255,0,255; + @assert pixel 99,49 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.scale.2 + desc: Highly scaled arcs are the right shape + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.scale(100, 100); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 1.2; + ctx.beginPath(); + ctx.arc(0, 0, 0.6, 0, Math.PI/2, false); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.nonfinite + desc: arc() with Infinity/NaN is ignored + testing: + - 2d.nonfinite + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + @nonfinite ctx.arc(<0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <50 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <2*Math.PI Infinity -Infinity NaN>, ); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 90,45 == 0,255,0,255; + expected: green + + +- name: 2d.path.rect.basic + testing: + - 2d.path.rect.subpath + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.rect(0, 0, 100, 50); + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.rect.newsubpath + testing: + - 2d.path.rect.subpath + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.beginPath(); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.moveTo(-100, 25); + ctx.lineTo(-50, 25); + ctx.rect(200, 25, 1, 1); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.rect.closed + testing: + - 2d.path.rect.closed + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 200; + ctx.lineJoin = 'miter'; + ctx.rect(100, 50, 100, 100); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.rect.end.1 + testing: + - 2d.path.rect.newsubpath + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.rect(200, 100, 400, 1000); + ctx.lineTo(-2000, -1000); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.rect.end.2 + testing: + - 2d.path.rect.newsubpath + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 450; + ctx.lineCap = 'round'; + ctx.lineJoin = 'bevel'; + ctx.rect(150, 150, 2000, 2000); + ctx.lineTo(160, 160); + ctx.stroke(); + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.rect.zero.1 + testing: + - 2d.path.rect.subpath + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.rect(0, 50, 100, 0); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.rect.zero.2 + testing: + - 2d.path.rect.subpath + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.rect(50, -100, 0, 250); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.rect.zero.3 + testing: + - 2d.path.rect.subpath + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.rect(50, 25, 0, 0); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.rect.zero.4 + testing: + - 2d.path.rect.subpath + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.rect(100, 25, 0, 0); + ctx.lineTo(0, 25); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.rect.zero.5 + testing: + - 2d.path.rect.subpath + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.moveTo(0, 0); + ctx.rect(100, 25, 0, 0); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.rect.zero.6 + testing: + - 2d.path.rect.subpath + #mozilla: { bug: TODO } + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineJoin = 'miter'; + ctx.miterLimit = 1.5; + ctx.lineWidth = 200; + ctx.beginPath(); + ctx.rect(100, 25, 1000, 0); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.path.rect.negative + testing: + - 2d.path.rect.subpath + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.beginPath(); + ctx.fillStyle = '#0f0'; + ctx.rect(0, 0, 50, 25); + ctx.rect(100, 0, -50, 25); + ctx.rect(0, 50, 50, -25); + ctx.rect(100, 50, -50, -25); + ctx.fill(); + @assert pixel 25,12 == 0,255,0,255; + @assert pixel 75,12 == 0,255,0,255; + @assert pixel 25,37 == 0,255,0,255; + @assert pixel 75,37 == 0,255,0,255; + +- name: 2d.path.rect.winding + testing: + - 2d.path.rect.subpath + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.beginPath(); + ctx.fillStyle = '#f00'; + ctx.rect(0, 0, 50, 50); + ctx.rect(100, 50, -50, -50); + ctx.rect(0, 25, 100, -25); + ctx.rect(100, 25, -100, 25); + ctx.fill(); + @assert pixel 25,12 == 0,255,0,255; + @assert pixel 75,12 == 0,255,0,255; + @assert pixel 25,37 == 0,255,0,255; + @assert pixel 75,37 == 0,255,0,255; + +- name: 2d.path.rect.selfintersect + #mozilla: { bug: TODO } + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 90; + ctx.beginPath(); + ctx.rect(45, 20, 10, 10); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.path.rect.nonfinite + desc: rect() with Infinity/NaN is ignored + testing: + - 2d.nonfinite + code: | + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + @nonfinite ctx.rect(<0 Infinity -Infinity NaN>, <50 Infinity -Infinity NaN>, <1 Infinity -Infinity NaN>, <1 Infinity -Infinity NaN>); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 90,45 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.newsubpath + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.beginPath(); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.moveTo(-100, 25); + ctx.lineTo(-50, 25); + ctx.roundRect(200, 25, 1, 1, [0]); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.closed + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 200; + ctx.lineJoin = 'miter'; + ctx.roundRect(100, 50, 100, 100, [0]); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.end.1 + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.roundRect(200, 100, 400, 1000, [0]); + ctx.lineTo(-2000, -1000); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.end.2 + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 450; + ctx.lineCap = 'round'; + ctx.lineJoin = 'bevel'; + ctx.roundRect(150, 150, 2000, 2000, [0]); + ctx.lineTo(160, 160); + ctx.stroke(); + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.end.3 + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.roundRect(101, 51, 2000, 2000, [500, 500, 500, 500]); + ctx.lineTo(-1, -1); + ctx.stroke(); + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.end.4 + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 10; + ctx.roundRect(-1, -1, 2000, 2000, [1000, 1000, 1000, 1000]); + ctx.lineTo(-150, -150); + ctx.stroke(); + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.zero.1 + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.roundRect(0, 50, 100, 0, [0]); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.zero.2 + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.roundRect(50, -100, 0, 250, [0]); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.zero.3 + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.roundRect(50, 25, 0, 0, [0]); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.zero.4 + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.roundRect(100, 25, 0, 0, [0]); + ctx.lineTo(0, 25); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.zero.5 + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.moveTo(0, 0); + ctx.roundRect(100, 25, 0, 0, [0]); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.zero.6 + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineJoin = 'miter'; + ctx.miterLimit = 1.5; + ctx.lineWidth = 200; + ctx.beginPath(); + ctx.roundRect(100, 25, 1000, 0, [0]); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.negative + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.beginPath(); + ctx.fillStyle = '#0f0'; + ctx.roundRect(0, 0, 50, 25, [10, 0, 0, 0]); + ctx.roundRect(100, 0, -50, 25, [10, 0, 0, 0]); + ctx.roundRect(0, 50, 50, -25, [10, 0, 0, 0]); + ctx.roundRect(100, 50, -50, -25, [10, 0, 0, 0]); + ctx.fill(); + // All rects drawn + @assert pixel 25,12 == 0,255,0,255; + @assert pixel 75,12 == 0,255,0,255; + @assert pixel 25,37 == 0,255,0,255; + @assert pixel 75,37 == 0,255,0,255; + // Correct corners are rounded. + @assert pixel 1,1 == 255,0,0,255; + @assert pixel 98,1 == 255,0,0,255; + @assert pixel 1,48 == 255,0,0,255; + @assert pixel 98,48 == 255,0,0,255; + +- name: 2d.path.roundrect.winding + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.beginPath(); + ctx.fillStyle = '#f00'; + ctx.roundRect(0, 0, 50, 50, [0]); + ctx.roundRect(100, 50, -50, -50, [0]); + ctx.roundRect(0, 25, 100, -25, [0]); + ctx.roundRect(100, 25, -100, 25, [0]); + ctx.fill(); + @assert pixel 25,12 == 0,255,0,255; + @assert pixel 75,12 == 0,255,0,255; + @assert pixel 25,37 == 0,255,0,255; + @assert pixel 75,37 == 0,255,0,255; + +- name: 2d.path.roundrect.selfintersect + code: | + ctx.fillStyle = '#f00'; + ctx.roundRect(0, 0, 100, 50, [0]); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 90; + ctx.beginPath(); + ctx.roundRect(45, 20, 10, 10, [0]); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.nonfinite + desc: roundRect() with Infinity/NaN is ignored + testing: + - 2d.nonfinite + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + @nonfinite ctx.roundRect(<0 Infinity -Infinity NaN>, <50 Infinity -Infinity NaN>, <1 Infinity -Infinity NaN>, <1 Infinity -Infinity NaN>, <[0] [Infinity] [-Infinity] [NaN] [Infinity,0] [-Infinity,0] [NaN,0] [0,Infinity] [0,-Infinity] [0,NaN] [Infinity,0,0] [-Infinity,0,0] [NaN,0,0] [0,Infinity,0] [0,-Infinity,0] [0,NaN,0] [0,0,Infinity] [0,0,-Infinity] [0,0,NaN] [Infinity,0,0,0] [-Infinity,0,0,0] [NaN,0,0,0] [0,Infinity,0,0] [0,-Infinity,0,0] [0,NaN,0,0] [0,0,Infinity,0] [0,0,-Infinity,0] [0,0,NaN,0] [0,0,0,Infinity] [0,0,0,-Infinity] [0,0,0,NaN]>); + ctx.roundRect(0, 0, 100, 100, [new DOMPoint(10, Infinity)]); + ctx.roundRect(0, 0, 100, 100, [new DOMPoint(10, -Infinity)]); + ctx.roundRect(0, 0, 100, 100, [new DOMPoint(10, NaN)]); + ctx.roundRect(0, 0, 100, 100, [new DOMPoint(Infinity, 10)]); + ctx.roundRect(0, 0, 100, 100, [new DOMPoint(-Infinity, 10)]); + ctx.roundRect(0, 0, 100, 100, [new DOMPoint(NaN, 10)]); + ctx.roundRect(0, 0, 100, 100, [{x: 10, y: Infinity}]); + ctx.roundRect(0, 0, 100, 100, [{x: 10, y: -Infinity}]); + ctx.roundRect(0, 0, 100, 100, [{x: 10, y: NaN}]); + ctx.roundRect(0, 0, 100, 100, [{x: Infinity, y: 10}]); + ctx.roundRect(0, 0, 100, 100, [{x: -Infinity, y: 10}]); + ctx.roundRect(0, 0, 100, 100, [{x: NaN, y: 10}]); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 90,45 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.4.radii.1.double + desc: Verify that when four radii are given to roundRect(), the first radius, specified as a double, applies to the top-left corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [20, 0, 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 1,1 == 255,0,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.4.radii.1.dompoint + desc: Verify that when four radii are given to roundRect(), the first radius, specified as a DOMPoint, applies to the top-left corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [new DOMPoint(40, 20), 0, 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + @assert pixel 20,1 == 255,0,0,255; + @assert pixel 41,1 == 0,255,0,255; + @assert pixel 1,10 == 255,0,0,255; + @assert pixel 1,21 == 0,255,0,255; + + // other corners + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.4.radii.1.dompointinit + desc: Verify that when four radii are given to roundRect(), the first radius, specified as a DOMPointInit, applies to the top-left corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [{x: 40, y: 20}, 0, 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + @assert pixel 20,1 == 255,0,0,255; + @assert pixel 41,1 == 0,255,0,255; + @assert pixel 1,10 == 255,0,0,255; + @assert pixel 1,21 == 0,255,0,255; + + // other corners + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.4.radii.2.double + desc: Verify that when four radii are given to roundRect(), the second radius, specified as a double, applies to the top-right corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 20, 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 255,0,0,255; + @assert pixel 98,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.4.radii.2.dompoint + desc: Verify that when four radii are given to roundRect(), the second radius, specified as a DOMPoint, applies to the top-right corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, new DOMPoint(40, 20), 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-right corner + @assert pixel 79,1 == 255,0,0,255; + @assert pixel 58,1 == 0,255,0,255; + @assert pixel 98,10 == 255,0,0,255; + @assert pixel 98,21 == 0,255,0,255; + + // other corners + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.4.radii.2.dompointinit + desc: Verify that when four radii are given to roundRect(), the second radius, specified as a DOMPointInit, applies to the top-right corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, {x: 40, y: 20}, 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-right corner + @assert pixel 79,1 == 255,0,0,255; + @assert pixel 58,1 == 0,255,0,255; + @assert pixel 98,10 == 255,0,0,255; + @assert pixel 98,21 == 0,255,0,255; + + // other corners + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.4.radii.3.double + desc: Verify that when four radii are given to roundRect(), the third radius, specified as a double, applies to the bottom-right corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, 20, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 98,48 == 255,0,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.4.radii.3.dompoint + desc: Verify that when four radii are given to roundRect(), the third radius, specified as a DOMPoint, applies to the bottom-right corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, new DOMPoint(40, 20), 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // bottom-right corner + @assert pixel 79,48 == 255,0,0,255; + @assert pixel 58,48 == 0,255,0,255; + @assert pixel 98,39 == 255,0,0,255; + @assert pixel 98,28 == 0,255,0,255; + + // other corners + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.4.radii.3.dompointinit + desc: Verify that when four radii are given to roundRect(), the third radius, specified as a DOMPointInit, applies to the bottom-right corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, {x: 40, y: 20}, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // bottom-right corner + @assert pixel 79,48 == 255,0,0,255; + @assert pixel 58,48 == 0,255,0,255; + @assert pixel 98,39 == 255,0,0,255; + @assert pixel 98,28 == 0,255,0,255; + + // other corners + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.4.radii.4.double + desc: Verify that when four radii are given to roundRect(), the fourth radius, specified as a double, applies to the bottom-left corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, 0, 20]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + @assert pixel 1,48 == 255,0,0,255; + +- name: 2d.path.roundrect.4.radii.4.dompoint + desc: Verify that when four radii are given to roundRect(), the fourth radius, specified as a DOMPoint, applies to the bottom-left corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, 0, new DOMPoint(40, 20)]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // bottom-left corner + @assert pixel 20,48 == 255,0,0,255; + @assert pixel 41,48 == 0,255,0,255; + @assert pixel 1,39 == 255,0,0,255; + @assert pixel 1,28 == 0,255,0,255; + + // other corners + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + +- name: 2d.path.roundrect.4.radii.4.dompointinit + desc: Verify that when four radii are given to roundRect(), the fourth radius, specified as a DOMPointInit, applies to the bottom-left corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, 0, {x: 40, y: 20}]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // bottom-left corner + @assert pixel 20,48 == 255,0,0,255; + @assert pixel 41,48 == 0,255,0,255; + @assert pixel 1,39 == 255,0,0,255; + @assert pixel 1,28 == 0,255,0,255; + + // other corners + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + +- name: 2d.path.roundrect.3.radii.1.double + desc: Verify that when three radii are given to roundRect(), the first radius, specified as a double, applies to the top-left corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [20, 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 1,1 == 255,0,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.3.radii.1.dompoint + desc: Verify that when three radii are given to roundRect(), the first radius, specified as a DOMPoint, applies to the top-left corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [new DOMPoint(40, 20), 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + @assert pixel 20,1 == 255,0,0,255; + @assert pixel 41,1 == 0,255,0,255; + @assert pixel 1,10 == 255,0,0,255; + @assert pixel 1,21 == 0,255,0,255; + + // other corners + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.3.radii.1.dompointinit + desc: Verify that when three radii are given to roundRect(), the first radius, specified as a DOMPointInit, applies to the top-left corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [{x: 40, y: 20}, 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + @assert pixel 20,1 == 255,0,0,255; + @assert pixel 41,1 == 0,255,0,255; + @assert pixel 1,10 == 255,0,0,255; + @assert pixel 1,21 == 0,255,0,255; + + // other corners + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.3.radii.2.double + desc: Verify that when three radii are given to roundRect(), the second radius, specified as a double, applies to the top-right and bottom-left corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 20, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 255,0,0,255; + @assert pixel 98,48 == 0,255,0,255; + @assert pixel 1,48 == 255,0,0,255; + +- name: 2d.path.roundrect.3.radii.2.dompoint + desc: Verify that when three radii are given to roundRect(), the second radius, specified as a DOMPoint, applies to the top-right and bottom-left corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, new DOMPoint(40, 20), 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-right corner + @assert pixel 79,1 == 255,0,0,255; + @assert pixel 58,1 == 0,255,0,255; + @assert pixel 98,10 == 255,0,0,255; + @assert pixel 98,21 == 0,255,0,255; + + // bottom-left corner + @assert pixel 20,48 == 255,0,0,255; + @assert pixel 41,48 == 0,255,0,255; + @assert pixel 1,39 == 255,0,0,255; + @assert pixel 1,28 == 0,255,0,255; + + // other corners + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + +- name: 2d.path.roundrect.3.radii.2.dompointinit + desc: Verify that when three radii are given to roundRect(), the second radius, specified as a DOMPoint, applies to the top-right and bottom-left corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, {x: 40, y: 20}, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-right corner + @assert pixel 79,1 == 255,0,0,255; + @assert pixel 58,1 == 0,255,0,255; + @assert pixel 98,10 == 255,0,0,255; + @assert pixel 98,21 == 0,255,0,255; + + // bottom-left corner + @assert pixel 20,48 == 255,0,0,255; + @assert pixel 41,48 == 0,255,0,255; + @assert pixel 1,39 == 255,0,0,255; + @assert pixel 1,28 == 0,255,0,255; + + // other corners + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + +- name: 2d.path.roundrect.3.radii.3.double + desc: Verify that when three radii are given to roundRect(), the third radius, specified as a double, applies to the bottom-right corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, 20]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 98,48 == 255,0,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.3.radii.3.dompoint + desc: Verify that when three radii are given to roundRect(), the third radius, specified as a DOMPoint, applies to the bottom-right corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, new DOMPoint(40, 20)]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // bottom-right corner + @assert pixel 79,48 == 255,0,0,255; + @assert pixel 58,48 == 0,255,0,255; + @assert pixel 98,39 == 255,0,0,255; + @assert pixel 98,28 == 0,255,0,255; + + // other corners + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.3.radii.3.dompointinit + desc: Verify that when three radii are given to roundRect(), the third radius, specified as a DOMPointInit, applies to the bottom-right corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, {x: 40, y: 20}]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // bottom-right corner + @assert pixel 79,48 == 255,0,0,255; + @assert pixel 58,48 == 0,255,0,255; + @assert pixel 98,39 == 255,0,0,255; + @assert pixel 98,28 == 0,255,0,255; + + // other corners + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.2.radii.1.double + desc: Verify that when two radii are given to roundRect(), the first radius, specified as a double, applies to the top-left and bottom-right corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [20, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 1,1 == 255,0,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 98,48 == 255,0,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.2.radii.1.dompoint + desc: Verify that when two radii are given to roundRect(), the first radius, specified as a DOMPoint, applies to the top-left and bottom-right corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [new DOMPoint(40, 20), 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + @assert pixel 20,1 == 255,0,0,255; + @assert pixel 41,1 == 0,255,0,255; + @assert pixel 1,10 == 255,0,0,255; + @assert pixel 1,21 == 0,255,0,255; + + // bottom-right corner + @assert pixel 79,48 == 255,0,0,255; + @assert pixel 58,48 == 0,255,0,255; + @assert pixel 98,39 == 255,0,0,255; + @assert pixel 98,28 == 0,255,0,255; + + // other corners + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.2.radii.1.dompointinit + desc: Verify that when two radii are given to roundRect(), the first radius, specified as a DOMPointInit, applies to the top-left and bottom-right corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [{x: 40, y: 20}, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + @assert pixel 20,1 == 255,0,0,255; + @assert pixel 41,1 == 0,255,0,255; + @assert pixel 1,10 == 255,0,0,255; + @assert pixel 1,21 == 0,255,0,255; + + // bottom-right corner + @assert pixel 79,48 == 255,0,0,255; + @assert pixel 58,48 == 0,255,0,255; + @assert pixel 98,39 == 255,0,0,255; + @assert pixel 98,28 == 0,255,0,255; + + // other corners + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.2.radii.2.double + desc: Verify that when two radii are given to roundRect(), the second radius, specified as a double, applies to the top-right and bottom-left corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 20]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 255,0,0,255; + @assert pixel 98,48 == 0,255,0,255; + @assert pixel 1,48 == 255,0,0,255; + +- name: 2d.path.roundrect.2.radii.2.dompoint + desc: Verify that when two radii are given to roundRect(), the second radius, specified as a DOMPoint, applies to the top-right and bottom-left corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, new DOMPoint(40, 20)]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-right corner + @assert pixel 79,1 == 255,0,0,255; + @assert pixel 58,1 == 0,255,0,255; + @assert pixel 98,10 == 255,0,0,255; + @assert pixel 98,21 == 0,255,0,255; + + // bottom-left corner + @assert pixel 20,48 == 255,0,0,255; + @assert pixel 41,48 == 0,255,0,255; + @assert pixel 1,39 == 255,0,0,255; + @assert pixel 1,28 == 0,255,0,255; + + // other corners + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + +- name: 2d.path.roundrect.2.radii.2.dompointinit + desc: Verify that when two radii are given to roundRect(), the second radius, specified as a DOMPointInit, applies to the top-right and bottom-left corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, {x: 40, y: 20}]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-right corner + @assert pixel 79,1 == 255,0,0,255; + @assert pixel 58,1 == 0,255,0,255; + @assert pixel 98,10 == 255,0,0,255; + @assert pixel 98,21 == 0,255,0,255; + + // bottom-left corner + @assert pixel 20,48 == 255,0,0,255; + @assert pixel 41,48 == 0,255,0,255; + @assert pixel 1,39 == 255,0,0,255; + @assert pixel 1,28 == 0,255,0,255; + + // other corners + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + +- name: 2d.path.roundrect.1.radius.double + desc: Verify that when one radius is given to roundRect(), specified as a double, it applies to all corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [20]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 1,1 == 255,0,0,255; + @assert pixel 98,1 == 255,0,0,255; + @assert pixel 98,48 == 255,0,0,255; + @assert pixel 1,48 == 255,0,0,255; + +- name: 2d.path.roundrect.1.radius.double.single.argument + desc: Verify that when one radius is given to roundRect() as a non-array argument, specified as a double, it applies to all corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, 20); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 1,1 == 255,0,0,255; + @assert pixel 98,1 == 255,0,0,255; + @assert pixel 98,48 == 255,0,0,255; + @assert pixel 1,48 == 255,0,0,255; + +- name: 2d.path.roundrect.1.radius.dompoint + desc: Verify that when one radius is given to roundRect(), specified as a DOMPoint, it applies to all corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [new DOMPoint(40, 20)]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + @assert pixel 20,1 == 255,0,0,255; + @assert pixel 41,1 == 0,255,0,255; + @assert pixel 1,10 == 255,0,0,255; + @assert pixel 1,21 == 0,255,0,255; + + // top-right corner + @assert pixel 79,1 == 255,0,0,255; + @assert pixel 58,1 == 0,255,0,255; + @assert pixel 98,10 == 255,0,0,255; + @assert pixel 98,21 == 0,255,0,255; + + // bottom-right corner + @assert pixel 79,48 == 255,0,0,255; + @assert pixel 58,48 == 0,255,0,255; + @assert pixel 98,39 == 255,0,0,255; + @assert pixel 98,28 == 0,255,0,255; + + // bottom-left corner + @assert pixel 20,48 == 255,0,0,255; + @assert pixel 41,48 == 0,255,0,255; + @assert pixel 1,39 == 255,0,0,255; + @assert pixel 1,28 == 0,255,0,255; + +- name: 2d.path.roundrect.1.radius.dompoint.single argument + desc: Verify that when one radius is given to roundRect() as a non-array argument, specified as a DOMPoint, it applies to all corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, new DOMPoint(40, 20)); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + @assert pixel 20,1 == 255,0,0,255; + @assert pixel 41,1 == 0,255,0,255; + @assert pixel 1,10 == 255,0,0,255; + @assert pixel 1,21 == 0,255,0,255; + + // top-right corner + @assert pixel 79,1 == 255,0,0,255; + @assert pixel 58,1 == 0,255,0,255; + @assert pixel 98,10 == 255,0,0,255; + @assert pixel 98,21 == 0,255,0,255; + + // bottom-right corner + @assert pixel 79,48 == 255,0,0,255; + @assert pixel 58,48 == 0,255,0,255; + @assert pixel 98,39 == 255,0,0,255; + @assert pixel 98,28 == 0,255,0,255; + + // bottom-left corner + @assert pixel 20,48 == 255,0,0,255; + @assert pixel 41,48 == 0,255,0,255; + @assert pixel 1,39 == 255,0,0,255; + @assert pixel 1,28 == 0,255,0,255; + +- name: 2d.path.roundrect.1.radius.dompointinit + desc: Verify that when one radius is given to roundRect(), specified as a DOMPointInit, applies to all corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [{x: 40, y: 20}]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + @assert pixel 20,1 == 255,0,0,255; + @assert pixel 41,1 == 0,255,0,255; + @assert pixel 1,10 == 255,0,0,255; + @assert pixel 1,21 == 0,255,0,255; + + // top-right corner + @assert pixel 79,1 == 255,0,0,255; + @assert pixel 58,1 == 0,255,0,255; + @assert pixel 98,10 == 255,0,0,255; + @assert pixel 98,21 == 0,255,0,255; + + // bottom-right corner + @assert pixel 79,48 == 255,0,0,255; + @assert pixel 58,48 == 0,255,0,255; + @assert pixel 98,39 == 255,0,0,255; + @assert pixel 98,28 == 0,255,0,255; + + // bottom-left corner + @assert pixel 20,48 == 255,0,0,255; + @assert pixel 41,48 == 0,255,0,255; + @assert pixel 1,39 == 255,0,0,255; + @assert pixel 1,28 == 0,255,0,255; + +- name: 2d.path.roundrect.1.radius.dompointinit.single.argument + desc: Verify that when one radius is given to roundRect() as a non-array argument, specified as a DOMPointInit, applies to all corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, {x: 40, y: 20}); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + @assert pixel 20,1 == 255,0,0,255; + @assert pixel 41,1 == 0,255,0,255; + @assert pixel 1,10 == 255,0,0,255; + @assert pixel 1,21 == 0,255,0,255; + + // top-right corner + @assert pixel 79,1 == 255,0,0,255; + @assert pixel 58,1 == 0,255,0,255; + @assert pixel 98,10 == 255,0,0,255; + @assert pixel 98,21 == 0,255,0,255; + + // bottom-right corner + @assert pixel 79,48 == 255,0,0,255; + @assert pixel 58,48 == 0,255,0,255; + @assert pixel 98,39 == 255,0,0,255; + @assert pixel 98,28 == 0,255,0,255; + + // bottom-left corner + @assert pixel 20,48 == 255,0,0,255; + @assert pixel 41,48 == 0,255,0,255; + @assert pixel 1,39 == 255,0,0,255; + @assert pixel 1,28 == 0,255,0,255; + +- name: 2d.path.roundrect.radius.intersecting.1 + desc: Check that roundRects with intersecting corner arcs are rendered correctly. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [40, 40, 40, 40]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 2,25 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 97,25 == 0,255,0,255; + @assert pixel 1,1 == 255,0,0,255; + @assert pixel 98,1 == 255,0,0,255; + @assert pixel 1,48 == 255,0,0,255; + @assert pixel 98,48 == 255,0,0,255; + +- name: 2d.path.roundrect.radius.intersecting.2 + desc: Check that roundRects with intersecting corner arcs are rendered correctly. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [1000, 1000, 1000, 1000]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 2,25 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 97,25 == 0,255,0,255; + @assert pixel 1,1 == 255,0,0,255; + @assert pixel 98,1 == 255,0,0,255; + @assert pixel 1,48 == 255,0,0,255; + @assert pixel 98,48 == 255,0,0,255; + +- name: 2d.path.roundrect.radius.none + desc: Check that roundRect throws an RangeError if radii is an empty array. + code: | + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 100, 50, [])}); + +- name: 2d.path.roundrect.radius.noargument + desc: Check that roundRect draws a rectangle when no radii are provided. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(10, 10, 80, 30); + ctx.fillStyle = '#0f0'; + ctx.fill(); + // upper left corner (10, 10) + @assert pixel 10,9 == 255,0,0,255; + @assert pixel 9,10 == 255,0,0,255; + @assert pixel 10,10 == 0,255,0,255; + + // upper right corner (89, 10) + @assert pixel 90,10 == 255,0,0,255; + @assert pixel 89,9 == 255,0,0,255; + @assert pixel 89,10 == 0,255,0,255; + + // lower right corner (89, 39) + @assert pixel 89,40 == 255,0,0,255; + @assert pixel 90,39 == 255,0,0,255; + @assert pixel 89,39 == 0,255,0,255; + + // lower left corner (10, 30) + @assert pixel 9,39 == 255,0,0,255; + @assert pixel 10,40 == 255,0,0,255; + @assert pixel 10,39 == 0,255,0,255; + +- name: 2d.path.roundrect.radius.toomany + desc: Check that roundRect throws an IndeSizeError if radii has more than four items. + code: | + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 100, 50, [0, 0, 0, 0, 0])}); + +- name: 2d.path.roundrect.radius.negative + desc: roundRect() with negative radius throws an exception + code: | + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 0, 0, [-1])}); + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 0, 0, [1, -1])}); + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 0, 0, [new DOMPoint(-1, 1), 1])}); + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 0, 0, [new DOMPoint(1, -1)])}); + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 0, 0, [{x: -1, y: 1}, 1])}); + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 0, 0, [{x: 1, y: -1}])}); + +- name: 2d.path.ellipse.basics + desc: Verify canvas throws error when drawing ellipse with negative radii. + testing: + - 2d.ellipse.basics + code: | + ctx.ellipse(10, 10, 10, 5, 0, 0, 1, false); + ctx.ellipse(10, 10, 10, 0, 0, 0, 1, false); + ctx.ellipse(10, 10, -0, 5, 0, 0, 1, false); + @assert throws INDEX_SIZE_ERR ctx.ellipse(10, 10, -2, 5, 0, 0, 1, false); + @assert throws INDEX_SIZE_ERR ctx.ellipse(10, 10, 0, -1.5, 0, 0, 1, false); + @assert throws INDEX_SIZE_ERR ctx.ellipse(10, 10, -2, -5, 0, 0, 1, false); + ctx.ellipse(80, 0, 10, 4294967277, Math.PI / -84, -Math.PI / 2147483436, false); + +- name: 2d.path.fill.overlap + testing: + - 2d.path.fill.basic + code: | + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = 'rgba(0, 255, 0, 0.5)'; + ctx.rect(0, 0, 100, 50); + ctx.closePath(); + ctx.rect(10, 10, 80, 30); + ctx.fill(); + + @assert pixel 50,25 ==~ 0,127,0,255 +/- 1; + expected: | + size 100 50 + cr.set_source_rgb(0, 0.5, 0) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.path.fill.winding.add + testing: + - 2d.path.fill.basic + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.moveTo(-10, -10); + ctx.lineTo(110, -10); + ctx.lineTo(110, 60); + ctx.lineTo(-10, 60); + ctx.lineTo(-10, -10); + ctx.lineTo(0, 0); + ctx.lineTo(100, 0); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fill(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.fill.winding.subtract.1 + testing: + - 2d.path.fill.basic + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#f00'; + ctx.moveTo(-10, -10); + ctx.lineTo(110, -10); + ctx.lineTo(110, 60); + ctx.lineTo(-10, 60); + ctx.lineTo(-10, -10); + ctx.lineTo(0, 0); + ctx.lineTo(0, 50); + ctx.lineTo(100, 50); + ctx.lineTo(100, 0); + ctx.fill(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.fill.winding.subtract.2 + testing: + - 2d.path.fill.basic + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#f00'; + ctx.moveTo(-10, -10); + ctx.lineTo(110, -10); + ctx.lineTo(110, 60); + ctx.lineTo(-10, 60); + ctx.moveTo(0, 0); + ctx.lineTo(0, 50); + ctx.lineTo(100, 50); + ctx.lineTo(100, 0); + ctx.fill(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.fill.winding.subtract.3 + testing: + - 2d.path.fill.basic + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.moveTo(-10, -10); + ctx.lineTo(110, -10); + ctx.lineTo(110, 60); + ctx.lineTo(-10, 60); + ctx.lineTo(-10, -10); + ctx.lineTo(-20, -20); + ctx.lineTo(120, -20); + ctx.lineTo(120, 70); + ctx.lineTo(-20, 70); + ctx.lineTo(-20, -20); + ctx.lineTo(0, 0); + ctx.lineTo(0, 50); + ctx.lineTo(100, 50); + ctx.lineTo(100, 0); + ctx.fill(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.fill.closed.basic + testing: + - 2d.path.fill.closed + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fill(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.fill.closed.unaffected + testing: + - 2d.path.fill.closed + code: | + ctx.fillStyle = '#00f'; + ctx.fillRect(0, 0, 100, 50); + + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + ctx.lineTo(100, 50); + ctx.fillStyle = '#f00'; + ctx.fill(); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + @assert pixel 90,10 == 0,255,0,255; + @assert pixel 10,40 == 0,255,0,255; + expected: green + +- name: 2d.path.stroke.overlap + desc: Stroked subpaths are combined before being drawn + testing: + - 2d.path.stroke.basic + code: | + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = 'rgba(0, 255, 0, 0.5)'; + ctx.lineWidth = 50; + ctx.moveTo(0, 20); + ctx.lineTo(100, 20); + ctx.moveTo(0, 30); + ctx.lineTo(100, 30); + ctx.stroke(); + + @assert pixel 50,25 ==~ 0,127,0,255 +/- 1; + expected: | + size 100 50 + cr.set_source_rgb(0, 0.5, 0) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.path.stroke.union + desc: Strokes in opposite directions are unioned, not subtracted + testing: + - 2d.path.stroke.basic + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 40; + ctx.moveTo(0, 10); + ctx.lineTo(100, 10); + ctx.moveTo(100, 40); + ctx.lineTo(0, 40); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.stroke.unaffected + desc: Stroking does not start a new path or subpath + testing: + - 2d.path.stroke.basic + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 50; + ctx.moveTo(-100, 25); + ctx.lineTo(-100, -100); + ctx.lineTo(200, -100); + ctx.lineTo(200, 25); + ctx.strokeStyle = '#f00'; + ctx.stroke(); + + ctx.closePath(); + ctx.strokeStyle = '#0f0'; + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.stroke.scale1 + desc: Stroke line widths are scaled by the current transformation matrix + testing: + - 2d.path.transformation + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.rect(25, 12.5, 50, 25); + ctx.save(); + ctx.scale(50, 25); + ctx.strokeStyle = '#0f0'; + ctx.stroke(); + ctx.restore(); + + ctx.beginPath(); + ctx.rect(-25, -12.5, 150, 75); + ctx.save(); + ctx.scale(50, 25); + ctx.strokeStyle = '#f00'; + ctx.stroke(); + ctx.restore(); + + @assert pixel 0,0 == 0,255,0,255; + @assert pixel 50,0 == 0,255,0,255; + @assert pixel 99,0 == 0,255,0,255; + @assert pixel 0,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 99,25 == 0,255,0,255; + @assert pixel 0,49 == 0,255,0,255; + @assert pixel 50,49 == 0,255,0,255; + @assert pixel 99,49 == 0,255,0,255; + expected: green + +- name: 2d.path.stroke.scale2 + desc: Stroke line widths are scaled by the current transformation matrix + testing: + - 2d.path.transformation + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.rect(25, 12.5, 50, 25); + ctx.save(); + ctx.rotate(Math.PI/2); + ctx.scale(25, 50); + ctx.strokeStyle = '#0f0'; + ctx.stroke(); + ctx.restore(); + + ctx.beginPath(); + ctx.rect(-25, -12.5, 150, 75); + ctx.save(); + ctx.rotate(Math.PI/2); + ctx.scale(25, 50); + ctx.strokeStyle = '#f00'; + ctx.stroke(); + ctx.restore(); + + @assert pixel 0,0 == 0,255,0,255; + @assert pixel 50,0 == 0,255,0,255; + @assert pixel 99,0 == 0,255,0,255; + @assert pixel 0,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 99,25 == 0,255,0,255; + @assert pixel 0,49 == 0,255,0,255; + @assert pixel 50,49 == 0,255,0,255; + @assert pixel 99,49 == 0,255,0,255; + expected: green + +- name: 2d.path.stroke.skew + desc: Strokes lines are skewed by the current transformation matrix + testing: + - 2d.path.transformation + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.save(); + ctx.beginPath(); + ctx.moveTo(49, -50); + ctx.lineTo(201, -50); + ctx.rotate(Math.PI/4); + ctx.scale(1, 283); + ctx.strokeStyle = '#0f0'; + ctx.stroke(); + ctx.restore(); + + ctx.save(); + ctx.beginPath(); + ctx.translate(-150, 0); + ctx.moveTo(49, -50); + ctx.lineTo(199, -50); + ctx.rotate(Math.PI/4); + ctx.scale(1, 142); + ctx.strokeStyle = '#f00'; + ctx.stroke(); + ctx.restore(); + + ctx.save(); + ctx.beginPath(); + ctx.translate(-150, 0); + ctx.moveTo(49, -50); + ctx.lineTo(199, -50); + ctx.rotate(Math.PI/4); + ctx.scale(1, 142); + ctx.strokeStyle = '#f00'; + ctx.stroke(); + ctx.restore(); + + @assert pixel 0,0 == 0,255,0,255; + @assert pixel 50,0 == 0,255,0,255; + @assert pixel 99,0 == 0,255,0,255; + @assert pixel 0,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 99,25 == 0,255,0,255; + @assert pixel 0,49 == 0,255,0,255; + @assert pixel 50,49 == 0,255,0,255; + @assert pixel 99,49 == 0,255,0,255; + expected: green + +- name: 2d.path.stroke.empty + desc: Empty subpaths are not stroked + testing: + - 2d.path.stroke.empty + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + ctx.beginPath(); + ctx.moveTo(40, 25); + ctx.moveTo(60, 25); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.stroke.prune.line + desc: Zero-length line segments from lineTo are removed before stroking + testing: + - 2d.path.stroke.prune + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.lineTo(50, 25); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.path.stroke.prune.closed + desc: Zero-length line segments from closed paths are removed before stroking + testing: + - 2d.path.stroke.prune + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.lineTo(50, 25); + ctx.closePath(); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.path.stroke.prune.curve + desc: Zero-length line segments from quadraticCurveTo and bezierCurveTo are removed + before stroking + testing: + - 2d.path.stroke.prune + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.quadraticCurveTo(50, 25, 50, 25); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.bezierCurveTo(50, 25, 50, 25, 50, 25); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.path.stroke.prune.arc + desc: Zero-length line segments from arcTo and arc are removed before stroking + testing: + - 2d.path.stroke.prune + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.arcTo(50, 25, 150, 25, 10); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(60, 25); + ctx.arc(50, 25, 10, 0, 0, false); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.path.stroke.prune.rect + desc: Zero-length line segments from rect and strokeRect are removed before stroking + testing: + - 2d.path.stroke.prune + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + ctx.beginPath(); + ctx.rect(50, 25, 0, 0); + ctx.stroke(); + + ctx.strokeRect(50, 25, 0, 0); + + @assert pixel 50,25 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.path.stroke.prune.corner + desc: Zero-length line segments are removed before stroking with miters + testing: + - 2d.path.stroke.prune + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 400; + ctx.lineJoin = 'miter'; + ctx.miterLimit = 1.4; + + ctx.beginPath(); + ctx.moveTo(-1000, 200); + ctx.lineTo(-100, 200); + ctx.lineTo(-100, 200); + ctx.lineTo(-100, 200); + ctx.lineTo(-100, 1000); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + + +- name: 2d.path.transformation.basic + testing: + - 2d.path.transformation + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(-100, 0); + ctx.rect(100, 0, 100, 50); + ctx.translate(0, -100); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.transformation.multiple + # TODO: change this name + desc: Transformations are applied while building paths, not when drawing + testing: + - 2d.path.transformation + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#f00'; + ctx.translate(-100, 0); + ctx.rect(0, 0, 100, 50); + ctx.fill(); + ctx.translate(100, 0); + ctx.fill(); + + ctx.beginPath(); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.translate(0, -50); + ctx.moveTo(0, 25); + ctx.lineTo(100, 25); + ctx.stroke(); + ctx.translate(0, 50); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.transformation.changing + desc: Transformations are applied while building paths, not when drawing + testing: + - 2d.path.transformation + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.moveTo(0, 0); + ctx.translate(100, 0); + ctx.lineTo(0, 0); + ctx.translate(0, 50); + ctx.lineTo(0, 0); + ctx.translate(-100, 0); + ctx.lineTo(0, 0); + ctx.translate(1000, 1000); + ctx.rotate(Math.PI/2); + ctx.scale(0.1, 0.1); + ctx.fill(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + + +- name: 2d.path.clip.empty + testing: + - 2d.path.clip.basic + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.clip(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.clip.basic.1 + testing: + - 2d.path.clip.basic + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.rect(0, 0, 100, 50); + ctx.clip(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.clip.basic.2 + testing: + - 2d.path.clip.basic + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.rect(-100, 0, 100, 50); + ctx.clip(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.clip.intersect + testing: + - 2d.path.clip.basic + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.rect(0, 0, 50, 50); + ctx.clip(); + ctx.beginPath(); + ctx.rect(50, 0, 50, 50) + ctx.clip(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.clip.winding.1 + testing: + - 2d.path.clip.basic + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.moveTo(-10, -10); + ctx.lineTo(110, -10); + ctx.lineTo(110, 60); + ctx.lineTo(-10, 60); + ctx.lineTo(-10, -10); + ctx.lineTo(0, 0); + ctx.lineTo(0, 50); + ctx.lineTo(100, 50); + ctx.lineTo(100, 0); + ctx.clip(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.clip.winding.2 + testing: + - 2d.path.clip.basic + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.moveTo(-10, -10); + ctx.lineTo(110, -10); + ctx.lineTo(110, 60); + ctx.lineTo(-10, 60); + ctx.lineTo(-10, -10); + ctx.clip(); + + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(0, 50); + ctx.lineTo(100, 50); + ctx.lineTo(100, 0); + ctx.lineTo(0, 0); + ctx.clip(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.clip.unaffected + testing: + - 2d.path.clip.closed + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(0, 50); + ctx.lineTo(100, 50); + ctx.lineTo(100, 0); + ctx.clip(); + + ctx.lineTo(0, 0); + ctx.fill(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + + + +- name: 2d.path.isPointInPath.basic.1 + desc: isPointInPath() detects whether the point is inside the path + testing: + - 2d.path.isPointInPath + code: | + ctx.rect(0, 0, 20, 20); + @assert ctx.isPointInPath(10, 10) === true; + @assert ctx.isPointInPath(30, 10) === false; + +- name: 2d.path.isPointInPath.basic.2 + desc: isPointInPath() detects whether the point is inside the path + testing: + - 2d.path.isPointInPath + code: | + ctx.rect(20, 0, 20, 20); + @assert ctx.isPointInPath(10, 10) === false; + @assert ctx.isPointInPath(30, 10) === true; + +- name: 2d.path.isPointInPath.edge + desc: isPointInPath() counts points on the path as being inside + testing: + - 2d.path.isPointInPath.edge + code: | + ctx.rect(0, 0, 20, 20); + @assert ctx.isPointInPath(0, 0) === true; + @assert ctx.isPointInPath(10, 0) === true; + @assert ctx.isPointInPath(20, 0) === true; + @assert ctx.isPointInPath(20, 10) === true; + @assert ctx.isPointInPath(20, 20) === true; + @assert ctx.isPointInPath(10, 20) === true; + @assert ctx.isPointInPath(0, 20) === true; + @assert ctx.isPointInPath(0, 10) === true; + @assert ctx.isPointInPath(10, -0.01) === false; + @assert ctx.isPointInPath(10, 20.01) === false; + @assert ctx.isPointInPath(-0.01, 10) === false; + @assert ctx.isPointInPath(20.01, 10) === false; + +- name: 2d.path.isPointInPath.empty + desc: isPointInPath() works when there is no path + testing: + - 2d.path.isPointInPath + code: | + @assert ctx.isPointInPath(0, 0) === false; + +- name: 2d.path.isPointInPath.subpath + desc: isPointInPath() uses the current path, not just the subpath + testing: + - 2d.path.isPointInPath + code: | + ctx.rect(0, 0, 20, 20); + ctx.beginPath(); + ctx.rect(20, 0, 20, 20); + ctx.closePath(); + ctx.rect(40, 0, 20, 20); + @assert ctx.isPointInPath(10, 10) === false; + @assert ctx.isPointInPath(30, 10) === true; + @assert ctx.isPointInPath(50, 10) === true; + +- name: 2d.path.isPointInPath.outside + desc: isPointInPath() works on paths outside the canvas + testing: + - 2d.path.isPointInPath + code: | + ctx.rect(0, -100, 20, 20); + ctx.rect(20, -10, 20, 20); + @assert ctx.isPointInPath(10, -110) === false; + @assert ctx.isPointInPath(10, -90) === true; + @assert ctx.isPointInPath(10, -70) === false; + @assert ctx.isPointInPath(30, -20) === false; + @assert ctx.isPointInPath(30, 0) === true; + @assert ctx.isPointInPath(30, 20) === false; + +- name: 2d.path.isPointInPath.unclosed + desc: isPointInPath() works on unclosed subpaths + testing: + - 2d.path.isPointInPath + code: | + ctx.moveTo(0, 0); + ctx.lineTo(20, 0); + ctx.lineTo(20, 20); + ctx.lineTo(0, 20); + @assert ctx.isPointInPath(10, 10) === true; + @assert ctx.isPointInPath(30, 10) === false; + +- name: 2d.path.isPointInPath.arc + desc: isPointInPath() works on arcs + testing: + - 2d.path.isPointInPath + code: | + ctx.arc(50, 25, 10, 0, Math.PI, false); + @assert ctx.isPointInPath(50, 10) === false; + @assert ctx.isPointInPath(50, 20) === false; + @assert ctx.isPointInPath(50, 30) === true; + @assert ctx.isPointInPath(50, 40) === false; + @assert ctx.isPointInPath(30, 20) === false; + @assert ctx.isPointInPath(70, 20) === false; + @assert ctx.isPointInPath(30, 30) === false; + @assert ctx.isPointInPath(70, 30) === false; + +- name: 2d.path.isPointInPath.bigarc + desc: isPointInPath() works on unclosed arcs larger than 2pi + opera: {bug: 320937} + testing: + - 2d.path.isPointInPath + code: | + ctx.arc(50, 25, 10, 0, 7, false); + @assert ctx.isPointInPath(50, 10) === false; + @assert ctx.isPointInPath(50, 20) === true; + @assert ctx.isPointInPath(50, 30) === true; + @assert ctx.isPointInPath(50, 40) === false; + @assert ctx.isPointInPath(30, 20) === false; + @assert ctx.isPointInPath(70, 20) === false; + @assert ctx.isPointInPath(30, 30) === false; + @assert ctx.isPointInPath(70, 30) === false; + +- name: 2d.path.isPointInPath.bezier + desc: isPointInPath() works on Bezier curves + testing: + - 2d.path.isPointInPath + code: | + ctx.moveTo(25, 25); + ctx.bezierCurveTo(50, -50, 50, 100, 75, 25); + @assert ctx.isPointInPath(25, 20) === false; + @assert ctx.isPointInPath(25, 30) === false; + @assert ctx.isPointInPath(30, 20) === true; + @assert ctx.isPointInPath(30, 30) === false; + @assert ctx.isPointInPath(40, 2) === false; + @assert ctx.isPointInPath(40, 20) === true; + @assert ctx.isPointInPath(40, 30) === false; + @assert ctx.isPointInPath(40, 47) === false; + @assert ctx.isPointInPath(45, 20) === true; + @assert ctx.isPointInPath(45, 30) === false; + @assert ctx.isPointInPath(55, 20) === false; + @assert ctx.isPointInPath(55, 30) === true; + @assert ctx.isPointInPath(60, 2) === false; + @assert ctx.isPointInPath(60, 20) === false; + @assert ctx.isPointInPath(60, 30) === true; + @assert ctx.isPointInPath(60, 47) === false; + @assert ctx.isPointInPath(70, 20) === false; + @assert ctx.isPointInPath(70, 30) === true; + @assert ctx.isPointInPath(75, 20) === false; + @assert ctx.isPointInPath(75, 30) === false; + +- name: 2d.path.isPointInPath.winding + desc: isPointInPath() uses the non-zero winding number rule + testing: + - 2d.path.isPointInPath + code: | + // Create a square ring, using opposite windings to make a hole in the centre + ctx.moveTo(0, 0); + ctx.lineTo(50, 0); + ctx.lineTo(50, 50); + ctx.lineTo(0, 50); + ctx.lineTo(0, 0); + ctx.lineTo(10, 10); + ctx.lineTo(10, 40); + ctx.lineTo(40, 40); + ctx.lineTo(40, 10); + ctx.lineTo(10, 10); + + @assert ctx.isPointInPath(5, 5) === true; + @assert ctx.isPointInPath(25, 5) === true; + @assert ctx.isPointInPath(45, 5) === true; + @assert ctx.isPointInPath(5, 25) === true; + @assert ctx.isPointInPath(25, 25) === false; + @assert ctx.isPointInPath(45, 25) === true; + @assert ctx.isPointInPath(5, 45) === true; + @assert ctx.isPointInPath(25, 45) === true; + @assert ctx.isPointInPath(45, 45) === true; + +- name: 2d.path.isPointInPath.transform.1 + desc: isPointInPath() handles transformations correctly + testing: + - 2d.path.isPointInPath + code: | + ctx.translate(50, 0); + ctx.rect(0, 0, 20, 20); + @assert ctx.isPointInPath(-40, 10) === false; + @assert ctx.isPointInPath(10, 10) === false; + @assert ctx.isPointInPath(49, 10) === false; + @assert ctx.isPointInPath(51, 10) === true; + @assert ctx.isPointInPath(69, 10) === true; + @assert ctx.isPointInPath(71, 10) === false; + +- name: 2d.path.isPointInPath.transform.2 + desc: isPointInPath() handles transformations correctly + testing: + - 2d.path.isPointInPath + code: | + ctx.rect(50, 0, 20, 20); + ctx.translate(50, 0); + @assert ctx.isPointInPath(-40, 10) === false; + @assert ctx.isPointInPath(10, 10) === false; + @assert ctx.isPointInPath(49, 10) === false; + @assert ctx.isPointInPath(51, 10) === true; + @assert ctx.isPointInPath(69, 10) === true; + @assert ctx.isPointInPath(71, 10) === false; + +- name: 2d.path.isPointInPath.transform.3 + desc: isPointInPath() handles transformations correctly + testing: + - 2d.path.isPointInPath + code: | + ctx.scale(-1, 1); + ctx.rect(-70, 0, 20, 20); + @assert ctx.isPointInPath(-40, 10) === false; + @assert ctx.isPointInPath(10, 10) === false; + @assert ctx.isPointInPath(49, 10) === false; + @assert ctx.isPointInPath(51, 10) === true; + @assert ctx.isPointInPath(69, 10) === true; + @assert ctx.isPointInPath(71, 10) === false; + +- name: 2d.path.isPointInPath.transform.4 + desc: isPointInPath() handles transformations correctly + testing: + - 2d.path.isPointInPath + code: | + ctx.translate(50, 0); + ctx.rect(50, 0, 20, 20); + ctx.translate(0, 50); + @assert ctx.isPointInPath(60, 10) === false; + @assert ctx.isPointInPath(110, 10) === true; + @assert ctx.isPointInPath(110, 60) === false; + +- name: 2d.path.isPointInPath.nonfinite + desc: isPointInPath() returns false for non-finite arguments + testing: + - 2d.path.isPointInPath.nonfinite + code: | + ctx.rect(-100, -50, 200, 100); + @assert ctx.isPointInPath(Infinity, 0) === false; + @assert ctx.isPointInPath(-Infinity, 0) === false; + @assert ctx.isPointInPath(NaN, 0) === false; + @assert ctx.isPointInPath(0, Infinity) === false; + @assert ctx.isPointInPath(0, -Infinity) === false; + @assert ctx.isPointInPath(0, NaN) === false; + @assert ctx.isPointInPath(NaN, NaN) === false; + + +- name: 2d.path.isPointInStroke.scaleddashes + desc: isPointInStroke() should return correct results on dashed paths at high scale + factors + testing: + - 2d.path.isPointInStroke + code: | + var scale = 20; + ctx.setLineDash([10, 21.4159]); // dash from t=0 to t=10 along the circle + ctx.scale(scale, scale); + ctx.ellipse(6, 10, 5, 5, 0, 2*Math.PI, false); + ctx.stroke(); + + // hit-test the beginning of the dash (t=0) + @assert ctx.isPointInStroke(11*scale, 10*scale) === true; + // hit-test the middle of the dash (t=5) + @assert ctx.isPointInStroke(8.70*scale, 14.21*scale) === true; + // hit-test the end of the dash (t=9.8) + @assert ctx.isPointInStroke(4.10*scale, 14.63*scale) === true; + // hit-test past the end of the dash (t=10.2) + @assert ctx.isPointInStroke(3.74*scale, 14.46*scale) === false; + +- name: 2d.path.isPointInPath.basic + desc: Verify the winding rule in isPointInPath works for for rect path. + testing: + - 2d.isPointInPath.basic + code: | + canvas.width = 200; + canvas.height = 200; + + // Testing default isPointInPath + ctx.beginPath(); + ctx.rect(0, 0, 100, 100); + ctx.rect(25, 25, 50, 50); + @assert ctx.isPointInPath(50, 50) === true; + @assert ctx.isPointInPath(NaN, 50) === false; + @assert ctx.isPointInPath(50, NaN) === false; + + // Testing nonzero isPointInPath + ctx.beginPath(); + ctx.rect(0, 0, 100, 100); + ctx.rect(25, 25, 50, 50); + @assert ctx.isPointInPath(50, 50, 'nonzero') === true; + + // Testing evenodd isPointInPath + ctx.beginPath(); + ctx.rect(0, 0, 100, 100); + ctx.rect(25, 25, 50, 50); + @assert ctx.isPointInPath(50, 50, 'evenodd') === false; + + // Testing extremely large scale + ctx.save(); + ctx.scale(Number.MAX_VALUE, Number.MAX_VALUE); + ctx.beginPath(); + ctx.rect(-10, -10, 20, 20); + @assert ctx.isPointInPath(0, 0, 'nonzero') === true; + @assert ctx.isPointInPath(0, 0, 'evenodd') === true; + ctx.restore(); + + // Check with non-invertible ctm. + ctx.save(); + ctx.scale(0, 0); + ctx.beginPath(); + ctx.rect(-10, -10, 20, 20); + @assert ctx.isPointInPath(0, 0, 'nonzero') === false; + @assert ctx.isPointInPath(0, 0, 'evenodd') === false; + ctx.restore(); + +- name: 2d.path.isPointInpath.multi.path + desc: Verify the winding rule in isPointInPath works for path object. + testing: + - 2d.isPointInPath.basic + code: | + canvas.width = 200; + canvas.height = 200; + + // Testing default isPointInPath with Path object'); + path = new Path2D(); + path.rect(0, 0, 100, 100); + path.rect(25, 25, 50, 50); + @assert ctx.isPointInPath(path, 50, 50) === true; + @assert ctx.isPointInPath(path, 50, 50, undefined) === true; + @assert ctx.isPointInPath(path, NaN, 50) === false; + @assert ctx.isPointInPath(path, 50, NaN) === false; + + // Testing nonzero isPointInPath with Path object'); + path = new Path2D(); + path.rect(0, 0, 100, 100); + path.rect(25, 25, 50, 50); + @assert ctx.isPointInPath(path, 50, 50, 'nonzero') === true; + + // Testing evenodd isPointInPath with Path object'); + path = new Path2D(); + path.rect(0, 0, 100, 100); + path.rect(25, 25, 50, 50); + assert_false(ctx.isPointInPath(path, 50, 50, 'evenodd')); + +- name: 2d.path.isPointInpath.invalid + desc: Verify isPointInPath throws exceptions with invalid inputs. + testing: + - 2d.isPointInPath.basic + code: | + canvas.width = 200; + canvas.height = 200; + path = new Path2D(); + path.rect(0, 0, 100, 100); + path.rect(25, 25, 50, 50); + // Testing invalid enumeration isPointInPath (w/ and w/o Path object'); + @assert throws TypeError ctx.isPointInPath(path, 50, 50, 'gazonk'); + @assert throws TypeError ctx.isPointInPath(50, 50, 'gazonk'); + + // Testing invalid type isPointInPath with Path object'); + @assert throws TypeError ctx.isPointInPath(null, 50, 50); + @assert throws TypeError ctx.isPointInPath(null, 50, 50, 'nonzero'); + @assert throws TypeError ctx.isPointInPath(null, 50, 50, 'evenodd'); + @assert throws TypeError ctx.isPointInPath(null, 50, 50, null); + @assert throws TypeError ctx.isPointInPath(path, 50, 50, null); + @assert throws TypeError ctx.isPointInPath(undefined, 50, 50); + @assert throws TypeError ctx.isPointInPath(undefined, 50, 50, 'nonzero'); + @assert throws TypeError ctx.isPointInPath(undefined, 50, 50, 'evenodd'); + @assert throws TypeError ctx.isPointInPath(undefined, 50, 50, undefined); + @assert throws TypeError ctx.isPointInPath([], 50, 50); + @assert throws TypeError ctx.isPointInPath([], 50, 50, 'nonzero'); + @assert throws TypeError ctx.isPointInPath([], 50, 50, 'evenodd'); + @assert throws TypeError ctx.isPointInPath({}, 50, 50); + @assert throws TypeError ctx.isPointInPath({}, 50, 50, 'nonzero'); + @assert throws TypeError ctx.isPointInPath({}, 50, 50, 'evenodd'); diff --git a/test/wpt/pixel-manipulation.yaml b/test/wpt/pixel-manipulation.yaml new file mode 100644 index 000000000..ddacaf441 --- /dev/null +++ b/test/wpt/pixel-manipulation.yaml @@ -0,0 +1,1145 @@ +- name: 2d.imageData.create2.basic + desc: createImageData(sw, sh) exists and returns something + testing: + - 2d.imageData.create2.object + code: | + @assert ctx.createImageData(1, 1) !== null; + +- name: 2d.imageData.create1.basic + desc: createImageData(imgdata) exists and returns something + testing: + - 2d.imageData.create1.object + code: | + @assert ctx.createImageData(ctx.createImageData(1, 1)) !== null; + +- name: 2d.imageData.create2.type + desc: createImageData(sw, sh) returns an ImageData object containing a Uint8ClampedArray + object + testing: + - 2d.imageData.create2.object + code: | + @assert window.ImageData !== undefined; + @assert window.Uint8ClampedArray !== undefined; + window.ImageData.prototype.thisImplementsImageData = true; + window.Uint8ClampedArray.prototype.thisImplementsUint8ClampedArray = true; + var imgdata = ctx.createImageData(1, 1); + @assert imgdata.thisImplementsImageData; + @assert imgdata.data.thisImplementsUint8ClampedArray; + +- name: 2d.imageData.create1.type + desc: createImageData(imgdata) returns an ImageData object containing a Uint8ClampedArray + object + testing: + - 2d.imageData.create1.object + code: | + @assert window.ImageData !== undefined; + @assert window.Uint8ClampedArray !== undefined; + window.ImageData.prototype.thisImplementsImageData = true; + window.Uint8ClampedArray.prototype.thisImplementsUint8ClampedArray = true; + var imgdata = ctx.createImageData(ctx.createImageData(1, 1)); + @assert imgdata.thisImplementsImageData; + @assert imgdata.data.thisImplementsUint8ClampedArray; + +- name: 2d.imageData.create2.this + desc: createImageData(sw, sh) should throw when called with the wrong |this| + notes: &bindings Defined in "Web IDL" (draft) + testing: + - 2d.imageData.create2.object + code: | + @assert throws TypeError CanvasRenderingContext2D.prototype.createImageData.call(null, 1, 1); @moz-todo + @assert throws TypeError CanvasRenderingContext2D.prototype.createImageData.call(undefined, 1, 1); @moz-todo + @assert throws TypeError CanvasRenderingContext2D.prototype.createImageData.call({}, 1, 1); @moz-todo + +- name: 2d.imageData.create1.this + desc: createImageData(imgdata) should throw when called with the wrong |this| + notes: *bindings + testing: + - 2d.imageData.create2.object + code: | + var imgdata = ctx.createImageData(1, 1); + @assert throws TypeError CanvasRenderingContext2D.prototype.createImageData.call(null, imgdata); @moz-todo + @assert throws TypeError CanvasRenderingContext2D.prototype.createImageData.call(undefined, imgdata); @moz-todo + @assert throws TypeError CanvasRenderingContext2D.prototype.createImageData.call({}, imgdata); @moz-todo + +- name: 2d.imageData.create2.initial + desc: createImageData(sw, sh) returns transparent black data of the right size + testing: + - 2d.imageData.create2.size + - 2d.imageData.create.initial + - 2d.imageData.initial + code: | + var imgdata = ctx.createImageData(10, 20); + @assert imgdata.data.length === imgdata.width*imgdata.height*4; + @assert imgdata.width < imgdata.height; + @assert imgdata.width > 0; + var isTransparentBlack = true; + for (var i = 0; i < imgdata.data.length; ++i) + if (imgdata.data[i] !== 0) + isTransparentBlack = false; + @assert isTransparentBlack; + +- name: 2d.imageData.create1.initial + desc: createImageData(imgdata) returns transparent black data of the right size + testing: + - 2d.imageData.create1.size + - 2d.imageData.create.initial + - 2d.imageData.initial + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + var imgdata1 = ctx.getImageData(0, 0, 10, 20); + var imgdata2 = ctx.createImageData(imgdata1); + @assert imgdata2.data.length === imgdata1.data.length; + @assert imgdata2.width === imgdata1.width; + @assert imgdata2.height === imgdata1.height; + var isTransparentBlack = true; + for (var i = 0; i < imgdata2.data.length; ++i) + if (imgdata2.data[i] !== 0) + isTransparentBlack = false; + @assert isTransparentBlack; + +- name: 2d.imageData.create2.large + desc: createImageData(sw, sh) works for sizes much larger than the canvas + testing: + - 2d.imageData.create2.size + code: | + var imgdata = ctx.createImageData(1000, 2000); + @assert imgdata.data.length === imgdata.width*imgdata.height*4; + @assert imgdata.width < imgdata.height; + @assert imgdata.width > 0; + var isTransparentBlack = true; + for (var i = 0; i < imgdata.data.length; i += 7813) // check ~1024 points (assuming normal scaling) + if (imgdata.data[i] !== 0) + isTransparentBlack = false; + @assert isTransparentBlack; + +- name: 2d.imageData.create2.negative + desc: createImageData(sw, sh) takes the absolute magnitude of the size arguments + testing: + - 2d.imageData.create2.size + code: | + var imgdata1 = ctx.createImageData(10, 20); + var imgdata2 = ctx.createImageData(-10, 20); + var imgdata3 = ctx.createImageData(10, -20); + var imgdata4 = ctx.createImageData(-10, -20); + @assert imgdata1.data.length === imgdata2.data.length; + @assert imgdata2.data.length === imgdata3.data.length; + @assert imgdata3.data.length === imgdata4.data.length; + +- name: 2d.imageData.create2.zero + desc: createImageData(sw, sh) throws INDEX_SIZE_ERR if size is zero + testing: + - 2d.imageData.getcreate.zero + code: | + @assert throws INDEX_SIZE_ERR ctx.createImageData(10, 0); + @assert throws INDEX_SIZE_ERR ctx.createImageData(0, 10); + @assert throws INDEX_SIZE_ERR ctx.createImageData(0, 0); + @assert throws INDEX_SIZE_ERR ctx.createImageData(0.99, 10); + @assert throws INDEX_SIZE_ERR ctx.createImageData(10, 0.1); + +- name: 2d.imageData.create2.nonfinite + desc: createImageData() throws TypeError if arguments are not finite + notes: *bindings + testing: + - 2d.imageData.getcreate.nonfinite + code: | + @nonfinite @assert throws TypeError ctx.createImageData(<10 Infinity -Infinity NaN>, <10 Infinity -Infinity NaN>); + var posinfobj = { valueOf: function() { return Infinity; } }, + neginfobj = { valueOf: function() { return -Infinity; } }, + nanobj = { valueOf: function() { return -Infinity; } }; + @nonfinite @assert throws TypeError ctx.createImageData(<10 posinfobj neginfobj nanobj>, <10 posinfobj neginfobj nanobj>); + +- name: 2d.imageData.create1.zero + desc: createImageData(null) throws TypeError + testing: + - 2d.imageData.create.null + code: | + @assert throws TypeError ctx.createImageData(null); + +- name: 2d.imageData.create2.double + desc: createImageData(w, h) double is converted to long + testing: + - 2d.imageData.create2.size + code: | + var imgdata1 = ctx.createImageData(10.01, 10.99); + var imgdata2 = ctx.createImageData(-10.01, -10.99); + @assert imgdata1.width === 10; + @assert imgdata1.height === 10; + @assert imgdata2.width === 10; + @assert imgdata2.height === 10; + +- name: 2d.imageData.create.and.resize + desc: Verify no crash when resizing an image bitmap to zero. + testing: + - 2d.imageData.resize + images: + - red.png + code: | + var image = new Image(); + image.onload = t.step_func(function() { + var options = { resizeHeight: 0 }; + var p1 = createImageBitmap(image, options); + p1.catch(function(error){}); + t.done(); + }); + image.src = 'red.png'; + +- name: 2d.imageData.get.basic + desc: getImageData() exists and returns something + testing: + - 2d.imageData.get.basic + code: | + @assert ctx.getImageData(0, 0, 100, 50) !== null; + +- name: 2d.imageData.get.type + desc: getImageData() returns an ImageData object containing a Uint8ClampedArray + object + testing: + - 2d.imageData.get.object + code: | + @assert window.ImageData !== undefined; + @assert window.Uint8ClampedArray !== undefined; + window.ImageData.prototype.thisImplementsImageData = true; + window.Uint8ClampedArray.prototype.thisImplementsUint8ClampedArray = true; + var imgdata = ctx.getImageData(0, 0, 1, 1); + @assert imgdata.thisImplementsImageData; + @assert imgdata.data.thisImplementsUint8ClampedArray; + +- name: 2d.imageData.get.zero + desc: getImageData() throws INDEX_SIZE_ERR if size is zero + testing: + - 2d.imageData.getcreate.zero + code: | + @assert throws INDEX_SIZE_ERR ctx.getImageData(1, 1, 10, 0); + @assert throws INDEX_SIZE_ERR ctx.getImageData(1, 1, 0, 10); + @assert throws INDEX_SIZE_ERR ctx.getImageData(1, 1, 0, 0); + @assert throws INDEX_SIZE_ERR ctx.getImageData(1, 1, 0.1, 10); + @assert throws INDEX_SIZE_ERR ctx.getImageData(1, 1, 10, 0.99); + @assert throws INDEX_SIZE_ERR ctx.getImageData(1, 1, -0.1, 10); + @assert throws INDEX_SIZE_ERR ctx.getImageData(1, 1, 10, -0.99); + +- name: 2d.imageData.get.nonfinite + desc: getImageData() throws TypeError if arguments are not finite + notes: *bindings + testing: + - 2d.imageData.getcreate.nonfinite + code: | + @nonfinite @assert throws TypeError ctx.getImageData(<10 Infinity -Infinity NaN>, <10 Infinity -Infinity NaN>, <10 Infinity -Infinity NaN>, <10 Infinity -Infinity NaN>); + var posinfobj = { valueOf: function() { return Infinity; } }, + neginfobj = { valueOf: function() { return -Infinity; } }, + nanobj = { valueOf: function() { return -Infinity; } }; + @nonfinite @assert throws TypeError ctx.getImageData(<10 posinfobj neginfobj nanobj>, <10 posinfobj neginfobj nanobj>, <10 posinfobj neginfobj nanobj>, <10 posinfobj neginfobj nanobj>); + +- name: 2d.imageData.get.source.outside + desc: getImageData() returns transparent black outside the canvas + testing: + - 2d.imageData.get.basic + - 2d.imageData.get.outside + code: | + ctx.fillStyle = '#08f'; + ctx.fillRect(0, 0, 100, 50); + + var imgdata1 = ctx.getImageData(-10, 5, 1, 1); + @assert imgdata1.data[0] === 0; + @assert imgdata1.data[1] === 0; + @assert imgdata1.data[2] === 0; + @assert imgdata1.data[3] === 0; + + var imgdata2 = ctx.getImageData(10, -5, 1, 1); + @assert imgdata2.data[0] === 0; + @assert imgdata2.data[1] === 0; + @assert imgdata2.data[2] === 0; + @assert imgdata2.data[3] === 0; + + var imgdata3 = ctx.getImageData(200, 5, 1, 1); + @assert imgdata3.data[0] === 0; + @assert imgdata3.data[1] === 0; + @assert imgdata3.data[2] === 0; + @assert imgdata3.data[3] === 0; + + var imgdata4 = ctx.getImageData(10, 60, 1, 1); + @assert imgdata4.data[0] === 0; + @assert imgdata4.data[1] === 0; + @assert imgdata4.data[2] === 0; + @assert imgdata4.data[3] === 0; + + var imgdata5 = ctx.getImageData(100, 10, 1, 1); + @assert imgdata5.data[0] === 0; + @assert imgdata5.data[1] === 0; + @assert imgdata5.data[2] === 0; + @assert imgdata5.data[3] === 0; + + var imgdata6 = ctx.getImageData(0, 10, 1, 1); + @assert imgdata6.data[0] === 0; + @assert imgdata6.data[1] === 136; + @assert imgdata6.data[2] === 255; + @assert imgdata6.data[3] === 255; + + var imgdata7 = ctx.getImageData(-10, 10, 20, 20); + @assert imgdata7.data[ 0*4+0] === 0; + @assert imgdata7.data[ 0*4+1] === 0; + @assert imgdata7.data[ 0*4+2] === 0; + @assert imgdata7.data[ 0*4+3] === 0; + @assert imgdata7.data[ 9*4+0] === 0; + @assert imgdata7.data[ 9*4+1] === 0; + @assert imgdata7.data[ 9*4+2] === 0; + @assert imgdata7.data[ 9*4+3] === 0; + @assert imgdata7.data[10*4+0] === 0; + @assert imgdata7.data[10*4+1] === 136; + @assert imgdata7.data[10*4+2] === 255; + @assert imgdata7.data[10*4+3] === 255; + @assert imgdata7.data[19*4+0] === 0; + @assert imgdata7.data[19*4+1] === 136; + @assert imgdata7.data[19*4+2] === 255; + @assert imgdata7.data[19*4+3] === 255; + @assert imgdata7.data[20*4+0] === 0; + @assert imgdata7.data[20*4+1] === 0; + @assert imgdata7.data[20*4+2] === 0; + @assert imgdata7.data[20*4+3] === 0; + +- name: 2d.imageData.get.source.negative + desc: getImageData() works with negative width and height, and returns top-to-bottom + left-to-right + testing: + - 2d.imageData.get.basic + - 2d.pixelarray.order + code: | + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#fff'; + ctx.fillRect(20, 10, 60, 10); + + var imgdata1 = ctx.getImageData(85, 25, -10, -10); + @assert imgdata1.data[0] === 255; + @assert imgdata1.data[1] === 255; + @assert imgdata1.data[2] === 255; + @assert imgdata1.data[3] === 255; + @assert imgdata1.data[imgdata1.data.length-4+0] === 0; + @assert imgdata1.data[imgdata1.data.length-4+1] === 0; + @assert imgdata1.data[imgdata1.data.length-4+2] === 0; + @assert imgdata1.data[imgdata1.data.length-4+3] === 255; + + var imgdata2 = ctx.getImageData(0, 0, -1, -1); + @assert imgdata2.data[0] === 0; + @assert imgdata2.data[1] === 0; + @assert imgdata2.data[2] === 0; + @assert imgdata2.data[3] === 0; + +- name: 2d.imageData.get.source.size + desc: getImageData() returns bigger ImageData for bigger source rectangle + testing: + - 2d.imageData.get.basic + code: | + var imgdata1 = ctx.getImageData(0, 0, 10, 10); + var imgdata2 = ctx.getImageData(0, 0, 20, 20); + @assert imgdata2.width > imgdata1.width; + @assert imgdata2.height > imgdata1.height; + +- name: 2d.imageData.get.double + desc: createImageData(w, h) double is converted to long + testing: + - 2d.imageData.get.basic + code: | + var imgdata1 = ctx.getImageData(0, 0, 10.01, 10.99); + var imgdata2 = ctx.getImageData(0, 0, -10.01, -10.99); + @assert imgdata1.width === 10; + @assert imgdata1.height === 10; + @assert imgdata2.width === 10; + @assert imgdata2.height === 10; + +- name: 2d.imageData.get.nonpremul + desc: getImageData() returns non-premultiplied colors + testing: + - 2d.imageData.get.premul + code: | + ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; + ctx.fillRect(0, 0, 100, 50); + var imgdata = ctx.getImageData(10, 10, 10, 10); + @assert imgdata.data[0] > 200; + @assert imgdata.data[1] > 200; + @assert imgdata.data[2] > 200; + @assert imgdata.data[3] > 100; + @assert imgdata.data[3] < 200; + +- name: 2d.imageData.get.range + desc: getImageData() returns values in the range [0, 255] + testing: + - 2d.pixelarray.range + - 2d.pixelarray.retrieve + code: | + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#fff'; + ctx.fillRect(20, 10, 60, 10); + var imgdata1 = ctx.getImageData(10, 5, 1, 1); + @assert imgdata1.data[0] === 0; + var imgdata2 = ctx.getImageData(30, 15, 1, 1); + @assert imgdata2.data[0] === 255; + +- name: 2d.imageData.get.clamp + desc: getImageData() clamps colors to the range [0, 255] + testing: + - 2d.pixelarray.range + code: | + ctx.fillStyle = 'rgb(-100, -200, -300)'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = 'rgb(256, 300, 400)'; + ctx.fillRect(20, 10, 60, 10); + var imgdata1 = ctx.getImageData(10, 5, 1, 1); + @assert imgdata1.data[0] === 0; + @assert imgdata1.data[1] === 0; + @assert imgdata1.data[2] === 0; + var imgdata2 = ctx.getImageData(30, 15, 1, 1); + @assert imgdata2.data[0] === 255; + @assert imgdata2.data[1] === 255; + @assert imgdata2.data[2] === 255; + +- name: 2d.imageData.get.length + desc: getImageData() returns a correctly-sized Uint8ClampedArray + testing: + - 2d.pixelarray.length + code: | + var imgdata = ctx.getImageData(0, 0, 10, 10); + @assert imgdata.data.length === imgdata.width*imgdata.height*4; + +- name: 2d.imageData.get.order.cols + desc: getImageData() returns leftmost columns first + testing: + - 2d.pixelarray.order + code: | + ctx.fillStyle = '#fff'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 2, 50); + var imgdata = ctx.getImageData(0, 0, 10, 10); + @assert imgdata.data[0] === 0; + @assert imgdata.data[Math.round(imgdata.width/2*4)] === 255; + @assert imgdata.data[Math.round((imgdata.height/2)*imgdata.width*4)] === 0; + +- name: 2d.imageData.get.order.rows + desc: getImageData() returns topmost rows first + testing: + - 2d.pixelarray.order + code: | + ctx.fillStyle = '#fff'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 2); + var imgdata = ctx.getImageData(0, 0, 10, 10); + @assert imgdata.data[0] === 0; + @assert imgdata.data[Math.floor(imgdata.width/2*4)] === 0; + @assert imgdata.data[(imgdata.height/2)*imgdata.width*4] === 255; + +- name: 2d.imageData.get.order.rgb + desc: getImageData() returns R then G then B + testing: + - 2d.pixelarray.order + - 2d.pixelarray.indexes + code: | + ctx.fillStyle = '#48c'; + ctx.fillRect(0, 0, 100, 50); + var imgdata = ctx.getImageData(0, 0, 10, 10); + @assert imgdata.data[0] === 0x44; + @assert imgdata.data[1] === 0x88; + @assert imgdata.data[2] === 0xCC; + @assert imgdata.data[3] === 255; + @assert imgdata.data[4] === 0x44; + @assert imgdata.data[5] === 0x88; + @assert imgdata.data[6] === 0xCC; + @assert imgdata.data[7] === 255; + +- name: 2d.imageData.get.order.alpha + desc: getImageData() returns A in the fourth component + testing: + - 2d.pixelarray.order + code: | + ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; + ctx.fillRect(0, 0, 100, 50); + var imgdata = ctx.getImageData(0, 0, 10, 10); + @assert imgdata.data[3] < 200; + @assert imgdata.data[3] > 100; + +- name: 2d.imageData.get.unaffected + desc: getImageData() is not affected by context state + testing: + - 2d.imageData.unaffected + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 50) + ctx.fillStyle = '#f00'; + ctx.fillRect(50, 0, 50, 50) + ctx.save(); + ctx.translate(50, 0); + ctx.globalAlpha = 0.1; + ctx.globalCompositeOperation = 'destination-atop'; + ctx.shadowColor = '#f00'; + ctx.rect(0, 0, 5, 5); + ctx.clip(); + var imgdata = ctx.getImageData(0, 0, 50, 50); + ctx.restore(); + ctx.putImageData(imgdata, 50, 0); + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + expected: green + + +- name: 2d.imageData.get.large.crash + desc: Test that canvas crash when image data cannot be allocated. + testing: + - 2d.getImageData + code: | + @assert throws TypeError ctx.getImageData(10, 0xffffffff, 2147483647, 10); + +- name: 2d.imageData.get.rounding + desc: Test the handling of non-integer source coordinates in getImageData(). + testing: + - 2d.getImageData + code: | + function testDimensions(sx, sy, sw, sh, width, height) + { + imageData = ctx.getImageData(sx, sy, sw, sh); + @assert imageData.width == width; + @assert imageData.height == height; + } + + testDimensions(0, 0, 20, 10, 20, 10); + + testDimensions(.1, .2, 20, 10, 20, 10); + testDimensions(.9, .8, 20, 10, 20, 10); + + testDimensions(0, 0, 20.9, 10.9, 20, 10); + testDimensions(0, 0, 20.1, 10.1, 20, 10); + + testDimensions(-1, -1, 20, 10, 20, 10); + + testDimensions(-1.1, 0, 20, 10, 20, 10); + testDimensions(-1.9, 0, 20, 10, 20, 10); + +- name: 2d.imageData.get.invalid + desc: Verify getImageData() behavior in invalid cases. + testing: + - 2d.imageData.get.invalid + code: | + imageData = ctx.getImageData(0,0,2,2); + var testValues = [NaN, true, false, "\"garbage\"", "-1", + "0", "1", "2", Infinity, -Infinity, + -5, -0.5, 0, 0.5, 5, + 5.4, 255, 256, null, undefined]; + var testResults = [0, 1, 0, 0, 0, + 0, 1, 2, 255, 0, + 0, 0, 0, 0, 5, + 5, 255, 255, 0, 0]; + for (var i = 0; i < testValues.length; i++) { + imageData.data[0] = testValues[i]; + @assert imageData.data[0] == testResults[i]; + } + imageData.data['foo']='garbage'; + @assert imageData.data['foo'] == 'garbage'; + imageData.data[-1]='garbage'; + @assert imageData.data[-1] == undefined; + imageData.data[17]='garbage'; + @assert imageData.data[17] == undefined; + +- name: 2d.imageData.object.properties + desc: ImageData objects have the right properties + testing: + - 2d.imageData.type + code: | + var imgdata = ctx.getImageData(0, 0, 10, 10); + @assert typeof(imgdata.width) === 'number'; + @assert typeof(imgdata.height) === 'number'; + @assert typeof(imgdata.data) === 'object'; + +- name: 2d.imageData.object.readonly + desc: ImageData objects properties are read-only + testing: + - 2d.imageData.type + code: | + var imgdata = ctx.getImageData(0, 0, 10, 10); + var w = imgdata.width; + var h = imgdata.height; + var d = imgdata.data; + imgdata.width = 123; + imgdata.height = 123; + imgdata.data = [100,100,100,100]; + @assert imgdata.width === w; + @assert imgdata.height === h; + @assert imgdata.data === d; + @assert imgdata.data[0] === 0; + @assert imgdata.data[1] === 0; + @assert imgdata.data[2] === 0; + @assert imgdata.data[3] === 0; + +- name: 2d.imageData.object.ctor.size + desc: ImageData has a usable constructor + testing: + - 2d.imageData.type + code: | + @assert window.ImageData !== undefined; + + var imgdata = new window.ImageData(2, 3); + @assert imgdata.width === 2; + @assert imgdata.height === 3; + @assert imgdata.data.length === 2 * 3 * 4; + for (var i = 0; i < imgdata.data.length; ++i) { + @assert imgdata.data[i] === 0; + } + +- name: 2d.imageData.object.ctor.basics + desc: Testing different type of ImageData constructor + testing: + - 2d.imageData.type + code: | + function setRGBA(imageData, i, rgba) + { + var s = i * 4; + imageData[s] = rgba[0]; + imageData[s + 1] = rgba[1]; + imageData[s + 2] = rgba[2]; + imageData[s + 3] = rgba[3]; + } + + function getRGBA(imageData, i) + { + var result = []; + var s = i * 4; + for (var j = 0; j < 4; j++) { + result[j] = imageData[s + j]; + } + return result; + } + + function assertArrayEquals(actual, expected) + { + @assert typeof actual === "object"; + @assert actual !== null; + @assert "length" in actual === true; + @assert actual.length === expected.length; + for (var i = 0; i < actual.length; i++) { + @assert actual.hasOwnProperty(i) === expected.hasOwnProperty(i); + @assert actual[i] === expected[i]; + } + } + + @assert ImageData !== undefined; + imageData = new ImageData(100, 50); + + @assert imageData !== null; + @assert imageData.data !== null; + @assert imageData.width === 100; + @assert imageData.height === 50; + assertArrayEquals(getRGBA(imageData.data, 4), [0, 0, 0, 0]); + + var testColor = [0, 255, 255, 128]; + setRGBA(imageData.data, 4, testColor); + assertArrayEquals(getRGBA(imageData.data, 4), testColor); + + @assert throws TypeError new ImageData(10); + @assert throws INDEX_SIZE_ERR new ImageData(0, 10); + @assert throws INDEX_SIZE_ERR new ImageData(10, 0); + @assert throws INDEX_SIZE_ERR new ImageData('width', 'height'); + @assert throws INDEX_SIZE_ERR new ImageData(1 << 31, 1 << 31); + @assert throws TypeError new ImageData(new Uint8ClampedArray(0)); + @assert throws INDEX_SIZE_ERR new ImageData(new Uint8Array(100), 25); + @assert throws INVALID_STATE_ERR new ImageData(new Uint8ClampedArray(27), 2); + @assert throws INDEX_SIZE_ERR new ImageData(new Uint8ClampedArray(28), 7, 0); + @assert throws INDEX_SIZE_ERR new ImageData(new Uint8ClampedArray(104), 14); + @assert throws INDEX_SIZE_ERR new ImageData(new Uint8ClampedArray([12, 34, 168, 65328]), 1, 151); + @assert throws TypeError new ImageData(self, 4, 4); + @assert throws TypeError new ImageData(null, 4, 4); + @assert throws INDEX_SIZE_ERR new ImageData(imageData.data, 0); + @assert throws INDEX_SIZE_ERR new ImageData(imageData.data, 13); + @assert throws INDEX_SIZE_ERR new ImageData(imageData.data, 1 << 31); + @assert throws INDEX_SIZE_ERR new ImageData(imageData.data, 'biggish'); + @assert throws INDEX_SIZE_ERR new ImageData(imageData.data, 1 << 24, 1 << 31); + @assert new ImageData(new Uint8ClampedArray(28), 7).height === 1; + + imageDataFromData = new ImageData(imageData.data, 100); + @assert imageDataFromData.width === 100; + @assert imageDataFromData.height === 50; + @assert imageDataFromData.data === imageData.data; + assertArrayEquals(getRGBA(imageDataFromData.data, 10), getRGBA(imageData.data, 10)); + setRGBA(imageData.data, 10, testColor); + assertArrayEquals(getRGBA(imageDataFromData.data, 10), getRGBA(imageData.data, 10)); + + var data = new Uint8ClampedArray(400); + data[22] = 129; + imageDataFromData = new ImageData(data, 20, 5); + @assert imageDataFromData.width === 20; + @assert imageDataFromData.height === 5; + @assert imageDataFromData.data === data; + assertArrayEquals(getRGBA(imageDataFromData.data, 2), getRGBA(data, 2)); + setRGBA(imageDataFromData.data, 2, testColor); + assertArrayEquals(getRGBA(imageDataFromData.data, 2), getRGBA(data, 2)); + + if (window.SharedArrayBuffer) { + @assert throws TypeError new ImageData(new Uint16Array(new SharedArrayBuffer(32)), 4, 2); + } + +- name: 2d.imageData.object.ctor.array + desc: ImageData has a usable constructor + testing: + - 2d.imageData.type + code: | + @assert window.ImageData !== undefined; + + var array = new Uint8ClampedArray(8); + var imgdata = new window.ImageData(array, 1, 2); + @assert imgdata.width === 1; + @assert imgdata.height === 2; + @assert imgdata.data === array; + +- name: 2d.imageData.object.ctor.array.bounds + desc: ImageData has a usable constructor + testing: + - 2d.imageData.type + code: | + @assert window.ImageData !== undefined; + + @assert throws INVALID_STATE_ERR new ImageData(new Uint8ClampedArray(0), 1); + @assert throws INVALID_STATE_ERR new ImageData(new Uint8ClampedArray(3), 1); + @assert throws INDEX_SIZE_ERR new ImageData(new Uint8ClampedArray(4), 0); + @assert throws INDEX_SIZE_ERR new ImageData(new Uint8ClampedArray(4), 1, 2); + @assert throws TypeError new ImageData(new Uint8Array(8), 1, 2); + @assert throws TypeError new ImageData(new Int8Array(8), 1, 2); + +- name: 2d.imageData.object.set + desc: ImageData.data can be modified + testing: + - 2d.pixelarray.modify + code: | + var imgdata = ctx.getImageData(0, 0, 10, 10); + imgdata.data[0] = 100; + @assert imgdata.data[0] === 100; + imgdata.data[0] = 200; + @assert imgdata.data[0] === 200; + +- name: 2d.imageData.object.undefined + desc: ImageData.data converts undefined to 0 + testing: + - 2d.pixelarray.modify + webidl: + - es-octet + code: | + var imgdata = ctx.getImageData(0, 0, 10, 10); + imgdata.data[0] = 100; + imgdata.data[0] = undefined; + @assert imgdata.data[0] === 0; + +- name: 2d.imageData.object.nan + desc: ImageData.data converts NaN to 0 + testing: + - 2d.pixelarray.modify + webidl: + - es-octet + code: | + var imgdata = ctx.getImageData(0, 0, 10, 10); + imgdata.data[0] = 100; + imgdata.data[0] = NaN; + @assert imgdata.data[0] === 0; + imgdata.data[0] = 100; + imgdata.data[0] = "cheese"; + @assert imgdata.data[0] === 0; + +- name: 2d.imageData.object.string + desc: ImageData.data converts strings to numbers with ToNumber + testing: + - 2d.pixelarray.modify + webidl: + - es-octet + code: | + var imgdata = ctx.getImageData(0, 0, 10, 10); + imgdata.data[0] = 100; + imgdata.data[0] = "110"; + @assert imgdata.data[0] === 110; + imgdata.data[0] = 100; + imgdata.data[0] = "0x78"; + @assert imgdata.data[0] === 120; + imgdata.data[0] = 100; + imgdata.data[0] = " +130e0 "; + @assert imgdata.data[0] === 130; + +- name: 2d.imageData.object.clamp + desc: ImageData.data clamps numbers to [0, 255] + testing: + - 2d.pixelarray.modify + webidl: + - es-octet + code: | + var imgdata = ctx.getImageData(0, 0, 10, 10); + + imgdata.data[0] = 100; + imgdata.data[0] = 300; + @assert imgdata.data[0] === 255; + imgdata.data[0] = 100; + imgdata.data[0] = -100; + @assert imgdata.data[0] === 0; + + imgdata.data[0] = 100; + imgdata.data[0] = 200+Math.pow(2, 32); + @assert imgdata.data[0] === 255; + imgdata.data[0] = 100; + imgdata.data[0] = -200-Math.pow(2, 32); + @assert imgdata.data[0] === 0; + + imgdata.data[0] = 100; + imgdata.data[0] = Math.pow(10, 39); + @assert imgdata.data[0] === 255; + imgdata.data[0] = 100; + imgdata.data[0] = -Math.pow(10, 39); + @assert imgdata.data[0] === 0; + + imgdata.data[0] = 100; + imgdata.data[0] = -Infinity; + @assert imgdata.data[0] === 0; + imgdata.data[0] = 100; + imgdata.data[0] = Infinity; + @assert imgdata.data[0] === 255; + +- name: 2d.imageData.object.round + desc: ImageData.data rounds numbers with round-to-zero + testing: + - 2d.pixelarray.modify + webidl: + - es-octet + code: | + var imgdata = ctx.getImageData(0, 0, 10, 10); + imgdata.data[0] = 0.499; + @assert imgdata.data[0] === 0; + imgdata.data[0] = 0.5; + @assert imgdata.data[0] === 0; + imgdata.data[0] = 0.501; + @assert imgdata.data[0] === 1; + imgdata.data[0] = 1.499; + @assert imgdata.data[0] === 1; + imgdata.data[0] = 1.5; + @assert imgdata.data[0] === 2; + imgdata.data[0] = 1.501; + @assert imgdata.data[0] === 2; + imgdata.data[0] = 2.5; + @assert imgdata.data[0] === 2; + imgdata.data[0] = 3.5; + @assert imgdata.data[0] === 4; + imgdata.data[0] = 252.5; + @assert imgdata.data[0] === 252; + imgdata.data[0] = 253.5; + @assert imgdata.data[0] === 254; + imgdata.data[0] = 254.5; + @assert imgdata.data[0] === 254; + imgdata.data[0] = 256.5; + @assert imgdata.data[0] === 255; + imgdata.data[0] = -0.5; + @assert imgdata.data[0] === 0; + imgdata.data[0] = -1.5; + @assert imgdata.data[0] === 0; + + + +- name: 2d.imageData.put.null + desc: putImageData() with null imagedata throws TypeError + testing: + - 2d.imageData.put.wrongtype + code: | + @assert throws TypeError ctx.putImageData(null, 0, 0); + +- name: 2d.imageData.put.nonfinite + desc: putImageData() throws TypeError if arguments are not finite + notes: *bindings + testing: + - 2d.imageData.put.nonfinite + code: | + var imgdata = ctx.getImageData(0, 0, 10, 10); + @nonfinite @assert throws TypeError ctx.putImageData(, <10 Infinity -Infinity NaN>, <10 Infinity -Infinity NaN>); + @nonfinite @assert throws TypeError ctx.putImageData(, <10 Infinity -Infinity NaN>, <10 Infinity -Infinity NaN>, <10 Infinity -Infinity NaN>, <10 Infinity -Infinity NaN>, <10 Infinity -Infinity NaN>, <10 Infinity -Infinity NaN>); + +- name: 2d.imageData.put.basic + desc: putImageData() puts image data from getImageData() onto the canvas + testing: + - 2d.imageData.put.normal + - 2d.imageData.put.3arg + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + var imgdata = ctx.getImageData(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.putImageData(imgdata, 0, 0); + @assert pixel 50,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.imageData.put.created + desc: putImageData() puts image data from createImageData() onto the canvas + testing: + - 2d.imageData.put.normal + code: | + var imgdata = ctx.createImageData(100, 50); + for (var i = 0; i < imgdata.data.length; i += 4) { + imgdata.data[i] = 0; + imgdata.data[i+1] = 255; + imgdata.data[i+2] = 0; + imgdata.data[i+3] = 255; + } + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.putImageData(imgdata, 0, 0); + @assert pixel 50,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.imageData.put.wrongtype + desc: putImageData() does not accept non-ImageData objects + testing: + - 2d.imageData.put.wrongtype + code: | + var imgdata = { width: 1, height: 1, data: [255, 0, 0, 255] }; + @assert throws TypeError ctx.putImageData(imgdata, 0, 0); + @assert throws TypeError ctx.putImageData("cheese", 0, 0); + @assert throws TypeError ctx.putImageData(42, 0, 0); + expected: green + +- name: 2d.imageData.put.cross + desc: putImageData() accepts image data got from a different canvas + testing: + - 2d.imageData.put.normal + code: | + var canvas2 = document.createElement('canvas'); + var ctx2 = canvas2.getContext('2d'); + ctx2.fillStyle = '#0f0'; + ctx2.fillRect(0, 0, 100, 50) + var imgdata = ctx2.getImageData(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.putImageData(imgdata, 0, 0); + @assert pixel 50,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.imageData.put.alpha + desc: putImageData() puts non-solid image data correctly + testing: + - 2d.imageData.put.normal + code: | + ctx.fillStyle = 'rgba(0, 255, 0, 0.25)'; + ctx.fillRect(0, 0, 100, 50) + var imgdata = ctx.getImageData(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.putImageData(imgdata, 0, 0); + @assert pixel 50,25 ==~ 0,255,0,64; + expected: | + size 100 50 + cr.set_source_rgba(0, 1, 0, 0.25) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.imageData.put.modified + desc: putImageData() puts modified image data correctly + testing: + - 2d.imageData.put.normal + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + ctx.fillStyle = '#f00'; + ctx.fillRect(45, 20, 10, 10) + var imgdata = ctx.getImageData(45, 20, 10, 10); + for (var i = 0, len = imgdata.width*imgdata.height*4; i < len; i += 4) + { + imgdata.data[i] = 0; + imgdata.data[i+1] = 255; + } + ctx.putImageData(imgdata, 45, 20); + @assert pixel 50,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.imageData.put.dirty.zero + desc: putImageData() with zero-sized dirty rectangle puts nothing + testing: + - 2d.imageData.put.normal + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + var imgdata = ctx.getImageData(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + ctx.putImageData(imgdata, 0, 0, 0, 0, 0, 0); + @assert pixel 50,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.imageData.put.dirty.rect1 + desc: putImageData() only modifies areas inside the dirty rectangle, using width + and height + testing: + - 2d.imageData.put.normal + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 20, 20) + + var imgdata = ctx.getImageData(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + ctx.fillStyle = '#f00'; + ctx.fillRect(40, 20, 20, 20) + ctx.putImageData(imgdata, 40, 20, 0, 0, 20, 20); + + @assert pixel 50,25 ==~ 0,255,0,255; + @assert pixel 35,25 ==~ 0,255,0,255; + @assert pixel 65,25 ==~ 0,255,0,255; + @assert pixel 50,15 ==~ 0,255,0,255; + @assert pixel 50,45 ==~ 0,255,0,255; + expected: green + +- name: 2d.imageData.put.dirty.rect2 + desc: putImageData() only modifies areas inside the dirty rectangle, using x and + y + testing: + - 2d.imageData.put.normal + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.fillStyle = '#0f0'; + ctx.fillRect(60, 30, 20, 20) + + var imgdata = ctx.getImageData(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + ctx.fillStyle = '#f00'; + ctx.fillRect(40, 20, 20, 20) + ctx.putImageData(imgdata, -20, -10, 60, 30, 20, 20); + + @assert pixel 50,25 ==~ 0,255,0,255; + @assert pixel 35,25 ==~ 0,255,0,255; + @assert pixel 65,25 ==~ 0,255,0,255; + @assert pixel 50,15 ==~ 0,255,0,255; + @assert pixel 50,45 ==~ 0,255,0,255; + expected: green + +- name: 2d.imageData.put.dirty.negative + desc: putImageData() handles negative-sized dirty rectangles correctly + testing: + - 2d.imageData.put.normal + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 20, 20) + + var imgdata = ctx.getImageData(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + ctx.fillStyle = '#f00'; + ctx.fillRect(40, 20, 20, 20) + ctx.putImageData(imgdata, 40, 20, 20, 20, -20, -20); + + @assert pixel 50,25 ==~ 0,255,0,255; + @assert pixel 35,25 ==~ 0,255,0,255; + @assert pixel 65,25 ==~ 0,255,0,255; + @assert pixel 50,15 ==~ 0,255,0,255; + @assert pixel 50,45 ==~ 0,255,0,255; + expected: green + +- name: 2d.imageData.put.dirty.outside + desc: putImageData() handles dirty rectangles outside the canvas correctly + testing: + - 2d.imageData.put.normal + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + + var imgdata = ctx.getImageData(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + + ctx.putImageData(imgdata, 100, 20, 20, 20, -20, -20); + ctx.putImageData(imgdata, 200, 200, 0, 0, 100, 50); + ctx.putImageData(imgdata, 40, 20, -30, -20, 30, 20); + ctx.putImageData(imgdata, -30, 20, 0, 0, 30, 20); + + @assert pixel 50,25 ==~ 0,255,0,255; + @assert pixel 98,15 ==~ 0,255,0,255; + @assert pixel 98,25 ==~ 0,255,0,255; + @assert pixel 98,45 ==~ 0,255,0,255; + @assert pixel 1,5 ==~ 0,255,0,255; + @assert pixel 1,25 ==~ 0,255,0,255; + @assert pixel 1,45 ==~ 0,255,0,255; + expected: green + +- name: 2d.imageData.put.unchanged + desc: putImageData(getImageData(...), ...) has no effect + testing: + - 2d.imageData.unchanged + code: | + var i = 0; + for (var y = 0; y < 16; ++y) { + for (var x = 0; x < 16; ++x, ++i) { + ctx.fillStyle = 'rgba(' + i + ',' + (Math.floor(i*1.5) % 256) + ',' + (Math.floor(i*23.3) % 256) + ',' + (i/256) + ')'; + ctx.fillRect(x, y, 1, 1); + } + } + var imgdata1 = ctx.getImageData(0.1, 0.2, 15.8, 15.9); + var olddata = []; + for (var i = 0; i < imgdata1.data.length; ++i) + olddata[i] = imgdata1.data[i]; + + ctx.putImageData(imgdata1, 0.1, 0.2); + + var imgdata2 = ctx.getImageData(0.1, 0.2, 15.8, 15.9); + for (var i = 0; i < imgdata2.data.length; ++i) { + @assert olddata[i] === imgdata2.data[i]; + } + +- name: 2d.imageData.put.unaffected + desc: putImageData() is not affected by context state + testing: + - 2d.imageData.unaffected + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + var imgdata = ctx.getImageData(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.globalAlpha = 0.1; + ctx.globalCompositeOperation = 'destination-atop'; + ctx.shadowColor = '#f00'; + ctx.shadowBlur = 1; + ctx.translate(100, 50); + ctx.scale(0.1, 0.1); + ctx.putImageData(imgdata, 0, 0); + @assert pixel 50,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.imageData.put.clip + desc: putImageData() is not affected by clipping regions + testing: + - 2d.imageData.unaffected + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + var imgdata = ctx.getImageData(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.beginPath(); + ctx.rect(0, 0, 50, 50); + ctx.clip(); + ctx.putImageData(imgdata, 0, 0); + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.imageData.put.path + desc: putImageData() does not affect the current path + testing: + - 2d.imageData.put.normal + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.rect(0, 0, 100, 50); + var imgdata = ctx.getImageData(0, 0, 100, 50); + ctx.putImageData(imgdata, 0, 0); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 50,25 ==~ 0,255,0,255; + expected: green diff --git a/test/wpt/shadows.yaml b/test/wpt/shadows.yaml new file mode 100644 index 000000000..1d8da0ede --- /dev/null +++ b/test/wpt/shadows.yaml @@ -0,0 +1,1150 @@ +- name: 2d.shadow.attributes.shadowBlur.initial + testing: + - 2d.shadow.blur.get + - 2d.shadow.blur.initial + code: | + @assert ctx.shadowBlur === 0; + +- name: 2d.shadow.attributes.shadowBlur.valid + testing: + - 2d.shadow.blur.get + - 2d.shadow.blur.set + code: | + ctx.shadowBlur = 1; + @assert ctx.shadowBlur === 1; + + ctx.shadowBlur = 0.5; + @assert ctx.shadowBlur === 0.5; + + ctx.shadowBlur = 1e6; + @assert ctx.shadowBlur === 1e6; + + ctx.shadowBlur = 0; + @assert ctx.shadowBlur === 0; + +- name: 2d.shadow.attributes.shadowBlur.invalid + testing: + - 2d.shadow.blur.invalid + code: | + ctx.shadowBlur = 1; + ctx.shadowBlur = -2; + @assert ctx.shadowBlur === 1; + + ctx.shadowBlur = 1; + ctx.shadowBlur = Infinity; + @assert ctx.shadowBlur === 1; + + ctx.shadowBlur = 1; + ctx.shadowBlur = -Infinity; + @assert ctx.shadowBlur === 1; + + ctx.shadowBlur = 1; + ctx.shadowBlur = NaN; + @assert ctx.shadowBlur === 1; + + ctx.shadowBlur = 1; + ctx.shadowBlur = 'string'; + @assert ctx.shadowBlur === 1; + + ctx.shadowBlur = 1; + ctx.shadowBlur = true; + @assert ctx.shadowBlur === 1; + + ctx.shadowBlur = 1; + ctx.shadowBlur = false; + @assert ctx.shadowBlur === 0; + +- name: 2d.shadow.attributes.shadowOffset.initial + testing: + - 2d.shadow.offset.initial + code: | + @assert ctx.shadowOffsetX === 0; + @assert ctx.shadowOffsetY === 0; + +- name: 2d.shadow.attributes.shadowOffset.valid + testing: + - 2d.shadow.offset.get + - 2d.shadow.offset.set + code: | + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 2; + @assert ctx.shadowOffsetX === 1; + @assert ctx.shadowOffsetY === 2; + + ctx.shadowOffsetX = 0.5; + ctx.shadowOffsetY = 0.25; + @assert ctx.shadowOffsetX === 0.5; + @assert ctx.shadowOffsetY === 0.25; + + ctx.shadowOffsetX = -0.5; + ctx.shadowOffsetY = -0.25; + @assert ctx.shadowOffsetX === -0.5; + @assert ctx.shadowOffsetY === -0.25; + + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + @assert ctx.shadowOffsetX === 0; + @assert ctx.shadowOffsetY === 0; + + ctx.shadowOffsetX = 1e6; + ctx.shadowOffsetY = 1e6; + @assert ctx.shadowOffsetX === 1e6; + @assert ctx.shadowOffsetY === 1e6; + +- name: 2d.shadow.attributes.shadowOffset.invalid + testing: + - 2d.shadow.offset.invalid + code: | + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 2; + ctx.shadowOffsetX = Infinity; + ctx.shadowOffsetY = Infinity; + @assert ctx.shadowOffsetX === 1; + @assert ctx.shadowOffsetY === 2; + + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 2; + ctx.shadowOffsetX = -Infinity; + ctx.shadowOffsetY = -Infinity; + @assert ctx.shadowOffsetX === 1; + @assert ctx.shadowOffsetY === 2; + + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 2; + ctx.shadowOffsetX = NaN; + ctx.shadowOffsetY = NaN; + @assert ctx.shadowOffsetX === 1; + @assert ctx.shadowOffsetY === 2; + + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 2; + ctx.shadowOffsetX = 'string'; + ctx.shadowOffsetY = 'string'; + @assert ctx.shadowOffsetX === 1; + @assert ctx.shadowOffsetY === 2; + + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 2; + ctx.shadowOffsetX = true; + ctx.shadowOffsetY = true; + @assert ctx.shadowOffsetX === 1; + @assert ctx.shadowOffsetY === 1; + + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 2; + ctx.shadowOffsetX = false; + ctx.shadowOffsetY = false; + @assert ctx.shadowOffsetX === 0; + @assert ctx.shadowOffsetY === 0; + +- name: 2d.shadow.attributes.shadowColor.initial + testing: + - 2d.shadow.color.initial + code: | + @assert ctx.shadowColor === 'rgba(0, 0, 0, 0)'; + +- name: 2d.shadow.attributes.shadowColor.valid + testing: + - 2d.shadow.color.get + - 2d.shadow.color.set + code: | + ctx.shadowColor = 'lime'; + @assert ctx.shadowColor === '#00ff00'; + + ctx.shadowColor = 'RGBA(0,255, 0,0)'; + @assert ctx.shadowColor === 'rgba(0, 255, 0, 0)'; + +- name: 2d.shadow.attributes.shadowColor.invalid + testing: + - 2d.shadow.color.invalid + code: | + ctx.shadowColor = '#00ff00'; + ctx.shadowColor = 'bogus'; + @assert ctx.shadowColor === '#00ff00'; + + ctx.shadowColor = '#00ff00'; + ctx.shadowColor = 'red bogus'; + @assert ctx.shadowColor === '#00ff00'; + + ctx.shadowColor = '#00ff00'; + ctx.shadowColor = ctx; + @assert ctx.shadowColor === '#00ff00'; + + ctx.shadowColor = '#00ff00'; + ctx.shadowColor = undefined; + @assert ctx.shadowColor === '#00ff00'; + +- name: 2d.shadow.enable.off.1 + desc: Shadows are not drawn when only shadowColor is set + testing: + - 2d.shadow.enable + - 2d.shadow.render + code: | + ctx.shadowColor = '#f00'; + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.enable.off.2 + desc: Shadows are not drawn when only shadowColor is set + testing: + - 2d.shadow.enable + - 2d.shadow.render + code: | + ctx.globalCompositeOperation = 'destination-atop'; + ctx.shadowColor = '#f00'; + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.enable.blur + desc: Shadows are drawn if shadowBlur is set + testing: + - 2d.shadow.enable + - 2d.shadow.render + code: | + ctx.globalCompositeOperation = 'destination-atop'; + ctx.shadowColor = '#0f0'; + ctx.shadowBlur = 0.1; + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.enable.x + desc: Shadows are drawn if shadowOffsetX is set + testing: + - 2d.shadow.enable + - 2d.shadow.render + code: | + ctx.globalCompositeOperation = 'destination-atop'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetX = 0.1; + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.enable.y + desc: Shadows are drawn if shadowOffsetY is set + testing: + - 2d.shadow.enable + - 2d.shadow.render + code: | + ctx.globalCompositeOperation = 'destination-atop'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 0.1; + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.offset.positiveX + desc: Shadows can be offset with positive x + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetX = 50; + ctx.fillRect(0, 0, 50, 50); + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.offset.negativeX + desc: Shadows can be offset with negative x + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetX = -50; + ctx.fillRect(50, 0, 50, 50); + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.offset.positiveY + desc: Shadows can be offset with positive y + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 25; + ctx.fillRect(0, 0, 100, 25); + @assert pixel 50,12 == 0,255,0,255; + @assert pixel 50,37 == 0,255,0,255; + expected: green + +- name: 2d.shadow.offset.negativeY + desc: Shadows can be offset with negative y + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = -25; + ctx.fillRect(0, 25, 100, 25); + @assert pixel 50,12 == 0,255,0,255; + @assert pixel 50,37 == 0,255,0,255; + expected: green + +- name: 2d.shadow.outside + desc: Shadows of shapes outside the visible area can be offset onto the visible + area + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetX = 100; + ctx.fillRect(-100, 0, 25, 50); + ctx.shadowOffsetX = -100; + ctx.fillRect(175, 0, 25, 50); + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 100; + ctx.fillRect(25, -100, 50, 25); + ctx.shadowOffsetY = -100; + ctx.fillRect(25, 125, 50, 25); + @assert pixel 12,25 == 0,255,0,255; + @assert pixel 87,25 == 0,255,0,255; + @assert pixel 50,12 == 0,255,0,255; + @assert pixel 50,37 == 0,255,0,255; + expected: green + +- name: 2d.shadow.clip.1 + desc: Shadows of clipped shapes are still drawn within the clipping region + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(50, 0, 50, 50); + + ctx.save(); + ctx.beginPath(); + ctx.rect(50, 0, 50, 50); + ctx.clip(); + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetX = 50; + ctx.fillRect(0, 0, 50, 50); + ctx.restore(); + + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.clip.2 + desc: Shadows are not drawn outside the clipping region + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(50, 0, 50, 50); + + ctx.save(); + ctx.beginPath(); + ctx.rect(0, 0, 50, 50); + ctx.clip(); + ctx.shadowColor = '#f00'; + ctx.shadowOffsetX = 50; + ctx.fillRect(0, 0, 50, 50); + ctx.restore(); + + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.clip.3 + desc: Shadows of clipped shapes are still drawn within the clipping region + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(50, 0, 50, 50); + + ctx.save(); + ctx.beginPath(); + ctx.rect(0, 0, 50, 50); + ctx.clip(); + ctx.fillStyle = '#f00'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetX = 50; + ctx.fillRect(-50, 0, 50, 50); + ctx.restore(); + + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.stroke.basic + desc: Shadows are drawn for strokes + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 50; + ctx.beginPath(); + ctx.lineWidth = 50; + ctx.moveTo(0, -25); + ctx.lineTo(100, -25); + ctx.stroke(); + + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.stroke.cap.1 + desc: Shadows are not drawn for areas outside stroke caps + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.shadowColor = '#f00'; + ctx.shadowOffsetY = 50; + ctx.beginPath(); + ctx.lineWidth = 50; + ctx.lineCap = 'butt'; + ctx.moveTo(-50, -25); + ctx.lineTo(0, -25); + ctx.moveTo(100, -25); + ctx.lineTo(150, -25); + ctx.stroke(); + + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.stroke.cap.2 + desc: Shadows are drawn for stroke caps + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 50; + ctx.beginPath(); + ctx.lineWidth = 50; + ctx.lineCap = 'square'; + ctx.moveTo(25, -25); + ctx.lineTo(75, -25); + ctx.stroke(); + + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.stroke.join.1 + desc: Shadows are not drawn for areas outside stroke joins + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.shadowColor = '#f00'; + ctx.shadowOffsetX = 100; + ctx.lineWidth = 200; + ctx.lineJoin = 'bevel'; + ctx.beginPath(); + ctx.moveTo(-200, -50); + ctx.lineTo(-150, -50); + ctx.lineTo(-151, -100); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.shadow.stroke.join.2 + desc: Shadows are drawn for stroke joins + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(50, 0, 50, 50); + ctx.strokeStyle = '#f00'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetX = 100; + ctx.lineWidth = 200; + ctx.lineJoin = 'miter'; + ctx.beginPath(); + ctx.moveTo(-200, -50); + ctx.lineTo(-150, -50); + ctx.lineTo(-151, -100); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.shadow.stroke.join.3 + desc: Shadows are drawn for stroke joins respecting miter limit + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.shadowColor = '#f00'; + ctx.shadowOffsetX = 100; + ctx.lineWidth = 200; + ctx.lineJoin = 'miter'; + ctx.miterLimit = 0.1; + ctx.beginPath(); + ctx.moveTo(-200, -50); + ctx.lineTo(-150, -50); + ctx.lineTo(-151, -100); // (not an exact right angle, to avoid some other bug in Firefox 3) + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.shadow.image.basic + desc: Shadows are drawn for images + testing: + - 2d.shadow.render + images: + - red.png + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 50; + ctx.drawImage(document.getElementById('red.png'), 0, -50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.image.transparent.1 + desc: Shadows are not drawn for transparent images + testing: + - 2d.shadow.render + images: + - transparent.png + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#f00'; + ctx.shadowOffsetY = 50; + ctx.drawImage(document.getElementById('transparent.png'), 0, -50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.image.transparent.2 + desc: Shadows are not drawn for transparent parts of images + testing: + - 2d.shadow.render + images: + - redtransparent.png + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(50, 0, 50, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#0f0'; + ctx.drawImage(document.getElementById('redtransparent.png'), 50, -50); + ctx.shadowColor = '#f00'; + ctx.drawImage(document.getElementById('redtransparent.png'), -50, -50); + + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.image.alpha + desc: Shadows are drawn correctly for partially-transparent images + testing: + - 2d.shadow.render + images: + - transparent50.png + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#00f'; + ctx.drawImage(document.getElementById('transparent50.png'), 0, -50); + + @assert pixel 50,25 ==~ 127,0,127,255; + expected: | + size 100 50 + cr.set_source_rgb(0.5, 0, 0.5) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.shadow.image.section + desc: Shadows are not drawn for areas outside image source rectangles + testing: + - 2d.shadow.render + images: + - redtransparent.png + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#f00'; + ctx.drawImage(document.getElementById('redtransparent.png'), 50, 0, 50, 50, 0, -50, 50, 50); + + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 50,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.shadow.image.scale + desc: Shadows are drawn correctly for scaled images + testing: + - 2d.shadow.render + images: + - redtransparent.png + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#0f0'; + ctx.drawImage(document.getElementById('redtransparent.png'), 0, 0, 100, 50, -10, -50, 240, 50); + + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 50,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.shadow.canvas.basic + desc: Shadows are drawn for canvases + testing: + - 2d.shadow.render + code: | + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var ctx2 = canvas2.getContext('2d'); + ctx2.fillStyle = '#f00'; + ctx2.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 50; + ctx.drawImage(canvas2, 0, -50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.canvas.transparent.1 + desc: Shadows are not drawn for transparent canvases + testing: + - 2d.shadow.render + code: | + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var ctx2 = canvas2.getContext('2d'); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#f00'; + ctx.shadowOffsetY = 50; + ctx.drawImage(canvas2, 0, -50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.canvas.transparent.2 + desc: Shadows are not drawn for transparent parts of canvases + testing: + - 2d.shadow.render + code: | + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var ctx2 = canvas2.getContext('2d'); + ctx2.fillStyle = '#f00'; + ctx2.fillRect(0, 0, 50, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(50, 0, 50, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#0f0'; + ctx.drawImage(canvas2, 50, -50); + ctx.shadowColor = '#f00'; + ctx.drawImage(canvas2, -50, -50); + + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.canvas.alpha + desc: Shadows are drawn correctly for partially-transparent canvases + testing: + - 2d.shadow.render + images: + - transparent50.png + code: | + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var ctx2 = canvas2.getContext('2d'); + ctx2.fillStyle = 'rgba(255, 0, 0, 0.5)'; + ctx2.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#00f'; + ctx.drawImage(canvas2, 0, -50); + + @assert pixel 50,25 ==~ 127,0,127,255; + expected: | + size 100 50 + cr.set_source_rgb(0.5, 0, 0.5) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.shadow.pattern.basic + desc: Shadows are drawn for fill patterns + testing: + - 2d.shadow.render + # http://bugs.webkit.org/show_bug.cgi?id=15266 + images: + - red.png + code: | + var pattern = ctx.createPattern(document.getElementById('red.png'), 'repeat'); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 50; + ctx.fillStyle = pattern; + ctx.fillRect(0, -50, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.pattern.transparent.1 + desc: Shadows are not drawn for transparent fill patterns + testing: + - 2d.shadow.render + # http://bugs.webkit.org/show_bug.cgi?id=15266 + images: + - transparent.png + code: | + var pattern = ctx.createPattern(document.getElementById('transparent.png'), 'repeat'); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#f00'; + ctx.shadowOffsetY = 50; + ctx.fillStyle = pattern; + ctx.fillRect(0, -50, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.pattern.transparent.2 + desc: Shadows are not drawn for transparent parts of fill patterns + testing: + - 2d.shadow.render + # http://bugs.webkit.org/show_bug.cgi?id=15266 + images: + - redtransparent.png + code: | + var pattern = ctx.createPattern(document.getElementById('redtransparent.png'), 'repeat'); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(50, 0, 50, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#0f0'; + ctx.fillStyle = pattern; + ctx.fillRect(0, -50, 100, 50); + + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.pattern.alpha + desc: Shadows are drawn correctly for partially-transparent fill patterns + testing: + - 2d.shadow.render + # http://bugs.webkit.org/show_bug.cgi?id=15266 + images: + - transparent50.png + code: | + var pattern = ctx.createPattern(document.getElementById('transparent50.png'), 'repeat'); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#00f'; + ctx.fillStyle = pattern; + ctx.fillRect(0, -50, 100, 50); + + @assert pixel 50,25 ==~ 127,0,127,255; + expected: | + size 100 50 + cr.set_source_rgb(0.5, 0, 0.5) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.shadow.gradient.basic + desc: Shadows are drawn for gradient fills + testing: + - 2d.shadow.render + # http://bugs.webkit.org/show_bug.cgi?id=15266 + code: | + var gradient = ctx.createLinearGradient(0, 0, 100, 0); + gradient.addColorStop(0, '#f00'); + gradient.addColorStop(1, '#f00'); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 50; + ctx.fillStyle = gradient; + ctx.fillRect(0, -50, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.gradient.transparent.1 + desc: Shadows are not drawn for transparent gradient fills + testing: + - 2d.shadow.render + # http://bugs.webkit.org/show_bug.cgi?id=15266 + code: | + var gradient = ctx.createLinearGradient(0, 0, 100, 0); + gradient.addColorStop(0, 'rgba(0,0,0,0)'); + gradient.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#f00'; + ctx.shadowOffsetY = 50; + ctx.fillStyle = gradient; + ctx.fillRect(0, -50, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.gradient.transparent.2 + desc: Shadows are not drawn for transparent parts of gradient fills + testing: + - 2d.shadow.render + # http://bugs.webkit.org/show_bug.cgi?id=15266 + code: | + var gradient = ctx.createLinearGradient(0, 0, 100, 0); + gradient.addColorStop(0, '#f00'); + gradient.addColorStop(0.499, '#f00'); + gradient.addColorStop(0.5, 'rgba(0,0,0,0)'); + gradient.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(50, 0, 50, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#0f0'; + ctx.fillStyle = gradient; + ctx.fillRect(0, -50, 100, 50); + + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.gradient.alpha + desc: Shadows are drawn correctly for partially-transparent gradient fills + testing: + - 2d.shadow.render + # http://bugs.webkit.org/show_bug.cgi?id=15266 + code: | + var gradient = ctx.createLinearGradient(0, 0, 100, 0); + gradient.addColorStop(0, 'rgba(255,0,0,0.5)'); + gradient.addColorStop(1, 'rgba(255,0,0,0.5)'); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#00f'; + ctx.fillStyle = gradient; + ctx.fillRect(0, -50, 100, 50); + + @assert pixel 50,25 ==~ 127,0,127,255; + expected: | + size 100 50 + cr.set_source_rgb(0.5, 0, 0.5) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.shadow.transform.1 + desc: Shadows take account of transformations + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#0f0'; + ctx.translate(100, 100); + ctx.fillRect(-100, -150, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.transform.2 + desc: Shadow offsets are not affected by transformations + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#0f0'; + ctx.rotate(Math.PI) + ctx.fillRect(-100, 0, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.blur.low + desc: Shadows look correct for small blurs + manual: + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#ff0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#00f'; + ctx.shadowOffsetY = 25; + for (var x = 0; x < 100; ++x) { + ctx.save(); + ctx.beginPath(); + ctx.rect(x, 0, 1, 50); + ctx.clip(); + ctx.shadowBlur = x; + ctx.fillRect(-200, -200, 500, 200); + ctx.restore(); + } + expected: | + size 100 50 + import math + cr.set_source_rgb(0, 0, 1) + cr.rectangle(0, 0, 1, 25) + cr.fill() + cr.set_source_rgb(1, 1, 0) + cr.rectangle(0, 25, 1, 25) + cr.fill() + for x in range(1, 100): + sigma = x/2.0 + filter = [] + for i in range(-24, 26): + filter.append(math.exp(-i*i / (2*sigma*sigma)) / (math.sqrt(2*math.pi)*sigma)) + accum = [0] + for f in filter: + accum.append(accum[-1] + f) + for y in range(0, 50): + cr.set_source_rgb(accum[y], accum[y], 1-accum[y]) + cr.rectangle(x, y, 1, 1) + cr.fill() + +- name: 2d.shadow.blur.high + desc: Shadows look correct for large blurs + manual: + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#ff0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#00f'; + ctx.shadowOffsetY = 0; + ctx.shadowBlur = 100; + ctx.fillRect(-200, -200, 200, 400); + expected: | + size 100 50 + import math + sigma = 100.0/2 + filter = [] + for i in range(-200, 100): + filter.append(math.exp(-i*i / (2*sigma*sigma)) / (math.sqrt(2*math.pi)*sigma)) + accum = [0] + for f in filter: + accum.append(accum[-1] + f) + for x in range(0, 100): + cr.set_source_rgb(accum[x+200], accum[x+200], 1-accum[x+200]) + cr.rectangle(x, 0, 1, 50) + cr.fill() + +- name: 2d.shadow.alpha.1 + desc: Shadow color alpha components are used + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = 'rgba(255, 0, 0, 0.01)'; + ctx.shadowOffsetY = 50; + ctx.fillRect(0, -50, 100, 50); + + @assert pixel 50,25 ==~ 0,255,0,255 +/- 4; + expected: green + +- name: 2d.shadow.alpha.2 + desc: Shadow color alpha components are used + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = 'rgba(0, 0, 255, 0.5)'; + ctx.shadowOffsetY = 50; + ctx.fillRect(0, -50, 100, 50); + + @assert pixel 50,25 ==~ 127,0,127,255; + expected: | + size 100 50 + cr.set_source_rgb(0.5, 0, 0.5) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.shadow.alpha.3 + desc: Shadows are affected by globalAlpha + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; // (work around broken Firefox globalAlpha caching) + ctx.shadowColor = '#00f'; + ctx.shadowOffsetY = 50; + ctx.globalAlpha = 0.5; + ctx.fillRect(0, -50, 100, 50); + + @assert pixel 50,25 ==~ 127,0,127,255; + expected: | + size 100 50 + cr.set_source_rgb(0.5, 0, 0.5) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.shadow.alpha.4 + desc: Shadows with alpha components are correctly affected by globalAlpha + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; // (work around broken Firefox globalAlpha caching) + ctx.shadowColor = 'rgba(0, 0, 255, 0.707)'; + ctx.shadowOffsetY = 50; + ctx.globalAlpha = 0.707; + ctx.fillRect(0, -50, 100, 50); + + @assert pixel 50,25 ==~ 127,0,127,255; + expected: | + size 100 50 + cr.set_source_rgb(0.5, 0, 0.5) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.shadow.alpha.5 + desc: Shadows of shapes with alpha components are drawn correctly + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = 'rgba(64, 0, 0, 0.5)'; + ctx.shadowColor = '#00f'; + ctx.shadowOffsetY = 50; + ctx.fillRect(0, -50, 100, 50); + + @assert pixel 50,25 ==~ 127,0,127,255; + expected: | + size 100 50 + cr.set_source_rgb(0.5, 0, 0.5) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.shadow.composite.1 + desc: Shadows are drawn using globalCompositeOperation + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.globalCompositeOperation = 'xor'; + ctx.shadowColor = '#f00'; + ctx.shadowOffsetX = 100; + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, 0, 200, 50); + + @assert pixel 50,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.shadow.composite.2 + desc: Shadows are drawn using globalCompositeOperation + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.globalCompositeOperation = 'xor'; + ctx.shadowColor = '#f00'; + ctx.shadowBlur = 1; + ctx.fillStyle = '#0f0'; + ctx.fillRect(-10, -10, 120, 70); + + @assert pixel 50,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.shadow.composite.3 + desc: Areas outside shadows are drawn correctly with destination-out + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.globalCompositeOperation = 'destination-out'; + ctx.shadowColor = '#f00'; + ctx.shadowBlur = 10; + ctx.fillStyle = '#f00'; + ctx.fillRect(200, 0, 100, 50); + + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 50,25 ==~ 0,255,0,255; + expected: green diff --git a/test/wpt/text-styles.yaml b/test/wpt/text-styles.yaml new file mode 100644 index 000000000..c4d2caf00 --- /dev/null +++ b/test/wpt/text-styles.yaml @@ -0,0 +1,525 @@ +- name: 2d.text.font.parse.basic + testing: + - 2d.text.font.parse + - 2d.text.font.get + code: | + ctx.font = '20px serif'; + @assert ctx.font === '20px serif'; + + ctx.font = '20PX SERIF'; + @assert ctx.font === '20px serif'; @moz-todo + +- name: 2d.text.font.parse.tiny + testing: + - 2d.text.font.parse + - 2d.text.font.get + code: | + ctx.font = '1px sans-serif'; + @assert ctx.font === '1px sans-serif'; + +- name: 2d.text.font.parse.complex + testing: + - 2d.text.font.parse + - 2d.text.font.get + - 2d.text.font.lineheight + code: | + ctx.font = 'small-caps italic 400 12px/2 Unknown Font, sans-serif'; + @assert ctx.font === 'italic small-caps 12px "Unknown Font", sans-serif'; @moz-todo + +- name: 2d.text.font.parse.family + testing: + - 2d.text.font.parse + - 2d.text.font.get + - 2d.text.font.lineheight + code: | + ctx.font = '20px cursive,fantasy,monospace,sans-serif,serif,UnquotedFont,"QuotedFont\\\\\\","'; + @assert ctx.font === '20px cursive, fantasy, monospace, sans-serif, serif, UnquotedFont, "QuotedFont\\\\\\","'; + + # TODO: + # 2d.text.font.parse.size.absolute + # xx-small x-small small medium large x-large xx-large + # 2d.text.font.parse.size.relative + # smaller larger + # 2d.text.font.parse.size.length.relative + # em ex px + # 2d.text.font.parse.size.length.absolute + # in cm mm pt pc + +- name: 2d.text.font.parse.size.percentage + testing: + - 2d.text.font.parse + - 2d.text.font.get + - 2d.text.font.fontsize + - 2d.text.font.size + canvas: 'style="font-size: 144px" width="100" height="50"' + code: | + ctx.font = '50% serif'; + @assert ctx.font === '72px serif'; @moz-todo + canvas.setAttribute('style', 'font-size: 100px'); + @assert ctx.font === '72px serif'; @moz-todo + +- name: 2d.text.font.parse.size.percentage.default + testing: + - 2d.text.font.undefined + code: | + var canvas2 = document.createElement('canvas'); + var ctx2 = canvas2.getContext('2d'); + ctx2.font = '1000% serif'; + @assert ctx2.font === '100px serif'; @moz-todo + +- name: 2d.text.font.parse.system + desc: System fonts must be computed to explicit values + testing: + - 2d.text.font.parse + - 2d.text.font.get + - 2d.text.font.systemfonts + code: | + ctx.font = 'message-box'; + @assert ctx.font !== 'message-box'; + +- name: 2d.text.font.parse.invalid + testing: + - 2d.text.font.invalid + code: | + ctx.font = '20px serif'; + @assert ctx.font === '20px serif'; + + ctx.font = '20px serif'; + ctx.font = ''; + @assert ctx.font === '20px serif'; + + ctx.font = '20px serif'; + ctx.font = 'bogus'; + @assert ctx.font === '20px serif'; + + ctx.font = '20px serif'; + ctx.font = 'inherit'; + @assert ctx.font === '20px serif'; + + ctx.font = '20px serif'; + ctx.font = '10px {bogus}'; + @assert ctx.font === '20px serif'; + + ctx.font = '20px serif'; + ctx.font = '10px initial'; + @assert ctx.font === '20px serif'; @moz-todo + + ctx.font = '20px serif'; + ctx.font = '10px default'; + @assert ctx.font === '20px serif'; @moz-todo + + ctx.font = '20px serif'; + ctx.font = '10px inherit'; + @assert ctx.font === '20px serif'; + + ctx.font = '20px serif'; + ctx.font = '10px revert'; + @assert ctx.font === '20px serif'; + + ctx.font = '20px serif'; + ctx.font = 'var(--x)'; + @assert ctx.font === '20px serif'; + + ctx.font = '20px serif'; + ctx.font = 'var(--x, 10px serif)'; + @assert ctx.font === '20px serif'; + + ctx.font = '20px serif'; + ctx.font = '1em serif; background: green; margin: 10px'; + @assert ctx.font === '20px serif'; + +- name: 2d.text.font.default + testing: + - 2d.text.font.default + code: | + @assert ctx.font === '10px sans-serif'; + +- name: 2d.text.font.relative_size + testing: + - 2d.text.font.relative_size + code: | + var canvas2 = document.createElement('canvas'); + var ctx2 = canvas2.getContext('2d'); + ctx2.font = '1em sans-serif'; + @assert ctx2.font === '10px sans-serif'; + +- name: 2d.text.align.valid + testing: + - 2d.text.align.get + - 2d.text.align.set + code: | + ctx.textAlign = 'start'; + @assert ctx.textAlign === 'start'; + + ctx.textAlign = 'end'; + @assert ctx.textAlign === 'end'; + + ctx.textAlign = 'left'; + @assert ctx.textAlign === 'left'; + + ctx.textAlign = 'right'; + @assert ctx.textAlign === 'right'; + + ctx.textAlign = 'center'; + @assert ctx.textAlign === 'center'; + +- name: 2d.text.align.invalid + testing: + - 2d.text.align.invalid + code: | + ctx.textAlign = 'start'; + ctx.textAlign = 'bogus'; + @assert ctx.textAlign === 'start'; + + ctx.textAlign = 'start'; + ctx.textAlign = 'END'; + @assert ctx.textAlign === 'start'; + + ctx.textAlign = 'start'; + ctx.textAlign = 'end '; + @assert ctx.textAlign === 'start'; + + ctx.textAlign = 'start'; + ctx.textAlign = 'end\0'; + @assert ctx.textAlign === 'start'; + +- name: 2d.text.align.default + testing: + - 2d.text.align.default + code: | + @assert ctx.textAlign === 'start'; + + +- name: 2d.text.baseline.valid + testing: + - 2d.text.baseline.get + - 2d.text.baseline.set + code: | + ctx.textBaseline = 'top'; + @assert ctx.textBaseline === 'top'; + + ctx.textBaseline = 'hanging'; + @assert ctx.textBaseline === 'hanging'; + + ctx.textBaseline = 'middle'; + @assert ctx.textBaseline === 'middle'; + + ctx.textBaseline = 'alphabetic'; + @assert ctx.textBaseline === 'alphabetic'; + + ctx.textBaseline = 'ideographic'; + @assert ctx.textBaseline === 'ideographic'; + + ctx.textBaseline = 'bottom'; + @assert ctx.textBaseline === 'bottom'; + +- name: 2d.text.baseline.invalid + testing: + - 2d.text.baseline.invalid + code: | + ctx.textBaseline = 'top'; + ctx.textBaseline = 'bogus'; + @assert ctx.textBaseline === 'top'; + + ctx.textBaseline = 'top'; + ctx.textBaseline = 'MIDDLE'; + @assert ctx.textBaseline === 'top'; + + ctx.textBaseline = 'top'; + ctx.textBaseline = 'middle '; + @assert ctx.textBaseline === 'top'; + + ctx.textBaseline = 'top'; + ctx.textBaseline = 'middle\0'; + @assert ctx.textBaseline === 'top'; + +- name: 2d.text.baseline.default + testing: + - 2d.text.baseline.default + code: | + @assert ctx.textBaseline === 'alphabetic'; + + + + + +- name: 2d.text.draw.baseline.top + desc: textBaseline top is the top of the em square (not the bounding box) + testing: + - 2d.text.baseline.top + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textBaseline = 'top'; + ctx.fillText('CC', 0, 0); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + @assert pixel 5,45 ==~ 0,255,0,255; + @assert pixel 95,45 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.baseline.bottom + desc: textBaseline bottom is the bottom of the em square (not the bounding box) + testing: + - 2d.text.baseline.bottom + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textBaseline = 'bottom'; + ctx.fillText('CC', 0, 50); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + @assert pixel 5,45 ==~ 0,255,0,255; + @assert pixel 95,45 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.baseline.middle + desc: textBaseline middle is the middle of the em square (not the bounding box) + testing: + - 2d.text.baseline.middle + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textBaseline = 'middle'; + ctx.fillText('CC', 0, 25); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + @assert pixel 5,45 ==~ 0,255,0,255; + @assert pixel 95,45 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.baseline.alphabetic + testing: + - 2d.text.baseline.alphabetic + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textBaseline = 'alphabetic'; + ctx.fillText('CC', 0, 37.5); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + @assert pixel 5,45 ==~ 0,255,0,255; + @assert pixel 95,45 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.baseline.ideographic + testing: + - 2d.text.baseline.ideographic + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textBaseline = 'ideographic'; + ctx.fillText('CC', 0, 31.25); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + @assert pixel 5,45 ==~ 0,255,0,255; @moz-todo + @assert pixel 95,45 ==~ 0,255,0,255; @moz-todo + }), 500); + expected: green + +- name: 2d.text.draw.baseline.hanging + testing: + - 2d.text.baseline.hanging + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textBaseline = 'hanging'; + ctx.fillText('CC', 0, 12.5); + @assert pixel 5,5 ==~ 0,255,0,255; @moz-todo + @assert pixel 95,5 ==~ 0,255,0,255; @moz-todo + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + @assert pixel 5,45 ==~ 0,255,0,255; + @assert pixel 95,45 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.space.collapse.space + desc: Space characters are converted to U+0020, and collapsed (per CSS) + testing: + - 2d.text.draw.spaces + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText('E EE', -100, 37.5); + @assert pixel 25,25 ==~ 0,255,0,255; @moz-todo + @assert pixel 75,25 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.space.collapse.other + desc: Space characters are converted to U+0020, and collapsed (per CSS) + testing: + - 2d.text.draw.spaces + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText('E \x09\x0a\x0c\x0d \x09\x0a\x0c\x0dEE', -100, 37.5); + @assert pixel 25,25 ==~ 0,255,0,255; @moz-todo + @assert pixel 75,25 ==~ 0,255,0,255; @moz-todo + }), 500); + expected: green + +- name: 2d.text.draw.space.collapse.start + desc: Space characters at the start of a line are collapsed (per CSS) + testing: + - 2d.text.draw.spaces + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText(' EE', 0, 37.5); + @assert pixel 25,25 ==~ 0,255,0,255; @moz-todo + @assert pixel 75,25 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.space.collapse.end + desc: Space characters at the end of a line are collapsed (per CSS) + testing: + - 2d.text.draw.spaces + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'right'; + ctx.fillText('EE ', 100, 37.5); + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; @moz-todo + }), 500); + expected: green + + +- name: 2d.text.measure.width.space + desc: Space characters are converted to U+0020 and collapsed (per CSS) + testing: + - 2d.text.measure.spaces + fonts: + - CanvasTest + code: | + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + @assert ctx.measureText('A B').width === 150; + @assert ctx.measureText('A B').width === 200; + @assert ctx.measureText('A \x09\x0a\x0c\x0d \x09\x0a\x0c\x0dB').width === 150; @moz-todo + @assert ctx.measureText('A \x0b B').width >= 200; + + @assert ctx.measureText(' AB').width === 100; @moz-todo + @assert ctx.measureText('AB ').width === 100; @moz-todo + }), 500); + }); + +- name: 2d.text.measure.rtl.text + desc: Measurement should follow canvas direction instead text direction + testing: + - 2d.text.measure.rtl.text + fonts: + - CanvasTest + code: | + metrics = ctx.measureText('اَلْعَرَبِيَّةُ'); + @assert metrics.actualBoundingBoxLeft < metrics.actualBoundingBoxRight; + + metrics = ctx.measureText('hello'); + @assert metrics.actualBoundingBoxLeft < metrics.actualBoundingBoxRight; + +- name: 2d.text.measure.boundingBox.textAlign + desc: Measurement should be related to textAlignment + testing: + - 2d.text.measure.boundingBox.textAlign + code: | + ctx.textAlign = "right"; + metrics = ctx.measureText('hello'); + @assert metrics.actualBoundingBoxLeft > metrics.actualBoundingBoxRight; + + ctx.textAlign = "left" + metrics = ctx.measureText('hello'); + @assert metrics.actualBoundingBoxLeft < metrics.actualBoundingBoxRight; + +- name: 2d.text.measure.boundingBox.direction + desc: Measurement should follow text direction + testing: + - 2d.text.measure.boundingBox.direction + code: | + ctx.direction = "ltr"; + metrics = ctx.measureText('hello'); + @assert metrics.actualBoundingBoxLeft < metrics.actualBoundingBoxRight; + + ctx.direction = "rtl"; + metrics = ctx.measureText('hello'); + @assert metrics.actualBoundingBoxLeft > metrics.actualBoundingBoxRight; diff --git a/test/wpt/the-canvas-element.yaml b/test/wpt/the-canvas-element.yaml new file mode 100644 index 000000000..5abee0300 --- /dev/null +++ b/test/wpt/the-canvas-element.yaml @@ -0,0 +1,169 @@ +- name: 2d.getcontext.exists + desc: The 2D context is implemented + testing: + - context.2d + code: | + @assert canvas.getContext('2d') !== null; + +- name: 2d.getcontext.invalid.args + desc: Calling getContext with invalid arguments. + testing: + - context.2d + code: | + @assert canvas.getContext('') === null; + @assert canvas.getContext('2d#') === null; + @assert canvas.getContext('This is clearly not a valid context name.') === null; + @assert canvas.getContext('2d\0') === null; + @assert canvas.getContext('2\uFF44') === null; + @assert canvas.getContext('2D') === null; + @assert throws TypeError canvas.getContext(); + @assert canvas.getContext('null') === null; + @assert canvas.getContext('undefined') === null; + +- name: 2d.getcontext.extraargs.create + desc: The 2D context doesn't throw with extra getContext arguments (new context) + testing: + - context.2d.extraargs + code: | + @assert document.createElement("canvas").getContext('2d', false, {}, [], 1, "2") !== null; + @assert document.createElement("canvas").getContext('2d', 123) !== null; + @assert document.createElement("canvas").getContext('2d', "test") !== null; + @assert document.createElement("canvas").getContext('2d', undefined) !== null; + @assert document.createElement("canvas").getContext('2d', null) !== null; + @assert document.createElement("canvas").getContext('2d', Symbol.hasInstance) !== null; + +- name: 2d.getcontext.extraargs.cache + desc: The 2D context doesn't throw with extra getContext arguments (cached) + testing: + - context.2d.extraargs + code: | + @assert canvas.getContext('2d', false, {}, [], 1, "2") !== null; + @assert canvas.getContext('2d', 123) !== null; + @assert canvas.getContext('2d', "test") !== null; + @assert canvas.getContext('2d', undefined) !== null; + @assert canvas.getContext('2d', null) !== null; + @assert canvas.getContext('2d', Symbol.hasInstance) !== null; + +- name: 2d.type.exists + desc: The 2D context interface is a property of 'window' + notes: &bindings Defined in "Web IDL" (draft) + testing: + - context.2d.type + code: | + @assert window.CanvasRenderingContext2D; + +- name: 2d.type.prototype + desc: window.CanvasRenderingContext2D.prototype are not [[Writable]] and not [[Configurable]], + and its methods are [[Configurable]]. + notes: *bindings + testing: + - context.2d.type + code: | + @assert window.CanvasRenderingContext2D.prototype; + @assert window.CanvasRenderingContext2D.prototype.fill; + window.CanvasRenderingContext2D.prototype = null; + @assert window.CanvasRenderingContext2D.prototype; + delete window.CanvasRenderingContext2D.prototype; + @assert window.CanvasRenderingContext2D.prototype; + window.CanvasRenderingContext2D.prototype.fill = 1; + @assert window.CanvasRenderingContext2D.prototype.fill === 1; + delete window.CanvasRenderingContext2D.prototype.fill; + @assert window.CanvasRenderingContext2D.prototype.fill === undefined; + +- name: 2d.type.replace + desc: Interface methods can be overridden + notes: *bindings + testing: + - context.2d.type + code: | + var fillRect = window.CanvasRenderingContext2D.prototype.fillRect; + window.CanvasRenderingContext2D.prototype.fillRect = function (x, y, w, h) + { + this.fillStyle = '#0f0'; + fillRect.call(this, x, y, w, h); + }; + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.type.extend + desc: Interface methods can be added + notes: *bindings + testing: + - context.2d.type + code: | + window.CanvasRenderingContext2D.prototype.fillRectGreen = function (x, y, w, h) + { + this.fillStyle = '#0f0'; + this.fillRect(x, y, w, h); + }; + ctx.fillStyle = '#f00'; + ctx.fillRectGreen(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.getcontext.unique + desc: getContext('2d') returns the same object + testing: + - context.unique + code: | + @assert canvas.getContext('2d') === canvas.getContext('2d'); + +- name: 2d.getcontext.shared + desc: getContext('2d') returns objects which share canvas state + testing: + - context.unique + code: | + var ctx2 = canvas.getContext('2d'); + ctx.fillStyle = '#f00'; + ctx2.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.scaled + desc: CSS-scaled canvases get drawn correctly + canvas: 'width="50" height="25" style="width: 100px; height: 50px"' + manual: + code: | + ctx.fillStyle = '#00f'; + ctx.fillRect(0, 0, 50, 25); + ctx.fillStyle = '#0ff'; + ctx.fillRect(0, 0, 25, 10); + expected: | + size 100 50 + cr.set_source_rgb(0, 0, 1) + cr.rectangle(0, 0, 100, 50) + cr.fill() + cr.set_source_rgb(0, 1, 1) + cr.rectangle(0, 0, 50, 20) + cr.fill() + +- name: 2d.canvas.reference + desc: CanvasRenderingContext2D.canvas refers back to its canvas + testing: + - 2d.canvas + code: | + @assert ctx.canvas === canvas; + +- name: 2d.canvas.readonly + desc: CanvasRenderingContext2D.canvas is readonly + testing: + - 2d.canvas.attribute + code: | + var c = document.createElement('canvas'); + var d = ctx.canvas; + @assert c !== d; + ctx.canvas = c; + @assert ctx.canvas === d; + +- name: 2d.canvas.context + desc: checks CanvasRenderingContext2D prototype + testing: + - 2d.path.contexttypexxx.basic + code: | + @assert Object.getPrototypeOf(CanvasRenderingContext2D.prototype) === Object.prototype; + @assert Object.getPrototypeOf(ctx) === CanvasRenderingContext2D.prototype; + t.done(); + diff --git a/test/wpt/the-canvas-state.yaml b/test/wpt/the-canvas-state.yaml new file mode 100644 index 000000000..dda6dc314 --- /dev/null +++ b/test/wpt/the-canvas-state.yaml @@ -0,0 +1,107 @@ +- name: 2d.state.saverestore.transformation + desc: save()/restore() affects the current transformation matrix + testing: + - 2d.state.transformation + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.save(); + ctx.translate(200, 0); + ctx.restore(); + ctx.fillStyle = '#f00'; + ctx.fillRect(-200, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.state.saverestore.clip + desc: save()/restore() affects the clipping path + testing: + - 2d.state.clip + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.save(); + ctx.rect(0, 0, 1, 1); + ctx.clip(); + ctx.restore(); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.state.saverestore.path + desc: save()/restore() does not affect the current path + testing: + - 2d.state.path + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.save(); + ctx.rect(0, 0, 100, 50); + ctx.restore(); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.state.saverestore.bitmap + desc: save()/restore() does not affect the current bitmap + testing: + - 2d.state.bitmap + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.save(); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.restore(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.state.saverestore.stack + desc: save()/restore() can be nested as a stack + testing: + - 2d.state.save + - 2d.state.restore + code: | + ctx.lineWidth = 1; + ctx.save(); + ctx.lineWidth = 2; + ctx.save(); + ctx.lineWidth = 3; + @assert ctx.lineWidth === 3; + ctx.restore(); + @assert ctx.lineWidth === 2; + ctx.restore(); + @assert ctx.lineWidth === 1; + +- name: 2d.state.saverestore.stackdepth + desc: save()/restore() stack depth is not unreasonably limited + testing: + - 2d.state.save + - 2d.state.restore + code: | + var limit = 512; + for (var i = 1; i < limit; ++i) + { + ctx.save(); + ctx.lineWidth = i; + } + for (var i = limit-1; i > 0; --i) + { + @assert ctx.lineWidth === i; + ctx.restore(); + } + +- name: 2d.state.saverestore.underflow + desc: restore() with an empty stack has no effect + testing: + - 2d.state.restore.underflow + code: | + for (var i = 0; i < 16; ++i) + ctx.restore(); + ctx.lineWidth = 0.5; + ctx.restore(); + @assert ctx.lineWidth === 0.5; + + diff --git a/test/wpt/transformations.yaml b/test/wpt/transformations.yaml new file mode 100644 index 000000000..b6aaec73c --- /dev/null +++ b/test/wpt/transformations.yaml @@ -0,0 +1,402 @@ +- name: 2d.transformation.order + desc: Transformations are applied in the right order + testing: + - 2d.transformation.order + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.scale(2, 1); + ctx.rotate(Math.PI / 2); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, -50, 50, 50); + @assert pixel 75,25 == 0,255,0,255; + expected: green + + +- name: 2d.transformation.scale.basic + desc: scale() works + testing: + - 2d.transformation.scale + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.scale(2, 4); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 12.5); + @assert pixel 90,40 == 0,255,0,255; + expected: green + +- name: 2d.transformation.scale.zero + desc: scale() with a scale factor of zero works + testing: + - 2d.transformation.scale + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.save(); + ctx.translate(50, 0); + ctx.scale(0, 1); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.restore(); + + ctx.save(); + ctx.translate(0, 25); + ctx.scale(1, 0); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.restore(); + + canvas.toDataURL(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.transformation.scale.negative + desc: scale() with negative scale factors works + testing: + - 2d.transformation.scale + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.save(); + ctx.scale(-1, 1); + ctx.fillStyle = '#0f0'; + ctx.fillRect(-50, 0, 50, 50); + ctx.restore(); + + ctx.save(); + ctx.scale(1, -1); + ctx.fillStyle = '#0f0'; + ctx.fillRect(50, -50, 50, 50); + ctx.restore(); + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.transformation.scale.large + desc: scale() with large scale factors works + notes: Not really that large at all, but it hits the limits in Firefox. + testing: + - 2d.transformation.scale + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.scale(1e5, 1e5); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 1, 1); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.transformation.scale.nonfinite + desc: scale() with Infinity/NaN is ignored + testing: + - 2d.nonfinite + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(100, 10); + @nonfinite ctx.scale(<0.1 Infinity -Infinity NaN>, <0.1 Infinity -Infinity NaN>); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -10, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.transformation.scale.multiple + desc: Multiple scale()s combine + testing: + - 2d.transformation.scale.multiple + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.scale(Math.sqrt(2), Math.sqrt(2)); + ctx.scale(Math.sqrt(2), Math.sqrt(2)); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 25); + @assert pixel 90,40 == 0,255,0,255; + expected: green + + +- name: 2d.transformation.rotate.zero + desc: rotate() by 0 does nothing + testing: + - 2d.transformation.rotate + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.rotate(0); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.transformation.rotate.radians + desc: rotate() uses radians + testing: + - 2d.transformation.rotate.radians + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.rotate(Math.PI); // should fail obviously if this is 3.1 degrees + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -50, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.transformation.rotate.direction + desc: rotate() is clockwise + testing: + - 2d.transformation.rotate.direction + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.rotate(Math.PI / 2); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, -100, 50, 100); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.transformation.rotate.wrap + desc: rotate() wraps large positive values correctly + testing: + - 2d.transformation.rotate + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.rotate(Math.PI * (1 + 4096)); // == pi (mod 2*pi) + // We need about pi +/- 0.001 in order to get correct-looking results + // 32-bit floats can store pi*4097 with precision 2^-10, so that should + // be safe enough on reasonable implementations + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -50, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,2 == 0,255,0,255; + @assert pixel 98,47 == 0,255,0,255; + expected: green + +- name: 2d.transformation.rotate.wrapnegative + desc: rotate() wraps large negative values correctly + testing: + - 2d.transformation.rotate + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.rotate(-Math.PI * (1 + 4096)); + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -50, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,2 == 0,255,0,255; + @assert pixel 98,47 == 0,255,0,255; + expected: green + +- name: 2d.transformation.rotate.nonfinite + desc: rotate() with Infinity/NaN is ignored + testing: + - 2d.nonfinite + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(100, 10); + @nonfinite ctx.rotate(<0.1 Infinity -Infinity NaN>); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -10, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.transformation.translate.basic + desc: translate() works + testing: + - 2d.transformation.translate + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -50, 100, 50); + @assert pixel 90,40 == 0,255,0,255; + expected: green + +- name: 2d.transformation.translate.nonfinite + desc: translate() with Infinity/NaN is ignored + testing: + - 2d.nonfinite + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(100, 10); + @nonfinite ctx.translate(<0.1 Infinity -Infinity NaN>, <0.1 Infinity -Infinity NaN>); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -10, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + + +- name: 2d.transformation.transform.identity + desc: transform() with the identity matrix does nothing + testing: + - 2d.transformation.transform + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.transform(1,0, 0,1, 0,0); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.transformation.transform.skewed + desc: transform() with skewy matrix transforms correctly + testing: + - 2d.transformation.transform + code: | + // Create green with a red square ring inside it + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(20, 10, 60, 30); + ctx.fillStyle = '#0f0'; + ctx.fillRect(40, 20, 20, 10); + + // Draw a skewed shape to fill that gap, to make sure it is aligned correctly + ctx.transform(1,4, 2,3, 5,6); + // Post-transform coordinates: + // [[20,10],[80,10],[80,40],[20,40],[20,10],[40,20],[40,30],[60,30],[60,20],[40,20],[20,10]]; + // Hence pre-transform coordinates: + var pts=[[-7.4,11.2],[-43.4,59.2],[-31.4,53.2],[4.6,5.2],[-7.4,11.2], + [-15.4,25.2],[-11.4,23.2],[-23.4,39.2],[-27.4,41.2],[-15.4,25.2], + [-7.4,11.2]]; + ctx.beginPath(); + ctx.moveTo(pts[0][0], pts[0][1]); + for (var i = 0; i < pts.length; ++i) + ctx.lineTo(pts[i][0], pts[i][1]); + ctx.fill(); + @assert pixel 21,11 == 0,255,0,255; + @assert pixel 79,11 == 0,255,0,255; + @assert pixel 21,39 == 0,255,0,255; + @assert pixel 79,39 == 0,255,0,255; + @assert pixel 39,19 == 0,255,0,255; + @assert pixel 61,19 == 0,255,0,255; + @assert pixel 39,31 == 0,255,0,255; + @assert pixel 61,31 == 0,255,0,255; + expected: green + +- name: 2d.transformation.transform.multiply + desc: transform() multiplies the CTM + testing: + - 2d.transformation.transform.multiply + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.transform(1,2, 3,4, 5,6); + ctx.transform(-2,1, 3/2,-1/2, 1,-2); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.transformation.transform.nonfinite + desc: transform() with Infinity/NaN is ignored + testing: + - 2d.nonfinite + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(100, 10); + @nonfinite ctx.transform(<0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -10, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.transformation.setTransform.skewed + testing: + - 2d.transformation.setTransform + code: | + // Create green with a red square ring inside it + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(20, 10, 60, 30); + ctx.fillStyle = '#0f0'; + ctx.fillRect(40, 20, 20, 10); + + // Draw a skewed shape to fill that gap, to make sure it is aligned correctly + ctx.setTransform(1,4, 2,3, 5,6); + // Post-transform coordinates: + // [[20,10],[80,10],[80,40],[20,40],[20,10],[40,20],[40,30],[60,30],[60,20],[40,20],[20,10]]; + // Hence pre-transform coordinates: + var pts=[[-7.4,11.2],[-43.4,59.2],[-31.4,53.2],[4.6,5.2],[-7.4,11.2], + [-15.4,25.2],[-11.4,23.2],[-23.4,39.2],[-27.4,41.2],[-15.4,25.2], + [-7.4,11.2]]; + ctx.beginPath(); + ctx.moveTo(pts[0][0], pts[0][1]); + for (var i = 0; i < pts.length; ++i) + ctx.lineTo(pts[i][0], pts[i][1]); + ctx.fill(); + @assert pixel 21,11 == 0,255,0,255; + @assert pixel 79,11 == 0,255,0,255; + @assert pixel 21,39 == 0,255,0,255; + @assert pixel 79,39 == 0,255,0,255; + @assert pixel 39,19 == 0,255,0,255; + @assert pixel 61,19 == 0,255,0,255; + @assert pixel 39,31 == 0,255,0,255; + @assert pixel 61,31 == 0,255,0,255; + expected: green + +- name: 2d.transformation.setTransform.multiple + testing: + - 2d.transformation.setTransform.identity + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.setTransform(1/2,0, 0,1/2, 0,0); + ctx.setTransform(); + ctx.setTransform(2,0, 0,2, 0,0); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 25); + @assert pixel 75,35 == 0,255,0,255; + expected: green + +- name: 2d.transformation.setTransform.nonfinite + desc: setTransform() with Infinity/NaN is ignored + testing: + - 2d.nonfinite + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(100, 10); + @nonfinite ctx.setTransform(<0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -10, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green \ No newline at end of file From a484cf2d1807c67c580622370023f48f2cc00fb8 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Wed, 27 Jul 2022 13:59:27 -0700 Subject: [PATCH 369/474] fix crashes and hangs in arc() The WPT tests for this now pass. See issue for test content; I think it makes more sense to land the WPT tests than to copy individual ones into the node-canvas tests. Fixes #2055 --- CHANGELOG.md | 1 + src/CanvasRenderingContext2d.cc | 39 ++++++++++++++++----------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07322e45d..1f19144f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * `rgba(r,g,b)` with no alpha should parse as opaque, not transparent. ([#2029](https://github.com/Automattic/node-canvas/issues/2029)) * Typo in `PngConfig.filters` types. ([#2072](https://github.com/Automattic/node-canvas/issues/2072)) * `createPattern()` always used "repeat" mode; now supports "repeat-x" and "repeat-y". ([#2066](https://github.com/Automattic/node-canvas/issues/2066)) +* Crashes and hangs when using non-finite values in `context.arc()`. ([#2055](https://github.com/Automattic/node-canvas/issues/2055)) 2.9.3 ================== diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 2bd0533f5..2264ea6fd 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -2936,35 +2936,34 @@ NAN_METHOD(Context2d::Rect) { } /* - * Adds an arc at x, y with the given radis and start/end angles. + * Adds an arc at x, y with the given radii and start/end angles. */ NAN_METHOD(Context2d::Arc) { - if (!info[0]->IsNumber() - || !info[1]->IsNumber() - || !info[2]->IsNumber() - || !info[3]->IsNumber() - || !info[4]->IsNumber()) return; + double args[5]; + if(!checkArgs(info, args, 5)) + return; - bool anticlockwise = Nan::To(info[5]).FromMaybe(false); + auto x = args[0]; + auto y = args[1]; + auto radius = args[2]; + auto startAngle = args[3]; + auto endAngle = args[4]; + + if (radius < 0) { + Nan::ThrowRangeError("The radius provided is negative."); + return; + } + + bool counterclockwise = Nan::To(info[5]).FromMaybe(false); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); - if (anticlockwise && M_PI * 2 != Nan::To(info[4]).FromMaybe(0)) { - cairo_arc_negative(ctx - , Nan::To(info[0]).FromMaybe(0) - , Nan::To(info[1]).FromMaybe(0) - , Nan::To(info[2]).FromMaybe(0) - , Nan::To(info[3]).FromMaybe(0) - , Nan::To(info[4]).FromMaybe(0)); + if (counterclockwise && M_PI * 2 != endAngle) { + cairo_arc_negative(ctx, x, y, radius, startAngle, endAngle); } else { - cairo_arc(ctx - , Nan::To(info[0]).FromMaybe(0) - , Nan::To(info[1]).FromMaybe(0) - , Nan::To(info[2]).FromMaybe(0) - , Nan::To(info[3]).FromMaybe(0) - , Nan::To(info[4]).FromMaybe(0)); + cairo_arc(ctx, x, y, radius, startAngle, endAngle); } } From 73d7893ccb44158f53d007f1918a6d9228d8137e Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Wed, 27 Jul 2022 14:32:35 -0700 Subject: [PATCH 370/474] fix arc geometry calculations Borrowed from Chromium instead of reinventing the wheel. Firefox's is similar: https://searchfox.org/mozilla-central/source/gfx/2d/PathHelpers.h#127 Fixes #1736 Fixes #1808 --- CHANGELOG.md | 1 + src/CanvasRenderingContext2d.cc | 53 ++++++++++++++++++++++++++++++++- test/public/tests.js | 29 ++++++++++++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f19144f6..6c354c6bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * Typo in `PngConfig.filters` types. ([#2072](https://github.com/Automattic/node-canvas/issues/2072)) * `createPattern()` always used "repeat" mode; now supports "repeat-x" and "repeat-y". ([#2066](https://github.com/Automattic/node-canvas/issues/2066)) * Crashes and hangs when using non-finite values in `context.arc()`. ([#2055](https://github.com/Automattic/node-canvas/issues/2055)) +* Incorrect `context.arc()` geometry logic for full ellipses. ([#1808](https://github.com/Automattic/node-canvas/issues/1808), ([#1736](https://github.com/Automattic/node-canvas/issues/1736))) 2.9.3 ================== diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 2264ea6fd..831f47652 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -36,6 +36,8 @@ Nan::Persistent Context2d::constructor; double width = args[2]; \ double height = args[3]; +constexpr double twoPi = M_PI * 2.; + /* * Text baselines. */ @@ -2935,6 +2937,52 @@ NAN_METHOD(Context2d::Rect) { } } +// Adapted from https://chromium.googlesource.com/chromium/blink/+/refs/heads/main/Source/modules/canvas2d/CanvasPathMethods.cpp +static void canonicalizeAngle(double& startAngle, double& endAngle) { + // Make 0 <= startAngle < 2*PI + double newStartAngle = std::fmod(startAngle, twoPi); + if (newStartAngle < 0) { + newStartAngle += twoPi; + // Check for possible catastrophic cancellation in cases where + // newStartAngle was a tiny negative number (c.f. crbug.com/503422) + if (newStartAngle >= twoPi) + newStartAngle -= twoPi; + } + double delta = newStartAngle - startAngle; + startAngle = newStartAngle; + endAngle = endAngle + delta; +} + +// Adapted from https://chromium.googlesource.com/chromium/blink/+/refs/heads/main/Source/modules/canvas2d/CanvasPathMethods.cpp +static double adjustEndAngle(double startAngle, double endAngle, bool counterclockwise) { + double newEndAngle = endAngle; + /* http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-arc + * If the counterclockwise argument is false and endAngle-startAngle is equal to or greater than 2pi, or, + * if the counterclockwise argument is true and startAngle-endAngle is equal to or greater than 2pi, + * then the arc is the whole circumference of this ellipse, and the point at startAngle along this circle's circumference, + * measured in radians clockwise from the ellipse's semi-major axis, acts as both the start point and the end point. + */ + if (!counterclockwise && endAngle - startAngle >= twoPi) + newEndAngle = startAngle + twoPi; + else if (counterclockwise && startAngle - endAngle >= twoPi) + newEndAngle = startAngle - twoPi; + /* + * Otherwise, the arc is the path along the circumference of this ellipse from the start point to the end point, + * going anti-clockwise if the counterclockwise argument is true, and clockwise otherwise. + * Since the points are on the ellipse, as opposed to being simply angles from zero, + * the arc can never cover an angle greater than 2pi radians. + */ + /* NOTE: When startAngle = 0, endAngle = 2Pi and counterclockwise = true, the spec does not indicate clearly. + * We draw the entire circle, because some web sites use arc(x, y, r, 0, 2*Math.PI, true) to draw circle. + * We preserve backward-compatibility. + */ + else if (!counterclockwise && startAngle > endAngle) + newEndAngle = startAngle + (twoPi - std::fmod(startAngle - endAngle, twoPi)); + else if (counterclockwise && startAngle < endAngle) + newEndAngle = startAngle - (twoPi - std::fmod(endAngle - startAngle, twoPi)); + return newEndAngle; +} + /* * Adds an arc at x, y with the given radii and start/end angles. */ @@ -2960,7 +3008,10 @@ NAN_METHOD(Context2d::Arc) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); - if (counterclockwise && M_PI * 2 != endAngle) { + canonicalizeAngle(startAngle, endAngle); + endAngle = adjustEndAngle(startAngle, endAngle, counterclockwise); + + if (counterclockwise) { cairo_arc_negative(ctx, x, y, radius, startAngle, endAngle); } else { cairo_arc(ctx, x, y, radius, startAngle, endAngle); diff --git a/test/public/tests.js b/test/public/tests.js index e079ad827..c852fcaed 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -146,6 +146,35 @@ tests['arc() 2'] = function (ctx) { } } +tests['arc()() #1736'] = function (ctx) { + let centerX = 512 + let centerY = 512 + let startAngle = 6.283185307179586 // exactly 2pi + let endAngle = 7.5398223686155035 + let innerRadius = 359.67999999999995 + let outerRadius = 368.64 + + ctx.scale(0.2, 0.2) + + ctx.beginPath() + ctx.moveTo(centerX + Math.cos(startAngle) * innerRadius, centerY + Math.sin(startAngle) * innerRadius) + ctx.lineTo(centerX + Math.cos(startAngle) * outerRadius, centerY + Math.sin(startAngle) * outerRadius) + ctx.arc(centerX, centerY, outerRadius, startAngle, endAngle, false) + ctx.lineTo(centerX + Math.cos(endAngle) * innerRadius, centerY + Math.sin(endAngle) * innerRadius) + ctx.arc(centerX, centerY, innerRadius, endAngle, startAngle, true) + ctx.closePath() + ctx.stroke() +} + +tests['arc()() #1808'] = function (ctx) { + ctx.scale(0.5, 0.5) + ctx.beginPath() + ctx.arc(256, 256, 50, 0, 2 * Math.PI, true) + ctx.arc(256, 256, 25, 0, 2 * Math.PI, false) + ctx.closePath() + ctx.fill() +} + tests['arcTo()'] = function (ctx) { ctx.fillStyle = '#08C8EE' ctx.translate(-50, -50) From 10b208e3594ba461b1e9f29798b8c2e38a5953ad Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Fri, 5 Aug 2022 03:38:26 -0700 Subject: [PATCH 371/474] un-skip 2d.path.arc.nonfinite; now fixed --- test/wpt/generate.js | 1 - test/wpt/generated/path-objects.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/test/wpt/generate.js b/test/wpt/generate.js index 4921d2277..74fbcb623 100644 --- a/test/wpt/generate.js +++ b/test/wpt/generate.js @@ -9,7 +9,6 @@ const yamlFiles = fs.readdirSync(__dirname).filter(f => f.endsWith(".yaml")); const SKIP_FILES = new Set("meta.yaml"); // Tests that should be skipped (e.g. because they cause hangs or V8 crashes): const SKIP_TESTS = new Set([ - "2d.path.arc.nonfinite", // https://github.com/Automattic/node-canvas/issues/2055 "2d.imageData.create2.negative", "2d.imageData.create2.zero", "2d.imageData.create2.nonfinite", diff --git a/test/wpt/generated/path-objects.js b/test/wpt/generated/path-objects.js index d01c89072..397ec76f6 100644 --- a/test/wpt/generated/path-objects.js +++ b/test/wpt/generated/path-objects.js @@ -1614,7 +1614,7 @@ describe("WPT: path-objects", function () { _assertPixel(canvas, 98,48, 0,255,0,255); }); - it.skip("2d.path.arc.nonfinite", function () { + it("2d.path.arc.nonfinite", function () { // arc() with Infinity/NaN is ignored const canvas = createCanvas(100, 50); const ctx = canvas.getContext("2d"); From eba1e4a7452cebddf9b3c4f5d6ff1b423c0562b5 Mon Sep 17 00:00:00 2001 From: Steve Rubin Date: Fri, 19 Aug 2022 15:26:09 -0500 Subject: [PATCH 372/474] Adds deregisterAllFonts to the typescript declaration file (#2096) * Adds deregisterAllFonts to the typescript declaration file * updates changelog with deregisterAllFonts type fix --- CHANGELOG.md | 1 + types/index.d.ts | 5 +++++ types/test.ts | 5 +++++ 3 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c354c6bb..bd53dde00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * `createPattern()` always used "repeat" mode; now supports "repeat-x" and "repeat-y". ([#2066](https://github.com/Automattic/node-canvas/issues/2066)) * Crashes and hangs when using non-finite values in `context.arc()`. ([#2055](https://github.com/Automattic/node-canvas/issues/2055)) * Incorrect `context.arc()` geometry logic for full ellipses. ([#1808](https://github.com/Automattic/node-canvas/issues/1808), ([#1736](https://github.com/Automattic/node-canvas/issues/1736))) +* Added missing `deregisterAllFonts` to the Typescript declaration file ([#2096](https://github.com/Automattic/node-canvas/pull/2096)) 2.9.3 ================== diff --git a/types/index.d.ts b/types/index.d.ts index 04691c4ed..7b53f4851 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -300,6 +300,11 @@ export function loadImage(src: string|Buffer, options?: any): Promise */ export function registerFont(path: string, fontFace: {family: string, weight?: string, style?: string}): void +/** + * Unloads all fonts + */ +export function deregisterAllFonts(): void; + /** This class must not be constructed directly; use `canvas.createPNGStream()`. */ export class PNGStream extends Readable {} /** This class must not be constructed directly; use `canvas.createJPEGStream()`. */ diff --git a/types/test.ts b/types/test.ts index bfbe26429..b48c78011 100644 --- a/types/test.ts +++ b/types/test.ts @@ -1,4 +1,7 @@ import * as Canvas from 'canvas' +import * as path from "path"; + +Canvas.registerFont(path.join(__dirname, '../pfennigFont/Pfennig.ttf'), {family: 'pfennigFont'}) Canvas.createCanvas(5, 10) Canvas.createCanvas(200, 200, 'pdf') @@ -39,3 +42,5 @@ img.onload = null; const id2: Canvas.ImageData = Canvas.createImageData(new Uint16Array(4), 1) ctx.drawImage(canv, 0, 0) + +Canvas.deregisterAllFonts(); From dce0fd166c387e562113a1c57b959dc4337e6682 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Fri, 5 Aug 2022 13:03:06 -0700 Subject: [PATCH 373/474] Add roundRect() support https://developer.chrome.com/blog/canvas2d/#round-rect WPT tests: 326 passing (1s) 9 pending 129 failing (down from 179) --- CHANGELOG.md | 1 + binding.gyp | 3 +- src/CanvasRenderingContext2d.cc | 174 ++++++++++++++++++++++++++++++++ src/CanvasRenderingContext2d.h | 1 + src/Point.h | 5 +- test/public/tests.js | 37 +++++++ 6 files changed, 218 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd53dde00..b6662cc62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed * Export `pangoVersion` ### Added +* [`ctx.roundRect()`](https://developer.chrome.com/blog/canvas2d/#round-rect) ### Fixed * `rgba(r,g,b)` with no alpha should parse as opaque, not transparent. ([#2029](https://github.com/Automattic/node-canvas/issues/2029)) * Typo in `PngConfig.filters` types. ([#2072](https://github.com/Automattic/node-canvas/issues/2072)) diff --git a/binding.gyp b/binding.gyp index 57f14ab8c..19a33e816 100644 --- a/binding.gyp +++ b/binding.gyp @@ -96,7 +96,8 @@ '<(GTK_Root)/lib/glib-2.0/include' ], 'defines': [ - '_USE_MATH_DEFINES' # for M_PI + '_USE_MATH_DEFINES', # for M_PI + 'NOMINMAX' # allow std::min/max to work ], 'configurations': { 'Debug': { diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 831f47652..10629cee7 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -126,6 +126,7 @@ Context2d::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { Nan::SetPrototypeMethod(ctor, "strokeRect", StrokeRect); Nan::SetPrototypeMethod(ctor, "clearRect", ClearRect); Nan::SetPrototypeMethod(ctor, "rect", Rect); + Nan::SetPrototypeMethod(ctor, "roundRect", RoundRect); Nan::SetPrototypeMethod(ctor, "measureText", MeasureText); Nan::SetPrototypeMethod(ctor, "moveTo", MoveTo); Nan::SetPrototypeMethod(ctor, "lineTo", LineTo); @@ -2937,6 +2938,179 @@ NAN_METHOD(Context2d::Rect) { } } +// Draws an arc with two potentially different radii. +inline static +void elli_arc(cairo_t* ctx, double xc, double yc, double rx, double ry, double a1, double a2, bool clockwise=true) { + if (rx == 0. || ry == 0.) { + cairo_line_to(ctx, xc + rx, yc + ry); + } else { + cairo_save(ctx); + cairo_translate(ctx, xc, yc); + cairo_scale(ctx, rx, ry); + if (clockwise) + cairo_arc(ctx, 0., 0., 1., a1, a2); + else + cairo_arc_negative(ctx, 0., 0., 1., a2, a1); + cairo_restore(ctx); + } +} + +inline static +bool getRadius(Point& p, const Local& v) { + if (v->IsObject()) { // 5.1 DOMPointInit + auto rx = Nan::Get(v.As(), Nan::New("x").ToLocalChecked()).ToLocalChecked(); + auto ry = Nan::Get(v.As(), Nan::New("y").ToLocalChecked()).ToLocalChecked(); + if (rx->IsNumber() && ry->IsNumber()) { + auto rxv = Nan::To(rx).FromJust(); + auto ryv = Nan::To(ry).FromJust(); + if (!std::isfinite(rxv) || !std::isfinite(ryv)) + return true; + if (rxv < 0 || ryv < 0) { + Nan::ThrowRangeError("radii must be positive."); + return true; + } + p.x = rxv; + p.y = ryv; + return false; + } + } else if (v->IsNumber()) { // 5.2 unrestricted double + auto rv = Nan::To(v).FromJust(); + if (!std::isfinite(rv)) + return true; + if (rv < 0) { + Nan::ThrowRangeError("radii must be positive."); + return true; + } + p.x = p.y = rv; + return false; + } + return true; +} + +/** + * https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-roundrect + * x, y, w, h, [radius|[radii]] + */ +NAN_METHOD(Context2d::RoundRect) { + RECT_ARGS; + Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); + cairo_t *ctx = context->context(); + + // 4. Let normalizedRadii be an empty list + Point normalizedRadii[4]; + size_t nRadii = 4; + + if (info[4]->IsUndefined()) { + for (size_t i = 0; i < 4; i++) + normalizedRadii[i].x = normalizedRadii[i].y = 0.; + + } else if (info[4]->IsArray()) { + auto radiiList = info[4].As(); + nRadii = radiiList->Length(); + if (!(nRadii >= 1 && nRadii <= 4)) { + Nan::ThrowRangeError("radii must be a list of one, two, three or four radii."); + return; + } + // 5. For each radius of radii + for (size_t i = 0; i < nRadii; i++) { + auto r = Nan::Get(radiiList, i).ToLocalChecked(); + if (getRadius(normalizedRadii[i], r)) + return; + } + + } else { + // 2. If radii is a double, then set radii to <> + if (getRadius(normalizedRadii[0], info[4])) + return; + for (size_t i = 1; i < 4; i++) { + normalizedRadii[i].x = normalizedRadii[0].x; + normalizedRadii[i].y = normalizedRadii[0].y; + } + } + + Point upperLeft, upperRight, lowerRight, lowerLeft; + if (nRadii == 4) { + upperLeft = normalizedRadii[0]; + upperRight = normalizedRadii[1]; + lowerRight = normalizedRadii[2]; + lowerLeft = normalizedRadii[3]; + } else if (nRadii == 3) { + upperLeft = normalizedRadii[0]; + upperRight = normalizedRadii[1]; + lowerLeft = normalizedRadii[1]; + lowerRight = normalizedRadii[2]; + } else if (nRadii == 2) { + upperLeft = normalizedRadii[0]; + lowerRight = normalizedRadii[0]; + upperRight = normalizedRadii[1]; + lowerLeft = normalizedRadii[1]; + } else { + upperLeft = normalizedRadii[0]; + upperRight = normalizedRadii[0]; + lowerRight = normalizedRadii[0]; + lowerLeft = normalizedRadii[0]; + } + + bool clockwise = true; + if (width < 0) { + clockwise = false; + x += width; + width = -width; + std::swap(upperLeft, upperRight); + std::swap(lowerLeft, lowerRight); + } + + if (height < 0) { + clockwise = !clockwise; + y += height; + height = -height; + std::swap(upperLeft, lowerLeft); + std::swap(upperRight, lowerRight); + } + + // 11. Corner curves must not overlap. Scale radii to prevent this. + { + auto top = upperLeft.x + upperRight.x; + auto right = upperRight.y + lowerRight.y; + auto bottom = lowerRight.x + lowerLeft.x; + auto left = upperLeft.y + lowerLeft.y; + auto scale = std::min({ width / top, height / right, width / bottom, height / left }); + if (scale < 1.) { + upperLeft.x *= scale; + upperLeft.y *= scale; + upperRight.x *= scale; + upperRight.x *= scale; + lowerLeft.y *= scale; + lowerLeft.y *= scale; + lowerRight.y *= scale; + lowerRight.y *= scale; + } + } + + // 12. Draw + cairo_move_to(ctx, x + upperLeft.x, y); + if (clockwise) { + cairo_line_to(ctx, x + width - upperRight.x, y); + elli_arc(ctx, x + width - upperRight.x, y + upperRight.y, upperRight.x, upperRight.y, 3. * M_PI / 2., 0.); + cairo_line_to(ctx, x + width, y + height - lowerRight.y); + elli_arc(ctx, x + width - lowerRight.x, y + height - lowerRight.y, lowerRight.x, lowerRight.y, 0, M_PI / 2.); + cairo_line_to(ctx, x + lowerLeft.x, y + height); + elli_arc(ctx, x + lowerLeft.x, y + height - lowerLeft.y, lowerLeft.x, lowerLeft.y, M_PI / 2., M_PI); + cairo_line_to(ctx, x, y + upperLeft.y); + elli_arc(ctx, x + upperLeft.x, y + upperLeft.y, upperLeft.x, upperLeft.y, M_PI, 3. * M_PI / 2.); + } else { + elli_arc(ctx, x + upperLeft.x, y + upperLeft.y, upperLeft.x, upperLeft.y, M_PI, 3. * M_PI / 2., false); + cairo_line_to(ctx, x, y + upperLeft.y); + elli_arc(ctx, x + lowerLeft.x, y + height - lowerLeft.y, lowerLeft.x, lowerLeft.y, M_PI / 2., M_PI, false); + cairo_line_to(ctx, x + lowerLeft.x, y + height); + elli_arc(ctx, x + width - lowerRight.x, y + height - lowerRight.y, lowerRight.x, lowerRight.y, 0, M_PI / 2., false); + cairo_line_to(ctx, x + width, y + height - lowerRight.y); + elli_arc(ctx, x + width - upperRight.x, y + upperRight.y, upperRight.x, upperRight.y, 3. * M_PI / 2., 0., false); + cairo_line_to(ctx, x + width - upperRight.x, y); + } + cairo_close_path(ctx); +} + // Adapted from https://chromium.googlesource.com/chromium/blink/+/refs/heads/main/Source/modules/canvas2d/CanvasPathMethods.cpp static void canonicalizeAngle(double& startAngle, double& endAngle) { // Make 0 <= startAngle < 2*PI diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index 8afb433d1..89c86df67 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -104,6 +104,7 @@ class Context2d: public Nan::ObjectWrap { static NAN_METHOD(StrokeRect); static NAN_METHOD(ClearRect); static NAN_METHOD(Rect); + static NAN_METHOD(RoundRect); static NAN_METHOD(Arc); static NAN_METHOD(ArcTo); static NAN_METHOD(Ellipse); diff --git a/src/Point.h b/src/Point.h index d3228acfa..50c7b711c 100644 --- a/src/Point.h +++ b/src/Point.h @@ -2,9 +2,10 @@ #pragma once -template +template class Point { public: T x, y; - Point(T x, T y): x(x), y(y) {} + Point(T x=0, T y=0): x(x), y(y) {} + Point(const Point&) = default; }; diff --git a/test/public/tests.js b/test/public/tests.js index c852fcaed..66bb14ddb 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -95,6 +95,43 @@ tests['fillRect()'] = function (ctx) { render(1) } +tests['roundRect()'] = function (ctx) { + if (!ctx.roundRect) { + ctx.textAlign = 'center' + ctx.fillText('roundRect() not supported', 100, 100, 190) + ctx.fillText('try Chrome instead', 100, 115, 190) + return + } + ctx.roundRect(5, 5, 60, 60, 20) + ctx.fillStyle = 'red' + ctx.fill() + + ctx.beginPath() + ctx.roundRect(5, 70, 60, 60, [10, 15, 20, 25]) + ctx.fillStyle = 'blue' + ctx.fill() + + ctx.beginPath() + ctx.roundRect(70, 5, 60, 60, [10]) + ctx.fillStyle = 'green' + ctx.fill() + + ctx.beginPath() + ctx.roundRect(70, 70, 60, 60, [10, 15]) + ctx.fillStyle = 'orange' + ctx.fill() + + ctx.beginPath() + ctx.roundRect(135, 5, 60, 60, [10, 15, 20]) + ctx.fillStyle = 'pink' + ctx.fill() + + ctx.beginPath() + ctx.roundRect(135, 70, 60, 60, [{ x: 30, y: 10 }, { x: 5, y: 20 }]) + ctx.fillStyle = 'darkseagreen' + ctx.fill() +} + tests['lineTo()'] = function (ctx) { // Filled triangle ctx.beginPath() From 3fb4ed9d7c460666daa26decc4784661b58c833c Mon Sep 17 00:00:00 2001 From: Antoine Apollis Date: Mon, 29 Aug 2022 16:54:49 +0200 Subject: [PATCH 374/474] fix: add user agent to remote images request --- CHANGELOG.md | 1 + lib/image.js | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6662cc62..886602912 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * Crashes and hangs when using non-finite values in `context.arc()`. ([#2055](https://github.com/Automattic/node-canvas/issues/2055)) * Incorrect `context.arc()` geometry logic for full ellipses. ([#1808](https://github.com/Automattic/node-canvas/issues/1808), ([#1736](https://github.com/Automattic/node-canvas/issues/1736))) * Added missing `deregisterAllFonts` to the Typescript declaration file ([#2096](https://github.com/Automattic/node-canvas/pull/2096)) +* Add `User-Agent` header when requesting remote images ([#2099](https://github.com/Automattic/node-canvas/issues/2099)) 2.9.3 ================== diff --git a/lib/image.js b/lib/image.js index c5b594f8a..4a37849ee 100644 --- a/lib/image.js +++ b/lib/image.js @@ -49,7 +49,10 @@ Object.defineProperty(Image.prototype, 'src', { if (!get) get = require('simple-get') - get.concat(val, (err, res, data) => { + get.concat({ + url: val, + headers: { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36' } + }, (err, res, data) => { if (err) return onerror(err) if (res.statusCode < 200 || res.statusCode >= 300) { From 561d933fe251c9c9ea28f715dccf496f08667c46 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 3 Sep 2022 19:52:26 -0700 Subject: [PATCH 375/474] v2.10.0 --- CHANGELOG.md | 7 ++++++- package.json | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 886602912..85b51d474 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,13 @@ project adheres to [Semantic Versioning](http://semver.org/). (Unreleased) ================== ### Changed -* Export `pangoVersion` ### Added +### Fixed + +2.10.0 +================== +### Added +* Export `pangoVersion` * [`ctx.roundRect()`](https://developer.chrome.com/blog/canvas2d/#round-rect) ### Fixed * `rgba(r,g,b)` with no alpha should parse as opaque, not transparent. ([#2029](https://github.com/Automattic/node-canvas/issues/2029)) diff --git a/package.json b/package.json index 5d4185d5e..72849d64d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.9.3", + "version": "2.10.0", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 6862532c593af0e86327ddb4c52341ee5bd0df54 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Tue, 6 Sep 2022 05:32:28 -0700 Subject: [PATCH 376/474] Fix actualBoundingBoxLeft/Right with center/right alignment (#2109) This bug goes back 10 years to the original implementation. Fixes #1909 --- CHANGELOG.md | 1 + src/CanvasRenderingContext2d.cc | 6 +++--- test/canvas.test.js | 37 ++++++++++++++++++++++++++++++--- test/public/tests.js | 14 ++++++++++++- 4 files changed, 51 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85b51d474..085dd412f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed +* Fix `actualBoundingBoxLeft` and `actualBoundingBoxRight` when `textAlign='center'` or `'right'` ([#1909](https://github.com/Automattic/node-canvas/issues/1909)) 2.10.0 ================== diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 10629cee7..60a86cb3b 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -2748,7 +2748,7 @@ NAN_METHOD(Context2d::MeasureText) { double x_offset; switch (context->state->textAlignment) { case 0: // center - x_offset = logical_rect.width / 2; + x_offset = logical_rect.width / 2.; break; case 1: // right x_offset = logical_rect.width; @@ -2766,10 +2766,10 @@ NAN_METHOD(Context2d::MeasureText) { Nan::New(logical_rect.width)).Check(); Nan::Set(obj, Nan::New("actualBoundingBoxLeft").ToLocalChecked(), - Nan::New(x_offset - PANGO_LBEARING(ink_rect))).Check(); + Nan::New(PANGO_LBEARING(ink_rect) + x_offset)).Check(); Nan::Set(obj, Nan::New("actualBoundingBoxRight").ToLocalChecked(), - Nan::New(x_offset + PANGO_RBEARING(ink_rect))).Check(); + Nan::New(PANGO_RBEARING(ink_rect) - x_offset)).Check(); Nan::Set(obj, Nan::New("actualBoundingBoxAscent").ToLocalChecked(), Nan::New(y_offset + PANGO_ASCENT(ink_rect))).Check(); diff --git a/test/canvas.test.js b/test/canvas.test.js index a81de892e..5abd45cb8 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -20,6 +20,11 @@ const { deregisterAllFonts } = require('../') +function assertApprox(actual, expected, tol) { + assert(Math.abs(expected - actual) <= tol, + "Expected " + actual + " to be " + expected + " +/- " + tol); +} + describe('Canvas', function () { // Run with --expose-gc and uncomment this line to help find memory problems: // afterEach(gc); @@ -946,20 +951,46 @@ describe('Canvas', function () { let metrics = ctx.measureText('Alphabet') // Actual value depends on font library version. Have observed values // between 0 and 0.769. - assert.ok(metrics.alphabeticBaseline >= 0 && metrics.alphabeticBaseline <= 1) + assertApprox(metrics.alphabeticBaseline, 0.5, 0.5) // Positive = going up from the baseline assert.ok(metrics.actualBoundingBoxAscent > 0) // Positive = going down from the baseline - assert.ok(metrics.actualBoundingBoxDescent > 0) // ~4-5 + assertApprox(metrics.actualBoundingBoxDescent, 5, 2) ctx.textBaseline = 'bottom' metrics = ctx.measureText('Alphabet') assert.strictEqual(ctx.textBaseline, 'bottom') - assert.ok(metrics.alphabeticBaseline > 0) // ~4-5 + assertApprox(metrics.alphabeticBaseline, 5, 2) assert.ok(metrics.actualBoundingBoxAscent > 0) // On the baseline or slightly above assert.ok(metrics.actualBoundingBoxDescent <= 0) }) + + it('actualBoundingBox is correct for left, center and right alignment (#1909)', function () { + const canvas = createCanvas(0, 0) + const ctx = canvas.getContext('2d') + + // positive actualBoundingBoxLeft indicates a distance going left from the + // given alignment point. + + // positive actualBoundingBoxRight indicates a distance going right from + // the given alignment point. + + ctx.textAlign = 'left' + const lm = ctx.measureText('aaaa') + assertApprox(lm.actualBoundingBoxLeft, -1, 6) + assertApprox(lm.actualBoundingBoxRight, 21, 6) + + ctx.textAlign = 'center' + const cm = ctx.measureText('aaaa') + assertApprox(cm.actualBoundingBoxLeft, 9, 6) + assertApprox(cm.actualBoundingBoxRight, 11, 6) + + ctx.textAlign = 'right' + const rm = ctx.measureText('aaaa') + assertApprox(rm.actualBoundingBoxLeft, 19, 6) + assertApprox(rm.actualBoundingBoxRight, 1, 6) + }) }) it('Context2d#fillText()', function () { diff --git a/test/public/tests.js b/test/public/tests.js index 66bb14ddb..651105e36 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -2658,7 +2658,8 @@ tests['measureText()'] = function (ctx) { const metrics = ctx.measureText(text) ctx.strokeStyle = 'blue' ctx.strokeRect( - x - metrics.actualBoundingBoxLeft + 0.5, + // positive numbers for actualBoundingBoxLeft indicate a distance going left + x + metrics.actualBoundingBoxLeft + 0.5, y - metrics.actualBoundingBoxAscent + 0.5, metrics.width, metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent @@ -2677,8 +2678,19 @@ tests['measureText()'] = function (ctx) { drawWithBBox('Alphabet bottom', 20, 90) ctx.textBaseline = 'alphabetic' + ctx.save() ctx.rotate(Math.PI / 8) drawWithBBox('Alphabet', 50, 100) + ctx.restore() + + ctx.textAlign = 'center' + drawWithBBox('Centered', 100, 195) + + ctx.textAlign = 'left' + drawWithBBox('Left', 10, 195) + + ctx.textAlign = 'right' + drawWithBBox('right', 195, 195) } tests['image sampling (#1084)'] = function (ctx, done) { From 93749430f49f506d4917129ed6cc3d7939b946f1 Mon Sep 17 00:00:00 2001 From: Sahel LUCAS--SAOUDI Date: Wed, 7 Sep 2022 18:24:55 +0200 Subject: [PATCH 377/474] Parse rgba(r,g,b,0) correctly --- .gitignore | 2 ++ src/color.cc | 5 ++++- test/canvas.test.js | 5 ++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index e5e14d5b6..ff66b1103 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ package-lock.json *.swp *.un~ npm-debug.log + +.idea diff --git a/src/color.cc b/src/color.cc index 1ea96e195..230fb8dbe 100644 --- a/src/color.cc +++ b/src/color.cc @@ -228,7 +228,10 @@ parse_clipped_percentage(const char** pStr, float *pFraction) { if (*str >= '1' && *str <= '9') { \ NAME = 1; \ } else { \ - if ('0' == *str) ++str; \ + if ('0' == *str) { \ + NAME = 0; \ + ++str; \ + } \ if ('.' == *str) { \ ++str; \ NAME = 0; \ diff --git a/test/canvas.test.js b/test/canvas.test.js index 5abd45cb8..670127783 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -210,6 +210,9 @@ describe('Canvas', function () { ctx.fillStyle = 'rgba(255, 250, 255)'; assert.equal('#fffaff', ctx.fillStyle); + ctx.fillStyle = 'rgba(124, 58, 26, 0)'; + assert.equal('rgba(124, 58, 26, 0.00)', ctx.fillStyle); + // hsl / hsla tests ctx.fillStyle = 'hsl(0, 0%, 0%)' @@ -985,7 +988,7 @@ describe('Canvas', function () { const cm = ctx.measureText('aaaa') assertApprox(cm.actualBoundingBoxLeft, 9, 6) assertApprox(cm.actualBoundingBoxRight, 11, 6) - + ctx.textAlign = 'right' const rm = ctx.measureText('aaaa') assertApprox(rm.actualBoundingBoxLeft, 19, 6) From bc75c6af9edc0f328271e7b84fa21b59b4f4df74 Mon Sep 17 00:00:00 2001 From: Sahel LUCAS--SAOUDI Date: Wed, 7 Sep 2022 18:28:40 +0200 Subject: [PATCH 378/474] add line in CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 085dd412f..ece076034 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Added ### Fixed * Fix `actualBoundingBoxLeft` and `actualBoundingBoxRight` when `textAlign='center'` or `'right'` ([#1909](https://github.com/Automattic/node-canvas/issues/1909)) +* Fix `rgba(r,g,b,0)` with alpha to 0 should parse as transparent, not opaque. 2.10.0 ================== From b3e7df319c045c1dc74e390f4b3af161304c9c55 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Wed, 7 Sep 2022 09:41:50 -0700 Subject: [PATCH 379/474] v2.10.1 --- CHANGELOG.md | 6 +++++- package.json | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ece076034..513643483 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,12 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed + +2.10.1 +================== +### Fixed * Fix `actualBoundingBoxLeft` and `actualBoundingBoxRight` when `textAlign='center'` or `'right'` ([#1909](https://github.com/Automattic/node-canvas/issues/1909)) -* Fix `rgba(r,g,b,0)` with alpha to 0 should parse as transparent, not opaque. +* Fix `rgba(r,g,b,0)` with alpha to 0 should parse as transparent, not opaque. ([#2110](https://github.com/Automattic/node-canvas/pull/2110)) 2.10.0 ================== diff --git a/package.json b/package.json index 72849d64d..034783003 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.10.0", + "version": "2.10.1", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 0e6504a1f6ad28eba5f40835fc233275a4170d46 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 10 Sep 2022 12:45:10 -0700 Subject: [PATCH 380/474] remove save() limit, improve save/restore perf, fix some props 1. One WPT test fails if there are not at least 512 save/restore slots. This removes that limit entirely. 2. Gets rid of clunky C code and uses `std::stack` with a proper C++ class. End result is >1.6x faster with MSVC. 3. Reorders fields and types some enums so the state struct shrinks from 192 bytes to 168 bytes (-24 bytes; i.e. 24 bytes saved per state). 4. Fixes several properties that were not saved/restored: `textBaseline`, `textAlign`. `quality` is not saved/restored, but it's not wired up to anything and needs to be removed. Fixes #1936 --- CHANGELOG.md | 3 + Readme.md | 2 +- benchmarks/run.js | 12 +++ src/Canvas.h | 32 +++++-- src/CanvasRenderingContext2d.cc | 155 ++++++++++++-------------------- src/CanvasRenderingContext2d.h | 84 ++++++++++------- test/canvas.test.js | 53 +++++++++++ 7 files changed, 200 insertions(+), 141 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 513643483..09678e346 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,11 @@ project adheres to [Semantic Versioning](http://semver.org/). (Unreleased) ================== ### Changed +* Improve performance and memory usage of `save()`/`restore()`. +* `save()`/`restore()` no longer have a maximum depth (previously 64 states). ### Added ### Fixed +* `textBaseline` and `textAlign` were not saved/restored by `save()`/`restore()`. ([#1936](https://github.com/Automattic/node-canvas/issues/2029)) 2.10.1 ================== diff --git a/Readme.md b/Readme.md index 992904ce3..cc945aa9a 100644 --- a/Readme.md +++ b/Readme.md @@ -91,7 +91,7 @@ This project is an implementation of the Web Canvas API and implements that API * [CanvasRenderingContext2D#patternQuality](#canvasrenderingcontext2dpatternquality) * [CanvasRenderingContext2D#quality](#canvasrenderingcontext2dquality) * [CanvasRenderingContext2D#textDrawingMode](#canvasrenderingcontext2dtextdrawingmode) -* [CanvasRenderingContext2D#globalCompositeOperator = 'saturate'](#canvasrenderingcontext2dglobalcompositeoperator--saturate) +* [CanvasRenderingContext2D#globalCompositeOperation = 'saturate'](#canvasrenderingcontext2dglobalcompositeoperation--saturate) * [CanvasRenderingContext2D#antialias](#canvasrenderingcontext2dantialias) ### createCanvas() diff --git a/benchmarks/run.js b/benchmarks/run.js index a6954da87..14f4db379 100644 --- a/benchmarks/run.js +++ b/benchmarks/run.js @@ -64,6 +64,18 @@ function done (benchmark, times, start, isAsync) { // node-canvas +bm('save/restore', function () { + for (let i = 0; i < 1000; i++) { + const max = i & 15 + for (let j = 0; j < max; ++j) { + ctx.save() + } + for (let j = 0; j < max; ++j) { + ctx.restore() + } + } +}) + bm('fillStyle= name', function () { for (let i = 0; i < 10000; i++) { ctx.fillStyle = '#fefefe' diff --git a/src/Canvas.h b/src/Canvas.h index f356af035..60d3b4216 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -11,15 +11,6 @@ #include #include -/* - * Maxmimum states per context. - * TODO: remove/resize - */ - -#ifndef CANVAS_MAX_STATES -#define CANVAS_MAX_STATES 64 -#endif - /* * FontFace describes a font file in terms of one PangoFontDescription that * will resolve to it and one that the user describes it as (like @font-face) @@ -31,6 +22,29 @@ class FontFace { unsigned char file_path[1024]; }; +enum text_baseline_t : uint8_t { + TEXT_BASELINE_ALPHABETIC = 0, + TEXT_BASELINE_TOP = 1, + TEXT_BASELINE_BOTTOM = 2, + TEXT_BASELINE_MIDDLE = 3, + TEXT_BASELINE_IDEOGRAPHIC = 4, + TEXT_BASELINE_HANGING = 5 +}; + +enum text_align_t : int8_t { + TEXT_ALIGNMENT_LEFT = -1, + TEXT_ALIGNMENT_CENTER = 0, + TEXT_ALIGNMENT_RIGHT = 1, + // Currently same as LEFT and RIGHT without RTL support: + TEXT_ALIGNMENT_START = -2, + TEXT_ALIGNMENT_END = 2 +}; + +enum canvas_draw_mode_t : uint8_t { + TEXT_DRAW_PATHS, + TEXT_DRAW_GLYPHS +}; + /* * Canvas. */ diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 60a86cb3b..5e37ea257 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -38,19 +38,6 @@ Nan::Persistent Context2d::constructor; constexpr double twoPi = M_PI * 2.; -/* - * Text baselines. - */ - -enum { - TEXT_BASELINE_ALPHABETIC - , TEXT_BASELINE_TOP - , TEXT_BASELINE_BOTTOM - , TEXT_BASELINE_MIDDLE - , TEXT_BASELINE_IDEOGRAPHIC - , TEXT_BASELINE_HANGING -}; - /* * Simple helper macro for a rather verbose function call. */ @@ -178,9 +165,9 @@ Context2d::Context2d(Canvas *canvas) { _canvas = canvas; _context = canvas->createCairoContext(); _layout = pango_cairo_create_layout(_context); - state = states[stateno = 0] = (canvas_state_t *) malloc(sizeof(canvas_state_t)); - - resetState(true); + states.emplace(); + state = &states.top(); + pango_layout_set_font_description(_layout, state->fontDescription); } /* @@ -188,10 +175,6 @@ Context2d::Context2d(Canvas *canvas) { */ Context2d::~Context2d() { - while(stateno >= 0) { - pango_font_description_free(states[stateno]->fontDescription); - free(states[stateno--]); - } g_object_unref(_layout); cairo_destroy(_context); _resetPersistentHandles(); @@ -201,32 +184,10 @@ Context2d::~Context2d() { * Reset canvas state. */ -void Context2d::resetState(bool init) { - if (!init) { - pango_font_description_free(state->fontDescription); - } - - state->shadowBlur = 0; - state->shadowOffsetX = state->shadowOffsetY = 0; - state->globalAlpha = 1; - state->textAlignment = -1; - state->fillPattern = nullptr; - state->strokePattern = nullptr; - state->fillGradient = nullptr; - state->strokeGradient = nullptr; - state->textBaseline = TEXT_BASELINE_ALPHABETIC; - rgba_t transparent = { 0, 0, 0, 1 }; - rgba_t transparent_black = { 0, 0, 0, 0 }; - state->fill = transparent; - state->stroke = transparent; - state->shadow = transparent_black; - state->patternQuality = CAIRO_FILTER_GOOD; - state->imageSmoothingEnabled = true; - state->textDrawingMode = TEXT_DRAW_PATHS; - state->fontDescription = pango_font_description_from_string("sans"); - pango_font_description_set_absolute_size(state->fontDescription, 10 * PANGO_SCALE); +void Context2d::resetState() { + states.pop(); + states.emplace(); pango_layout_set_font_description(_layout, state->fontDescription); - _resetPersistentHandles(); } @@ -234,8 +195,6 @@ void Context2d::_resetPersistentHandles() { _fillStyle.Reset(); _strokeStyle.Reset(); _font.Reset(); - _textBaseline.Reset(); - _textAlign.Reset(); } /* @@ -244,13 +203,9 @@ void Context2d::_resetPersistentHandles() { void Context2d::save() { - if (stateno < CANVAS_MAX_STATES) { - cairo_save(_context); - states[++stateno] = (canvas_state_t *) malloc(sizeof(canvas_state_t)); - memcpy(states[stateno], state, sizeof(canvas_state_t)); - states[stateno]->fontDescription = pango_font_description_copy(states[stateno-1]->fontDescription); - state = states[stateno]; - } + cairo_save(_context); + states.emplace(states.top()); + state = &states.top(); } /* @@ -259,12 +214,10 @@ Context2d::save() { void Context2d::restore() { - if (stateno > 0) { + if (states.size() > 1) { cairo_restore(_context); - pango_font_description_free(states[stateno]->fontDescription); - free(states[stateno]); - states[stateno] = NULL; - state = states[--stateno]; + states.pop(); + state = &states.top(); pango_layout_set_font_description(_layout, state->fontDescription); } } @@ -2496,13 +2449,12 @@ Context2d::setTextPath(double x, double y) { PangoRectangle logical_rect; switch (state->textAlignment) { - // center - case 0: + case TEXT_ALIGNMENT_CENTER: pango_layout_get_pixel_extents(_layout, NULL, &logical_rect); x -= logical_rect.width / 2; break; - // right - case 1: + case TEXT_ALIGNMENT_END: + case TEXT_ALIGNMENT_RIGHT: pango_layout_get_pixel_extents(_layout, NULL, &logical_rect); x -= logical_rect.width; break; @@ -2629,15 +2581,17 @@ NAN_SETTER(Context2d::SetFont) { NAN_GETTER(Context2d::GetTextBaseline) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Isolate *iso = Isolate::GetCurrent(); - Local font; - - if (context->_textBaseline.IsEmpty()) - font = Nan::New("alphabetic").ToLocalChecked(); - else - font = context->_textBaseline.Get(iso); - - info.GetReturnValue().Set(font); + const char* baseline; + switch (context->state->textBaseline) { + default: + case TEXT_BASELINE_ALPHABETIC: baseline = "alphabetic"; break; + case TEXT_BASELINE_TOP: baseline = "top"; break; + case TEXT_BASELINE_BOTTOM: baseline = "bottom"; break; + case TEXT_BASELINE_MIDDLE: baseline = "middle"; break; + case TEXT_BASELINE_IDEOGRAPHIC: baseline = "ideographic"; break; + case TEXT_BASELINE_HANGING: baseline = "hanging"; break; + } + info.GetReturnValue().Set(Nan::New(baseline).ToLocalChecked()); } /* @@ -2648,20 +2602,19 @@ NAN_SETTER(Context2d::SetTextBaseline) { if (!value->IsString()) return; Nan::Utf8String opStr(Nan::To(value).ToLocalChecked()); - const std::map modes = { - {"alphabetic", 0}, - {"top", 1}, - {"bottom", 2}, - {"middle", 3}, - {"ideographic", 4}, - {"hanging", 5} + const std::map modes = { + {"alphabetic", TEXT_BASELINE_ALPHABETIC}, + {"top", TEXT_BASELINE_TOP}, + {"bottom", TEXT_BASELINE_BOTTOM}, + {"middle", TEXT_BASELINE_MIDDLE}, + {"ideographic", TEXT_BASELINE_IDEOGRAPHIC}, + {"hanging", TEXT_BASELINE_HANGING} }; auto op = modes.find(*opStr); if (op == modes.end()) return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); context->state->textBaseline = op->second; - context->_textBaseline.Reset(value); } /* @@ -2670,15 +2623,17 @@ NAN_SETTER(Context2d::SetTextBaseline) { NAN_GETTER(Context2d::GetTextAlign) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Isolate *iso = Isolate::GetCurrent(); - Local font; - - if (context->_textAlign.IsEmpty()) - font = Nan::New("start").ToLocalChecked(); - else - font = context->_textAlign.Get(iso); - - info.GetReturnValue().Set(font); + const char* align; + switch (context->state->textAlignment) { + default: + // TODO the default is supposed to be "start" + case TEXT_ALIGNMENT_LEFT: align = "left"; break; + case TEXT_ALIGNMENT_START: align = "start"; break; + case TEXT_ALIGNMENT_CENTER: align = "center"; break; + case TEXT_ALIGNMENT_RIGHT: align = "right"; break; + case TEXT_ALIGNMENT_END: align = "end"; break; + } + info.GetReturnValue().Set(Nan::New(align).ToLocalChecked()); } /* @@ -2689,19 +2644,18 @@ NAN_SETTER(Context2d::SetTextAlign) { if (!value->IsString()) return; Nan::Utf8String opStr(Nan::To(value).ToLocalChecked()); - const std::map modes = { - {"center", 0}, - {"left", -1}, - {"start", -1}, - {"right", 1}, - {"end", 1} + const std::map modes = { + {"center", TEXT_ALIGNMENT_CENTER}, + {"left", TEXT_ALIGNMENT_LEFT}, + {"start", TEXT_ALIGNMENT_START}, + {"right", TEXT_ALIGNMENT_RIGHT}, + {"end", TEXT_ALIGNMENT_END} }; auto op = modes.find(*opStr); if (op == modes.end()) return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); context->state->textAlignment = op->second; - context->_textAlign.Reset(value); } /* @@ -2747,13 +2701,16 @@ NAN_METHOD(Context2d::MeasureText) { double x_offset; switch (context->state->textAlignment) { - case 0: // center + case TEXT_ALIGNMENT_CENTER: x_offset = logical_rect.width / 2.; break; - case 1: // right + case TEXT_ALIGNMENT_END: + case TEXT_ALIGNMENT_RIGHT: x_offset = logical_rect.width; break; - default: // left + case TEXT_ALIGNMENT_START: + case TEXT_ALIGNMENT_LEFT: + default: x_offset = 0.0; } diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index 89c86df67..568ebc8cc 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -7,11 +7,7 @@ #include "color.h" #include "nan.h" #include - -typedef enum { - TEXT_DRAW_PATHS, - TEXT_DRAW_GLYPHS -} canvas_draw_mode_t; +#include /* * State struct. @@ -20,25 +16,54 @@ typedef enum { * cairo's gstate maintains only a single source pattern at a time. */ -typedef struct { - rgba_t fill; - rgba_t stroke; - cairo_filter_t patternQuality; - cairo_pattern_t *fillPattern; - cairo_pattern_t *strokePattern; - cairo_pattern_t *fillGradient; - cairo_pattern_t *strokeGradient; - float globalAlpha; - short textAlignment; - short textBaseline; - rgba_t shadow; - int shadowBlur; - double shadowOffsetX; - double shadowOffsetY; - canvas_draw_mode_t textDrawingMode; - PangoFontDescription *fontDescription; - bool imageSmoothingEnabled; -} canvas_state_t; +struct canvas_state_t { + rgba_t fill = { 0, 0, 0, 1 }; + rgba_t stroke = { 0, 0, 0, 1 }; + rgba_t shadow = { 0, 0, 0, 0 }; + double shadowOffsetX = 0.; + double shadowOffsetY = 0.; + cairo_pattern_t* fillPattern = nullptr; + cairo_pattern_t* strokePattern = nullptr; + cairo_pattern_t* fillGradient = nullptr; + cairo_pattern_t* strokeGradient = nullptr; + PangoFontDescription* fontDescription = nullptr; + cairo_filter_t patternQuality = CAIRO_FILTER_GOOD; + float globalAlpha = 1.f; + int shadowBlur = 0; + text_align_t textAlignment = TEXT_ALIGNMENT_LEFT; // TODO default is supposed to be START + text_baseline_t textBaseline = TEXT_BASELINE_ALPHABETIC; + canvas_draw_mode_t textDrawingMode = TEXT_DRAW_PATHS; + bool imageSmoothingEnabled = true; + + canvas_state_t() { + fontDescription = pango_font_description_from_string("sans"); + pango_font_description_set_absolute_size(fontDescription, 10 * PANGO_SCALE); + } + + canvas_state_t(const canvas_state_t& other) { + fill = other.fill; + stroke = other.stroke; + patternQuality = other.patternQuality; + fillPattern = other.fillPattern; + strokePattern = other.strokePattern; + fillGradient = other.fillGradient; + strokeGradient = other.strokeGradient; + globalAlpha = other.globalAlpha; + textAlignment = other.textAlignment; + textBaseline = other.textBaseline; + shadow = other.shadow; + shadowBlur = other.shadowBlur; + shadowOffsetX = other.shadowOffsetX; + shadowOffsetY = other.shadowOffsetY; + textDrawingMode = other.textDrawingMode; + fontDescription = pango_font_description_copy(other.fontDescription); + imageSmoothingEnabled = other.imageSmoothingEnabled; + } + + ~canvas_state_t() { + pango_font_description_free(fontDescription); + } +}; /* * Equivalent to a PangoRectangle but holds floats instead of ints @@ -54,12 +79,9 @@ typedef struct { float height; } float_rectangle; -void state_assign_fontFamily(canvas_state_t *state, const char *str); - -class Context2d: public Nan::ObjectWrap { +class Context2d : public Nan::ObjectWrap { public: - short stateno; - canvas_state_t *states[CANVAS_MAX_STATES]; + std::stack states; canvas_state_t *state; Context2d(Canvas *canvas); static Nan::Persistent _DOMMatrix; @@ -180,7 +202,7 @@ class Context2d: public Nan::ObjectWrap { void save(); void restore(); void setFontFromState(); - void resetState(bool init = false); + void resetState(); inline PangoLayout *layout(){ return _layout; } private: @@ -195,8 +217,6 @@ class Context2d: public Nan::ObjectWrap { Nan::Persistent _fillStyle; Nan::Persistent _strokeStyle; Nan::Persistent _font; - Nan::Persistent _textBaseline; - Nan::Persistent _textAlign; Canvas *_canvas; cairo_t *_context; cairo_path_t *_path; diff --git a/test/canvas.test.js b/test/canvas.test.js index 670127783..af33befaa 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -547,6 +547,8 @@ describe('Canvas', function () { const canvas = createCanvas(200, 200) const ctx = canvas.getContext('2d') + assert.equal('left', ctx.textAlign) // default TODO wrong default + ctx.textAlign = 'start' assert.equal('start', ctx.textAlign) ctx.textAlign = 'center' assert.equal('center', ctx.textAlign) @@ -1900,4 +1902,55 @@ describe('Canvas', function () { if (index + 1 & 3) { assert.strictEqual(byte, 128) } else { assert.strictEqual(byte, 255) } }) }) + + describe('Context2d#save()/restore()', function () { + // Based on WPT meta:2d.state.saverestore + const state = [ // non-default values to test with + ['strokeStyle', '#ff0000'], + ['fillStyle', '#ff0000'], + ['globalAlpha', 0.5], + ['lineWidth', 0.5], + ['lineCap', 'round'], + ['lineJoin', 'round'], + ['miterLimit', 0.5], + ['shadowOffsetX', 5], + ['shadowOffsetY', 5], + ['shadowBlur', 5], + ['shadowColor', '#ff0000'], + ['globalCompositeOperation', 'copy'], + // ['font', '25px serif'], // TODO #1946 + ['textAlign', 'center'], + ['textBaseline', 'bottom'], + // Added vs. WPT + ['imageSmoothingEnabled', false], + // ['imageSmoothingQuality', ], // not supported by node-canvas, #2114 + ['lineDashOffset', 1.0], + // Non-standard properties: + ['patternQuality', 'best'], + // ['quality', 'best'], // doesn't do anything, TODO remove + ['textDrawingMode', 'glyph'], + ['antialias', 'gray'] + ] + + for (const [k, v] of state) { + it(`2d.state.saverestore.${k}`, function () { + const canvas = createCanvas(0, 0) + const ctx = canvas.getContext('2d') + + // restore() undoes modification: + let old = ctx[k] + ctx.save() + ctx[k] = v + ctx.restore() + assert.strictEqual(ctx[k], old) + + // save() doesn't modify the value: + ctx[k] = v + old = ctx[k] + ctx.save() + assert.strictEqual(ctx[k], old) + ctx.restore() + }) + } + }) }) From 2876b6e380029a9396d67cf0c8c5f9d2535d3484 Mon Sep 17 00:00:00 2001 From: TheDadi Date: Sun, 30 Oct 2022 02:23:06 +0100 Subject: [PATCH 381/474] Bugfix/Node.js 18 -> Assertion failed: (object->InternalFieldCount() > 0) (#2133) * add node 16 & 18 to build * bump version * fix Assertion failed: (object->InternalFieldCount() > 0), function Unwrap, file nan_object_wrap.h, line 32. * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * reimplement accessors on prototype * Update CHANGELOG.md * add review comments * remove deprecated constructor overload --- .github/workflows/ci.yaml | 6 +- .github/workflows/prebuild.yaml | 6 +- CHANGELOG.md | 8 ++ package.json | 2 +- src/Canvas.cc | 36 ++++- src/CanvasRenderingContext2d.cc | 226 ++++++++++++++++++++++++++++---- src/Image.cc | 46 ++++++- src/ImageData.cc | 14 +- src/Point.h | 8 +- src/Util.h | 25 ---- 10 files changed, 304 insertions(+), 73 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3c92ea1c8..31b7497bc 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [10, 12, 14, 16] + node: [10, 12, 14, 16, 18] steps: - uses: actions/setup-node@v3 with: @@ -33,7 +33,7 @@ jobs: runs-on: windows-2019 strategy: matrix: - node: [10, 12, 14, 16] + node: [10, 12, 14, 16, 18] steps: - uses: actions/setup-node@v3 with: @@ -57,7 +57,7 @@ jobs: runs-on: macos-latest strategy: matrix: - node: [10, 12, 14, 16] + node: [10, 12, 14, 16, 18] steps: - uses: actions/setup-node@v3 with: diff --git a/.github/workflows/prebuild.yaml b/.github/workflows/prebuild.yaml index b10cc1522..a112eef87 100644 --- a/.github/workflows/prebuild.yaml +++ b/.github/workflows/prebuild.yaml @@ -24,7 +24,7 @@ jobs: Linux: strategy: matrix: - node: [8, 9, 10, 11, 12, 13, 14] + node: [8, 9, 10, 11, 12, 13, 14, 16, 18] canvas_tag: [] # e.g. "v2.6.1" name: ${{ matrix.canvas_tag}}, Node.js ${{ matrix.node }}, Linux runs-on: ubuntu-latest @@ -97,7 +97,7 @@ jobs: macOS: strategy: matrix: - node: [8, 9, 10, 11, 12, 13, 14] + node: [8, 9, 10, 11, 12, 13, 14, 16, 18] canvas_tag: [] # e.g. "v2.6.1" name: ${{ matrix.canvas_tag}}, Node.js ${{ matrix.node }}, macOS runs-on: macos-latest @@ -163,7 +163,7 @@ jobs: Win: strategy: matrix: - node: [8, 9, 10, 11, 12, 13, 14] + node: [8, 9, 10, 11, 12, 13, 14, 16, 18] canvas_tag: [] # e.g. "v2.6.1" name: ${{ matrix.canvas_tag}}, Node.js ${{ matrix.node }}, Windows runs-on: windows-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 09678e346..a666d5a23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,14 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Fixed * `textBaseline` and `textAlign` were not saved/restored by `save()`/`restore()`. ([#1936](https://github.com/Automattic/node-canvas/issues/2029)) +2.10.2 +================== +### Fixed +* Fix `Assertion failed: (object->InternalFieldCount() > 0), function Unwrap, file nan_object_wrap.h, line 32.` ([#2025](https://github.com/Automattic/node-canvas/issues/2025)) +### Changed +* Update nan to v2.17.0 to ensure Node.js v18+ support. +* Implement valid `this` checks in all `SetAccessor` methods. + 2.10.1 ================== ### Fixed diff --git a/package.json b/package.json index 034783003..cd6754ea4 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "types": "types/index.d.ts", "dependencies": { "@mapbox/node-pre-gyp": "^1.0.0", - "nan": "^2.15.0", + "nan": "^2.17.0", "simple-get": "^3.0.3" }, "devDependencies": { diff --git a/src/Canvas.cc b/src/Canvas.cc index 3e339f033..a7318ca82 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -64,10 +64,10 @@ Canvas::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { #ifdef HAVE_JPEG Nan::SetPrototypeMethod(ctor, "streamJPEGSync", StreamJPEGSync); #endif - SetProtoAccessor(proto, Nan::New("type").ToLocalChecked(), GetType, NULL, ctor); - SetProtoAccessor(proto, Nan::New("stride").ToLocalChecked(), GetStride, NULL, ctor); - SetProtoAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, SetWidth, ctor); - SetProtoAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, SetHeight, ctor); + Nan::SetAccessor(proto, Nan::New("type").ToLocalChecked(), GetType); + Nan::SetAccessor(proto, Nan::New("stride").ToLocalChecked(), GetStride); + Nan::SetAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, SetWidth); + Nan::SetAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, SetHeight); Nan::SetTemplate(proto, "PNG_NO_FILTERS", Nan::New(PNG_NO_FILTERS)); Nan::SetTemplate(proto, "PNG_FILTER_NONE", Nan::New(PNG_FILTER_NONE)); @@ -144,6 +144,10 @@ NAN_METHOD(Canvas::New) { */ NAN_GETTER(Canvas::GetType) { + if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Canvas.GetType called on incompatible receiver"); + return; + } Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(canvas->backend()->getName()).ToLocalChecked()); } @@ -152,6 +156,10 @@ NAN_GETTER(Canvas::GetType) { * Get stride. */ NAN_GETTER(Canvas::GetStride) { + if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Canvas.GetStride called on incompatible receiver"); + return; + } Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(canvas->stride())); } @@ -161,6 +169,10 @@ NAN_GETTER(Canvas::GetStride) { */ NAN_GETTER(Canvas::GetWidth) { + if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Canvas.GetWidth called on incompatible receiver"); + return; + } Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(canvas->getWidth())); } @@ -170,6 +182,10 @@ NAN_GETTER(Canvas::GetWidth) { */ NAN_SETTER(Canvas::SetWidth) { + if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Canvas.SetWidth called on incompatible receiver"); + return; + } if (value->IsNumber()) { Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); canvas->backend()->setWidth(Nan::To(value).FromMaybe(0)); @@ -182,6 +198,10 @@ NAN_SETTER(Canvas::SetWidth) { */ NAN_GETTER(Canvas::GetHeight) { + if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Canvas.GetHeight called on incompatible receiver"); + return; + } Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(canvas->getHeight())); } @@ -191,6 +211,10 @@ NAN_GETTER(Canvas::GetHeight) { */ NAN_SETTER(Canvas::SetHeight) { + if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Canvas.SetHeight called on incompatible receiver"); + return; + } if (value->IsNumber()) { Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); canvas->backend()->setHeight(Nan::To(value).FromMaybe(0)); @@ -773,13 +797,13 @@ NAN_METHOD(Canvas::RegisterFont) { NAN_METHOD(Canvas::DeregisterAllFonts) { // Unload all fonts from pango to free up memory bool success = true; - + std::for_each(font_face_list.begin(), font_face_list.end(), [&](FontFace& f) { if (!deregister_font( (unsigned char *)f.file_path )) success = false; pango_font_description_free(f.user_desc); pango_font_description_free(f.sys_desc); }); - + font_face_list.clear(); if (!success) Nan::ThrowError("Could not deregister one or more fonts"); } diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 5e37ea257..667e1cf93 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -129,29 +129,29 @@ Context2d::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { Nan::SetPrototypeMethod(ctor, "createPattern", CreatePattern); Nan::SetPrototypeMethod(ctor, "createLinearGradient", CreateLinearGradient); Nan::SetPrototypeMethod(ctor, "createRadialGradient", CreateRadialGradient); - SetProtoAccessor(proto, Nan::New("pixelFormat").ToLocalChecked(), GetFormat, NULL, ctor); - SetProtoAccessor(proto, Nan::New("patternQuality").ToLocalChecked(), GetPatternQuality, SetPatternQuality, ctor); - SetProtoAccessor(proto, Nan::New("imageSmoothingEnabled").ToLocalChecked(), GetImageSmoothingEnabled, SetImageSmoothingEnabled, ctor); - SetProtoAccessor(proto, Nan::New("globalCompositeOperation").ToLocalChecked(), GetGlobalCompositeOperation, SetGlobalCompositeOperation, ctor); - SetProtoAccessor(proto, Nan::New("globalAlpha").ToLocalChecked(), GetGlobalAlpha, SetGlobalAlpha, ctor); - SetProtoAccessor(proto, Nan::New("shadowColor").ToLocalChecked(), GetShadowColor, SetShadowColor, ctor); - SetProtoAccessor(proto, Nan::New("miterLimit").ToLocalChecked(), GetMiterLimit, SetMiterLimit, ctor); - SetProtoAccessor(proto, Nan::New("lineWidth").ToLocalChecked(), GetLineWidth, SetLineWidth, ctor); - SetProtoAccessor(proto, Nan::New("lineCap").ToLocalChecked(), GetLineCap, SetLineCap, ctor); - SetProtoAccessor(proto, Nan::New("lineJoin").ToLocalChecked(), GetLineJoin, SetLineJoin, ctor); - SetProtoAccessor(proto, Nan::New("lineDashOffset").ToLocalChecked(), GetLineDashOffset, SetLineDashOffset, ctor); - SetProtoAccessor(proto, Nan::New("shadowOffsetX").ToLocalChecked(), GetShadowOffsetX, SetShadowOffsetX, ctor); - SetProtoAccessor(proto, Nan::New("shadowOffsetY").ToLocalChecked(), GetShadowOffsetY, SetShadowOffsetY, ctor); - SetProtoAccessor(proto, Nan::New("shadowBlur").ToLocalChecked(), GetShadowBlur, SetShadowBlur, ctor); - SetProtoAccessor(proto, Nan::New("antialias").ToLocalChecked(), GetAntiAlias, SetAntiAlias, ctor); - SetProtoAccessor(proto, Nan::New("textDrawingMode").ToLocalChecked(), GetTextDrawingMode, SetTextDrawingMode, ctor); - SetProtoAccessor(proto, Nan::New("quality").ToLocalChecked(), GetQuality, SetQuality, ctor); - SetProtoAccessor(proto, Nan::New("currentTransform").ToLocalChecked(), GetCurrentTransform, SetCurrentTransform, ctor); - SetProtoAccessor(proto, Nan::New("fillStyle").ToLocalChecked(), GetFillStyle, SetFillStyle, ctor); - SetProtoAccessor(proto, Nan::New("strokeStyle").ToLocalChecked(), GetStrokeStyle, SetStrokeStyle, ctor); - SetProtoAccessor(proto, Nan::New("font").ToLocalChecked(), GetFont, SetFont, ctor); - SetProtoAccessor(proto, Nan::New("textBaseline").ToLocalChecked(), GetTextBaseline, SetTextBaseline, ctor); - SetProtoAccessor(proto, Nan::New("textAlign").ToLocalChecked(), GetTextAlign, SetTextAlign, ctor); + Nan::SetAccessor(proto, Nan::New("pixelFormat").ToLocalChecked(), GetFormat); + Nan::SetAccessor(proto, Nan::New("patternQuality").ToLocalChecked(), GetPatternQuality, SetPatternQuality); + Nan::SetAccessor(proto, Nan::New("imageSmoothingEnabled").ToLocalChecked(), GetImageSmoothingEnabled, SetImageSmoothingEnabled); + Nan::SetAccessor(proto, Nan::New("globalCompositeOperation").ToLocalChecked(), GetGlobalCompositeOperation, SetGlobalCompositeOperation); + Nan::SetAccessor(proto, Nan::New("globalAlpha").ToLocalChecked(), GetGlobalAlpha, SetGlobalAlpha); + Nan::SetAccessor(proto, Nan::New("shadowColor").ToLocalChecked(), GetShadowColor, SetShadowColor); + Nan::SetAccessor(proto, Nan::New("miterLimit").ToLocalChecked(), GetMiterLimit, SetMiterLimit); + Nan::SetAccessor(proto, Nan::New("lineWidth").ToLocalChecked(), GetLineWidth, SetLineWidth); + Nan::SetAccessor(proto, Nan::New("lineCap").ToLocalChecked(), GetLineCap, SetLineCap); + Nan::SetAccessor(proto, Nan::New("lineJoin").ToLocalChecked(), GetLineJoin, SetLineJoin); + Nan::SetAccessor(proto, Nan::New("lineDashOffset").ToLocalChecked(), GetLineDashOffset, SetLineDashOffset); + Nan::SetAccessor(proto, Nan::New("shadowOffsetX").ToLocalChecked(), GetShadowOffsetX, SetShadowOffsetX); + Nan::SetAccessor(proto, Nan::New("shadowOffsetY").ToLocalChecked(), GetShadowOffsetY, SetShadowOffsetY); + Nan::SetAccessor(proto, Nan::New("shadowBlur").ToLocalChecked(), GetShadowBlur, SetShadowBlur); + Nan::SetAccessor(proto, Nan::New("antialias").ToLocalChecked(), GetAntiAlias, SetAntiAlias); + Nan::SetAccessor(proto, Nan::New("textDrawingMode").ToLocalChecked(), GetTextDrawingMode, SetTextDrawingMode); + Nan::SetAccessor(proto, Nan::New("quality").ToLocalChecked(), GetQuality, SetQuality); + Nan::SetAccessor(proto, Nan::New("currentTransform").ToLocalChecked(), GetCurrentTransform, SetCurrentTransform); + Nan::SetAccessor(proto, Nan::New("fillStyle").ToLocalChecked(), GetFillStyle, SetFillStyle); + Nan::SetAccessor(proto, Nan::New("strokeStyle").ToLocalChecked(), GetStrokeStyle, SetStrokeStyle); + Nan::SetAccessor(proto, Nan::New("font").ToLocalChecked(), GetFont, SetFont); + Nan::SetAccessor(proto, Nan::New("textBaseline").ToLocalChecked(), GetTextBaseline, SetTextBaseline); + Nan::SetAccessor(proto, Nan::New("textAlign").ToLocalChecked(), GetTextAlign, SetTextAlign); Local ctx = Nan::GetCurrentContext(); Nan::Set(target, Nan::New("CanvasRenderingContext2d").ToLocalChecked(), ctor->GetFunction(ctx).ToLocalChecked()); Nan::Set(target, Nan::New("CanvasRenderingContext2dInit").ToLocalChecked(), Nan::New(SaveExternalModules)); @@ -713,6 +713,10 @@ NAN_METHOD(Context2d::SaveExternalModules) { */ NAN_GETTER(Context2d::GetFormat) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetFormat called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); std::string pixelFormatString; switch (context->canvas()->backend()->getFormat()) { @@ -1392,6 +1396,10 @@ NAN_METHOD(Context2d::DrawImage) { */ NAN_GETTER(Context2d::GetGlobalAlpha) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetGlobalAlpha called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(context->state->globalAlpha)); } @@ -1401,6 +1409,10 @@ NAN_GETTER(Context2d::GetGlobalAlpha) { */ NAN_SETTER(Context2d::SetGlobalAlpha) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetGlobalAlpha called on incompatible receiver"); + return; + } double n = Nan::To(value).FromMaybe(0); if (n >= 0 && n <= 1) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -1413,6 +1425,10 @@ NAN_SETTER(Context2d::SetGlobalAlpha) { */ NAN_GETTER(Context2d::GetGlobalCompositeOperation) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetGlobalCompositeOperation called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); @@ -1463,6 +1479,10 @@ NAN_GETTER(Context2d::GetGlobalCompositeOperation) { */ NAN_SETTER(Context2d::SetPatternQuality) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetPatternQuality called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Nan::Utf8String quality(Nan::To(value).ToLocalChecked()); if (0 == strcmp("fast", *quality)) { @@ -1483,6 +1503,10 @@ NAN_SETTER(Context2d::SetPatternQuality) { */ NAN_GETTER(Context2d::GetPatternQuality) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetPatternQuality called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *quality; switch (context->state->patternQuality) { @@ -1500,6 +1524,10 @@ NAN_GETTER(Context2d::GetPatternQuality) { */ NAN_SETTER(Context2d::SetImageSmoothingEnabled) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetImageSmoothingEnabled called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); context->state->imageSmoothingEnabled = Nan::To(value).FromMaybe(false); } @@ -1509,6 +1537,10 @@ NAN_SETTER(Context2d::SetImageSmoothingEnabled) { */ NAN_GETTER(Context2d::GetImageSmoothingEnabled) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetImageSmoothingEnabled called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(context->state->imageSmoothingEnabled)); } @@ -1518,6 +1550,10 @@ NAN_GETTER(Context2d::GetImageSmoothingEnabled) { */ NAN_SETTER(Context2d::SetGlobalCompositeOperation) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetGlobalCompositeOperation called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); Nan::Utf8String opStr(Nan::To(value).ToLocalChecked()); // Unlike CSS colors, this *is* case-sensitive @@ -1565,6 +1601,10 @@ NAN_SETTER(Context2d::SetGlobalCompositeOperation) { */ NAN_GETTER(Context2d::GetShadowOffsetX) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetShadowOffsetX called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(context->state->shadowOffsetX)); } @@ -1574,6 +1614,10 @@ NAN_GETTER(Context2d::GetShadowOffsetX) { */ NAN_SETTER(Context2d::SetShadowOffsetX) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetShadowOffsetX called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); context->state->shadowOffsetX = Nan::To(value).FromMaybe(0); } @@ -1583,6 +1627,10 @@ NAN_SETTER(Context2d::SetShadowOffsetX) { */ NAN_GETTER(Context2d::GetShadowOffsetY) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetShadowOffsetY called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(context->state->shadowOffsetY)); } @@ -1592,6 +1640,10 @@ NAN_GETTER(Context2d::GetShadowOffsetY) { */ NAN_SETTER(Context2d::SetShadowOffsetY) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetShadowOffsetY called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); context->state->shadowOffsetY = Nan::To(value).FromMaybe(0); } @@ -1601,6 +1653,10 @@ NAN_SETTER(Context2d::SetShadowOffsetY) { */ NAN_GETTER(Context2d::GetShadowBlur) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetShadowBlur called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(context->state->shadowBlur)); } @@ -1610,6 +1666,10 @@ NAN_GETTER(Context2d::GetShadowBlur) { */ NAN_SETTER(Context2d::SetShadowBlur) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetShadowBlur called on incompatible receiver"); + return; + } int n = Nan::To(value).FromMaybe(0); if (n >= 0) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -1622,6 +1682,10 @@ NAN_SETTER(Context2d::SetShadowBlur) { */ NAN_GETTER(Context2d::GetAntiAlias) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetAntiAlias called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *aa; switch (cairo_get_antialias(context->context())) { @@ -1638,6 +1702,10 @@ NAN_GETTER(Context2d::GetAntiAlias) { */ NAN_SETTER(Context2d::SetAntiAlias) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetAntiAlias called on incompatible receiver"); + return; + } Nan::Utf8String str(Nan::To(value).ToLocalChecked()); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); @@ -1661,6 +1729,10 @@ NAN_SETTER(Context2d::SetAntiAlias) { */ NAN_GETTER(Context2d::GetTextDrawingMode) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetTextDrawingMode called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *mode; if (context->state->textDrawingMode == TEXT_DRAW_PATHS) { @@ -1678,6 +1750,10 @@ NAN_GETTER(Context2d::GetTextDrawingMode) { */ NAN_SETTER(Context2d::SetTextDrawingMode) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetTextDrawingMode called on incompatible receiver"); + return; + } Nan::Utf8String str(Nan::To(value).ToLocalChecked()); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); if (0 == strcmp("path", *str)) { @@ -1692,6 +1768,10 @@ NAN_SETTER(Context2d::SetTextDrawingMode) { */ NAN_GETTER(Context2d::GetQuality) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetQuality called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *filter; switch (cairo_pattern_get_filter(cairo_get_source(context->context()))) { @@ -1709,6 +1789,10 @@ NAN_GETTER(Context2d::GetQuality) { */ NAN_SETTER(Context2d::SetQuality) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetQuality called on incompatible receiver"); + return; + } Nan::Utf8String str(Nan::To(value).ToLocalChecked()); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_filter_t filter; @@ -1771,6 +1855,10 @@ void parse_matrix_from_object(cairo_matrix_t &matrix, Local mat) { */ NAN_GETTER(Context2d::GetCurrentTransform) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetCurrentTransform called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Local instance = get_current_transform(context); @@ -1782,6 +1870,10 @@ NAN_GETTER(Context2d::GetCurrentTransform) { */ NAN_SETTER(Context2d::SetCurrentTransform) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetCurrentTransform called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Local ctx = Nan::GetCurrentContext(); Local mat = Nan::To(value).ToLocalChecked(); @@ -1803,6 +1895,10 @@ NAN_SETTER(Context2d::SetCurrentTransform) { */ NAN_GETTER(Context2d::GetFillStyle) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetFillStyle called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Isolate *iso = Isolate::GetCurrent(); Local style; @@ -1820,6 +1916,10 @@ NAN_GETTER(Context2d::GetFillStyle) { */ NAN_SETTER(Context2d::SetFillStyle) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetFillStyle called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); if (value->IsString()) { @@ -1847,6 +1947,10 @@ NAN_SETTER(Context2d::SetFillStyle) { */ NAN_GETTER(Context2d::GetStrokeStyle) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetStrokeStyle called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Local style; @@ -1863,6 +1967,10 @@ NAN_GETTER(Context2d::GetStrokeStyle) { */ NAN_SETTER(Context2d::SetStrokeStyle) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetStrokeStyle called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); if (value->IsString()) { @@ -1890,6 +1998,10 @@ NAN_SETTER(Context2d::SetStrokeStyle) { */ NAN_GETTER(Context2d::GetMiterLimit) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetMiterLimit called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(cairo_get_miter_limit(context->context()))); } @@ -1899,6 +2011,10 @@ NAN_GETTER(Context2d::GetMiterLimit) { */ NAN_SETTER(Context2d::SetMiterLimit) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetMiterLimit called on incompatible receiver"); + return; + } double n = Nan::To(value).FromMaybe(0); if (n > 0) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -1911,6 +2027,10 @@ NAN_SETTER(Context2d::SetMiterLimit) { */ NAN_GETTER(Context2d::GetLineWidth) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetLineWidth called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(cairo_get_line_width(context->context()))); } @@ -1920,6 +2040,10 @@ NAN_GETTER(Context2d::GetLineWidth) { */ NAN_SETTER(Context2d::SetLineWidth) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetLineWidth called on incompatible receiver"); + return; + } double n = Nan::To(value).FromMaybe(0); if (n > 0 && n != std::numeric_limits::infinity()) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -1932,6 +2056,10 @@ NAN_SETTER(Context2d::SetLineWidth) { */ NAN_GETTER(Context2d::GetLineJoin) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetLineJoin called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *join; switch (cairo_get_line_join(context->context())) { @@ -1947,6 +2075,10 @@ NAN_GETTER(Context2d::GetLineJoin) { */ NAN_SETTER(Context2d::SetLineJoin) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetLineJoin called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); Nan::Utf8String type(Nan::To(value).ToLocalChecked()); @@ -1964,6 +2096,10 @@ NAN_SETTER(Context2d::SetLineJoin) { */ NAN_GETTER(Context2d::GetLineCap) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetLineCap called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *cap; switch (cairo_get_line_cap(context->context())) { @@ -1979,6 +2115,10 @@ NAN_GETTER(Context2d::GetLineCap) { */ NAN_SETTER(Context2d::SetLineCap) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetLineCap called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); Nan::Utf8String type(Nan::To(value).ToLocalChecked()); @@ -2013,6 +2153,10 @@ NAN_METHOD(Context2d::IsPointInPath) { */ NAN_SETTER(Context2d::SetShadowColor) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetShadowColor called on incompatible receiver"); + return; + } short ok; Nan::Utf8String str(Nan::To(value).ToLocalChecked()); uint32_t rgba = rgba_from_string(*str, &ok); @@ -2027,6 +2171,10 @@ NAN_SETTER(Context2d::SetShadowColor) { */ NAN_GETTER(Context2d::GetShadowColor) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetShadowColor called on incompatible receiver"); + return; + } char buf[64]; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); rgba_to_string(context->state->shadow, buf, sizeof(buf)); @@ -2501,6 +2649,10 @@ NAN_METHOD(Context2d::MoveTo) { */ NAN_GETTER(Context2d::GetFont) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetFont called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Isolate *iso = Isolate::GetCurrent(); Local font; @@ -2523,6 +2675,10 @@ NAN_GETTER(Context2d::GetFont) { */ NAN_SETTER(Context2d::SetFont) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetFont called on incompatible receiver"); + return; + } if (!value->IsString()) return; Isolate *iso = Isolate::GetCurrent(); @@ -2580,6 +2736,10 @@ NAN_SETTER(Context2d::SetFont) { */ NAN_GETTER(Context2d::GetTextBaseline) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetTextBaseline called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char* baseline; switch (context->state->textBaseline) { @@ -2599,6 +2759,10 @@ NAN_GETTER(Context2d::GetTextBaseline) { */ NAN_SETTER(Context2d::SetTextBaseline) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetTextBaseline called on incompatible receiver"); + return; + } if (!value->IsString()) return; Nan::Utf8String opStr(Nan::To(value).ToLocalChecked()); @@ -2622,6 +2786,10 @@ NAN_SETTER(Context2d::SetTextBaseline) { */ NAN_GETTER(Context2d::GetTextAlign) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetTextAlign called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char* align; switch (context->state->textAlignment) { @@ -2641,6 +2809,10 @@ NAN_GETTER(Context2d::GetTextAlign) { */ NAN_SETTER(Context2d::SetTextAlign) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetTextAlign called on incompatible receiver"); + return; + } if (!value->IsString()) return; Nan::Utf8String opStr(Nan::To(value).ToLocalChecked()); @@ -2803,6 +2975,10 @@ NAN_METHOD(Context2d::GetLineDash) { * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash */ NAN_SETTER(Context2d::SetLineDashOffset) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetLineDashOffset called on incompatible receiver"); + return; + } double offset = Nan::To(value).FromMaybe(0); if (!std::isfinite(offset)) return; @@ -2820,6 +2996,10 @@ NAN_SETTER(Context2d::SetLineDashOffset) { * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash */ NAN_GETTER(Context2d::GetLineDashOffset) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetLineDashOffset called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); double offset; diff --git a/src/Image.cc b/src/Image.cc index 103b65ee7..35ee7947a 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -8,7 +8,6 @@ #include #include #include -#include "Util.h" /* Cairo limit: * https://lists.cairographics.org/archives/cairo/2010-December/021422.html @@ -60,12 +59,12 @@ Image::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { // Prototype Local proto = ctor->PrototypeTemplate(); - SetProtoAccessor(proto, Nan::New("complete").ToLocalChecked(), GetComplete, NULL, ctor); - SetProtoAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, SetWidth, ctor); - SetProtoAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, SetHeight, ctor); - SetProtoAccessor(proto, Nan::New("naturalWidth").ToLocalChecked(), GetNaturalWidth, NULL, ctor); - SetProtoAccessor(proto, Nan::New("naturalHeight").ToLocalChecked(), GetNaturalHeight, NULL, ctor); - SetProtoAccessor(proto, Nan::New("dataMode").ToLocalChecked(), GetDataMode, SetDataMode, ctor); + Nan::SetAccessor(proto, Nan::New("complete").ToLocalChecked(), GetComplete); + Nan::SetAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, SetWidth); + Nan::SetAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, SetHeight); + Nan::SetAccessor(proto, Nan::New("naturalWidth").ToLocalChecked(), GetNaturalWidth); + Nan::SetAccessor(proto, Nan::New("naturalHeight").ToLocalChecked(), GetNaturalHeight); + Nan::SetAccessor(proto, Nan::New("dataMode").ToLocalChecked(), GetDataMode, SetDataMode); ctor->Set(Nan::New("MODE_IMAGE").ToLocalChecked(), Nan::New(DATA_IMAGE)); ctor->Set(Nan::New("MODE_MIME").ToLocalChecked(), Nan::New(DATA_MIME)); @@ -108,6 +107,10 @@ NAN_GETTER(Image::GetComplete) { */ NAN_GETTER(Image::GetDataMode) { + if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Image.GetDataMode called on incompatible receiver"); + return; + } Image *img = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(img->data_mode)); } @@ -117,6 +120,10 @@ NAN_GETTER(Image::GetDataMode) { */ NAN_SETTER(Image::SetDataMode) { + if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Image.SetDataMode called on incompatible receiver"); + return; + } if (value->IsNumber()) { Image *img = Nan::ObjectWrap::Unwrap(info.This()); int mode = Nan::To(value).FromMaybe(0); @@ -129,6 +136,10 @@ NAN_SETTER(Image::SetDataMode) { */ NAN_GETTER(Image::GetNaturalWidth) { + if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Image.GetNaturalWidth called on incompatible receiver"); + return; + } Image *img = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(img->naturalWidth)); } @@ -138,6 +149,10 @@ NAN_GETTER(Image::GetNaturalWidth) { */ NAN_GETTER(Image::GetWidth) { + if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Image.GetWidth called on incompatible receiver"); + return; + } Image *img = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(img->width)); } @@ -147,6 +162,10 @@ NAN_GETTER(Image::GetWidth) { */ NAN_SETTER(Image::SetWidth) { + if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Image.SetWidth called on incompatible receiver"); + return; + } if (value->IsNumber()) { Image *img = Nan::ObjectWrap::Unwrap(info.This()); img->width = Nan::To(value).FromMaybe(0); @@ -158,6 +177,10 @@ NAN_SETTER(Image::SetWidth) { */ NAN_GETTER(Image::GetNaturalHeight) { + if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Image.GetNaturalHeight called on incompatible receiver"); + return; + } Image *img = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(img->naturalHeight)); } @@ -167,6 +190,10 @@ NAN_GETTER(Image::GetNaturalHeight) { */ NAN_GETTER(Image::GetHeight) { + if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Image.GetHeight called on incompatible receiver"); + return; + } Image *img = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(img->height)); } @@ -175,6 +202,11 @@ NAN_GETTER(Image::GetHeight) { */ NAN_SETTER(Image::SetHeight) { + if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + // #1534 + Nan::ThrowTypeError("Method Image.SetHeight called on incompatible receiver"); + return; + } if (value->IsNumber()) { Image *img = Nan::ObjectWrap::Unwrap(info.This()); img->height = Nan::To(value).FromMaybe(0); diff --git a/src/ImageData.cc b/src/ImageData.cc index 668733d39..03da2e270 100644 --- a/src/ImageData.cc +++ b/src/ImageData.cc @@ -2,8 +2,6 @@ #include "ImageData.h" -#include "Util.h" - using namespace v8; Nan::Persistent ImageData::constructor; @@ -24,8 +22,8 @@ ImageData::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { // Prototype Local proto = ctor->PrototypeTemplate(); - SetProtoAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, NULL, ctor); - SetProtoAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, NULL, ctor); + Nan::SetAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth); + Nan::SetAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight); Local ctx = Nan::GetCurrentContext(); Nan::Set(target, Nan::New("ImageData").ToLocalChecked(), ctor->GetFunction(ctx).ToLocalChecked()); } @@ -126,6 +124,10 @@ NAN_METHOD(ImageData::New) { */ NAN_GETTER(ImageData::GetWidth) { + if (!ImageData::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method ImageData.GetWidth called on incompatible receiver"); + return; + } ImageData *imageData = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(imageData->width())); } @@ -135,6 +137,10 @@ NAN_GETTER(ImageData::GetWidth) { */ NAN_GETTER(ImageData::GetHeight) { + if (!ImageData::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method ImageData.GetHeight called on incompatible receiver"); + return; + } ImageData *imageData = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(imageData->height())); } diff --git a/src/Point.h b/src/Point.h index 50c7b711c..a797dc46c 100644 --- a/src/Point.h +++ b/src/Point.h @@ -1,11 +1,17 @@ // Copyright (c) 2010 LearnBoost - #pragma once +#include + template class Point { public: T x, y; Point(T x=0, T y=0): x(x), y(y) {} Point(const Point&) = default; + Point& operator=(Point other) { + std::swap(x, other.x); + std::swap(y, other.y); + return *this; + } }; diff --git a/src/Util.h b/src/Util.h index dba6883a2..0e6d1d89c 100644 --- a/src/Util.h +++ b/src/Util.h @@ -1,32 +1,7 @@ #pragma once -#include -#include #include -// Wrapper around Nan::SetAccessor that makes it easier to change the last -// argument (signature). Getters/setters must be accessed only when there is -// actually an instance, i.e. MyClass.prototype.getter1 should not try to -// unwrap the non-existent 'this'. See #803, #847, #885, nodejs/node#15099, ... -inline void SetProtoAccessor( - v8::Local tpl, - v8::Local name, - Nan::GetterCallback getter, - Nan::SetterCallback setter, - v8::Local ctor - ) { - Nan::SetAccessor( - tpl, - name, - getter, - setter, - v8::Local(), - v8::DEFAULT, - v8::None, - v8::AccessorSignature::New(v8::Isolate::GetCurrent(), ctor) - ); -} - inline bool streq_casein(std::string& str1, std::string& str2) { return str1.size() == str2.size() && std::equal(str1.begin(), str1.end(), str2.begin(), [](char& c1, char& c2) { return c1 == c2 || std::toupper(c1) == std::toupper(c2); From ad18c6ce0ab2452d64df36a199c740a926ceb939 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 29 Oct 2022 18:24:19 -0700 Subject: [PATCH 382/474] src: shorten copy assignment operator decl for Point --- src/Point.h | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Point.h b/src/Point.h index a797dc46c..a61f8b1ba 100644 --- a/src/Point.h +++ b/src/Point.h @@ -1,17 +1,11 @@ // Copyright (c) 2010 LearnBoost #pragma once -#include - template class Point { public: T x, y; Point(T x=0, T y=0): x(x), y(y) {} Point(const Point&) = default; - Point& operator=(Point other) { - std::swap(x, other.x); - std::swap(y, other.y); - return *this; - } + Point& operator=(const Point&) = default; }; From cc32159bf5db44edbded532249f56f7844b36aeb Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 29 Oct 2022 18:32:13 -0700 Subject: [PATCH 383/474] src: shorten receiver checks --- src/Canvas.cc | 38 ++---- src/CanvasRenderingContext2d.cc | 233 ++++++++------------------------ 2 files changed, 67 insertions(+), 204 deletions(-) diff --git a/src/Canvas.cc b/src/Canvas.cc index a7318ca82..860d5bb46 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -35,6 +35,12 @@ "with at least a family (string) and optionally weight (string/number) " \ "and style (string)." +#define CHECK_RECEIVER(prop) \ + if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { \ + Nan::ThrowTypeError("Method " #prop " called on incompatible receiver"); \ + return; \ + } + using namespace v8; using namespace std; @@ -144,10 +150,7 @@ NAN_METHOD(Canvas::New) { */ NAN_GETTER(Canvas::GetType) { - if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Canvas.GetType called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Canvas.GetType); Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(canvas->backend()->getName()).ToLocalChecked()); } @@ -156,10 +159,7 @@ NAN_GETTER(Canvas::GetType) { * Get stride. */ NAN_GETTER(Canvas::GetStride) { - if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Canvas.GetStride called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Canvas.GetStride); Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(canvas->stride())); } @@ -169,10 +169,7 @@ NAN_GETTER(Canvas::GetStride) { */ NAN_GETTER(Canvas::GetWidth) { - if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Canvas.GetWidth called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Canvas.GetWidth); Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(canvas->getWidth())); } @@ -182,10 +179,7 @@ NAN_GETTER(Canvas::GetWidth) { */ NAN_SETTER(Canvas::SetWidth) { - if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Canvas.SetWidth called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Canvas.SetWidth); if (value->IsNumber()) { Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); canvas->backend()->setWidth(Nan::To(value).FromMaybe(0)); @@ -198,10 +192,7 @@ NAN_SETTER(Canvas::SetWidth) { */ NAN_GETTER(Canvas::GetHeight) { - if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Canvas.GetHeight called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Canvas.GetHeight); Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(canvas->getHeight())); } @@ -211,10 +202,7 @@ NAN_GETTER(Canvas::GetHeight) { */ NAN_SETTER(Canvas::SetHeight) { - if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Canvas.SetHeight called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Canvas.SetHeight); if (value->IsNumber()) { Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); canvas->backend()->setHeight(Nan::To(value).FromMaybe(0)); @@ -973,3 +961,5 @@ Local Canvas::Error(cairo_status_t status) { return Exception::Error(Nan::New(cairo_status_to_string(status)).ToLocalChecked()); } + +#undef CHECK_RECEIVER diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 667e1cf93..699ec88fc 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -36,6 +36,12 @@ Nan::Persistent Context2d::constructor; double width = args[2]; \ double height = args[3]; +#define CHECK_RECEIVER(prop) \ + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { \ + Nan::ThrowTypeError("Method " #prop " called on incompatible receiver"); \ + return; \ + } + constexpr double twoPi = M_PI * 2.; /* @@ -713,10 +719,7 @@ NAN_METHOD(Context2d::SaveExternalModules) { */ NAN_GETTER(Context2d::GetFormat) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetFormat called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetFormat); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); std::string pixelFormatString; switch (context->canvas()->backend()->getFormat()) { @@ -1396,10 +1399,7 @@ NAN_METHOD(Context2d::DrawImage) { */ NAN_GETTER(Context2d::GetGlobalAlpha) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetGlobalAlpha called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetGlobalAlpha); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(context->state->globalAlpha)); } @@ -1409,10 +1409,7 @@ NAN_GETTER(Context2d::GetGlobalAlpha) { */ NAN_SETTER(Context2d::SetGlobalAlpha) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetGlobalAlpha called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetGlobalAlpha); double n = Nan::To(value).FromMaybe(0); if (n >= 0 && n <= 1) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -1425,10 +1422,7 @@ NAN_SETTER(Context2d::SetGlobalAlpha) { */ NAN_GETTER(Context2d::GetGlobalCompositeOperation) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetGlobalCompositeOperation called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetGlobalCompositeOperation); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); @@ -1479,10 +1473,7 @@ NAN_GETTER(Context2d::GetGlobalCompositeOperation) { */ NAN_SETTER(Context2d::SetPatternQuality) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetPatternQuality called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetPatternQuality); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Nan::Utf8String quality(Nan::To(value).ToLocalChecked()); if (0 == strcmp("fast", *quality)) { @@ -1503,10 +1494,7 @@ NAN_SETTER(Context2d::SetPatternQuality) { */ NAN_GETTER(Context2d::GetPatternQuality) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetPatternQuality called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetPatternQuality); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *quality; switch (context->state->patternQuality) { @@ -1524,10 +1512,7 @@ NAN_GETTER(Context2d::GetPatternQuality) { */ NAN_SETTER(Context2d::SetImageSmoothingEnabled) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetImageSmoothingEnabled called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetImageSmoothingEnabled); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); context->state->imageSmoothingEnabled = Nan::To(value).FromMaybe(false); } @@ -1537,10 +1522,7 @@ NAN_SETTER(Context2d::SetImageSmoothingEnabled) { */ NAN_GETTER(Context2d::GetImageSmoothingEnabled) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetImageSmoothingEnabled called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetImageSmoothingEnabled); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(context->state->imageSmoothingEnabled)); } @@ -1550,10 +1532,7 @@ NAN_GETTER(Context2d::GetImageSmoothingEnabled) { */ NAN_SETTER(Context2d::SetGlobalCompositeOperation) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetGlobalCompositeOperation called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetGlobalCompositeOperation); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); Nan::Utf8String opStr(Nan::To(value).ToLocalChecked()); // Unlike CSS colors, this *is* case-sensitive @@ -1601,10 +1580,7 @@ NAN_SETTER(Context2d::SetGlobalCompositeOperation) { */ NAN_GETTER(Context2d::GetShadowOffsetX) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetShadowOffsetX called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetShadowOffsetX); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(context->state->shadowOffsetX)); } @@ -1614,10 +1590,7 @@ NAN_GETTER(Context2d::GetShadowOffsetX) { */ NAN_SETTER(Context2d::SetShadowOffsetX) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetShadowOffsetX called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetShadowOffsetX); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); context->state->shadowOffsetX = Nan::To(value).FromMaybe(0); } @@ -1627,10 +1600,7 @@ NAN_SETTER(Context2d::SetShadowOffsetX) { */ NAN_GETTER(Context2d::GetShadowOffsetY) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetShadowOffsetY called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetShadowOffsetY); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(context->state->shadowOffsetY)); } @@ -1640,10 +1610,7 @@ NAN_GETTER(Context2d::GetShadowOffsetY) { */ NAN_SETTER(Context2d::SetShadowOffsetY) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetShadowOffsetY called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetShadowOffsetY); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); context->state->shadowOffsetY = Nan::To(value).FromMaybe(0); } @@ -1653,10 +1620,7 @@ NAN_SETTER(Context2d::SetShadowOffsetY) { */ NAN_GETTER(Context2d::GetShadowBlur) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetShadowBlur called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetShadowBlur); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(context->state->shadowBlur)); } @@ -1666,10 +1630,7 @@ NAN_GETTER(Context2d::GetShadowBlur) { */ NAN_SETTER(Context2d::SetShadowBlur) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetShadowBlur called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetShadowBlur); int n = Nan::To(value).FromMaybe(0); if (n >= 0) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -1682,10 +1643,7 @@ NAN_SETTER(Context2d::SetShadowBlur) { */ NAN_GETTER(Context2d::GetAntiAlias) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetAntiAlias called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetAntiAlias); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *aa; switch (cairo_get_antialias(context->context())) { @@ -1702,10 +1660,7 @@ NAN_GETTER(Context2d::GetAntiAlias) { */ NAN_SETTER(Context2d::SetAntiAlias) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetAntiAlias called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetAntiAlias); Nan::Utf8String str(Nan::To(value).ToLocalChecked()); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); @@ -1729,10 +1684,7 @@ NAN_SETTER(Context2d::SetAntiAlias) { */ NAN_GETTER(Context2d::GetTextDrawingMode) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetTextDrawingMode called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetTextDrawingMode); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *mode; if (context->state->textDrawingMode == TEXT_DRAW_PATHS) { @@ -1750,10 +1702,7 @@ NAN_GETTER(Context2d::GetTextDrawingMode) { */ NAN_SETTER(Context2d::SetTextDrawingMode) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetTextDrawingMode called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetTextDrawingMode); Nan::Utf8String str(Nan::To(value).ToLocalChecked()); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); if (0 == strcmp("path", *str)) { @@ -1768,10 +1717,7 @@ NAN_SETTER(Context2d::SetTextDrawingMode) { */ NAN_GETTER(Context2d::GetQuality) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetQuality called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetQuality); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *filter; switch (cairo_pattern_get_filter(cairo_get_source(context->context()))) { @@ -1789,10 +1735,7 @@ NAN_GETTER(Context2d::GetQuality) { */ NAN_SETTER(Context2d::SetQuality) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetQuality called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetQuality); Nan::Utf8String str(Nan::To(value).ToLocalChecked()); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_filter_t filter; @@ -1855,10 +1798,7 @@ void parse_matrix_from_object(cairo_matrix_t &matrix, Local mat) { */ NAN_GETTER(Context2d::GetCurrentTransform) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetCurrentTransform called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetCurrentTransform); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Local instance = get_current_transform(context); @@ -1870,10 +1810,7 @@ NAN_GETTER(Context2d::GetCurrentTransform) { */ NAN_SETTER(Context2d::SetCurrentTransform) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetCurrentTransform called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetCurrentTransform); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Local ctx = Nan::GetCurrentContext(); Local mat = Nan::To(value).ToLocalChecked(); @@ -1895,10 +1832,7 @@ NAN_SETTER(Context2d::SetCurrentTransform) { */ NAN_GETTER(Context2d::GetFillStyle) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetFillStyle called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetFillStyle); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Isolate *iso = Isolate::GetCurrent(); Local style; @@ -1916,10 +1850,7 @@ NAN_GETTER(Context2d::GetFillStyle) { */ NAN_SETTER(Context2d::SetFillStyle) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetFillStyle called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetFillStyle); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); if (value->IsString()) { @@ -1947,10 +1878,7 @@ NAN_SETTER(Context2d::SetFillStyle) { */ NAN_GETTER(Context2d::GetStrokeStyle) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetStrokeStyle called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetStrokeStyle); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Local style; @@ -1967,10 +1895,7 @@ NAN_GETTER(Context2d::GetStrokeStyle) { */ NAN_SETTER(Context2d::SetStrokeStyle) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetStrokeStyle called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetStrokeStyle); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); if (value->IsString()) { @@ -1998,10 +1923,7 @@ NAN_SETTER(Context2d::SetStrokeStyle) { */ NAN_GETTER(Context2d::GetMiterLimit) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetMiterLimit called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetMiterLimit); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(cairo_get_miter_limit(context->context()))); } @@ -2011,10 +1933,7 @@ NAN_GETTER(Context2d::GetMiterLimit) { */ NAN_SETTER(Context2d::SetMiterLimit) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetMiterLimit called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetMiterLimit); double n = Nan::To(value).FromMaybe(0); if (n > 0) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -2027,10 +1946,7 @@ NAN_SETTER(Context2d::SetMiterLimit) { */ NAN_GETTER(Context2d::GetLineWidth) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetLineWidth called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetLineWidth); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(cairo_get_line_width(context->context()))); } @@ -2040,10 +1956,7 @@ NAN_GETTER(Context2d::GetLineWidth) { */ NAN_SETTER(Context2d::SetLineWidth) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetLineWidth called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetLineWidth); double n = Nan::To(value).FromMaybe(0); if (n > 0 && n != std::numeric_limits::infinity()) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -2056,10 +1969,7 @@ NAN_SETTER(Context2d::SetLineWidth) { */ NAN_GETTER(Context2d::GetLineJoin) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetLineJoin called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetLineJoin); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *join; switch (cairo_get_line_join(context->context())) { @@ -2075,10 +1985,7 @@ NAN_GETTER(Context2d::GetLineJoin) { */ NAN_SETTER(Context2d::SetLineJoin) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetLineJoin called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetLineJoin); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); Nan::Utf8String type(Nan::To(value).ToLocalChecked()); @@ -2096,10 +2003,7 @@ NAN_SETTER(Context2d::SetLineJoin) { */ NAN_GETTER(Context2d::GetLineCap) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetLineCap called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetLineCap); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *cap; switch (cairo_get_line_cap(context->context())) { @@ -2115,10 +2019,7 @@ NAN_GETTER(Context2d::GetLineCap) { */ NAN_SETTER(Context2d::SetLineCap) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetLineCap called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetLineCap); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); Nan::Utf8String type(Nan::To(value).ToLocalChecked()); @@ -2153,10 +2054,7 @@ NAN_METHOD(Context2d::IsPointInPath) { */ NAN_SETTER(Context2d::SetShadowColor) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetShadowColor called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetShadowColor); short ok; Nan::Utf8String str(Nan::To(value).ToLocalChecked()); uint32_t rgba = rgba_from_string(*str, &ok); @@ -2171,10 +2069,7 @@ NAN_SETTER(Context2d::SetShadowColor) { */ NAN_GETTER(Context2d::GetShadowColor) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetShadowColor called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetShadowColor); char buf[64]; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); rgba_to_string(context->state->shadow, buf, sizeof(buf)); @@ -2649,10 +2544,7 @@ NAN_METHOD(Context2d::MoveTo) { */ NAN_GETTER(Context2d::GetFont) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetFont called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetFont); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Isolate *iso = Isolate::GetCurrent(); Local font; @@ -2675,10 +2567,7 @@ NAN_GETTER(Context2d::GetFont) { */ NAN_SETTER(Context2d::SetFont) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetFont called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetFont); if (!value->IsString()) return; Isolate *iso = Isolate::GetCurrent(); @@ -2736,10 +2625,7 @@ NAN_SETTER(Context2d::SetFont) { */ NAN_GETTER(Context2d::GetTextBaseline) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetTextBaseline called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetTextBaseline); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char* baseline; switch (context->state->textBaseline) { @@ -2759,10 +2645,7 @@ NAN_GETTER(Context2d::GetTextBaseline) { */ NAN_SETTER(Context2d::SetTextBaseline) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetTextBaseline called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetTextBaseline); if (!value->IsString()) return; Nan::Utf8String opStr(Nan::To(value).ToLocalChecked()); @@ -2786,10 +2669,7 @@ NAN_SETTER(Context2d::SetTextBaseline) { */ NAN_GETTER(Context2d::GetTextAlign) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetTextAlign called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetTextAlign); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char* align; switch (context->state->textAlignment) { @@ -2809,10 +2689,7 @@ NAN_GETTER(Context2d::GetTextAlign) { */ NAN_SETTER(Context2d::SetTextAlign) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetTextAlign called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetTextAlign); if (!value->IsString()) return; Nan::Utf8String opStr(Nan::To(value).ToLocalChecked()); @@ -2975,10 +2852,7 @@ NAN_METHOD(Context2d::GetLineDash) { * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash */ NAN_SETTER(Context2d::SetLineDashOffset) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetLineDashOffset called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetLineDashOffset); double offset = Nan::To(value).FromMaybe(0); if (!std::isfinite(offset)) return; @@ -2996,10 +2870,7 @@ NAN_SETTER(Context2d::SetLineDashOffset) { * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash */ NAN_GETTER(Context2d::GetLineDashOffset) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetLineDashOffset called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetLineDashOffset); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); double offset; @@ -3486,3 +3357,5 @@ NAN_METHOD(Context2d::Ellipse) { } cairo_set_matrix(ctx, &save_matrix); } + +#undef CHECK_RECEIVER From 672104c1a4bd202e56d8837ef83ebf7aee2dfce2 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 29 Oct 2022 18:35:33 -0700 Subject: [PATCH 384/474] v2.10.2 --- CHANGELOG.md | 9 ++++----- package.json | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a666d5a23..f17f6c077 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,19 +8,18 @@ project adheres to [Semantic Versioning](http://semver.org/). (Unreleased) ================== ### Changed -* Improve performance and memory usage of `save()`/`restore()`. -* `save()`/`restore()` no longer have a maximum depth (previously 64 states). ### Added ### Fixed -* `textBaseline` and `textAlign` were not saved/restored by `save()`/`restore()`. ([#1936](https://github.com/Automattic/node-canvas/issues/2029)) 2.10.2 ================== ### Fixed * Fix `Assertion failed: (object->InternalFieldCount() > 0), function Unwrap, file nan_object_wrap.h, line 32.` ([#2025](https://github.com/Automattic/node-canvas/issues/2025)) -### Changed +* `textBaseline` and `textAlign` were not saved/restored by `save()`/`restore()`. ([#1936](https://github.com/Automattic/node-canvas/issues/2029)) * Update nan to v2.17.0 to ensure Node.js v18+ support. -* Implement valid `this` checks in all `SetAccessor` methods. +### Changed +* Improve performance and memory usage of `save()`/`restore()`. +* `save()`/`restore()` no longer have a maximum depth (previously 64 states). 2.10.1 ================== diff --git a/package.json b/package.json index cd6754ea4..bfc152224 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.10.1", + "version": "2.10.2", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 40b43822a478ceb251b5f86b29f2763208bd2f03 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Tue, 13 Dec 2022 23:15:07 -0500 Subject: [PATCH 385/474] use tailored types instead of extending DOM Fixes #1656 --- CHANGELOG.md | 1 + types/index.d.ts | 263 ++++++++++++++++++++++++++++++++++++----------- 2 files changed, 204 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f17f6c077..f1a61ef90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed +* Replace triple-slash directive in types with own types to avoid polluting TS modules with globals ([#1656](https://github.com/Automattic/node-canvas/issues/1656)) 2.10.2 ================== diff --git a/types/index.d.ts b/types/index.d.ts index 7b53f4851..3537fcf75 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,5 +1,4 @@ // TypeScript Version: 3.0 -/// import { Readable } from 'stream' @@ -80,7 +79,7 @@ export class Canvas { constructor(width: number, height: number, type?: 'image'|'pdf'|'svg') - getContext(contextId: '2d', contextAttributes?: NodeCanvasRenderingContext2DSettings): NodeCanvasRenderingContext2D + getContext(contextId: '2d', contextAttributes?: NodeCanvasRenderingContext2DSettings): CanvasRenderingContext2D /** * For image canvases, encodes the canvas as a PNG. For PDF canvases, @@ -128,19 +127,126 @@ export class Canvas { toDataURL(mimeType: 'image/jpeg', quality: number, cb: (err: Error|null, result: string) => void): void } -declare class NodeCanvasRenderingContext2D extends CanvasRenderingContext2D { +export interface TextMetrics { + readonly actualBoundingBoxAscent: number; + readonly actualBoundingBoxDescent: number; + readonly actualBoundingBoxLeft: number; + readonly actualBoundingBoxRight: number; + readonly fontBoundingBoxAscent: number; + readonly fontBoundingBoxDescent: number; + readonly width: number; +} + +export type CanvasFillRule = 'evenodd' | 'nonzero'; + +export type GlobalCompositeOperation = + | 'clear' + | 'copy' + | 'destination' + | 'source-over' + | 'destination-over' + | 'source-in' + | 'destination-in' + | 'source-out' + | 'destination-out' + | 'source-atop' + | 'destination-atop' + | 'xor' + | 'lighter' + | 'normal' + | 'multiply' + | 'screen' + | 'overlay' + | 'darken' + | 'lighten' + | 'color-dodge' + | 'color-burn' + | 'hard-light' + | 'soft-light' + | 'difference' + | 'exclusion' + | 'hue' + | 'saturation' + | 'color' + | 'luminosity' + | 'saturate'; + +export type CanvasLineCap = 'butt' | 'round' | 'square'; + +export type CanvasLineJoin = 'bevel' | 'miter' | 'round'; + +export type CanvasTextBaseline = 'alphabetic' | 'bottom' | 'hanging' | 'ideographic' | 'middle' | 'top'; + +export type CanvasTextAlign = 'center' | 'end' | 'left' | 'right' | 'start'; + +export class CanvasRenderingContext2D { + drawImage(image: Canvas|Image, dx: number, dy: number): void + drawImage(image: Canvas|Image, dx: number, dy: number, dw: number, dh: number): void + drawImage(image: Canvas|Image, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void + putImageData(imagedata: ImageData, dx: number, dy: number): void; + putImageData(imagedata: ImageData, dx: number, dy: number, dirtyX: number, dirtyY: number, dirtyWidth: number, dirtyHeight: number): void; + getImageData(sx: number, sy: number, sw: number, sh: number): ImageData; + createImageData(sw: number, sh: number): ImageData; + createImageData(imagedata: ImageData): ImageData; + /** + * For PDF canvases, adds another page. If width and/or height are omitted, + * the canvas's initial size is used. + */ + addPage(width?: number, height?: number): void + save(): void; + restore(): void; + rotate(angle: number): void; + translate(x: number, y: number): void; + transform(a: number, b: number, c: number, d: number, e: number, f: number): void; + getTransform(): DOMMatrix; + resetTransform(): void; + setTransform(transform?: DOMMatrix): void; + isPointInPath(x: number, y: number, fillRule?: CanvasFillRule): boolean; + scale(x: number, y: number): void; + clip(fillRule?: CanvasFillRule): void; + fill(fillRule?: CanvasFillRule): void; + stroke(): void; + fillText(text: string, x: number, y: number, maxWidth?: number): void; + strokeText(text: string, x: number, y: number, maxWidth?: number): void; + fillRect(x: number, y: number, w: number, h: number): void; + strokeRect(x: number, y: number, w: number, h: number): void; + clearRect(x: number, y: number, w: number, h: number): void; + rect(x: number, y: number, w: number, h: number): void; + roundRect(x: number, y: number, w: number, h: number, radii?: number | number[]): void; + measureText(text: string): TextMetrics; + moveTo(x: number, y: number): void; + lineTo(x: number, y: number): void; + bezierCurveTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): void; + quadraticCurveTo(cpx: number, cpy: number, x: number, y: number): void; + beginPath(): void; + closePath(): void; + arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, counterclockwise?: boolean): void; + arcTo(x1: number, y1: number, x2: number, y2: number, radius: number): void; + ellipse(x: number, y: number, radiusX: number, radiusY: number, rotation: number, startAngle: number, endAngle: number, counterclockwise?: boolean): void; + setLineDash(segments: number[]): void; + getLineDash(): number[]; + createPattern(image: Canvas|Image, repetition: 'repeat' | 'repeat-x' | 'repeat-y' | 'no-repeat' | '' | null): CanvasPattern + createLinearGradient(x0: number, y0: number, x1: number, y1: number): CanvasGradient; + createRadialGradient(x0: number, y0: number, r0: number, x1: number, y1: number, r1: number): CanvasGradient; /** * _Non-standard_. Defaults to 'good'. Affects pattern (gradient, image, * etc.) rendering quality. */ patternQuality: 'fast' | 'good' | 'best' | 'nearest' | 'bilinear' - - /** - * _Non-standard_. Defaults to 'good'. Like `patternQuality`, but applies to - * transformations affecting more than just patterns. - */ - quality: 'fast' | 'good' | 'best' | 'nearest' | 'bilinear' - + imageSmoothingEnabled: boolean; + globalCompositeOperation: GlobalCompositeOperation; + globalAlpha: number; + shadowColor: string; + miterLimit: number; + lineWidth: number; + lineCap: CanvasLineCap; + lineJoin: CanvasLineJoin; + lineDashOffset: number; + shadowOffsetX: number; + shadowOffsetY: number; + shadowBlur: number; + /** _Non-standard_. Sets the antialiasing mode. */ + antialias: 'default' | 'gray' | 'none' | 'subpixel' /** * Defaults to 'path'. The effect depends on the canvas type: * @@ -165,55 +271,27 @@ declare class NodeCanvasRenderingContext2D extends CanvasRenderingContext2D { * (aside from using the stroke and fill style, respectively). */ textDrawingMode: 'path' | 'glyph' - - /** _Non-standard_. Sets the antialiasing mode. */ - antialias: 'default' | 'gray' | 'none' | 'subpixel' - - // Standard, but not in the TS lib and needs node-canvas class return type. - /** Returns or sets a `DOMMatrix` for the current transformation matrix. */ - currentTransform: NodeCanvasDOMMatrix - - // Standard, but need node-canvas class versions: - getTransform(): NodeCanvasDOMMatrix - setTransform(a: number, b: number, c: number, d: number, e: number, f: number): void - setTransform(transform?: NodeCanvasDOMMatrix): void - createImageData(sw: number, sh: number): NodeCanvasImageData - createImageData(imagedata: NodeCanvasImageData): NodeCanvasImageData - getImageData(sx: number, sy: number, sw: number, sh: number): NodeCanvasImageData - putImageData(imagedata: NodeCanvasImageData, dx: number, dy: number): void - putImageData(imagedata: NodeCanvasImageData, dx: number, dy: number, dirtyX: number, dirtyY: number, dirtyWidth: number, dirtyHeight: number): void - drawImage(image: Canvas|Image, dx: number, dy: number): void - drawImage(image: Canvas|Image, dx: number, dy: number, dw: number, dh: number): void - drawImage(image: Canvas|Image, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void /** - * **Do not use this overload. Use one of the other three overloads.** This - * is a catch-all definition required for compatibility with the base - * `CanvasRenderingContext2D` interface. - */ - drawImage(...args: any[]): void - createPattern(image: Canvas|Image, repetition: 'repeat' | 'repeat-x' | 'repeat-y' | 'no-repeat' | '' | null): NodeCanvasCanvasPattern - /** - * **Do not use this overload. Use the other three overload.** This is a - * catch-all definition required for compatibility with the base - * `CanvasRenderingContext2D` interface. - */ - createPattern(...args: any[]): NodeCanvasCanvasPattern - createLinearGradient(x0: number, y0: number, x1: number, y1: number): NodeCanvasCanvasGradient; - createRadialGradient(x0: number, y0: number, r0: number, x1: number, y1: number, r1: number): NodeCanvasCanvasGradient; - - /** - * For PDF canvases, adds another page. If width and/or height are omitted, - * the canvas's initial size is used. + * _Non-standard_. Defaults to 'good'. Like `patternQuality`, but applies to + * transformations affecting more than just patterns. */ - addPage(width?: number, height?: number): void + quality: 'fast' | 'good' | 'best' | 'nearest' | 'bilinear' + /** Returns or sets a `DOMMatrix` for the current transformation matrix. */ + currentTransform: DOMMatrix + fillStyle: string | CanvasGradient | CanvasPattern; + strokeStyle: string | CanvasGradient | CanvasPattern; + font: string; + textBaseline: CanvasTextBaseline; + textAlign: CanvasTextAlign; } -export { NodeCanvasRenderingContext2D as CanvasRenderingContext2D } -declare class NodeCanvasCanvasGradient extends CanvasGradient {} -export { NodeCanvasCanvasGradient as CanvasGradient } +export class CanvasGradient { + addColorStop(offset: number, color: string): void; +} -declare class NodeCanvasCanvasPattern extends CanvasPattern {} -export { NodeCanvasCanvasPattern as CanvasPattern } +export class CanvasPattern { + setTransform(transform?: DOMMatrix): void; +} // This does not extend HTMLImageElement because there are dozens of inherited // methods and properties that we do not provide. @@ -312,14 +390,79 @@ export class JPEGStream extends Readable {} /** This class must not be constructed directly; use `canvas.createPDFStream()`. */ export class PDFStream extends Readable {} -declare class NodeCanvasDOMMatrix extends DOMMatrix {} -export { NodeCanvasDOMMatrix as DOMMatrix } +export class DOMPoint { + w: number; + x: number; + y: number; + z: number; +} -declare class NodeCanvasDOMPoint extends DOMPoint {} -export { NodeCanvasDOMPoint as DOMPoint } +export class DOMMatrix { + constructor(init: string | number[]); + toString(): string; + multiply(other?: DOMMatrix): DOMMatrix; + multiplySelf(other?: DOMMatrix): DOMMatrix; + preMultiplySelf(other?: DOMMatrix): DOMMatrix; + translate(tx?: number, ty?: number, tz?: number): DOMMatrix; + translateSelf(tx?: number, ty?: number, tz?: number): DOMMatrix; + scale(scaleX?: number, scaleY?: number, scaleZ?: number, originX?: number, originY?: number, originZ?: number): DOMMatrix; + scale3d(scale?: number, originX?: number, originY?: number, originZ?: number): DOMMatrix; + scale3dSelf(scale?: number, originX?: number, originY?: number, originZ?: number): DOMMatrix; + scaleSelf(scaleX?: number, scaleY?: number, scaleZ?: number, originX?: number, originY?: number, originZ?: number): DOMMatrix; + rotateFromVector(x?: number, y?: number): DOMMatrix; + rotateFromVectorSelf(x?: number, y?: number): DOMMatrix; + rotate(rotX?: number, rotY?: number, rotZ?: number): DOMMatrix; + rotateSelf(rotX?: number, rotY?: number, rotZ?: number): DOMMatrix; + rotateAxisAngle(x?: number, y?: number, z?: number, angle?: number): DOMMatrix; + rotateAxisAngleSelf(x?: number, y?: number, z?: number, angle?: number): DOMMatrix; + skewX(sx?: number): DOMMatrix; + skewXSelf(sx?: number): DOMMatrix; + skewY(sy?: number): DOMMatrix; + skewYSelf(sy?: number): DOMMatrix; + flipX(): DOMMatrix; + flipY(): DOMMatrix; + inverse(): DOMMatrix; + invertSelf(): DOMMatrix; + setMatrixValue(transformList: string): DOMMatrix; + transformPoint(point?: DOMPoint): DOMPoint; + toFloat32Array(): Float32Array; + toFloat64Array(): Float64Array; + readonly is2D: boolean; + readonly isIdentity: boolean; + a: number; + b: number; + c: number; + d: number; + e: number; + f: number; + m11: number; + m12: number; + m13: number; + m14: number; + m21: number; + m22: number; + m23: number; + m24: number; + m31: number; + m32: number; + m33: number; + m34: number; + m41: number; + m42: number; + m43: number; + m44: number; + static fromMatrix(other: DOMMatrix): DOMMatrix; + static fromFloat32Array(a: Float32Array): DOMMatrix; + static fromFloat64Array(a: Float64Array): DOMMatrix; +} -declare class NodeCanvasImageData extends ImageData {} -export { NodeCanvasImageData as ImageData } +export class ImageData { + constructor(sw: number, sh: number); + constructor(data: Uint8ClampedArray, sw: number, sh?: number); + readonly data: Uint8ClampedArray; + readonly height: number; + readonly width: number; +} // This is marked private, but is exported... // export function parseFont(description: string): object From fc160f5d3a4bc1171fa012391dda923561fb497e Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 22 Dec 2022 09:07:02 -0800 Subject: [PATCH 386/474] v2.11.0 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1a61ef90..75e335c0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed + +2.11.0 +================== +### Fixed * Replace triple-slash directive in types with own types to avoid polluting TS modules with globals ([#1656](https://github.com/Automattic/node-canvas/issues/1656)) 2.10.2 diff --git a/package.json b/package.json index bfc152224..c45c4ea4d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.10.2", + "version": "2.11.0", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 9f313dffb285218937b6ca12bb2637599439bb37 Mon Sep 17 00:00:00 2001 From: Suyooo Date: Sat, 24 Dec 2022 11:47:23 +0100 Subject: [PATCH 387/474] Add canvas property to CanvasRenderingContext2D type --- CHANGELOG.md | 1 + types/index.d.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75e335c0a..484dca949 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed +* Add missing property `canvas` to the `CanvasRenderingContext2D` type 2.11.0 ================== diff --git a/types/index.d.ts b/types/index.d.ts index 3537fcf75..8bcfd105e 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -283,6 +283,7 @@ export class CanvasRenderingContext2D { font: string; textBaseline: CanvasTextBaseline; textAlign: CanvasTextAlign; + canvas: Canvas; } export class CanvasGradient { From 55d39cb532a3a8e828516603d7bbc9fafe5c190b Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Mon, 2 Jan 2023 18:40:33 -0500 Subject: [PATCH 388/474] fix macos CI --- .github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 31b7497bc..89916e479 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -66,6 +66,7 @@ jobs: - name: Install Dependencies run: | brew update + brew install python3 || : # python doesn't need to be linked brew install pkg-config cairo pango libpng jpeg giflib librsvg - name: Install run: npm install --build-from-source From 4c276e07f570ee6a727f88dcc67251823895c671 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Mon, 2 Jan 2023 17:50:32 -0500 Subject: [PATCH 389/474] fix incorrect text width with newer (1.43?) Pango Text renders wider or narrower, as if letter-spacing is set, than it should with newer versions of Pango. My best understanding of this problem is that around version 1.43 Pango dropped support for font hinting because it switched to Harbuzz for glyph postions instead of Freetype. For some reason it still rounds the glyph positions by default. There's no need for node-canvas to support font hinting. The maintainers of the Linux font stack (Behdad and Matthias) have stated that they wont, and font hinting is subjective, and browsers have moved to subpixel positioning too. Reading (warning: lots of drama to wade through): - https://gitlab.gnome.org/GNOME/pango/-/issues/404 - https://gitlab.gnome.org/GNOME/pango/-/issues/463 - https://github.com/harfbuzz/harfbuzz/issues/1892 - https://github.com/harfbuzz/harfbuzz/issues/2394 --- CHANGELOG.md | 1 + src/CanvasRenderingContext2d.cc | 7 +++++++ test/public/tests.js | 5 +++++ 3 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 484dca949..4222c6ffd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Added ### Fixed * Add missing property `canvas` to the `CanvasRenderingContext2D` type +* Fixed glyph positions getting rounded, resulting text having a slight `letter-spacing` effect 2.11.0 ================== diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 699ec88fc..c40b00fc2 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -171,6 +171,13 @@ Context2d::Context2d(Canvas *canvas) { _canvas = canvas; _context = canvas->createCairoContext(); _layout = pango_cairo_create_layout(_context); + + // As of January 2023, Pango rounds glyph positions which renders text wider + // or narrower than the browser. See #2184 for more information +#if PANGO_VERSION_CHECK(1, 44, 0) + pango_context_set_round_glyph_positions(pango_layout_get_context(_layout), FALSE); +#endif + states.emplace(); state = &states.top(); pango_layout_set_font_description(_layout, state->fontDescription); diff --git a/test/public/tests.js b/test/public/tests.js index 651105e36..d24202602 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -2693,6 +2693,11 @@ tests['measureText()'] = function (ctx) { drawWithBBox('right', 195, 195) } +tests['glyph advances (#2184)'] = function (ctx) { + ctx.font = '8px Arial' + ctx.fillText('A float is a box that is shifted to the left or right on the current line.', 0, 8) +} + tests['image sampling (#1084)'] = function (ctx, done) { let loaded1, loaded2 const img1 = new Image() From fdf709a7b08abae33a93c510b96f71df6c13c7b0 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sat, 7 Jan 2023 14:39:53 -0500 Subject: [PATCH 390/474] move ctx.font string to the state struct fixes #1946 --- CHANGELOG.md | 1 + src/CanvasRenderingContext2d.cc | 12 ++---------- src/CanvasRenderingContext2d.h | 3 ++- test/canvas.test.js | 2 +- 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4222c6ffd..11023e97c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Fixed * Add missing property `canvas` to the `CanvasRenderingContext2D` type * Fixed glyph positions getting rounded, resulting text having a slight `letter-spacing` effect +* Fixed `ctx.font` not being restored correctly after `ctx.restore()` (#1946) 2.11.0 ================== diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index c40b00fc2..b99c01602 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -207,7 +207,6 @@ void Context2d::resetState() { void Context2d::_resetPersistentHandles() { _fillStyle.Reset(); _strokeStyle.Reset(); - _font.Reset(); } /* @@ -2553,15 +2552,8 @@ NAN_METHOD(Context2d::MoveTo) { NAN_GETTER(Context2d::GetFont) { CHECK_RECEIVER(Context2d.GetFont); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Isolate *iso = Isolate::GetCurrent(); - Local font; - - if (context->_font.IsEmpty()) - font = Nan::New("10px sans-serif").ToLocalChecked(); - else - font = context->_font.Get(iso); - info.GetReturnValue().Set(font); + info.GetReturnValue().Set(Nan::New(context->state->font).ToLocalChecked()); } /* @@ -2624,7 +2616,7 @@ NAN_SETTER(Context2d::SetFont) { context->state->fontDescription = sys_desc; pango_layout_set_font_description(context->_layout, sys_desc); - context->_font.Reset(value); + context->state->font = *Nan::Utf8String(value); } /* diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index 568ebc8cc..8ea4d60b8 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -27,6 +27,7 @@ struct canvas_state_t { cairo_pattern_t* fillGradient = nullptr; cairo_pattern_t* strokeGradient = nullptr; PangoFontDescription* fontDescription = nullptr; + std::string font = "10px sans-serif"; cairo_filter_t patternQuality = CAIRO_FILTER_GOOD; float globalAlpha = 1.f; int shadowBlur = 0; @@ -57,6 +58,7 @@ struct canvas_state_t { shadowOffsetY = other.shadowOffsetY; textDrawingMode = other.textDrawingMode; fontDescription = pango_font_description_copy(other.fontDescription); + font = other.font; imageSmoothingEnabled = other.imageSmoothingEnabled; } @@ -216,7 +218,6 @@ class Context2d : public Nan::ObjectWrap { void _setStrokePattern(v8::Local arg); Nan::Persistent _fillStyle; Nan::Persistent _strokeStyle; - Nan::Persistent _font; Canvas *_canvas; cairo_t *_context; cairo_path_t *_path; diff --git a/test/canvas.test.js b/test/canvas.test.js index af33befaa..083459ab6 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -1918,7 +1918,7 @@ describe('Canvas', function () { ['shadowBlur', 5], ['shadowColor', '#ff0000'], ['globalCompositeOperation', 'copy'], - // ['font', '25px serif'], // TODO #1946 + ['font', '25px serif'], ['textAlign', 'center'], ['textBaseline', 'bottom'], // Added vs. WPT From 9ecfb70518889735ad61354824c4590403f5edaa Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Mon, 20 Mar 2023 21:51:35 -0400 Subject: [PATCH 391/474] v2.11.1 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11023e97c..396cd7ff0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed + +2.11.1 +================== +### Fixed * Add missing property `canvas` to the `CanvasRenderingContext2D` type * Fixed glyph positions getting rounded, resulting text having a slight `letter-spacing` effect * Fixed `ctx.font` not being restored correctly after `ctx.restore()` (#1946) diff --git a/package.json b/package.json index c45c4ea4d..29025e2d8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.11.0", + "version": "2.11.1", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 9910243d84941a8c6ced459f46abd1971ebf287b Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sat, 25 Mar 2023 13:39:04 -0400 Subject: [PATCH 392/474] fix not compiling on certain windows versions --- src/register_font.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/register_font.cc b/src/register_font.cc index ae44c9aba..e43dd8125 100644 --- a/src/register_font.cc +++ b/src/register_font.cc @@ -264,7 +264,7 @@ get_pango_font_description(unsigned char* filepath) { FILE_SHARE_READ, NULL, OPEN_EXISTING, - NULL, + FILE_ATTRIBUTE_NORMAL, NULL ); if(!hFile){ From 38e0a3285a6e005e02a6505f3fc2809d0484e43b Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Mon, 27 Mar 2023 20:16:47 -0400 Subject: [PATCH 393/474] v2.11.2 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 396cd7ff0..cbbdb8254 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Added ### Fixed +2.11.2 +================== +### Fixed +* Building on Windows in CI (and maybe other Windows configurations?) (#2216) + 2.11.1 ================== ### Fixed diff --git a/package.json b/package.json index 29025e2d8..77e72328c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.11.1", + "version": "2.11.2", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From f3184ba9da2737dbe25275631678a7ef5924fe6b Mon Sep 17 00:00:00 2001 From: Mohammed Keyvanzadeh Date: Thu, 13 Apr 2023 20:09:08 +0330 Subject: [PATCH 394/474] src: refactor and apply fixes - Defer the initialization of the `op` variable to the `default` switch case to avoid a compiler warning. - Use a `default` switch case with a null statement if some enum values aren't suppsed to be handled, this avoids a compiler warning. - Migrate from librsvg's deprecated `rsvg_handle_get_dimensions()` and `rsvg_handle_render_cairo()` functions to the new `rsvg_handle_get_intrinsic_size_in_pixels()` and `rsvg_handle_render_document()` respectively. - Avoid calling virtual methods in constructors/destructors to avoid bypassing virtual dispatch. - Remove unused private field `backend` in the `Backend` class. - Fix a case of use-after-free. - Fix usage of garbage value by filling the allocated memory entirely with zeros if it's not modified. - Fix a potential memory leak. --- CHANGELOG.md | 8 ++++++++ src/Canvas.cc | 3 ++- src/CanvasRenderingContext2d.cc | 15 ++++++++++++--- src/Image.cc | 18 +++++++++++++----- src/backend/Backend.cc | 5 ++--- src/backend/Backend.h | 1 - src/backend/PdfBackend.cc | 2 +- src/backend/SvgBackend.cc | 2 +- src/register_font.cc | 1 + 9 files changed, 40 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbbdb8254..be424e687 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,16 @@ project adheres to [Semantic Versioning](http://semver.org/). (Unreleased) ================== ### Changed +* Defer the initialization of the `op` variable to the `default` switch case to avoid a compiler warning. (#2229) +* Use a `default` switch case with a null statement if some enum values aren't suppsed to be handled, this avoids a compiler warning. (#2229) +* Migrate from librsvg's deprecated `rsvg_handle_get_dimensions()` and `rsvg_handle_render_cairo()` functions to the new `rsvg_handle_get_intrinsic_size_in_pixels()` and `rsvg_handle_render_document()` respectively. (#2229) +* Avoid calling virtual methods in constructors/destructors to avoid bypassing virtual dispatch. (#2229) +* Remove unused private field `backend` in the `Backend` class. (#2229) ### Added ### Fixed +* Fix a case of use-after-free. (#2229) +* Fix usage of garbage value by filling the allocated memory entirely with zeros if it's not modified. (#2229) +* Fix a potential memory leak. (#2229) 2.11.2 ================== diff --git a/src/Canvas.cc b/src/Canvas.cc index 860d5bb46..0cfe750d6 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -133,8 +133,9 @@ NAN_METHOD(Canvas::New) { } if (!backend->isSurfaceValid()) { + const char *error = backend->getError(); delete backend; - return Nan::ThrowError(backend->getError()); + return Nan::ThrowError(error); } Canvas* canvas = new Canvas(backend); diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index b99c01602..5bfe08d6a 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -607,8 +607,8 @@ Context2d::blur(cairo_surface_t *surface, int radius) { // get width, height int width = cairo_image_surface_get_width( surface ); int height = cairo_image_surface_get_height( surface ); - unsigned* precalc = - (unsigned*)malloc(width*height*sizeof(unsigned)); + const unsigned int size = width * height * sizeof(unsigned); + unsigned* precalc = (unsigned*)malloc(size); cairo_surface_flush( surface ); unsigned char* src = cairo_image_surface_get_data( surface ); double mul=1.f/((radius*2)*(radius*2)); @@ -627,6 +627,8 @@ Context2d::blur(cairo_surface_t *surface, int radius) { unsigned char* pix = src; unsigned* pre = precalc; + bool modified = false; + pix += channel; for (y=0;y0) tot+=pre[-width]; if (x>0 && y>0) tot-=pre[-width-1]; *pre++=tot; + if (!modified) modified = true; pix += 4; } } + if (!modified) { + memset(precalc, 0, size); + } + // blur step. pix = src + (int)radius * width * 4 + (int)radius * 4 + channel; for (y=radius;y(info.This()); cairo_t *ctx = context->context(); - const char *op = "source-over"; + const char *op{}; switch (cairo_get_operator(ctx)) { // composite modes: case CAIRO_OPERATOR_CLEAR: op = "clear"; break; @@ -1469,6 +1476,7 @@ NAN_GETTER(Context2d::GetGlobalCompositeOperation) { case CAIRO_OPERATOR_HSL_LUMINOSITY: op = "luminosity"; break; // non-standard: case CAIRO_OPERATOR_SATURATE: op = "saturate"; break; + default: op = "source-over"; } info.GetReturnValue().Set(Nan::New(op).ToLocalChecked()); @@ -2507,6 +2515,7 @@ Context2d::setTextPath(double x, double y) { pango_layout_get_pixel_extents(_layout, NULL, &logical_rect); x -= logical_rect.width; break; + default: ; } y -= getBaselineAdjustment(_layout, state->textBaseline); diff --git a/src/Image.cc b/src/Image.cc index 35ee7947a..301257769 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -1192,11 +1192,13 @@ Image::loadSVGFromBuffer(uint8_t *buf, unsigned len) { return CAIRO_STATUS_READ_ERROR; } - RsvgDimensionData *dims = new RsvgDimensionData(); - rsvg_handle_get_dimensions(_rsvg, dims); + double d_width; + double d_height; - width = naturalWidth = dims->width; - height = naturalHeight = dims->height; + rsvg_handle_get_intrinsic_size_in_pixels(_rsvg, &d_width, &d_height); + + width = naturalWidth = d_width; + height = naturalHeight = d_height; status = renderSVGToSurface(); if (status != CAIRO_STATUS_SUCCESS) { @@ -1232,7 +1234,13 @@ Image::renderSVGToSurface() { return status; } - gboolean render_ok = rsvg_handle_render_cairo(_rsvg, cr); + RsvgRectangle viewport = { + .x = 0, + .y = 0, + .width = static_cast(width), + .height = static_cast(height), + }; + gboolean render_ok = rsvg_handle_render_document(_rsvg, cr, &viewport, nullptr); if (!render_ok) { g_object_unref(_rsvg); cairo_destroy(cr); diff --git a/src/backend/Backend.cc b/src/backend/Backend.cc index 528f61a08..9f2b39dd3 100644 --- a/src/backend/Backend.cc +++ b/src/backend/Backend.cc @@ -9,7 +9,7 @@ Backend::Backend(std::string name, int width, int height) Backend::~Backend() { - this->destroySurface(); + Backend::destroySurface(); } void Backend::init(const Nan::FunctionCallbackInfo &info) { @@ -97,8 +97,7 @@ bool Backend::isSurfaceValid(){ BackendOperationNotAvailable::BackendOperationNotAvailable(Backend* backend, std::string operation_name) - : backend(backend) - , operation_name(operation_name) + : operation_name(operation_name) { msg = "operation " + operation_name + " not supported by backend " + backend->getName(); diff --git a/src/backend/Backend.h b/src/backend/Backend.h index df65194b9..f8448c41a 100644 --- a/src/backend/Backend.h +++ b/src/backend/Backend.h @@ -57,7 +57,6 @@ class Backend : public Nan::ObjectWrap class BackendOperationNotAvailable: public std::exception { private: - Backend* backend; std::string operation_name; std::string msg; diff --git a/src/backend/PdfBackend.cc b/src/backend/PdfBackend.cc index d8bd23422..fe831a68d 100644 --- a/src/backend/PdfBackend.cc +++ b/src/backend/PdfBackend.cc @@ -8,7 +8,7 @@ using namespace v8; PdfBackend::PdfBackend(int width, int height) : Backend("pdf", width, height) { - createSurface(); + PdfBackend::createSurface(); } PdfBackend::~PdfBackend() { diff --git a/src/backend/SvgBackend.cc b/src/backend/SvgBackend.cc index 10bf4caa7..7d4181fc2 100644 --- a/src/backend/SvgBackend.cc +++ b/src/backend/SvgBackend.cc @@ -9,7 +9,7 @@ using namespace v8; SvgBackend::SvgBackend(int width, int height) : Backend("svg", width, height) { - createSurface(); + SvgBackend::createSurface(); } SvgBackend::~SvgBackend() { diff --git a/src/register_font.cc b/src/register_font.cc index e43dd8125..37182c0ac 100644 --- a/src/register_font.cc +++ b/src/register_font.cc @@ -304,6 +304,7 @@ get_pango_font_description(unsigned char* filepath) { } pango_font_description_set_family_static(desc, family); + free(family); pango_font_description_set_weight(desc, get_pango_weight(table->usWeightClass)); pango_font_description_set_stretch(desc, get_pango_stretch(table->usWidthClass)); pango_font_description_set_style(desc, get_pango_style(face->style_flags)); From 59022195f7efc02205c7da50224c392e057b597e Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 18 Apr 2023 02:54:52 +0800 Subject: [PATCH 395/474] add string tags for browser polyglot classes (#2214) * add string tags * set toStringTag property to configurable: true --- CHANGELOG.md | 1 + lib/bindings.js | 30 ++++++++++++++++++++++++ test/canvas.test.js | 22 ++++++++++++++++- test/image.test.js | 5 ++++ test/imageData.test.js | 5 ++++ test/wpt/generated/the-canvas-element.js | 6 +++++ 6 files changed, 68 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be424e687..b3ba9a13b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * Avoid calling virtual methods in constructors/destructors to avoid bypassing virtual dispatch. (#2229) * Remove unused private field `backend` in the `Backend` class. (#2229) ### Added +* Added string tags to support class detection ### Fixed * Fix a case of use-after-free. (#2229) * Fix usage of garbage value by filling the allocated memory entirely with zeros if it's not modified. (#2229) diff --git a/lib/bindings.js b/lib/bindings.js index c638a5878..40cef3c69 100644 --- a/lib/bindings.js +++ b/lib/bindings.js @@ -4,10 +4,40 @@ const bindings = require('../build/Release/canvas.node') module.exports = bindings +Object.defineProperty(bindings.Canvas.prototype, Symbol.toStringTag, { + value: 'HTMLCanvasElement', + configurable: true +}) + +Object.defineProperty(bindings.Image.prototype, Symbol.toStringTag, { + value: 'HTMLImageElement', + configurable: true +}) + bindings.ImageData.prototype.toString = function () { return '[object ImageData]' } +Object.defineProperty(bindings.ImageData.prototype, Symbol.toStringTag, { + value: 'ImageData', + configurable: true +}) + bindings.CanvasGradient.prototype.toString = function () { return '[object CanvasGradient]' } + +Object.defineProperty(bindings.CanvasGradient.prototype, Symbol.toStringTag, { + value: 'CanvasGradient', + configurable: true +}) + +Object.defineProperty(bindings.CanvasPattern.prototype, Symbol.toStringTag, { + value: 'CanvasPattern', + configurable: true +}) + +Object.defineProperty(bindings.CanvasRenderingContext2d.prototype, Symbol.toStringTag, { + value: 'CanvasRenderingContext2d', + configurable: true +}) diff --git a/test/canvas.test.js b/test/canvas.test.js index 083459ab6..9573688f5 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -1410,6 +1410,14 @@ describe('Canvas', function () { assert.strictEqual(pattern.toString(), '[object CanvasPattern]') }) + it('CanvasPattern has class string of `CanvasPattern`', async function () { + const img = await loadImage(path.join(__dirname, '/fixtures/checkers.png')); + const canvas = createCanvas(20, 20) + const ctx = canvas.getContext('2d') + const pattern = ctx.createPattern(img) + assert.strictEqual(Object.prototype.toString.call(pattern), '[object CanvasPattern]') + }) + it('Context2d#createLinearGradient()', function () { const canvas = createCanvas(20, 1) const ctx = canvas.getContext('2d') @@ -1439,6 +1447,11 @@ describe('Canvas', function () { assert.equal(0, imageData.data[i + 2]) assert.equal(255, imageData.data[i + 3]) }) + it('Canvas has class string of `HTMLCanvasElement`', function () { + const canvas = createCanvas(20, 1) + + assert.strictEqual(Object.prototype.toString.call(canvas), '[object HTMLCanvasElement]') + }) it('CanvasGradient stringifies as [object CanvasGradient]', function () { const canvas = createCanvas(20, 1) @@ -1447,6 +1460,13 @@ describe('Canvas', function () { assert.strictEqual(gradient.toString(), '[object CanvasGradient]') }) + it('CanvasGradient has class string of `CanvasGradient`', function () { + const canvas = createCanvas(20, 1) + const ctx = canvas.getContext('2d') + const gradient = ctx.createLinearGradient(1, 1, 19, 1) + assert.strictEqual(Object.prototype.toString.call(gradient), '[object CanvasGradient]') + }) + describe('Context2d#putImageData()', function () { it('throws for invalid arguments', function () { const canvas = createCanvas(2, 1) @@ -1943,7 +1963,7 @@ describe('Canvas', function () { ctx[k] = v ctx.restore() assert.strictEqual(ctx[k], old) - + // save() doesn't modify the value: ctx[k] = v old = ctx[k] diff --git a/test/image.test.js b/test/image.test.js index 8d54dd90f..ec1631a10 100644 --- a/test/image.test.js +++ b/test/image.test.js @@ -30,6 +30,11 @@ describe('Image', function () { assert(Image.prototype.hasOwnProperty('width')) }) + it('Image has class string of `HTMLImageElement`', async function () { + const img = new Image() + assert.strictEqual(Object.prototype.toString.call(img), '[object HTMLImageElement]') + }) + it('loads JPEG image', function () { return loadImage(jpgFace).then((img) => { assert.strictEqual(img.onerror, null) diff --git a/test/imageData.test.js b/test/imageData.test.js index d3c84c29a..04b117b45 100644 --- a/test/imageData.test.js +++ b/test/imageData.test.js @@ -17,6 +17,11 @@ describe('ImageData', function () { assert.strictEqual(imageData.toString(), '[object ImageData]') }) + it('gives class string as `ImageData`', function () { + const imageData = createImageData(2, 3) + assert.strictEqual(Object.prototype.toString.call(imageData), '[object ImageData]') + }) + it('should throw with invalid numeric arguments', function () { assert.throws(() => { createImageData(0, 0) }, /width is zero/) assert.throws(() => { createImageData(1, 0) }, /height is zero/) diff --git a/test/wpt/generated/the-canvas-element.js b/test/wpt/generated/the-canvas-element.js index 8b1a6817e..cea4fd9b4 100644 --- a/test/wpt/generated/the-canvas-element.js +++ b/test/wpt/generated/the-canvas-element.js @@ -171,6 +171,12 @@ describe("WPT: the-canvas-element", function () { assert.strictEqual(window.CanvasRenderingContext2D.prototype.fill, undefined, "window.CanvasRenderingContext2D.prototype.fill", "undefined") }); + it("2d.type class string", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + assert.strictEqual(Object.prototype.toString.call(ctx), '[object CanvasRenderingContext2D]') + }) + it("2d.type.replace", function () { // Interface methods can be overridden const canvas = createCanvas(100, 50); From 5bd5b243c4353fb9bdf03287e993104964cc0c39 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Tue, 18 Apr 2023 04:44:15 +0200 Subject: [PATCH 396/474] Upgrade GitHub Actions to use checkout@v3 (#2213) * Upgrade GitHub Actions * Fix typo * Update ci.yaml --- .github/workflows/ci.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 89916e479..613c91740 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,7 +18,7 @@ jobs: - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install Dependencies run: | sudo apt update @@ -38,7 +38,7 @@ jobs: - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install Dependencies run: | Invoke-WebRequest "https://ftp-osl.osuosl.org/pub/gnome/binaries/win64/gtk+/2.22/gtk+-bundle_2.22.1-20101229_win64.zip" -OutFile "gtk.zip" @@ -62,7 +62,7 @@ jobs: - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install Dependencies run: | brew update @@ -80,7 +80,7 @@ jobs: - uses: actions/setup-node@v3 with: node-version: 14 - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install run: npm install --ignore-scripts - name: Lint From adf73ee39e2676b5c67b02bef5742b941033495c Mon Sep 17 00:00:00 2001 From: Hubert Argasinski Date: Thu, 27 Apr 2023 12:41:56 -0400 Subject: [PATCH 397/474] Add node 20 to CI * prebuild binaries for node 20 * update changelog * update changelog --- .github/workflows/ci.yaml | 8 ++++---- .github/workflows/prebuild.yaml | 6 +++--- CHANGELOG.md | 1 + 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 613c91740..c21812da6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -7,13 +7,13 @@ on: paths-ignore: - ".github/workflows/prebuild.yaml" -jobs: +jobs: Linux: name: Test on Linux runs-on: ubuntu-latest strategy: matrix: - node: [10, 12, 14, 16, 18] + node: [10, 12, 14, 16, 18, 20] steps: - uses: actions/setup-node@v3 with: @@ -33,7 +33,7 @@ jobs: runs-on: windows-2019 strategy: matrix: - node: [10, 12, 14, 16, 18] + node: [10, 12, 14, 16, 18, 20] steps: - uses: actions/setup-node@v3 with: @@ -57,7 +57,7 @@ jobs: runs-on: macos-latest strategy: matrix: - node: [10, 12, 14, 16, 18] + node: [10, 12, 14, 16, 18, 20] steps: - uses: actions/setup-node@v3 with: diff --git a/.github/workflows/prebuild.yaml b/.github/workflows/prebuild.yaml index a112eef87..784069e06 100644 --- a/.github/workflows/prebuild.yaml +++ b/.github/workflows/prebuild.yaml @@ -24,7 +24,7 @@ jobs: Linux: strategy: matrix: - node: [8, 9, 10, 11, 12, 13, 14, 16, 18] + node: [8, 9, 10, 11, 12, 13, 14, 16, 18, 20] canvas_tag: [] # e.g. "v2.6.1" name: ${{ matrix.canvas_tag}}, Node.js ${{ matrix.node }}, Linux runs-on: ubuntu-latest @@ -97,7 +97,7 @@ jobs: macOS: strategy: matrix: - node: [8, 9, 10, 11, 12, 13, 14, 16, 18] + node: [8, 9, 10, 11, 12, 13, 14, 16, 18, 20] canvas_tag: [] # e.g. "v2.6.1" name: ${{ matrix.canvas_tag}}, Node.js ${{ matrix.node }}, macOS runs-on: macos-latest @@ -163,7 +163,7 @@ jobs: Win: strategy: matrix: - node: [8, 9, 10, 11, 12, 13, 14, 16, 18] + node: [8, 9, 10, 11, 12, 13, 14, 16, 18, 20] canvas_tag: [] # e.g. "v2.6.1" name: ${{ matrix.canvas_tag}}, Node.js ${{ matrix.node }}, Windows runs-on: windows-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index b3ba9a13b..e0768f8c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * Migrate from librsvg's deprecated `rsvg_handle_get_dimensions()` and `rsvg_handle_render_cairo()` functions to the new `rsvg_handle_get_intrinsic_size_in_pixels()` and `rsvg_handle_render_document()` respectively. (#2229) * Avoid calling virtual methods in constructors/destructors to avoid bypassing virtual dispatch. (#2229) * Remove unused private field `backend` in the `Backend` class. (#2229) +* Add Node.js v20 to CI. (#2237) ### Added * Added string tags to support class detection ### Fixed From ce29f697ced288b8d948e92b93b91d32ca3353d5 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sat, 15 Apr 2023 11:49:49 -0400 Subject: [PATCH 398/474] port to node-addon-api (remove NAN, v8, libuv) --- CHANGELOG.md | 1 + Readme.md | 2 +- binding.gyp | 5 +- index.js | 3 + lib/context2d.js | 3 - lib/pattern.js | 2 - package.json | 4 +- src/Backends.cc | 10 +- src/Backends.h | 7 +- src/Canvas.cc | 756 ++++++------- src/Canvas.h | 55 +- src/CanvasError.h | 14 + src/CanvasGradient.cc | 128 +-- src/CanvasGradient.h | 18 +- src/CanvasPattern.cc | 135 ++- src/CanvasPattern.h | 20 +- src/CanvasRenderingContext2d.cc | 1830 +++++++++++++++---------------- src/CanvasRenderingContext2d.h | 222 ++-- src/Image.cc | 271 ++--- src/Image.h | 37 +- src/ImageData.cc | 148 ++- src/ImageData.h | 17 +- src/InstanceData.h | 15 + src/JPEGStream.h | 32 +- src/backend/Backend.cc | 26 +- src/backend/Backend.h | 11 +- src/backend/ImageBackend.cc | 41 +- src/backend/ImageBackend.h | 12 +- src/backend/PdfBackend.cc | 33 +- src/backend/PdfBackend.h | 13 +- src/backend/SvgBackend.cc | 33 +- src/backend/SvgBackend.h | 11 +- src/closure.cc | 26 + src/closure.h | 16 +- src/init.cc | 56 +- test/canvas.test.js | 2 +- test/image.test.js | 4 +- test/imageData.test.js | 2 +- 38 files changed, 1920 insertions(+), 2101 deletions(-) create mode 100644 src/InstanceData.h diff --git a/CHANGELOG.md b/CHANGELOG.md index e0768f8c3..97b2c2661 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * Avoid calling virtual methods in constructors/destructors to avoid bypassing virtual dispatch. (#2229) * Remove unused private field `backend` in the `Backend` class. (#2229) * Add Node.js v20 to CI. (#2237) +* Migrated to N-API (by way of node-addon-api) and removed libuv and v8 dependencies ### Added * Added string tags to support class detection ### Fixed diff --git a/Readme.md b/Readme.md index cc945aa9a..c029e27cb 100644 --- a/Readme.md +++ b/Readme.md @@ -13,7 +13,7 @@ $ npm install canvas By default, binaries for macOS, Linux and Windows will be downloaded. If you want to build from source, use `npm install --build-from-source` and see the **Compiling** section below. -The minimum version of Node.js required is **6.0.0**. +The minimum version of Node.js required is **10.20.0**. ### Compiling diff --git a/binding.gyp b/binding.gyp index 19a33e816..166842641 100644 --- a/binding.gyp +++ b/binding.gyp @@ -57,7 +57,8 @@ }, { 'target_name': 'canvas', - 'include_dirs': ["=6" + "node": ">=10.20.0" }, "license": "MIT" } diff --git a/src/Backends.cc b/src/Backends.cc index 2256c32b6..3a557669c 100644 --- a/src/Backends.cc +++ b/src/Backends.cc @@ -4,15 +4,15 @@ #include "backend/PdfBackend.h" #include "backend/SvgBackend.h" -using namespace v8; +using namespace Napi; -void Backends::Initialize(Local target) { - Nan::HandleScope scope; +void +Backends::Initialize(Napi::Env env, Napi::Object exports) { + Napi::Object obj = Napi::Object::New(env); - Local obj = Nan::New(); ImageBackend::Initialize(obj); PdfBackend::Initialize(obj); SvgBackend::Initialize(obj); - Nan::Set(target, Nan::New("Backends").ToLocalChecked(), obj).Check(); + exports.Set("Backends", obj); } diff --git a/src/Backends.h b/src/Backends.h index dbea053ce..66a1c1db8 100644 --- a/src/Backends.h +++ b/src/Backends.h @@ -1,10 +1,9 @@ #pragma once #include "backend/Backend.h" -#include -#include +#include -class Backends : public Nan::ObjectWrap { +class Backends : public Napi::ObjectWrap { public: - static void Initialize(v8::Local target); + static void Initialize(Napi::Env env, Napi::Object exports); }; diff --git a/src/Canvas.cc b/src/Canvas.cc index 0cfe750d6..2555605f9 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -1,7 +1,7 @@ // Copyright (c) 2010 LearnBoost #include "Canvas.h" - +#include "InstanceData.h" #include // std::min #include #include @@ -35,17 +35,8 @@ "with at least a family (string) and optionally weight (string/number) " \ "and style (string)." -#define CHECK_RECEIVER(prop) \ - if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { \ - Nan::ThrowTypeError("Method " #prop " called on incompatible receiver"); \ - return; \ - } - -using namespace v8; using namespace std; -Nan::Persistent Canvas::constructor; - std::vector font_face_list; /* @@ -53,138 +44,138 @@ std::vector font_face_list; */ void -Canvas::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { - Nan::HandleScope scope; +Canvas::Initialize(Napi::Env& env, Napi::Object& exports) { + Napi::HandleScope scope(env); + InstanceData* data = env.GetInstanceData(); // Constructor - Local ctor = Nan::New(Canvas::New); - constructor.Reset(ctor); - ctor->InstanceTemplate()->SetInternalFieldCount(1); - ctor->SetClassName(Nan::New("Canvas").ToLocalChecked()); - - // Prototype - Local proto = ctor->PrototypeTemplate(); - Nan::SetPrototypeMethod(ctor, "toBuffer", ToBuffer); - Nan::SetPrototypeMethod(ctor, "streamPNGSync", StreamPNGSync); - Nan::SetPrototypeMethod(ctor, "streamPDFSync", StreamPDFSync); + Napi::Function ctor = DefineClass(env, "Canvas", { + InstanceMethod<&Canvas::ToBuffer>("toBuffer"), + InstanceMethod<&Canvas::StreamPNGSync>("streamPNGSync"), + InstanceMethod<&Canvas::StreamPDFSync>("streamPDFSync"), #ifdef HAVE_JPEG - Nan::SetPrototypeMethod(ctor, "streamJPEGSync", StreamJPEGSync); + InstanceMethod<&Canvas::StreamJPEGSync>("streamJPEGSync"), #endif - Nan::SetAccessor(proto, Nan::New("type").ToLocalChecked(), GetType); - Nan::SetAccessor(proto, Nan::New("stride").ToLocalChecked(), GetStride); - Nan::SetAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, SetWidth); - Nan::SetAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, SetHeight); - - Nan::SetTemplate(proto, "PNG_NO_FILTERS", Nan::New(PNG_NO_FILTERS)); - Nan::SetTemplate(proto, "PNG_FILTER_NONE", Nan::New(PNG_FILTER_NONE)); - Nan::SetTemplate(proto, "PNG_FILTER_SUB", Nan::New(PNG_FILTER_SUB)); - Nan::SetTemplate(proto, "PNG_FILTER_UP", Nan::New(PNG_FILTER_UP)); - Nan::SetTemplate(proto, "PNG_FILTER_AVG", Nan::New(PNG_FILTER_AVG)); - Nan::SetTemplate(proto, "PNG_FILTER_PAETH", Nan::New(PNG_FILTER_PAETH)); - Nan::SetTemplate(proto, "PNG_ALL_FILTERS", Nan::New(PNG_ALL_FILTERS)); - - // Class methods - Nan::SetMethod(ctor, "_registerFont", RegisterFont); - Nan::SetMethod(ctor, "_deregisterAllFonts", DeregisterAllFonts); + InstanceAccessor<&Canvas::GetType>("type"), + InstanceAccessor<&Canvas::GetStride>("stride"), + InstanceAccessor<&Canvas::GetWidth, &Canvas::SetWidth>("width"), + InstanceAccessor<&Canvas::GetHeight, &Canvas::SetHeight>("height"), + InstanceValue("PNG_NO_FILTERS", Napi::Number::New(env, PNG_NO_FILTERS)), + InstanceValue("PNG_FILTER_NONE", Napi::Number::New(env, PNG_FILTER_NONE)), + InstanceValue("PNG_FILTER_SUB", Napi::Number::New(env, PNG_FILTER_SUB)), + InstanceValue("PNG_FILTER_UP", Napi::Number::New(env, PNG_FILTER_UP)), + InstanceValue("PNG_FILTER_AVG", Napi::Number::New(env, PNG_FILTER_AVG)), + InstanceValue("PNG_FILTER_PAETH", Napi::Number::New(env, PNG_FILTER_PAETH)), + InstanceValue("PNG_ALL_FILTERS", Napi::Number::New(env, PNG_ALL_FILTERS)), + StaticMethod<&Canvas::RegisterFont>("_registerFont"), + StaticMethod<&Canvas::DeregisterAllFonts>("_deregisterAllFonts") + }); - Local ctx = Nan::GetCurrentContext(); - Nan::Set(target, - Nan::New("Canvas").ToLocalChecked(), - ctor->GetFunction(ctx).ToLocalChecked()); + data->CanvasCtor = Napi::Persistent(ctor); + exports.Set("Canvas", ctor); } /* * Initialize a Canvas with the given width and height. */ -NAN_METHOD(Canvas::New) { - if (!info.IsConstructCall()) { - return Nan::ThrowTypeError("Class constructors cannot be invoked without 'new'"); - } - +Canvas::Canvas(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info), env(info.Env()) { + InstanceData* data = env.GetInstanceData(); + ctor = Napi::Persistent(data->CanvasCtor.Value()); Backend* backend = NULL; - if (info[0]->IsNumber()) { - int width = Nan::To(info[0]).FromMaybe(0), height = 0; - - if (info[1]->IsNumber()) height = Nan::To(info[1]).FromMaybe(0); - - if (info[2]->IsString()) { - if (0 == strcmp("pdf", *Nan::Utf8String(info[2]))) - backend = new PdfBackend(width, height); - else if (0 == strcmp("svg", *Nan::Utf8String(info[2]))) - backend = new SvgBackend(width, height); - else - backend = new ImageBackend(width, height); + Napi::Object jsBackend; + + if (info[0].IsNumber()) { + Napi::Number width = info[0].As(); + Napi::Number height = Napi::Number::New(env, 0); + + if (info[1].IsNumber()) height = info[1].As(); + + if (info[2].IsString()) { + std::string str = info[2].As(); + if (str == "pdf") { + Napi::Maybe instance = data->PdfBackendCtor.New({ width, height }); + if (instance.IsJust()) backend = PdfBackend::Unwrap(jsBackend = instance.Unwrap()); + } else if (str == "svg") { + Napi::Maybe instance = data->SvgBackendCtor.New({ width, height }); + if (instance.IsJust()) backend = SvgBackend::Unwrap(jsBackend = instance.Unwrap()); + } else { + Napi::Maybe instance = data->ImageBackendCtor.New({ width, height }); + if (instance.IsJust()) backend = ImageBackend::Unwrap(jsBackend = instance.Unwrap()); + } + } else { + Napi::Maybe instance = data->ImageBackendCtor.New({ width, height }); + if (instance.IsJust()) backend = ImageBackend::Unwrap(jsBackend = instance.Unwrap()); } - else - backend = new ImageBackend(width, height); - } - else if (info[0]->IsObject()) { - if (Nan::New(ImageBackend::constructor)->HasInstance(info[0]) || - Nan::New(PdfBackend::constructor)->HasInstance(info[0]) || - Nan::New(SvgBackend::constructor)->HasInstance(info[0])) { - backend = Nan::ObjectWrap::Unwrap(Nan::To(info[0]).ToLocalChecked()); - }else{ - return Nan::ThrowTypeError("Invalid arguments"); + } else if (info[0].IsObject()) { + jsBackend = info[0].As(); + if (jsBackend.InstanceOf(data->ImageBackendCtor.Value()).UnwrapOr(false)) { + backend = ImageBackend::Unwrap(jsBackend); + } else if (jsBackend.InstanceOf(data->PdfBackendCtor.Value()).UnwrapOr(false)) { + backend = PdfBackend::Unwrap(jsBackend); + } else if (jsBackend.InstanceOf(data->SvgBackendCtor.Value()).UnwrapOr(false)) { + backend = SvgBackend::Unwrap(jsBackend); + } else { + Napi::TypeError::New(env, "Invalid arguments").ThrowAsJavaScriptException(); + return; } - } - else { - backend = new ImageBackend(0, 0); + } else { + Napi::Number width = Napi::Number::New(env, 0); + Napi::Number height = Napi::Number::New(env, 0); + Napi::Maybe instance = data->ImageBackendCtor.New({ width, height }); + if (instance.IsJust()) backend = ImageBackend::Unwrap(jsBackend = instance.Unwrap()); } if (!backend->isSurfaceValid()) { - const char *error = backend->getError(); - delete backend; - return Nan::ThrowError(error); + Napi::Error::New(env, backend->getError()).ThrowAsJavaScriptException(); + return; } - Canvas* canvas = new Canvas(backend); - canvas->Wrap(info.This()); + backend->setCanvas(this); - backend->setCanvas(canvas); - - info.GetReturnValue().Set(info.This()); + // Note: the backend gets destroyed when the jsBackend is GC'd. The cleaner + // way would be to only store the jsBackend and unwrap it when the c++ ref is + // needed, but that's slower and a burden. The _backend might be null if we + // returned early, but since an exception was thrown it gets destroyed soon. + _backend = backend; + _jsBackend = Napi::Persistent(jsBackend); } /* * Get type string. */ -NAN_GETTER(Canvas::GetType) { - CHECK_RECEIVER(Canvas.GetType); - Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(canvas->backend()->getName()).ToLocalChecked()); +Napi::Value +Canvas::GetType(const Napi::CallbackInfo& info) { + return Napi::String::New(env, backend()->getName()); } /* * Get stride. */ -NAN_GETTER(Canvas::GetStride) { - CHECK_RECEIVER(Canvas.GetStride); - Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(canvas->stride())); +Napi::Value +Canvas::GetStride(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, stride()); } /* * Get width. */ -NAN_GETTER(Canvas::GetWidth) { - CHECK_RECEIVER(Canvas.GetWidth); - Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(canvas->getWidth())); +Napi::Value +Canvas::GetWidth(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, getWidth()); } /* * Set width. */ -NAN_SETTER(Canvas::SetWidth) { - CHECK_RECEIVER(Canvas.SetWidth); - if (value->IsNumber()) { - Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - canvas->backend()->setWidth(Nan::To(value).FromMaybe(0)); - canvas->resurface(info.This()); +void +Canvas::SetWidth(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (value.IsNumber()) { + backend()->setWidth(value.As().Uint32Value()); + resurface(info.This().As()); } } @@ -192,22 +183,20 @@ NAN_SETTER(Canvas::SetWidth) { * Get height. */ -NAN_GETTER(Canvas::GetHeight) { - CHECK_RECEIVER(Canvas.GetHeight); - Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(canvas->getHeight())); +Napi::Value +Canvas::GetHeight(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, getHeight()); } /* * Set height. */ -NAN_SETTER(Canvas::SetHeight) { - CHECK_RECEIVER(Canvas.SetHeight); - if (value->IsNumber()) { - Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - canvas->backend()->setHeight(Nan::To(value).FromMaybe(0)); - canvas->resurface(info.This()); +void +Canvas::SetHeight(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (value.IsNumber()) { + backend()->setHeight(value.As().Uint32Value()); + resurface(info.This().As()); } } @@ -216,8 +205,8 @@ NAN_SETTER(Canvas::SetHeight) { */ void -Canvas::ToPngBufferAsync(uv_work_t *req) { - PngClosure* closure = static_cast(req->data); +Canvas::ToPngBufferAsync(Closure* base) { + PngClosure* closure = static_cast(base); closure->status = canvas_write_to_png_stream( closure->canvas->surface(), @@ -227,102 +216,84 @@ Canvas::ToPngBufferAsync(uv_work_t *req) { #ifdef HAVE_JPEG void -Canvas::ToJpegBufferAsync(uv_work_t *req) { - JpegClosure* closure = static_cast(req->data); +Canvas::ToJpegBufferAsync(Closure* base) { + JpegClosure* closure = static_cast(base); write_to_jpeg_buffer(closure->canvas->surface(), closure); } #endif -/* - * EIO after toBuffer callback. - */ +static void +parsePNGArgs(Napi::Value arg, PngClosure& pngargs) { + if (arg.IsObject()) { + Napi::Object obj = arg.As(); + Napi::Value cLevel; -void -Canvas::ToBufferAsyncAfter(uv_work_t *req) { - Nan::HandleScope scope; - Nan::AsyncResource async("canvas:ToBufferAsyncAfter"); - Closure* closure = static_cast(req->data); - delete req; - - if (closure->status) { - Local argv[1] = { Canvas::Error(closure->status) }; - closure->cb.Call(1, argv, &async); - } else { - Local buf = Nan::CopyBuffer((char*)&closure->vec[0], closure->vec.size()).ToLocalChecked(); - Local argv[2] = { Nan::Null(), buf }; - closure->cb.Call(sizeof argv / sizeof *argv, argv, &async); - } - - closure->canvas->Unref(); - delete closure; -} - -static void parsePNGArgs(Local arg, PngClosure& pngargs) { - if (arg->IsObject()) { - Local obj = Nan::To(arg).ToLocalChecked(); - - Local cLevel = Nan::Get(obj, Nan::New("compressionLevel").ToLocalChecked()).ToLocalChecked(); - if (cLevel->IsUint32()) { - uint32_t val = Nan::To(cLevel).FromMaybe(0); + if (obj.Get("compressionLevel").UnwrapTo(&cLevel) && cLevel.IsNumber()) { + uint32_t val = cLevel.As().Uint32Value(); // See quote below from spec section 4.12.5.5. if (val <= 9) pngargs.compressionLevel = val; } - Local rez = Nan::Get(obj, Nan::New("resolution").ToLocalChecked()).ToLocalChecked(); - if (rez->IsUint32()) { - uint32_t val = Nan::To(rez).FromMaybe(0); + Napi::Value rez; + if (obj.Get("resolution").UnwrapTo(&rez) && rez.IsNumber()) { + uint32_t val = rez.As().Uint32Value(); if (val > 0) pngargs.resolution = val; } - Local filters = Nan::Get(obj, Nan::New("filters").ToLocalChecked()).ToLocalChecked(); - if (filters->IsUint32()) pngargs.filters = Nan::To(filters).FromMaybe(0); + Napi::Value filters; + if (obj.Get("filters").UnwrapTo(&filters) && filters.IsNumber()) { + pngargs.filters = filters.As().Uint32Value(); + } - Local palette = Nan::Get(obj, Nan::New("palette").ToLocalChecked()).ToLocalChecked(); - if (palette->IsUint8ClampedArray()) { - Local palette_ta = palette.As(); - pngargs.nPaletteColors = palette_ta->Length(); - if (pngargs.nPaletteColors % 4 != 0) { - throw "Palette length must be a multiple of 4."; - } - pngargs.nPaletteColors /= 4; - Nan::TypedArrayContents _paletteColors(palette_ta); - pngargs.palette = *_paletteColors; - // Optional background color index: - Local backgroundIndexVal = Nan::Get(obj, Nan::New("backgroundIndex").ToLocalChecked()).ToLocalChecked(); - if (backgroundIndexVal->IsUint32()) { - pngargs.backgroundIndex = static_cast(Nan::To(backgroundIndexVal).FromMaybe(0)); + Napi::Value palette; + if (obj.Get("palette").UnwrapTo(&palette) && palette.IsTypedArray()) { + Napi::TypedArray palette_ta = palette.As(); + if (palette_ta.TypedArrayType() == napi_uint8_clamped_array) { + pngargs.nPaletteColors = palette_ta.ElementLength(); + if (pngargs.nPaletteColors % 4 != 0) { + throw "Palette length must be a multiple of 4."; + } + pngargs.palette = palette_ta.As().Data(); + pngargs.nPaletteColors /= 4; + // Optional background color index: + Napi::Value backgroundIndexVal; + if (obj.Get("backgroundIndex").UnwrapTo(&backgroundIndexVal) && backgroundIndexVal.IsNumber()) { + pngargs.backgroundIndex = backgroundIndexVal.As().Uint32Value(); + } } } } } #ifdef HAVE_JPEG -static void parseJPEGArgs(Local arg, JpegClosure& jpegargs) { +static void parseJPEGArgs(Napi::Value arg, JpegClosure& jpegargs) { // "If Type(quality) is not Number, or if quality is outside that range, the // user agent must use its default quality value, as if the quality argument // had not been given." - 4.12.5.5 - if (arg->IsObject()) { - Local obj = Nan::To(arg).ToLocalChecked(); + if (arg.IsObject()) { + Napi::Object obj = arg.As(); - Local qual = Nan::Get(obj, Nan::New("quality").ToLocalChecked()).ToLocalChecked(); - if (qual->IsNumber()) { - double quality = Nan::To(qual).FromMaybe(0); + Napi::Value qual; + if (obj.Get("quality").UnwrapTo(&qual) && qual.IsNumber()) { + double quality = qual.As().DoubleValue(); if (quality >= 0.0 && quality <= 1.0) { jpegargs.quality = static_cast(100.0 * quality); } } - Local chroma = Nan::Get(obj, Nan::New("chromaSubsampling").ToLocalChecked()).ToLocalChecked(); - if (chroma->IsBoolean()) { - bool subsample = Nan::To(chroma).FromMaybe(0); - jpegargs.chromaSubsampling = subsample ? 2 : 1; - } else if (chroma->IsNumber()) { - jpegargs.chromaSubsampling = Nan::To(chroma).FromMaybe(0); + Napi::Value chroma; + if (obj.Get("chromaSubsampling").UnwrapTo(&chroma)) { + if (chroma.IsBoolean()) { + bool subsample = chroma.As().Value(); + jpegargs.chromaSubsampling = subsample ? 2 : 1; + } else if (chroma.IsNumber()) { + jpegargs.chromaSubsampling = chroma.As().Uint32Value(); + } } - Local progressive = Nan::Get(obj, Nan::New("progressive").ToLocalChecked()).ToLocalChecked(); - if (!progressive->IsUndefined()) { - jpegargs.progressive = Nan::To(progressive).FromMaybe(0); + Napi::Value progressive; + if (obj.Get("progressive").UnwrapTo(&progressive) && progressive.IsBoolean()) { + jpegargs.progressive = progressive.As().Value(); } } } @@ -330,29 +301,27 @@ static void parseJPEGArgs(Local arg, JpegClosure& jpegargs) { #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0) -static inline void setPdfMetaStr(cairo_surface_t* surf, Local opts, - cairo_pdf_metadata_t t, const char* pName) { - auto propName = Nan::New(pName).ToLocalChecked(); - auto propValue = Nan::Get(opts, propName).ToLocalChecked(); - if (propValue->IsString()) { +static inline void setPdfMetaStr(cairo_surface_t* surf, Napi::Object opts, + cairo_pdf_metadata_t t, const char* propName) { + Napi::Value propValue; + if (opts.Get(propName).UnwrapTo(&propValue) && propValue.IsString()) { // (copies char data) - cairo_pdf_surface_set_metadata(surf, t, *Nan::Utf8String(propValue)); + cairo_pdf_surface_set_metadata(surf, t, propValue.As().Utf8Value().c_str()); } } -static inline void setPdfMetaDate(cairo_surface_t* surf, Local opts, - cairo_pdf_metadata_t t, const char* pName) { - auto propName = Nan::New(pName).ToLocalChecked(); - auto propValue = Nan::Get(opts, propName).ToLocalChecked(); - if (propValue->IsDate()) { - auto date = static_cast(propValue.As()->ValueOf() / 1000); // ms -> s +static inline void setPdfMetaDate(cairo_surface_t* surf, Napi::Object opts, + cairo_pdf_metadata_t t, const char* propName) { + Napi::Value propValue; + if (opts.Get(propName).UnwrapTo(&propValue) && propValue.IsDate()) { + auto date = static_cast(propValue.As().ValueOf() / 1000); // ms -> s char buf[sizeof "2011-10-08T07:07:09Z"]; strftime(buf, sizeof buf, "%FT%TZ", gmtime(&date)); cairo_pdf_surface_set_metadata(surf, t, buf); } } -static void setPdfMetadata(Canvas* canvas, Local opts) { +static void setPdfMetadata(Canvas* canvas, Napi::Object opts) { cairo_surface_t* surf = canvas->surface(); setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_TITLE, "title"); @@ -392,146 +361,137 @@ static void setPdfMetadata(Canvas* canvas, Local opts) { ((err: null|Error, buffer) => any, "image/jpeg", {quality?: number, progressive?: Boolean, chromaSubsampling?: Boolean|number}) */ -NAN_METHOD(Canvas::ToBuffer) { +Napi::Value +Canvas::ToBuffer(const Napi::CallbackInfo& info) { + EncodingWorker *worker = new EncodingWorker(info.Env()); cairo_status_t status; - Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); // Vector canvases, sync only - const std::string name = canvas->backend()->getName(); + const std::string name = backend()->getName(); if (name == "pdf" || name == "svg") { // mime type may be present, but it's not checked PdfSvgClosure* closure; if (name == "pdf") { - closure = static_cast(canvas->backend())->closure(); + closure = static_cast(backend())->closure(); #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0) - if (info[1]->IsObject()) { // toBuffer("application/pdf", config) - setPdfMetadata(canvas, Nan::To(info[1]).ToLocalChecked()); + if (info[1].IsObject()) { // toBuffer("application/pdf", config) + setPdfMetadata(this, info[1].As()); } #endif // CAIRO 16+ } else { - closure = static_cast(canvas->backend())->closure(); + closure = static_cast(backend())->closure(); } - cairo_surface_finish(canvas->surface()); - Local buf = Nan::CopyBuffer((char*)&closure->vec[0], closure->vec.size()).ToLocalChecked(); - info.GetReturnValue().Set(buf); - return; + cairo_surface_finish(surface()); + return Napi::Buffer::Copy(env, &closure->vec[0], closure->vec.size()); } // Raw ARGB data -- just a memcpy() - if (info[0]->StrictEquals(Nan::New("raw").ToLocalChecked())) { - cairo_surface_t *surface = canvas->surface(); + if (info[0].StrictEquals(Napi::String::New(env, "raw"))) { + cairo_surface_t *surface = this->surface(); cairo_surface_flush(surface); - if (canvas->nBytes() > node::Buffer::kMaxLength) { - Nan::ThrowError("Data exceeds maximum buffer length."); - return; + if (nBytes() > node::Buffer::kMaxLength) { + Napi::Error::New(env, "Data exceeds maximum buffer length.").ThrowAsJavaScriptException(); + return env.Undefined(); } - const unsigned char *data = cairo_image_surface_get_data(surface); - Isolate* iso = Nan::GetCurrentContext()->GetIsolate(); - Local buf = node::Buffer::Copy(iso, reinterpret_cast(data), canvas->nBytes()).ToLocalChecked(); - info.GetReturnValue().Set(buf); - return; + return Napi::Buffer::Copy(env, cairo_image_surface_get_data(surface), nBytes()); } // Sync PNG, default - if (info[0]->IsUndefined() || info[0]->StrictEquals(Nan::New("image/png").ToLocalChecked())) { + if (info[0].IsUndefined() || info[0].StrictEquals(Napi::String::New(env, "image/png"))) { try { - PngClosure closure(canvas); + PngClosure closure(this); parsePNGArgs(info[1], closure); if (closure.nPaletteColors == 0xFFFFFFFF) { - Nan::ThrowError("Palette length must be a multiple of 4."); - return; + Napi::Error::New(env, "Palette length must be a multiple of 4.").ThrowAsJavaScriptException(); + return env.Undefined(); } - Nan::TryCatch try_catch; - status = canvas_write_to_png_stream(canvas->surface(), PngClosure::writeVec, &closure); + status = canvas_write_to_png_stream(surface(), PngClosure::writeVec, &closure); - if (try_catch.HasCaught()) { - try_catch.ReThrow(); - } else if (status) { - throw status; - } else { - // TODO it's possible to avoid this copy - Local buf = Nan::CopyBuffer((char *)&closure.vec[0], closure.vec.size()).ToLocalChecked(); - info.GetReturnValue().Set(buf); + if (!env.IsExceptionPending()) { + if (status) { + throw status; // TODO: throw in js? + } else { + // TODO it's possible to avoid this copy + return Napi::Buffer::Copy(env, &closure.vec[0], closure.vec.size()); + } } } catch (cairo_status_t ex) { - Nan::ThrowError(Canvas::Error(ex)); + CairoError(ex).ThrowAsJavaScriptException(); } catch (const char* ex) { - Nan::ThrowError(ex); + Napi::Error::New(env, ex).ThrowAsJavaScriptException(); + } - return; + + return env.Undefined(); } // Async PNG - if (info[0]->IsFunction() && - (info[1]->IsUndefined() || info[1]->StrictEquals(Nan::New("image/png").ToLocalChecked()))) { + if (info[0].IsFunction() && + (info[1].IsUndefined() || info[1].StrictEquals(Napi::String::New(env, "image/png")))) { PngClosure* closure; try { - closure = new PngClosure(canvas); + closure = new PngClosure(this); parsePNGArgs(info[2], *closure); } catch (cairo_status_t ex) { - Nan::ThrowError(Canvas::Error(ex)); - return; + CairoError(ex).ThrowAsJavaScriptException(); + return env.Undefined(); } catch (const char* ex) { - Nan::ThrowError(ex); - return; + Napi::Error::New(env, ex).ThrowAsJavaScriptException(); + return env.Undefined(); } - canvas->Ref(); - closure->cb.Reset(info[0].As()); + Ref(); + closure->cb = Napi::Persistent(info[0].As()); - uv_work_t* req = new uv_work_t; - req->data = closure; // Make sure the surface exists since we won't have an isolate context in the async block: - canvas->surface(); - uv_queue_work(uv_default_loop(), req, ToPngBufferAsync, (uv_after_work_cb)ToBufferAsyncAfter); + surface(); + worker->Init(&ToPngBufferAsync, closure); + worker->Queue(); - return; + return env.Undefined(); } #ifdef HAVE_JPEG // Sync JPEG - Local jpegStr = Nan::New("image/jpeg").ToLocalChecked(); - if (info[0]->StrictEquals(jpegStr)) { + Napi::Value jpegStr = Napi::String::New(env, "image/jpeg"); + if (info[0].StrictEquals(jpegStr)) { try { - JpegClosure closure(canvas); + JpegClosure closure(this); parseJPEGArgs(info[1], closure); - Nan::TryCatch try_catch; - write_to_jpeg_buffer(canvas->surface(), &closure); + write_to_jpeg_buffer(surface(), &closure); - if (try_catch.HasCaught()) { - try_catch.ReThrow(); - } else { + if (!env.IsExceptionPending()) { // TODO it's possible to avoid this copy. - Local buf = Nan::CopyBuffer((char *)&closure.vec[0], closure.vec.size()).ToLocalChecked(); - info.GetReturnValue().Set(buf); + return Napi::Buffer::Copy(env, &closure.vec[0], closure.vec.size()); } } catch (cairo_status_t ex) { - Nan::ThrowError(Canvas::Error(ex)); + CairoError(ex).ThrowAsJavaScriptException(); + return env.Undefined(); } - return; + return env.Undefined(); } // Async JPEG - if (info[0]->IsFunction() && info[1]->StrictEquals(jpegStr)) { - JpegClosure* closure = new JpegClosure(canvas); + if (info[0].IsFunction() && info[1].StrictEquals(jpegStr)) { + JpegClosure* closure = new JpegClosure(this); parseJPEGArgs(info[2], *closure); - canvas->Ref(); - closure->cb.Reset(info[0].As()); + Ref(); + closure->cb = Napi::Persistent(info[0].As()); - uv_work_t* req = new uv_work_t; - req->data = closure; // Make sure the surface exists since we won't have an isolate context in the async block: - canvas->surface(); - uv_queue_work(uv_default_loop(), req, ToJpegBufferAsync, (uv_after_work_cb)ToBufferAsyncAfter); - - return; + surface(); + worker->Init(&ToJpegBufferAsync, closure); + worker->Queue(); + return env.Undefined(); } #endif + + return env.Undefined(); } /* @@ -540,15 +500,12 @@ NAN_METHOD(Canvas::ToBuffer) { static cairo_status_t streamPNG(void *c, const uint8_t *data, unsigned len) { - Nan::HandleScope scope; - Nan::AsyncResource async("canvas:StreamPNG"); PngClosure* closure = (PngClosure*) c; - Local buf = Nan::CopyBuffer((char *)data, len).ToLocalChecked(); - Local argv[3] = { - Nan::Null() - , buf - , Nan::New(len) }; - closure->cb.Call(sizeof argv / sizeof *argv, argv, &async); + Napi::Env env = closure->canvas->env; + Napi::HandleScope scope(env); + Napi::AsyncContext async(env, "canvas:StreamPNG"); + Napi::Value buf = Napi::Buffer::Copy(env, data, len); + closure->cb.MakeCallback(env.Global(), { env.Null(), buf, Napi::Number::New(env, len) }, async); return CAIRO_STATUS_SUCCESS; } @@ -557,69 +514,51 @@ streamPNG(void *c, const uint8_t *data, unsigned len) { * StreamPngSync(this, options: {palette?: Uint8ClampedArray, backgroundIndex?: uint32, compressionLevel: uint32, filters: uint32}) */ -NAN_METHOD(Canvas::StreamPNGSync) { - if (!info[0]->IsFunction()) - return Nan::ThrowTypeError("callback function required"); - - Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); +void +Canvas::StreamPNGSync(const Napi::CallbackInfo& info) { + if (!info[0].IsFunction()) { + Napi::TypeError::New(env, "callback function required").ThrowAsJavaScriptException(); + return; + } - PngClosure closure(canvas); + PngClosure closure(this); parsePNGArgs(info[1], closure); - closure.cb.Reset(Local::Cast(info[0])); - - Nan::TryCatch try_catch; + closure.cb = Napi::Persistent(info[0].As()); - cairo_status_t status = canvas_write_to_png_stream(canvas->surface(), streamPNG, &closure); + cairo_status_t status = canvas_write_to_png_stream(surface(), streamPNG, &closure); - if (try_catch.HasCaught()) { - try_catch.ReThrow(); - return; - } else if (status) { - Local argv[1] = { Canvas::Error(status) }; - Nan::Call(closure.cb, Nan::GetCurrentContext()->Global(), sizeof argv / sizeof *argv, argv); - } else { - Local argv[3] = { - Nan::Null() - , Nan::Null() - , Nan::New(0) }; - Nan::Call(closure.cb, Nan::GetCurrentContext()->Global(), sizeof argv / sizeof *argv, argv); + if (!env.IsExceptionPending()) { + if (status) { + closure.cb.Call(env.Global(), { CairoError(status).Value() }); + } else { + closure.cb.Call(env.Global(), { env.Null(), env.Null(), Napi::Number::New(env, 0) }); + } } - return; } struct PdfStreamInfo { - Local fn; + Napi::Function fn; uint32_t len; uint8_t* data; }; - -/* - * Canvas::StreamPDF FreeCallback - */ - -void stream_pdf_free(char *, void *) {} - /* * Canvas::StreamPDF callback. */ static cairo_status_t streamPDF(void *c, const uint8_t *data, unsigned len) { - Nan::HandleScope scope; - Nan::AsyncResource async("canvas:StreamPDF"); PdfStreamInfo* streaminfo = static_cast(c); + Napi::Env env = streaminfo->fn.Env(); + Napi::HandleScope scope(env); + Napi::AsyncContext async(env, "canvas:StreamPDF"); // TODO this is technically wrong, we're returning a pointer to the data in a // vector in a class with automatic storage duration. If the canvas goes out // of scope while we're in the handler, a use-after-free could happen. - Local buf = Nan::NewBuffer(const_cast(reinterpret_cast(data)), len, stream_pdf_free, 0).ToLocalChecked(); - Local argv[3] = { - Nan::Null() - , buf - , Nan::New(len) }; - async.runInAsyncScope(Nan::GetCurrentContext()->Global(), streaminfo->fn, sizeof argv / sizeof *argv, argv); + Napi::Value buf = Napi::Buffer::New(env, (uint8_t *)(data), len); + streaminfo->fn.MakeCallback(env.Global(), { env.Null(), buf, Napi::Number::New(env, len) }, async); return CAIRO_STATUS_SUCCESS; } @@ -643,45 +582,41 @@ cairo_status_t canvas_write_to_pdf_stream(cairo_surface_t *surface, cairo_write_ * Stream PDF data synchronously. */ -NAN_METHOD(Canvas::StreamPDFSync) { - if (!info[0]->IsFunction()) - return Nan::ThrowTypeError("callback function required"); - - Canvas *canvas = Nan::ObjectWrap::Unwrap(info.Holder()); +void +Canvas::StreamPDFSync(const Napi::CallbackInfo& info) { + if (!info[0].IsFunction()) { + Napi::TypeError::New(env, "callback function required").ThrowAsJavaScriptException(); + return; + } - if (canvas->backend()->getName() != "pdf") - return Nan::ThrowTypeError("wrong canvas type"); + if (backend()->getName() != "pdf") { + Napi::TypeError::New(env, "wrong canvas type").ThrowAsJavaScriptException(); + return; + } #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0) - if (info[1]->IsObject()) { - setPdfMetadata(canvas, Nan::To(info[1]).ToLocalChecked()); + if (info[1].IsObject()) { + setPdfMetadata(this, info[1].As()); } #endif - cairo_surface_finish(canvas->surface()); + cairo_surface_finish(surface()); - PdfSvgClosure* closure = static_cast(canvas->backend())->closure(); - Local fn = info[0].As(); + PdfSvgClosure* closure = static_cast(backend())->closure(); + Napi::Function fn = info[0].As(); PdfStreamInfo streaminfo; streaminfo.fn = fn; streaminfo.data = &closure->vec[0]; streaminfo.len = closure->vec.size(); - Nan::TryCatch try_catch; - - cairo_status_t status = canvas_write_to_pdf_stream(canvas->surface(), streamPDF, &streaminfo); + cairo_status_t status = canvas_write_to_pdf_stream(surface(), streamPDF, &streaminfo); - if (try_catch.HasCaught()) { - try_catch.ReThrow(); - } else if (status) { - Local error = Canvas::Error(status); - Nan::Call(fn, Nan::GetCurrentContext()->Global(), 1, &error); - } else { - Local argv[3] = { - Nan::Null() - , Nan::Null() - , Nan::New(0) }; - Nan::Call(fn, Nan::GetCurrentContext()->Global(), sizeof argv / sizeof *argv, argv); + if (!env.IsExceptionPending()) { + if (status) { + fn.Call(env.Global(), { CairoError(status).Value() }); + } else { + fn.Call(env.Global(), { env.Null(), env.Null(), Napi::Number::New(env, 0) }); + } } } @@ -696,60 +631,64 @@ static uint32_t getSafeBufSize(Canvas* canvas) { return (std::min)(canvas->getWidth() * canvas->getHeight() * 4, static_cast(PAGE_SIZE)); } -NAN_METHOD(Canvas::StreamJPEGSync) { - if (!info[1]->IsFunction()) - return Nan::ThrowTypeError("callback function required"); +void +Canvas::StreamJPEGSync(const Napi::CallbackInfo& info) { + if (!info[1].IsFunction()) { + Napi::TypeError::New(env, "callback function required").ThrowAsJavaScriptException(); + return; + } - Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - JpegClosure closure(canvas); + JpegClosure closure(this); parseJPEGArgs(info[0], closure); - closure.cb.Reset(Local::Cast(info[1])); - - Nan::TryCatch try_catch; - uint32_t bufsize = getSafeBufSize(canvas); - write_to_jpeg_stream(canvas->surface(), bufsize, &closure); + closure.cb = Napi::Persistent(info[1].As()); - if (try_catch.HasCaught()) { - try_catch.ReThrow(); - } - return; + uint32_t bufsize = getSafeBufSize(this); + write_to_jpeg_stream(surface(), bufsize, &closure); } #endif char * -str_value(Local val, const char *fallback, bool can_be_number) { - if (val->IsString() || (can_be_number && val->IsNumber())) { - return strdup(*Nan::Utf8String(val)); - } else if (fallback) { - return strdup(fallback); - } else { - return NULL; +str_value(Napi::Maybe maybe, const char *fallback, bool can_be_number) { + Napi::Value val; + if (maybe.UnwrapTo(&val)) { + if (val.IsString() || (can_be_number && val.IsNumber())) { + Napi::String strVal; + if (val.ToString().UnwrapTo(&strVal)) return strdup(strVal.Utf8Value().c_str()); + } else if (fallback) { + return strdup(fallback); + } } + + return NULL; } -NAN_METHOD(Canvas::RegisterFont) { - if (!info[0]->IsString()) { - return Nan::ThrowError("Wrong argument type"); - } else if (!info[1]->IsObject()) { - return Nan::ThrowError(GENERIC_FACE_ERROR); +void +Canvas::RegisterFont(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + if (!info[0].IsString()) { + Napi::Error::New(env, "Wrong argument type").ThrowAsJavaScriptException(); + return; + } else if (!info[1].IsObject()) { + Napi::Error::New(env, GENERIC_FACE_ERROR).ThrowAsJavaScriptException(); + return; } - Nan::Utf8String filePath(info[0]); - PangoFontDescription *sys_desc = get_pango_font_description((unsigned char *) *filePath); + std::string filePath = info[0].As(); + PangoFontDescription *sys_desc = get_pango_font_description((unsigned char *)(filePath.c_str())); - if (!sys_desc) return Nan::ThrowError("Could not parse font file"); + if (!sys_desc) { + Napi::Error::New(env, "Could not parse font file").ThrowAsJavaScriptException(); + return; + } PangoFontDescription *user_desc = pango_font_description_new(); // now check the attrs, there are many ways to be wrong - Local js_user_desc = Nan::To(info[1]).ToLocalChecked(); - Local family_prop = Nan::New("family").ToLocalChecked(); - Local weight_prop = Nan::New("weight").ToLocalChecked(); - Local style_prop = Nan::New("style").ToLocalChecked(); + Napi::Object js_user_desc = info[1].As(); - char *family = str_value(Nan::Get(js_user_desc, family_prop).ToLocalChecked(), NULL, false); - char *weight = str_value(Nan::Get(js_user_desc, weight_prop).ToLocalChecked(), "normal", true); - char *style = str_value(Nan::Get(js_user_desc, style_prop).ToLocalChecked(), "normal", false); + char *family = str_value(js_user_desc.Get("family"), NULL, false); + char *weight = str_value(js_user_desc.Get("weight"), "normal", true); + char *style = str_value(js_user_desc.Get("style"), "normal", false); if (family && weight && style) { pango_font_description_set_weight(user_desc, Canvas::GetWeightFromCSSString(weight)); @@ -763,19 +702,22 @@ NAN_METHOD(Canvas::RegisterFont) { if (found != font_face_list.end()) { pango_font_description_free(found->user_desc); found->user_desc = user_desc; - } else if (register_font((unsigned char *) *filePath)) { + } else if (register_font((unsigned char *) filePath.c_str())) { FontFace face; face.user_desc = user_desc; face.sys_desc = sys_desc; - strncpy((char *)face.file_path, (char *) *filePath, 1023); + strncpy((char *)face.file_path, (char *) filePath.c_str(), 1023); font_face_list.push_back(face); } else { pango_font_description_free(user_desc); - Nan::ThrowError("Could not load font to the system's font host"); + Napi::Error::New(env, "Could not load font to the system's font host").ThrowAsJavaScriptException(); + } } else { pango_font_description_free(user_desc); - Nan::ThrowError(GENERIC_FACE_ERROR); + if (!env.IsExceptionPending()) { + Napi::Error::New(env, GENERIC_FACE_ERROR).ThrowAsJavaScriptException(); + } } free(family); @@ -783,7 +725,9 @@ NAN_METHOD(Canvas::RegisterFont) { free(style); } -NAN_METHOD(Canvas::DeregisterAllFonts) { +void +Canvas::DeregisterAllFonts(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); // Unload all fonts from pango to free up memory bool success = true; @@ -794,25 +738,7 @@ NAN_METHOD(Canvas::DeregisterAllFonts) { }); font_face_list.clear(); - if (!success) Nan::ThrowError("Could not deregister one or more fonts"); -} - -/* - * Initialize cairo surface. - */ - -Canvas::Canvas(Backend* backend) : ObjectWrap() { - _backend = backend; -} - -/* - * Destroy cairo surface. - */ - -Canvas::~Canvas() { - if (_backend != NULL) { - delete _backend; - } + if (!success) Napi::Error::New(env, "Could not deregister one or more fonts").ThrowAsJavaScriptException(); } /* @@ -926,21 +852,19 @@ Canvas::ResolveFontDescription(const PangoFontDescription *desc) { */ void -Canvas::resurface(Local canvas) { - Nan::HandleScope scope; - Local context; - - backend()->recreateSurface(); - - // Reset context - context = Nan::Get(canvas, Nan::New("context").ToLocalChecked()).ToLocalChecked(); - if (!context->IsUndefined()) { - Context2d *context2d = ObjectWrap::Unwrap(Nan::To(context).ToLocalChecked()); - cairo_t *prev = context2d->context(); - context2d->setContext(createCairoContext()); - context2d->resetState(); - cairo_destroy(prev); - } +Canvas::resurface(Napi::Object This) { + Napi::HandleScope scope(env); + Napi::Value context; + + if (This.Get("context").UnwrapTo(&context) && context.IsObject()) { + backend()->recreateSurface(); + // Reset context + Context2d *context2d = Context2d::Unwrap(context.As()); + cairo_t *prev = context2d->context(); + context2d->setContext(createCairoContext()); + context2d->resetState(); + cairo_destroy(prev); + } } /** @@ -958,9 +882,7 @@ Canvas::createCairoContext() { * Construct an Error from the given cairo status. */ -Local -Canvas::Error(cairo_status_t status) { - return Exception::Error(Nan::New(cairo_status_to_string(status)).ToLocalChecked()); +Napi::Error +Canvas::CairoError(cairo_status_t status) { + return Napi::Error::New(env, cairo_status_to_string(status)); } - -#undef CHECK_RECEIVER diff --git a/src/Canvas.h b/src/Canvas.h index 60d3b4216..5f35b356b 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -2,12 +2,14 @@ #pragma once +struct Closure; + #include "backend/Backend.h" +#include "closure.h" #include #include "dll_visibility.h" -#include +#include #include -#include #include #include @@ -49,27 +51,26 @@ enum canvas_draw_mode_t : uint8_t { * Canvas. */ -class Canvas: public Nan::ObjectWrap { +class Canvas : public Napi::ObjectWrap { public: - static Nan::Persistent constructor; - static void Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target); - static NAN_METHOD(New); - static NAN_METHOD(ToBuffer); - static NAN_GETTER(GetType); - static NAN_GETTER(GetStride); - static NAN_GETTER(GetWidth); - static NAN_GETTER(GetHeight); - static NAN_SETTER(SetWidth); - static NAN_SETTER(SetHeight); - static NAN_METHOD(StreamPNGSync); - static NAN_METHOD(StreamPDFSync); - static NAN_METHOD(StreamJPEGSync); - static NAN_METHOD(RegisterFont); - static NAN_METHOD(DeregisterAllFonts); - static v8::Local Error(cairo_status_t status); - static void ToPngBufferAsync(uv_work_t *req); - static void ToJpegBufferAsync(uv_work_t *req); - static void ToBufferAsyncAfter(uv_work_t *req); + Canvas(const Napi::CallbackInfo& info); + static void Initialize(Napi::Env& env, Napi::Object& target); + + Napi::Value ToBuffer(const Napi::CallbackInfo& info); + Napi::Value GetType(const Napi::CallbackInfo& info); + Napi::Value GetStride(const Napi::CallbackInfo& info); + Napi::Value GetWidth(const Napi::CallbackInfo& info); + Napi::Value GetHeight(const Napi::CallbackInfo& info); + void SetWidth(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetHeight(const Napi::CallbackInfo& info, const Napi::Value& value); + void StreamPNGSync(const Napi::CallbackInfo& info); + void StreamPDFSync(const Napi::CallbackInfo& info); + void StreamJPEGSync(const Napi::CallbackInfo& info); + static void RegisterFont(const Napi::CallbackInfo& info); + static void DeregisterAllFonts(const Napi::CallbackInfo& info); + Napi::Error CairoError(cairo_status_t status); + static void ToPngBufferAsync(Closure* closure); + static void ToJpegBufferAsync(Closure* closure); static PangoWeight GetWeightFromCSSString(const char *weight); static PangoStyle GetStyleFromCSSString(const char *style); static PangoFontDescription *ResolveFontDescription(const PangoFontDescription *desc); @@ -81,16 +82,18 @@ class Canvas: public Nan::ObjectWrap { DLL_PUBLIC inline uint8_t *data(){ return cairo_image_surface_get_data(surface()); } DLL_PUBLIC inline int stride(){ return cairo_image_surface_get_stride(surface()); } DLL_PUBLIC inline std::size_t nBytes(){ - return static_cast(getHeight()) * stride(); + return static_cast(backend()->getHeight()) * stride(); } DLL_PUBLIC inline int getWidth() { return backend()->getWidth(); } DLL_PUBLIC inline int getHeight() { return backend()->getHeight(); } - Canvas(Backend* backend); - void resurface(v8::Local canvas); + void resurface(Napi::Object This); + + Napi::Env env; private: - ~Canvas(); Backend* _backend; + Napi::ObjectReference _jsBackend; + Napi::FunctionReference ctor; }; diff --git a/src/CanvasError.h b/src/CanvasError.h index cb751e312..535d153fa 100644 --- a/src/CanvasError.h +++ b/src/CanvasError.h @@ -1,6 +1,7 @@ #pragma once #include +#include class CanvasError { public: @@ -20,4 +21,17 @@ class CanvasError { path.clear(); cerrno = 0; } + bool empty() { + return cerrno == 0 && message.empty(); + } + Napi::Error toError(Napi::Env env) { + if (cerrno) { + Napi::Error err = Napi::Error::New(env, strerror(cerrno)); + if (!syscall.empty()) err.Value().Set("syscall", syscall); + if (!path.empty()) err.Value().Set("path", path); + return err; + } else { + return Napi::Error::New(env, message); + } + } }; diff --git a/src/CanvasGradient.cc b/src/CanvasGradient.cc index 280fc2e8c..9c2d42360 100644 --- a/src/CanvasGradient.cc +++ b/src/CanvasGradient.cc @@ -1,123 +1,113 @@ // Copyright (c) 2010 LearnBoost #include "CanvasGradient.h" +#include "InstanceData.h" #include "Canvas.h" #include "color.h" -using namespace v8; - -Nan::Persistent Gradient::constructor; +using namespace Napi; /* * Initialize CanvasGradient. */ void -Gradient::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { - Nan::HandleScope scope; - - // Constructor - Local ctor = Nan::New(Gradient::New); - constructor.Reset(ctor); - ctor->InstanceTemplate()->SetInternalFieldCount(1); - ctor->SetClassName(Nan::New("CanvasGradient").ToLocalChecked()); - - // Prototype - Nan::SetPrototypeMethod(ctor, "addColorStop", AddColorStop); - Local ctx = Nan::GetCurrentContext(); - Nan::Set(target, - Nan::New("CanvasGradient").ToLocalChecked(), - ctor->GetFunction(ctx).ToLocalChecked()); +Gradient::Initialize(Napi::Env& env, Napi::Object& exports) { + Napi::HandleScope scope(env); + InstanceData* data = env.GetInstanceData(); + + Napi::Function ctor = DefineClass(env, "CanvasGradient", { + InstanceMethod<&Gradient::AddColorStop>("addColorStop") + }); + + exports.Set("CanvasGradient", ctor); + data->CanvasGradientCtor = Napi::Persistent(ctor); } /* * Initialize a new CanvasGradient. */ -NAN_METHOD(Gradient::New) { - if (!info.IsConstructCall()) { - return Nan::ThrowTypeError("Class constructors cannot be invoked without 'new'"); - } - +Gradient::Gradient(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info), env(info.Env()) { // Linear - if (4 == info.Length()) { - Gradient *grad = new Gradient( - Nan::To(info[0]).FromMaybe(0) - , Nan::To(info[1]).FromMaybe(0) - , Nan::To(info[2]).FromMaybe(0) - , Nan::To(info[3]).FromMaybe(0)); - grad->Wrap(info.This()); - info.GetReturnValue().Set(info.This()); + if ( + 4 == info.Length() && + info[0].IsNumber() && + info[1].IsNumber() && + info[2].IsNumber() && + info[3].IsNumber() + ) { + double x0 = info[0].As().DoubleValue(); + double y0 = info[1].As().DoubleValue(); + double x1 = info[2].As().DoubleValue(); + double y1 = info[3].As().DoubleValue(); + _pattern = cairo_pattern_create_linear(x0, y0, x1, y1); return; } // Radial - if (6 == info.Length()) { - Gradient *grad = new Gradient( - Nan::To(info[0]).FromMaybe(0) - , Nan::To(info[1]).FromMaybe(0) - , Nan::To(info[2]).FromMaybe(0) - , Nan::To(info[3]).FromMaybe(0) - , Nan::To(info[4]).FromMaybe(0) - , Nan::To(info[5]).FromMaybe(0)); - grad->Wrap(info.This()); - info.GetReturnValue().Set(info.This()); + if ( + 6 == info.Length() && + info[0].IsNumber() && + info[1].IsNumber() && + info[2].IsNumber() && + info[3].IsNumber() && + info[4].IsNumber() && + info[5].IsNumber() + ) { + double x0 = info[0].As().DoubleValue(); + double y0 = info[1].As().DoubleValue(); + double r0 = info[2].As().DoubleValue(); + double x1 = info[3].As().DoubleValue(); + double y1 = info[4].As().DoubleValue(); + double r1 = info[5].As().DoubleValue(); + _pattern = cairo_pattern_create_radial(x0, y0, r0, x1, y1, r1); return; } - return Nan::ThrowTypeError("invalid arguments"); + Napi::TypeError::New(env, "invalid arguments").ThrowAsJavaScriptException(); } /* * Add color stop. */ -NAN_METHOD(Gradient::AddColorStop) { - if (!info[0]->IsNumber()) - return Nan::ThrowTypeError("offset required"); - if (!info[1]->IsString()) - return Nan::ThrowTypeError("color string required"); +void +Gradient::AddColorStop(const Napi::CallbackInfo& info) { + if (!info[0].IsNumber()) { + Napi::TypeError::New(env, "offset required").ThrowAsJavaScriptException(); + return; + } + + if (!info[1].IsString()) { + Napi::TypeError::New(env, "color string required").ThrowAsJavaScriptException(); + return; + } - Gradient *grad = Nan::ObjectWrap::Unwrap(info.This()); short ok; - Nan::Utf8String str(info[1]); - uint32_t rgba = rgba_from_string(*str, &ok); + std::string str = info[1].As(); + uint32_t rgba = rgba_from_string(str.c_str(), &ok); if (ok) { rgba_t color = rgba_create(rgba); cairo_pattern_add_color_stop_rgba( - grad->pattern() - , Nan::To(info[0]).FromMaybe(0) + _pattern + , info[0].As().DoubleValue() , color.r , color.g , color.b , color.a); } else { - return Nan::ThrowTypeError("parse color failed"); + Napi::TypeError::New(env, "parse color failed").ThrowAsJavaScriptException(); } } -/* - * Initialize linear gradient. - */ - -Gradient::Gradient(double x0, double y0, double x1, double y1) { - _pattern = cairo_pattern_create_linear(x0, y0, x1, y1); -} - -/* - * Initialize radial gradient. - */ - -Gradient::Gradient(double x0, double y0, double r0, double x1, double y1, double r1) { - _pattern = cairo_pattern_create_radial(x0, y0, r0, x1, y1, r1); -} /* * Destroy the pattern. */ Gradient::~Gradient() { - cairo_pattern_destroy(_pattern); + if (_pattern) cairo_pattern_destroy(_pattern); } diff --git a/src/CanvasGradient.h b/src/CanvasGradient.h index b6902c428..103e80748 100644 --- a/src/CanvasGradient.h +++ b/src/CanvasGradient.h @@ -2,21 +2,19 @@ #pragma once -#include -#include +#include #include -class Gradient: public Nan::ObjectWrap { +class Gradient : public Napi::ObjectWrap { public: - static Nan::Persistent constructor; - static void Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target); - static NAN_METHOD(New); - static NAN_METHOD(AddColorStop); - Gradient(double x0, double y0, double x1, double y1); - Gradient(double x0, double y0, double r0, double x1, double y1, double r1); + static void Initialize(Napi::Env& env, Napi::Object& target); + Gradient(const Napi::CallbackInfo& info); + void AddColorStop(const Napi::CallbackInfo& info); inline cairo_pattern_t *pattern(){ return _pattern; } + ~Gradient(); + + Napi::Env env; private: - ~Gradient(); cairo_pattern_t *_pattern; }; diff --git a/src/CanvasPattern.cc b/src/CanvasPattern.cc index fa3848b37..55b8bb7fb 100644 --- a/src/CanvasPattern.cc +++ b/src/CanvasPattern.cc @@ -4,122 +4,115 @@ #include "Canvas.h" #include "Image.h" +#include "InstanceData.h" -using namespace v8; +using namespace Napi; const cairo_user_data_key_t *pattern_repeat_key; -Nan::Persistent Pattern::constructor; -Nan::Persistent Pattern::_DOMMatrix; - /* * Initialize CanvasPattern. */ void -Pattern::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { - Nan::HandleScope scope; +Pattern::Initialize(Napi::Env& env, Napi::Object& exports) { + Napi::HandleScope scope(env); + InstanceData* data = env.GetInstanceData(); // Constructor - Local ctor = Nan::New(Pattern::New); - constructor.Reset(ctor); - ctor->InstanceTemplate()->SetInternalFieldCount(1); - ctor->SetClassName(Nan::New("CanvasPattern").ToLocalChecked()); - Nan::SetPrototypeMethod(ctor, "setTransform", SetTransform); + Napi::Function ctor = DefineClass(env, "CanvasPattern", { + InstanceMethod<&Pattern::setTransform>("setTransform") + }); // Prototype - Local ctx = Nan::GetCurrentContext(); - Nan::Set(target, Nan::New("CanvasPattern").ToLocalChecked(), ctor->GetFunction(ctx).ToLocalChecked()); - Nan::Set(target, Nan::New("CanvasPatternInit").ToLocalChecked(), Nan::New(SaveExternalModules)); -} - -/* - * Save some external modules as private references. - */ - -NAN_METHOD(Pattern::SaveExternalModules) { - _DOMMatrix.Reset(Nan::To(info[0]).ToLocalChecked()); + exports.Set("CanvasPattern", ctor); + data->CanvasPatternCtor = Napi::Persistent(ctor); } /* * Initialize a new CanvasPattern. */ -NAN_METHOD(Pattern::New) { - if (!info.IsConstructCall()) { - return Nan::ThrowTypeError("Class constructors cannot be invoked without 'new'"); +Pattern::Pattern(const Napi::CallbackInfo& info) : ObjectWrap(info), env(info.Env()) { + if (!info[0].IsObject()) { + Napi::TypeError::New(env, "Image or Canvas expected").ThrowAsJavaScriptException(); + return; } + Napi::Object obj = info[0].As(); + InstanceData* data = env.GetInstanceData(); cairo_surface_t *surface; - Local obj = Nan::To(info[0]).ToLocalChecked(); - // Image - if (Nan::New(Image::constructor)->HasInstance(obj)) { - Image *img = Nan::ObjectWrap::Unwrap(obj); + if (obj.InstanceOf(data->ImageCtor.Value()).UnwrapOr(false)) { + Image *img = Image::Unwrap(obj); if (!img->isComplete()) { - return Nan::ThrowError("Image given has not completed loading"); + Napi::Error::New(env, "Image given has not completed loading").ThrowAsJavaScriptException(); + return; } surface = img->surface(); // Canvas - } else if (Nan::New(Canvas::constructor)->HasInstance(obj)) { - Canvas *canvas = Nan::ObjectWrap::Unwrap(obj); + } else if (obj.InstanceOf(data->CanvasCtor.Value()).UnwrapOr(false)) { + Canvas *canvas = Canvas::Unwrap(obj); surface = canvas->surface(); // Invalid } else { - return Nan::ThrowTypeError("Image or Canvas expected"); + if (!env.IsExceptionPending()) { + Napi::TypeError::New(env, "Image or Canvas expected").ThrowAsJavaScriptException(); + } + return; } - repeat_type_t repeat = REPEAT; - if (0 == strcmp("no-repeat", *Nan::Utf8String(info[1]))) { - repeat = NO_REPEAT; - } else if (0 == strcmp("repeat-x", *Nan::Utf8String(info[1]))) { - repeat = REPEAT_X; - } else if (0 == strcmp("repeat-y", *Nan::Utf8String(info[1]))) { - repeat = REPEAT_Y; + _pattern = cairo_pattern_create_for_surface(surface); + + if (info[1].IsString()) { + if ("no-repeat" == info[1].As().Utf8Value()) { + _repeat = NO_REPEAT; + } else if ("repeat-x" == info[1].As().Utf8Value()) { + _repeat = REPEAT_X; + } else if ("repeat-y" == info[1].As().Utf8Value()) { + _repeat = REPEAT_Y; + } } - Pattern *pattern = new Pattern(surface, repeat); - pattern->Wrap(info.This()); - info.GetReturnValue().Set(info.This()); + + cairo_pattern_set_user_data(_pattern, pattern_repeat_key, &_repeat, NULL); } /* * Set the pattern-space to user-space transform. */ -NAN_METHOD(Pattern::SetTransform) { - Pattern *pattern = Nan::ObjectWrap::Unwrap(info.This()); - Local ctx = Nan::GetCurrentContext(); - Local mat = Nan::To(info[0]).ToLocalChecked(); - -#if NODE_MAJOR_VERSION >= 8 - if (!mat->InstanceOf(ctx, _DOMMatrix.Get(Isolate::GetCurrent())).ToChecked()) { - return Nan::ThrowTypeError("Expected DOMMatrix"); +void +Pattern::setTransform(const Napi::CallbackInfo& info) { + if (!info[0].IsObject()) { + Napi::TypeError::New(env, "Expected DOMMatrix").ThrowAsJavaScriptException(); + return; } -#endif + + Napi::Object mat = info[0].As(); + + InstanceData* data = env.GetInstanceData(); + if (!mat.InstanceOf(data->DOMMatrixCtor.Value()).UnwrapOr(false)) { + if (!env.IsExceptionPending()) { + Napi::TypeError::New(env, "Expected DOMMatrix").ThrowAsJavaScriptException(); + } + return; + } + + Napi::Value one = Napi::Number::New(env, 1); + Napi::Value zero = Napi::Number::New(env, 0); cairo_matrix_t matrix; cairo_matrix_init(&matrix, - Nan::To(Nan::Get(mat, Nan::New("a").ToLocalChecked()).ToLocalChecked()).FromMaybe(1), - Nan::To(Nan::Get(mat, Nan::New("b").ToLocalChecked()).ToLocalChecked()).FromMaybe(0), - Nan::To(Nan::Get(mat, Nan::New("c").ToLocalChecked()).ToLocalChecked()).FromMaybe(0), - Nan::To(Nan::Get(mat, Nan::New("d").ToLocalChecked()).ToLocalChecked()).FromMaybe(1), - Nan::To(Nan::Get(mat, Nan::New("e").ToLocalChecked()).ToLocalChecked()).FromMaybe(0), - Nan::To(Nan::Get(mat, Nan::New("f").ToLocalChecked()).ToLocalChecked()).FromMaybe(0) + mat.Get("a").UnwrapOr(one).As().DoubleValue(), + mat.Get("b").UnwrapOr(zero).As().DoubleValue(), + mat.Get("c").UnwrapOr(zero).As().DoubleValue(), + mat.Get("d").UnwrapOr(one).As().DoubleValue(), + mat.Get("e").UnwrapOr(zero).As().DoubleValue(), + mat.Get("f").UnwrapOr(zero).As().DoubleValue() ); cairo_matrix_invert(&matrix); - cairo_pattern_set_matrix(pattern->_pattern, &matrix); -} - - -/* - * Initialize pattern. - */ - -Pattern::Pattern(cairo_surface_t *surface, repeat_type_t repeat) { - _pattern = cairo_pattern_create_for_surface(surface); - _repeat = repeat; - cairo_pattern_set_user_data(_pattern, pattern_repeat_key, &_repeat, NULL); + cairo_pattern_set_matrix(_pattern, &matrix); } repeat_type_t Pattern::get_repeat_type_for_cairo_pattern(cairo_pattern_t *pattern) { @@ -132,5 +125,5 @@ repeat_type_t Pattern::get_repeat_type_for_cairo_pattern(cairo_pattern_t *patter */ Pattern::~Pattern() { - cairo_pattern_destroy(_pattern); + if (_pattern) cairo_pattern_destroy(_pattern); } diff --git a/src/CanvasPattern.h b/src/CanvasPattern.h index 29e2171b6..1f768e03b 100644 --- a/src/CanvasPattern.h +++ b/src/CanvasPattern.h @@ -3,8 +3,7 @@ #pragma once #include -#include -#include +#include /* * Canvas types. @@ -19,19 +18,16 @@ typedef enum { extern const cairo_user_data_key_t *pattern_repeat_key; -class Pattern: public Nan::ObjectWrap { +class Pattern : public Napi::ObjectWrap { public: - static Nan::Persistent constructor; - static Nan::Persistent _DOMMatrix; - static void Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target); - static NAN_METHOD(New); - static NAN_METHOD(SaveExternalModules); - static NAN_METHOD(SetTransform); + Pattern(const Napi::CallbackInfo& info); + static void Initialize(Napi::Env& env, Napi::Object& target); + void setTransform(const Napi::CallbackInfo& info); static repeat_type_t get_repeat_type_for_cairo_pattern(cairo_pattern_t *pattern); - Pattern(cairo_surface_t *surface, repeat_type_t repeat); inline cairo_pattern_t *pattern(){ return _pattern; } - private: ~Pattern(); + Napi::Env env; + private: cairo_pattern_t *_pattern; - repeat_type_t _repeat; + repeat_type_t _repeat = REPEAT; }; diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 5bfe08d6a..9457122d0 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -8,6 +8,7 @@ #include "Canvas.h" #include "CanvasGradient.h" #include "CanvasPattern.h" +#include "InstanceData.h" #include #include #include "Image.h" @@ -19,10 +20,6 @@ #include "Util.h" #include -using namespace v8; - -Nan::Persistent Context2d::constructor; - /* * Rectangle arg assertions. */ @@ -36,12 +33,6 @@ Nan::Persistent Context2d::constructor; double width = args[2]; \ double height = args[3]; -#define CHECK_RECEIVER(prop) \ - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { \ - Nan::ThrowTypeError("Method " #prop " called on incompatible receiver"); \ - return; \ - } - constexpr double twoPi = M_PI * 2.; /* @@ -53,12 +44,13 @@ constexpr double twoPi = M_PI * 2.; pango_layout_get_font_description(LAYOUT), \ pango_context_get_language(pango_layout_get_context(LAYOUT))) -inline static bool checkArgs(const Nan::FunctionCallbackInfo &info, double *args, int argsNum, int offset = 0){ +inline static bool checkArgs(const Napi::CallbackInfo&info, double *args, int argsNum, int offset = 0){ + Napi::Number zero = Napi::Number::New(info.Env(), 0); int argsEnd = offset + argsNum; bool areArgsValid = true; for (int i = offset; i < argsEnd; i++) { - double val = Nan::To(info[i]).FromMaybe(0); + double val = info[i].ToNumber().UnwrapOr(zero).DoubleValue(); if (areArgsValid) { if (!std::isfinite(val)) { @@ -76,100 +68,139 @@ inline static bool checkArgs(const Nan::FunctionCallbackInfo &info, doubl return areArgsValid; } -Nan::Persistent Context2d::_DOMMatrix; -Nan::Persistent Context2d::_parseFont; - /* * Initialize Context2d. */ void -Context2d::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { - Nan::HandleScope scope; - - // Constructor - Local ctor = Nan::New(Context2d::New); - constructor.Reset(ctor); - ctor->InstanceTemplate()->SetInternalFieldCount(1); - ctor->SetClassName(Nan::New("CanvasRenderingContext2D").ToLocalChecked()); - - // Prototype - Local proto = ctor->PrototypeTemplate(); - Nan::SetPrototypeMethod(ctor, "drawImage", DrawImage); - Nan::SetPrototypeMethod(ctor, "putImageData", PutImageData); - Nan::SetPrototypeMethod(ctor, "getImageData", GetImageData); - Nan::SetPrototypeMethod(ctor, "createImageData", CreateImageData); - Nan::SetPrototypeMethod(ctor, "addPage", AddPage); - Nan::SetPrototypeMethod(ctor, "save", Save); - Nan::SetPrototypeMethod(ctor, "restore", Restore); - Nan::SetPrototypeMethod(ctor, "rotate", Rotate); - Nan::SetPrototypeMethod(ctor, "translate", Translate); - Nan::SetPrototypeMethod(ctor, "transform", Transform); - Nan::SetPrototypeMethod(ctor, "getTransform", GetTransform); - Nan::SetPrototypeMethod(ctor, "resetTransform", ResetTransform); - Nan::SetPrototypeMethod(ctor, "setTransform", SetTransform); - Nan::SetPrototypeMethod(ctor, "isPointInPath", IsPointInPath); - Nan::SetPrototypeMethod(ctor, "scale", Scale); - Nan::SetPrototypeMethod(ctor, "clip", Clip); - Nan::SetPrototypeMethod(ctor, "fill", Fill); - Nan::SetPrototypeMethod(ctor, "stroke", Stroke); - Nan::SetPrototypeMethod(ctor, "fillText", FillText); - Nan::SetPrototypeMethod(ctor, "strokeText", StrokeText); - Nan::SetPrototypeMethod(ctor, "fillRect", FillRect); - Nan::SetPrototypeMethod(ctor, "strokeRect", StrokeRect); - Nan::SetPrototypeMethod(ctor, "clearRect", ClearRect); - Nan::SetPrototypeMethod(ctor, "rect", Rect); - Nan::SetPrototypeMethod(ctor, "roundRect", RoundRect); - Nan::SetPrototypeMethod(ctor, "measureText", MeasureText); - Nan::SetPrototypeMethod(ctor, "moveTo", MoveTo); - Nan::SetPrototypeMethod(ctor, "lineTo", LineTo); - Nan::SetPrototypeMethod(ctor, "bezierCurveTo", BezierCurveTo); - Nan::SetPrototypeMethod(ctor, "quadraticCurveTo", QuadraticCurveTo); - Nan::SetPrototypeMethod(ctor, "beginPath", BeginPath); - Nan::SetPrototypeMethod(ctor, "closePath", ClosePath); - Nan::SetPrototypeMethod(ctor, "arc", Arc); - Nan::SetPrototypeMethod(ctor, "arcTo", ArcTo); - Nan::SetPrototypeMethod(ctor, "ellipse", Ellipse); - Nan::SetPrototypeMethod(ctor, "setLineDash", SetLineDash); - Nan::SetPrototypeMethod(ctor, "getLineDash", GetLineDash); - Nan::SetPrototypeMethod(ctor, "createPattern", CreatePattern); - Nan::SetPrototypeMethod(ctor, "createLinearGradient", CreateLinearGradient); - Nan::SetPrototypeMethod(ctor, "createRadialGradient", CreateRadialGradient); - Nan::SetAccessor(proto, Nan::New("pixelFormat").ToLocalChecked(), GetFormat); - Nan::SetAccessor(proto, Nan::New("patternQuality").ToLocalChecked(), GetPatternQuality, SetPatternQuality); - Nan::SetAccessor(proto, Nan::New("imageSmoothingEnabled").ToLocalChecked(), GetImageSmoothingEnabled, SetImageSmoothingEnabled); - Nan::SetAccessor(proto, Nan::New("globalCompositeOperation").ToLocalChecked(), GetGlobalCompositeOperation, SetGlobalCompositeOperation); - Nan::SetAccessor(proto, Nan::New("globalAlpha").ToLocalChecked(), GetGlobalAlpha, SetGlobalAlpha); - Nan::SetAccessor(proto, Nan::New("shadowColor").ToLocalChecked(), GetShadowColor, SetShadowColor); - Nan::SetAccessor(proto, Nan::New("miterLimit").ToLocalChecked(), GetMiterLimit, SetMiterLimit); - Nan::SetAccessor(proto, Nan::New("lineWidth").ToLocalChecked(), GetLineWidth, SetLineWidth); - Nan::SetAccessor(proto, Nan::New("lineCap").ToLocalChecked(), GetLineCap, SetLineCap); - Nan::SetAccessor(proto, Nan::New("lineJoin").ToLocalChecked(), GetLineJoin, SetLineJoin); - Nan::SetAccessor(proto, Nan::New("lineDashOffset").ToLocalChecked(), GetLineDashOffset, SetLineDashOffset); - Nan::SetAccessor(proto, Nan::New("shadowOffsetX").ToLocalChecked(), GetShadowOffsetX, SetShadowOffsetX); - Nan::SetAccessor(proto, Nan::New("shadowOffsetY").ToLocalChecked(), GetShadowOffsetY, SetShadowOffsetY); - Nan::SetAccessor(proto, Nan::New("shadowBlur").ToLocalChecked(), GetShadowBlur, SetShadowBlur); - Nan::SetAccessor(proto, Nan::New("antialias").ToLocalChecked(), GetAntiAlias, SetAntiAlias); - Nan::SetAccessor(proto, Nan::New("textDrawingMode").ToLocalChecked(), GetTextDrawingMode, SetTextDrawingMode); - Nan::SetAccessor(proto, Nan::New("quality").ToLocalChecked(), GetQuality, SetQuality); - Nan::SetAccessor(proto, Nan::New("currentTransform").ToLocalChecked(), GetCurrentTransform, SetCurrentTransform); - Nan::SetAccessor(proto, Nan::New("fillStyle").ToLocalChecked(), GetFillStyle, SetFillStyle); - Nan::SetAccessor(proto, Nan::New("strokeStyle").ToLocalChecked(), GetStrokeStyle, SetStrokeStyle); - Nan::SetAccessor(proto, Nan::New("font").ToLocalChecked(), GetFont, SetFont); - Nan::SetAccessor(proto, Nan::New("textBaseline").ToLocalChecked(), GetTextBaseline, SetTextBaseline); - Nan::SetAccessor(proto, Nan::New("textAlign").ToLocalChecked(), GetTextAlign, SetTextAlign); - Local ctx = Nan::GetCurrentContext(); - Nan::Set(target, Nan::New("CanvasRenderingContext2d").ToLocalChecked(), ctor->GetFunction(ctx).ToLocalChecked()); - Nan::Set(target, Nan::New("CanvasRenderingContext2dInit").ToLocalChecked(), Nan::New(SaveExternalModules)); +Context2d::Initialize(Napi::Env& env, Napi::Object& exports) { + Napi::HandleScope scope(env); + InstanceData* data = env.GetInstanceData(); + + Napi::Function ctor = DefineClass(env, "CanvasRenderingContext2D", { + InstanceMethod<&Context2d::DrawImage>("drawImage"), + InstanceMethod<&Context2d::PutImageData>("putImageData"), + InstanceMethod<&Context2d::GetImageData>("getImageData"), + InstanceMethod<&Context2d::CreateImageData>("createImageData"), + InstanceMethod<&Context2d::AddPage>("addPage"), + InstanceMethod<&Context2d::Save>("save"), + InstanceMethod<&Context2d::Restore>("restore"), + InstanceMethod<&Context2d::Rotate>("rotate"), + InstanceMethod<&Context2d::Translate>("translate"), + InstanceMethod<&Context2d::Transform>("transform"), + InstanceMethod<&Context2d::GetTransform>("getTransform"), + InstanceMethod<&Context2d::ResetTransform>("resetTransform"), + InstanceMethod<&Context2d::SetTransform>("setTransform"), + InstanceMethod<&Context2d::IsPointInPath>("isPointInPath"), + InstanceMethod<&Context2d::Scale>("scale"), + InstanceMethod<&Context2d::Clip>("clip"), + InstanceMethod<&Context2d::Fill>("fill"), + InstanceMethod<&Context2d::Stroke>("stroke"), + InstanceMethod<&Context2d::FillText>("fillText"), + InstanceMethod<&Context2d::StrokeText>("strokeText"), + InstanceMethod<&Context2d::FillRect>("fillRect"), + InstanceMethod<&Context2d::StrokeRect>("strokeRect"), + InstanceMethod<&Context2d::ClearRect>("clearRect"), + InstanceMethod<&Context2d::Rect>("rect"), + InstanceMethod<&Context2d::RoundRect>("roundRect"), + InstanceMethod<&Context2d::MeasureText>("measureText"), + InstanceMethod<&Context2d::MoveTo>("moveTo"), + InstanceMethod<&Context2d::LineTo>("lineTo"), + InstanceMethod<&Context2d::BezierCurveTo>("bezierCurveTo"), + InstanceMethod<&Context2d::QuadraticCurveTo>("quadraticCurveTo"), + InstanceMethod<&Context2d::BeginPath>("beginPath"), + InstanceMethod<&Context2d::ClosePath>("closePath"), + InstanceMethod<&Context2d::Arc>("arc"), + InstanceMethod<&Context2d::ArcTo>("arcTo"), + InstanceMethod<&Context2d::Ellipse>("ellipse"), + InstanceMethod<&Context2d::SetLineDash>("setLineDash"), + InstanceMethod<&Context2d::GetLineDash>("getLineDash"), + InstanceMethod<&Context2d::CreatePattern>("createPattern"), + InstanceMethod<&Context2d::CreateLinearGradient>("createLinearGradient"), + InstanceMethod<&Context2d::CreateRadialGradient>("createRadialGradient"), + InstanceAccessor<&Context2d::GetFormat>("pixelFormat"), + InstanceAccessor<&Context2d::GetPatternQuality, &Context2d::SetPatternQuality>("patternQuality"), + InstanceAccessor<&Context2d::GetImageSmoothingEnabled, &Context2d::SetImageSmoothingEnabled>("imageSmoothingEnabled"), + InstanceAccessor<&Context2d::GetGlobalCompositeOperation, &Context2d::SetGlobalCompositeOperation>("globalCompositeOperation"), + InstanceAccessor<&Context2d::GetGlobalAlpha, &Context2d::SetGlobalAlpha>("globalAlpha"), + InstanceAccessor<&Context2d::GetShadowColor, &Context2d::SetShadowColor>("shadowColor"), + InstanceAccessor<&Context2d::GetMiterLimit, &Context2d::SetMiterLimit>("miterLimit"), + InstanceAccessor<&Context2d::GetLineWidth, &Context2d::SetLineWidth>("lineWidth"), + InstanceAccessor<&Context2d::GetLineCap, &Context2d::SetLineCap>("lineCap"), + InstanceAccessor<&Context2d::GetLineJoin, &Context2d::SetLineJoin>("lineJoin"), + InstanceAccessor<&Context2d::GetLineDashOffset, &Context2d::SetLineDashOffset>("lineDashOffset"), + InstanceAccessor<&Context2d::GetShadowOffsetX, &Context2d::SetShadowOffsetX>("shadowOffsetX"), + InstanceAccessor<&Context2d::GetShadowOffsetY, &Context2d::SetShadowOffsetY>("shadowOffsetY"), + InstanceAccessor<&Context2d::GetShadowBlur, &Context2d::SetShadowBlur>("shadowBlur"), + InstanceAccessor<&Context2d::GetAntiAlias, &Context2d::SetAntiAlias>("antialias"), + InstanceAccessor<&Context2d::GetTextDrawingMode, &Context2d::SetTextDrawingMode>("textDrawingMode"), + InstanceAccessor<&Context2d::GetQuality, &Context2d::SetQuality>("quality"), + InstanceAccessor<&Context2d::GetCurrentTransform, &Context2d::SetCurrentTransform>("currentTransform"), + InstanceAccessor<&Context2d::GetFillStyle, &Context2d::SetFillStyle>("fillStyle"), + InstanceAccessor<&Context2d::GetStrokeStyle, &Context2d::SetStrokeStyle>("strokeStyle"), + InstanceAccessor<&Context2d::GetFont, &Context2d::SetFont>("font"), + InstanceAccessor<&Context2d::GetTextBaseline, &Context2d::SetTextBaseline>("textBaseline"), + InstanceAccessor<&Context2d::GetTextAlign, &Context2d::SetTextAlign>("textAlign") + }); + + exports.Set("CanvasRenderingContext2d", ctor); + data->Context2dCtor = Napi::Persistent(ctor); } /* * Create a cairo context. */ -Context2d::Context2d(Canvas *canvas) { - _canvas = canvas; - _context = canvas->createCairoContext(); +Context2d::Context2d(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info), env(info.Env()) { + InstanceData* data = env.GetInstanceData(); + + if (!info[0].IsObject()) { + Napi::TypeError::New(env, "Canvas expected").ThrowAsJavaScriptException(); + return; + } + + Napi::Object obj = info[0].As(); + if (!obj.InstanceOf(data->CanvasCtor.Value()).UnwrapOr(false)) { + if (!env.IsExceptionPending()) { + Napi::TypeError::New(env, "Canvas expected").ThrowAsJavaScriptException(); + } + return; + } + + _canvas = Canvas::Unwrap(obj); + + bool isImageBackend = _canvas->backend()->getName() == "image"; + if (isImageBackend) { + cairo_format_t format = ImageBackend::DEFAULT_FORMAT; + + if (info[1].IsObject()) { + Napi::Object ctxAttributes = info[1].As(); + Napi::Value pixelFormat; + + if (ctxAttributes.Get("pixelFormat").UnwrapTo(&pixelFormat) && pixelFormat.IsString()) { + std::string utf8PixelFormat = pixelFormat.As(); + if (utf8PixelFormat == "RGBA32") format = CAIRO_FORMAT_ARGB32; + else if (utf8PixelFormat == "RGB24") format = CAIRO_FORMAT_RGB24; + else if (utf8PixelFormat == "A8") format = CAIRO_FORMAT_A8; + else if (utf8PixelFormat == "RGB16_565") format = CAIRO_FORMAT_RGB16_565; + else if (utf8PixelFormat == "A1") format = CAIRO_FORMAT_A1; +#ifdef CAIRO_FORMAT_RGB30 + else if (utf8PixelFormat == "RGB30") format = CAIRO_FORMAT_RGB30; +#endif + } + + // alpha: false forces use of RGB24 + Napi::Value alpha; + + if (ctxAttributes.Get("alpha").UnwrapTo(&alpha) && alpha.IsBoolean() && !alpha.As().Value()) { + format = CAIRO_FORMAT_RGB24; + } + } + + static_cast(_canvas->backend())->setFormat(format); + } + + _context = _canvas->createCairoContext(); _layout = pango_cairo_create_layout(_context); // As of January 2023, Pango rounds glyph positions which renders text wider @@ -188,8 +219,8 @@ Context2d::Context2d(Canvas *canvas) { */ Context2d::~Context2d() { - g_object_unref(_layout); - cairo_destroy(_context); + if (_layout) g_object_unref(_layout); + if (_context) cairo_destroy(_context); _resetPersistentHandles(); } @@ -283,7 +314,6 @@ create_transparent_gradient(cairo_pattern_t *source, float alpha) { cairo_pattern_get_radial_circles(source, &x0, &y0, &r0, &x1, &y1, &r1); newGradient = cairo_pattern_create_radial(x0, y0, r0, x1, y1, r1); } else { - Nan::ThrowError("Unexpected gradient type"); return NULL; } for ( i = 0; i < count; i++ ) { @@ -305,7 +335,6 @@ create_transparent_pattern(cairo_pattern_t *source, float alpha) { height); cairo_t *mask_context = cairo_create(mask_surface); if (cairo_status(mask_context) != CAIRO_STATUS_SUCCESS) { - Nan::ThrowError("Failed to initialize context"); return NULL; } cairo_set_source(mask_context, source); @@ -321,11 +350,11 @@ create_transparent_pattern(cairo_pattern_t *source, float alpha) { */ void -Context2d::setFillRule(v8::Local value) { +Context2d::setFillRule(Napi::Value value) { cairo_fill_rule_t rule = CAIRO_FILL_RULE_WINDING; - if (value->IsString()) { - Nan::Utf8String str(value); - if (std::strcmp(*str, "evenodd") == 0) { + if (value.IsString()) { + std::string str = value.As().Utf8Value(); + if (str == "evenodd") { rule = CAIRO_FILL_RULE_EVEN_ODD; } } @@ -340,7 +369,8 @@ Context2d::fill(bool preserve) { if (state->globalAlpha < 1) { new_pattern = create_transparent_pattern(state->fillPattern, state->globalAlpha); if (new_pattern == NULL) { - // failed to allocate; Nan::ThrowError has already been called, so return from this fn. + Napi::Error::New(env, "Failed to initialize context").ThrowAsJavaScriptException(); + // failed to allocate return; } cairo_set_source(_context, new_pattern); @@ -385,7 +415,8 @@ Context2d::fill(bool preserve) { if (state->globalAlpha < 1) { new_pattern = create_transparent_gradient(state->fillGradient, state->globalAlpha); if (new_pattern == NULL) { - // failed to recognize gradient; Nan::ThrowError has already been called, so return from this fn. + Napi::Error::New(env, "Unexpected gradient type").ThrowAsJavaScriptException(); + // failed to recognize gradient return; } cairo_pattern_set_filter(new_pattern, state->patternQuality); @@ -423,7 +454,8 @@ Context2d::stroke(bool preserve) { if (state->globalAlpha < 1) { new_pattern = create_transparent_pattern(state->strokePattern, state->globalAlpha); if (new_pattern == NULL) { - // failed to allocate; Nan::ThrowError has already been called, so return from this fn. + Napi::Error::New(env, "Failed to initialize context").ThrowAsJavaScriptException(); + // failed to allocate return; } cairo_set_source(_context, new_pattern); @@ -442,7 +474,8 @@ Context2d::stroke(bool preserve) { if (state->globalAlpha < 1) { new_pattern = create_transparent_gradient(state->strokeGradient, state->globalAlpha); if (new_pattern == NULL) { - // failed to recognize gradient; Nan::ThrowError has already been called, so return from this fn. + Napi::Error::New(env, "Unexpected gradient type").ThrowAsJavaScriptException(); + // failed to recognize gradient return; } cairo_pattern_set_filter(new_pattern, state->patternQuality); @@ -668,74 +701,14 @@ Context2d::blur(cairo_surface_t *surface, int radius) { free(precalc); } -/* - * Initialize a new Context2d with the given canvas. - */ - -NAN_METHOD(Context2d::New) { - if (!info.IsConstructCall()) { - return Nan::ThrowTypeError("Class constructors cannot be invoked without 'new'"); - } - - if (!info[0]->IsObject()) - return Nan::ThrowTypeError("Canvas expected"); - Local obj = Nan::To(info[0]).ToLocalChecked(); - if (!Nan::New(Canvas::constructor)->HasInstance(obj)) - return Nan::ThrowTypeError("Canvas expected"); - Canvas *canvas = Nan::ObjectWrap::Unwrap(obj); - - bool isImageBackend = canvas->backend()->getName() == "image"; - if (isImageBackend) { - cairo_format_t format = ImageBackend::DEFAULT_FORMAT; - if (info[1]->IsObject()) { - Local ctxAttributes = Nan::To(info[1]).ToLocalChecked(); - - Local pixelFormat = Nan::Get(ctxAttributes, Nan::New("pixelFormat").ToLocalChecked()).ToLocalChecked(); - if (pixelFormat->IsString()) { - Nan::Utf8String utf8PixelFormat(pixelFormat); - if (!strcmp(*utf8PixelFormat, "RGBA32")) format = CAIRO_FORMAT_ARGB32; - else if (!strcmp(*utf8PixelFormat, "RGB24")) format = CAIRO_FORMAT_RGB24; - else if (!strcmp(*utf8PixelFormat, "A8")) format = CAIRO_FORMAT_A8; - else if (!strcmp(*utf8PixelFormat, "RGB16_565")) format = CAIRO_FORMAT_RGB16_565; - else if (!strcmp(*utf8PixelFormat, "A1")) format = CAIRO_FORMAT_A1; -#ifdef CAIRO_FORMAT_RGB30 - else if (!strcmp(utf8PixelFormat, "RGB30")) format = CAIRO_FORMAT_RGB30; -#endif - } - - // alpha: false forces use of RGB24 - Local alpha = Nan::Get(ctxAttributes, Nan::New("alpha").ToLocalChecked()).ToLocalChecked(); - if (alpha->IsBoolean() && !Nan::To(alpha).FromMaybe(false)) { - format = CAIRO_FORMAT_RGB24; - } - } - static_cast(canvas->backend())->setFormat(format); - } - - Context2d *context = new Context2d(canvas); - - context->Wrap(info.This()); - info.GetReturnValue().Set(info.This()); -} - -/* - * Save some external modules as private references. - */ - -NAN_METHOD(Context2d::SaveExternalModules) { - _DOMMatrix.Reset(Nan::To(info[0]).ToLocalChecked()); - _parseFont.Reset(Nan::To(info[1]).ToLocalChecked()); -} - /* * Get format (string). */ -NAN_GETTER(Context2d::GetFormat) { - CHECK_RECEIVER(Context2d.GetFormat); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); +Napi::Value +Context2d::GetFormat(const Napi::CallbackInfo& info) { std::string pixelFormatString; - switch (context->canvas()->backend()->getFormat()) { + switch (canvas()->backend()->getFormat()) { case CAIRO_FORMAT_ARGB32: pixelFormatString = "RGBA32"; break; case CAIRO_FORMAT_RGB24: pixelFormatString = "RGB24"; break; case CAIRO_FORMAT_A8: pixelFormatString = "A8"; break; @@ -744,27 +717,28 @@ NAN_GETTER(Context2d::GetFormat) { #ifdef CAIRO_FORMAT_RGB30 case CAIRO_FORMAT_RGB30: pixelFormatString = "RGB30"; break; #endif - default: return info.GetReturnValue().SetNull(); + default: return env.Null(); } - info.GetReturnValue().Set(Nan::New(pixelFormatString).ToLocalChecked()); + return Napi::String::New(env, pixelFormatString); } /* * Create a new page. */ -NAN_METHOD(Context2d::AddPage) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - if (context->canvas()->backend()->getName() != "pdf") { - return Nan::ThrowError("only PDF canvases support .addPage()"); +void +Context2d::AddPage(const Napi::CallbackInfo& info) { + if (canvas()->backend()->getName() != "pdf") { + Napi::Error::New(env, "only PDF canvases support .addPage()").ThrowAsJavaScriptException(); + return; } - cairo_show_page(context->context()); - int width = Nan::To(info[0]).FromMaybe(0); - int height = Nan::To(info[1]).FromMaybe(0); - if (width < 1) width = context->canvas()->getWidth(); - if (height < 1) height = context->canvas()->getHeight(); - cairo_pdf_surface_set_size(context->canvas()->surface(), width, height); - return; + cairo_show_page(context()); + Napi::Number zero = Napi::Number::New(env, 0); + int width = info[0].ToNumber().UnwrapOr(zero).Int32Value(); + int height = info[1].ToNumber().UnwrapOr(zero).Int32Value(); + if (width < 1) width = canvas()->getWidth(); + if (height < 1) height = canvas()->getHeight(); + cairo_pdf_surface_set_size(canvas()->surface(), width, height); } /* @@ -775,29 +749,37 @@ NAN_METHOD(Context2d::AddPage) { * */ -NAN_METHOD(Context2d::PutImageData) { - if (!info[0]->IsObject()) - return Nan::ThrowTypeError("ImageData expected"); - Local obj = Nan::To(info[0]).ToLocalChecked(); - if (!Nan::New(ImageData::constructor)->HasInstance(obj)) - return Nan::ThrowTypeError("ImageData expected"); +void +Context2d::PutImageData(const Napi::CallbackInfo& info) { + if (!info[0].IsObject()) { + Napi::TypeError::New(env, "ImageData expected").ThrowAsJavaScriptException(); + return; + } + Napi::Object obj = info[0].As(); + InstanceData* data = env.GetInstanceData(); + if (!obj.InstanceOf(data->ImageDataCtor.Value()).UnwrapOr(false)) { + if (!env.IsExceptionPending()) { + Napi::TypeError::New(env, "ImageData expected").ThrowAsJavaScriptException(); + } + return; + } - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - ImageData *imageData = Nan::ObjectWrap::Unwrap(obj); + ImageData *imageData = ImageData::Unwrap(obj); + Napi::Number zero = Napi::Number::New(env, 0); uint8_t *src = imageData->data(); - uint8_t *dst = context->canvas()->data(); + uint8_t *dst = canvas()->data(); - int dstStride = context->canvas()->stride(); - int Bpp = dstStride / context->canvas()->getWidth(); + int dstStride = canvas()->stride(); + int Bpp = dstStride / canvas()->getWidth(); int srcStride = Bpp * imageData->width(); int sx = 0 , sy = 0 , sw = 0 , sh = 0 - , dx = Nan::To(info[1]).FromMaybe(0) - , dy = Nan::To(info[2]).FromMaybe(0) + , dx = info[1].ToNumber().UnwrapOr(zero).Int32Value() + , dy = info[2].ToNumber().UnwrapOr(zero).Int32Value() , rows , cols; @@ -809,10 +791,10 @@ NAN_METHOD(Context2d::PutImageData) { break; // imageData, dx, dy, sx, sy, sw, sh case 7: - sx = Nan::To(info[3]).FromMaybe(0); - sy = Nan::To(info[4]).FromMaybe(0); - sw = Nan::To(info[5]).FromMaybe(0); - sh = Nan::To(info[6]).FromMaybe(0); + sx = info[3].ToNumber().UnwrapOr(zero).Int32Value(); + sy = info[4].ToNumber().UnwrapOr(zero).Int32Value(); + sw = info[5].ToNumber().UnwrapOr(zero).Int32Value(); + sh = info[6].ToNumber().UnwrapOr(zero).Int32Value(); // fix up negative height, width if (sw < 0) sx += sw, sw = -sw; if (sh < 0) sy += sh, sh = -sh; @@ -827,7 +809,8 @@ NAN_METHOD(Context2d::PutImageData) { dy += sy; break; default: - return Nan::ThrowError("invalid arguments"); + Napi::Error::New(env, "invalid arguments").ThrowAsJavaScriptException(); + return; } // chop off outlying source data @@ -836,12 +819,12 @@ NAN_METHOD(Context2d::PutImageData) { // clamp width at canvas size // Need to wrap std::min calls using parens to prevent macro expansion on // windows. See http://stackoverflow.com/questions/5004858/stdmin-gives-error - cols = (std::min)(sw, context->canvas()->getWidth() - dx); - rows = (std::min)(sh, context->canvas()->getHeight() - dy); + cols = (std::min)(sw, canvas()->getWidth() - dx); + rows = (std::min)(sh, canvas()->getHeight() - dy); if (cols <= 0 || rows <= 0) return; - switch (context->canvas()->backend()->getFormat()) { + switch (canvas()->backend()->getFormat()) { case CAIRO_FORMAT_ARGB32: { src += sy * srcStride + sx * 4; dst += dstStride * dy + 4 * dx; @@ -922,7 +905,8 @@ NAN_METHOD(Context2d::PutImageData) { } case CAIRO_FORMAT_A1: { // TODO Should this be totally packed, or maintain a stride divisible by 4? - Nan::ThrowError("putImageData for CANVAS_FORMAT_A1 is not yet implemented"); + Napi::Error::New(env, "putImageData for CANVAS_FORMAT_A1 is not yet implemented").ThrowAsJavaScriptException(); + break; } case CAIRO_FORMAT_RGB16_565: { @@ -938,18 +922,19 @@ NAN_METHOD(Context2d::PutImageData) { #ifdef CAIRO_FORMAT_RGB30 case CAIRO_FORMAT_RGB30: { // TODO - Nan::ThrowError("putImageData for CANVAS_FORMAT_RGB30 is not yet implemented"); + Napi::Error::New(env, "putImageData for CANVAS_FORMAT_RGB30 is not yet implemented").ThrowAsJavaScriptException(); + break; } #endif default: { - Nan::ThrowError("Invalid pixel format or not an image canvas"); + Napi::Error::New(env, "Invalid pixel format or not an image canvas").ThrowAsJavaScriptException(); return; } } cairo_surface_mark_dirty_rectangle( - context->canvas()->surface() + canvas()->surface() , dx , dy , cols @@ -963,27 +948,36 @@ NAN_METHOD(Context2d::PutImageData) { * */ -NAN_METHOD(Context2d::GetImageData) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Canvas *canvas = context->canvas(); +Napi::Value +Context2d::GetImageData(const Napi::CallbackInfo& info) { + Napi::Number zero = Napi::Number::New(env, 0); + Canvas *canvas = this->canvas(); - int sx = Nan::To(info[0]).FromMaybe(0); - int sy = Nan::To(info[1]).FromMaybe(0); - int sw = Nan::To(info[2]).FromMaybe(0); - int sh = Nan::To(info[3]).FromMaybe(0); + int sx = info[0].ToNumber().UnwrapOr(zero).Int32Value(); + int sy = info[1].ToNumber().UnwrapOr(zero).Int32Value(); + int sw = info[2].ToNumber().UnwrapOr(zero).Int32Value(); + int sh = info[3].ToNumber().UnwrapOr(zero).Int32Value(); - if (!sw) - return Nan::ThrowError("IndexSizeError: The source width is 0."); - if (!sh) - return Nan::ThrowError("IndexSizeError: The source height is 0."); + if (!sw) { + Napi::Error::New(env, "IndexSizeError: The source width is 0.").ThrowAsJavaScriptException(); + return env.Undefined(); + } + if (!sh) { + Napi::Error::New(env, "IndexSizeError: The source height is 0.").ThrowAsJavaScriptException(); + return env.Undefined(); + } int width = canvas->getWidth(); int height = canvas->getHeight(); - if (!width) - return Nan::ThrowTypeError("Canvas width is 0"); - if (!height) - return Nan::ThrowTypeError("Canvas height is 0"); + if (!width) { + Napi::TypeError::New(env, "Canvas width is 0").ThrowAsJavaScriptException(); + return env.Undefined(); + } + if (!height) { + Napi::TypeError::New(env, "Canvas height is 0").ThrowAsJavaScriptException(); + return env.Undefined(); + } // WebKit and Firefox have this behavior: // Flip the coordinates so the origin is top/left-most: @@ -1021,17 +1015,16 @@ NAN_METHOD(Context2d::GetImageData) { uint8_t *src = canvas->data(); - Local buffer = ArrayBuffer::New(Isolate::GetCurrent(), size); - Local dataArray; + Napi::ArrayBuffer buffer = Napi::ArrayBuffer::New(env, size); + Napi::TypedArray dataArray; if (canvas->backend()->getFormat() == CAIRO_FORMAT_RGB16_565) { - dataArray = Uint16Array::New(buffer, 0, size >> 1); + dataArray = Napi::Uint16Array::New(env, size >> 1, buffer, 0); } else { - dataArray = Uint8ClampedArray::New(buffer, 0, size); + dataArray = Napi::Uint8Array::New(env, size, buffer, 0, napi_uint8_clamped_array); } - Nan::TypedArrayContents typedArrayContents(dataArray); - uint8_t* dst = *typedArrayContents; + uint8_t *dst = (uint8_t *)buffer.Data(); switch (canvas->backend()->getFormat()) { case CAIRO_FORMAT_ARGB32: { @@ -1097,7 +1090,8 @@ NAN_METHOD(Context2d::GetImageData) { } case CAIRO_FORMAT_A1: { // TODO Should this be totally packed, or maintain a stride divisible by 4? - Nan::ThrowError("getImageData for CANVAS_FORMAT_A1 is not yet implemented"); + Napi::Error::New(env, "getImageData for CANVAS_FORMAT_A1 is not yet implemented").ThrowAsJavaScriptException(); + break; } case CAIRO_FORMAT_RGB16_565: { @@ -1111,26 +1105,24 @@ NAN_METHOD(Context2d::GetImageData) { #ifdef CAIRO_FORMAT_RGB30 case CAIRO_FORMAT_RGB30: { // TODO - Nan::ThrowError("getImageData for CANVAS_FORMAT_RGB30 is not yet implemented"); + Napi::Error::New(env, "getImageData for CANVAS_FORMAT_RGB30 is not yet implemented").ThrowAsJavaScriptException(); + break; } #endif default: { // Unlikely - Nan::ThrowError("Invalid pixel format or not an image canvas"); - return; + Napi::Error::New(env, "Invalid pixel format or not an image canvas").ThrowAsJavaScriptException(); + return env.Null(); } } - const int argc = 3; - Local swHandle = Nan::New(sw); - Local shHandle = Nan::New(sh); - Local argv[argc] = { dataArray, swHandle, shHandle }; - - Local ctor = Nan::GetFunction(Nan::New(ImageData::constructor)).ToLocalChecked(); - Local instance = Nan::NewInstance(ctor, argc, argv).ToLocalChecked(); + Napi::Number swHandle = Napi::Number::New(env, sw); + Napi::Number shHandle = Napi::Number::New(env, sh); + Napi::Function ctor = env.GetInstanceData()->ImageDataCtor.Value(); + Napi::Maybe ret = ctor.New({ dataArray, swHandle, shHandle }); - info.GetReturnValue().Set(instance); + return ret.IsJust() ? ret.Unwrap() : env.Undefined(); } /** @@ -1138,40 +1130,37 @@ NAN_METHOD(Context2d::GetImageData) { * `ImageData` instance for dimensions. */ -NAN_METHOD(Context2d::CreateImageData){ - Isolate *iso = Isolate::GetCurrent(); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Canvas *canvas = context->canvas(); +Napi::Value +Context2d::CreateImageData(const Napi::CallbackInfo& info){ + Canvas *canvas = this->canvas(); + Napi::Number zero = Napi::Number::New(env, 0); int32_t width, height; - if (info[0]->IsObject()) { - Local obj = Nan::To(info[0]).ToLocalChecked(); - width = Nan::To(Nan::Get(obj, Nan::New("width").ToLocalChecked()).ToLocalChecked()).FromMaybe(0); - height = Nan::To(Nan::Get(obj, Nan::New("height").ToLocalChecked()).ToLocalChecked()).FromMaybe(0); + if (info[0].IsObject()) { + Napi::Object obj = info[0].As(); + width = obj.Get("width").UnwrapOr(zero).ToNumber().UnwrapOr(zero).Int32Value(); + height = obj.Get("height").UnwrapOr(zero).ToNumber().UnwrapOr(zero).Int32Value(); } else { - width = Nan::To(info[0]).FromMaybe(0); - height = Nan::To(info[1]).FromMaybe(0); + width = info[0].ToNumber().UnwrapOr(zero).Int32Value(); + height = info[1].ToNumber().UnwrapOr(zero).Int32Value(); } int stride = canvas->stride(); double Bpp = static_cast(stride) / canvas->getWidth(); int nBytes = static_cast(Bpp * width * height + .5); - Local ab = ArrayBuffer::New(iso, nBytes); - Local arr; + Napi::ArrayBuffer ab = Napi::ArrayBuffer::New(env, nBytes); + Napi::Value arr; if (canvas->backend()->getFormat() == CAIRO_FORMAT_RGB16_565) - arr = Uint16Array::New(ab, 0, nBytes / 2); + arr = Napi::Uint16Array::New(env, nBytes / 2, ab, 0); else - arr = Uint8ClampedArray::New(ab, 0, nBytes); + arr = Napi::Uint8Array::New(env, nBytes, ab, 0, napi_uint8_clamped_array); - const int argc = 3; - Local argv[argc] = { arr, Nan::New(width), Nan::New(height) }; + Napi::Function ctor = env.GetInstanceData()->ImageDataCtor.Value(); + Napi::Maybe ret = ctor.New({ arr, Napi::Number::New(env, width), Napi::Number::New(env, height) }); - Local ctor = Nan::GetFunction(Nan::New(ImageData::constructor)).ToLocalChecked(); - Local instance = Nan::NewInstance(ctor, argc, argv).ToLocalChecked(); - - info.GetReturnValue().Set(instance); + return ret.IsJust() ? ret.Unwrap() : env.Undefined(); } /* @@ -1197,13 +1186,19 @@ void decompose_matrix(cairo_matrix_t matrix, double *destination) { * */ -NAN_METHOD(Context2d::DrawImage) { +void +Context2d::DrawImage(const Napi::CallbackInfo& info) { int infoLen = info.Length(); - if (infoLen != 3 && infoLen != 5 && infoLen != 9) - return Nan::ThrowTypeError("Invalid arguments"); - if (!info[0]->IsObject()) - return Nan::ThrowTypeError("The first argument must be an object"); + if (infoLen != 3 && infoLen != 5 && infoLen != 9) { + Napi::TypeError::New(env, "Invalid arguments").ThrowAsJavaScriptException(); + return; + } + + if (!info[0].IsObject()) { + Napi::TypeError::New(env, "The first argument must be an object").ThrowAsJavaScriptException(); + return; + } double args[8]; if(!checkArgs(info, args, infoLen - 1, 1)) @@ -1222,32 +1217,35 @@ NAN_METHOD(Context2d::DrawImage) { cairo_surface_t *surface; - Local obj = Nan::To(info[0]).ToLocalChecked(); + Napi::Object obj = info[0].As(); // Image - if (Nan::New(Image::constructor)->HasInstance(obj)) { - Image *img = Nan::ObjectWrap::Unwrap(obj); + if (obj.InstanceOf(env.GetInstanceData()->ImageCtor.Value()).UnwrapOr(false)) { + Image *img = Image::Unwrap(obj); if (!img->isComplete()) { - return Nan::ThrowError("Image given has not completed loading"); + Napi::Error::New(env, "Image given has not completed loading").ThrowAsJavaScriptException(); + return; } source_w = sw = img->width; source_h = sh = img->height; surface = img->surface(); // Canvas - } else if (Nan::New(Canvas::constructor)->HasInstance(obj)) { - Canvas *canvas = Nan::ObjectWrap::Unwrap(obj); + } else if (obj.InstanceOf(env.GetInstanceData()->CanvasCtor.Value()).UnwrapOr(false)) { + Canvas *canvas = Canvas::Unwrap(obj); source_w = sw = canvas->getWidth(); source_h = sh = canvas->getHeight(); surface = canvas->surface(); // Invalid } else { - return Nan::ThrowTypeError("Image or Canvas expected"); + if (!env.IsExceptionPending()) { + Napi::TypeError::New(env, "Image or Canvas expected").ThrowAsJavaScriptException(); + } + return; } - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); + cairo_t *ctx = context(); // Arguments switch (infoLen) { @@ -1286,7 +1284,7 @@ NAN_METHOD(Context2d::DrawImage) { cairo_matrix_t matrix; double transforms[6]; - cairo_get_matrix(context->context(), &matrix); + cairo_get_matrix(ctx, &matrix); decompose_matrix(matrix, transforms); // extract the scale value from the current transform so that we know how many pixels we // need for our extra canvas in the drawImage operation. @@ -1298,7 +1296,7 @@ NAN_METHOD(Context2d::DrawImage) { double fy = dh / sh * current_scale_y; // transforms[2] is scale on X bool needScale = dw != sw || dh != sh; bool needCut = sw != source_w || sh != source_h || sx < 0 || sy < 0; - bool sameCanvas = surface == context->canvas()->surface(); + bool sameCanvas = surface == canvas()->surface(); bool needsExtraSurface = sameCanvas || needCut || needScale; cairo_surface_t *surfTemp = NULL; cairo_t *ctxTemp = NULL; @@ -1346,23 +1344,23 @@ NAN_METHOD(Context2d::DrawImage) { translate_y = sy; } cairo_set_source_surface(ctxTemp, surface, -translate_x, -translate_y); - cairo_pattern_set_filter(cairo_get_source(ctxTemp), context->state->imageSmoothingEnabled ? context->state->patternQuality : CAIRO_FILTER_NEAREST); + cairo_pattern_set_filter(cairo_get_source(ctxTemp), state->imageSmoothingEnabled ? state->patternQuality : CAIRO_FILTER_NEAREST); cairo_pattern_set_extend(cairo_get_source(ctxTemp), CAIRO_EXTEND_REFLECT); cairo_paint_with_alpha(ctxTemp, 1); surface = surfTemp; } // apply shadow if there is one - if (context->hasShadow()) { - if(context->state->shadowBlur) { + if (hasShadow()) { + if(state->shadowBlur) { // we need to create a new surface in order to blur - int pad = context->state->shadowBlur * 2; + int pad = state->shadowBlur * 2; cairo_surface_t *shadow_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, dw + 2 * pad, dh + 2 * pad); cairo_t *shadow_context = cairo_create(shadow_surface); // mask and blur - context->setSourceRGBA(shadow_context, context->state->shadow); + setSourceRGBA(shadow_context, state->shadow); cairo_mask_surface(shadow_context, surface, pad, pad); - context->blur(shadow_surface, context->state->shadowBlur); + blur(shadow_surface, state->shadowBlur); // paint // @note: ShadowBlur looks different in each browser. This implementation matches chrome as close as possible. @@ -1370,17 +1368,17 @@ NAN_METHOD(Context2d::DrawImage) { // implementation, and its not immediately clear why an offset is necessary, but without it, the result // in chrome is different. cairo_set_source_surface(ctx, shadow_surface, - dx + context->state->shadowOffsetX - pad + 1.4, - dy + context->state->shadowOffsetY - pad + 1.4); + dx + state->shadowOffsetX - pad + 1.4, + dy + state->shadowOffsetY - pad + 1.4); cairo_paint(ctx); // cleanup cairo_destroy(shadow_context); cairo_surface_destroy(shadow_surface); } else { - context->setSourceRGBA(context->state->shadow); + setSourceRGBA(state->shadow); cairo_mask_surface(ctx, surface, - dx + (context->state->shadowOffsetX), - dy + (context->state->shadowOffsetY)); + dx + (state->shadowOffsetX), + dy + (state->shadowOffsetY)); } } @@ -1395,9 +1393,9 @@ NAN_METHOD(Context2d::DrawImage) { } // Paint cairo_set_source_surface(ctx, surface, scaled_dx + extra_dx, scaled_dy + extra_dy); - cairo_pattern_set_filter(cairo_get_source(ctx), context->state->imageSmoothingEnabled ? context->state->patternQuality : CAIRO_FILTER_NEAREST); + cairo_pattern_set_filter(cairo_get_source(ctx), state->imageSmoothingEnabled ? state->patternQuality : CAIRO_FILTER_NEAREST); cairo_pattern_set_extend(cairo_get_source(ctx), CAIRO_EXTEND_NONE); - cairo_paint_with_alpha(ctx, context->state->globalAlpha); + cairo_paint_with_alpha(ctx, state->globalAlpha); cairo_restore(ctx); @@ -1411,22 +1409,21 @@ NAN_METHOD(Context2d::DrawImage) { * Get global alpha. */ -NAN_GETTER(Context2d::GetGlobalAlpha) { - CHECK_RECEIVER(Context2d.GetGlobalAlpha); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(context->state->globalAlpha)); +Napi::Value +Context2d::GetGlobalAlpha(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, state->globalAlpha); } /* * Set global alpha. */ -NAN_SETTER(Context2d::SetGlobalAlpha) { - CHECK_RECEIVER(Context2d.SetGlobalAlpha); - double n = Nan::To(value).FromMaybe(0); - if (n >= 0 && n <= 1) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->globalAlpha = n; +void +Context2d::SetGlobalAlpha(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::Maybe numberValue = value.ToNumber(); + if (numberValue.IsJust()) { + double n = numberValue.Unwrap().DoubleValue(); + if (n >= 0 && n <= 1) state->globalAlpha = n; } } @@ -1434,10 +1431,9 @@ NAN_SETTER(Context2d::SetGlobalAlpha) { * Get global composite operation. */ -NAN_GETTER(Context2d::GetGlobalCompositeOperation) { - CHECK_RECEIVER(Context2d.GetGlobalCompositeOperation); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); +Napi::Value +Context2d::GetGlobalCompositeOperation(const Napi::CallbackInfo& info) { + cairo_t *ctx = context(); const char *op{}; switch (cairo_get_operator(ctx)) { @@ -1479,27 +1475,28 @@ NAN_GETTER(Context2d::GetGlobalCompositeOperation) { default: op = "source-over"; } - info.GetReturnValue().Set(Nan::New(op).ToLocalChecked()); + return Napi::String::New(env, op); } /* * Set pattern quality. */ -NAN_SETTER(Context2d::SetPatternQuality) { - CHECK_RECEIVER(Context2d.SetPatternQuality); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Nan::Utf8String quality(Nan::To(value).ToLocalChecked()); - if (0 == strcmp("fast", *quality)) { - context->state->patternQuality = CAIRO_FILTER_FAST; - } else if (0 == strcmp("good", *quality)) { - context->state->patternQuality = CAIRO_FILTER_GOOD; - } else if (0 == strcmp("best", *quality)) { - context->state->patternQuality = CAIRO_FILTER_BEST; - } else if (0 == strcmp("nearest", *quality)) { - context->state->patternQuality = CAIRO_FILTER_NEAREST; - } else if (0 == strcmp("bilinear", *quality)) { - context->state->patternQuality = CAIRO_FILTER_BILINEAR; +void +Context2d::SetPatternQuality(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (value.IsString()) { + std::string quality = value.As().Utf8Value(); + if (quality == "fast") { + state->patternQuality = CAIRO_FILTER_FAST; + } else if (quality == "good") { + state->patternQuality = CAIRO_FILTER_GOOD; + } else if (quality == "best") { + state->patternQuality = CAIRO_FILTER_BEST; + } else if (quality == "nearest") { + state->patternQuality = CAIRO_FILTER_NEAREST; + } else if (quality == "bilinear") { + state->patternQuality = CAIRO_FILTER_BILINEAR; + } } } @@ -1507,148 +1504,146 @@ NAN_SETTER(Context2d::SetPatternQuality) { * Get pattern quality. */ -NAN_GETTER(Context2d::GetPatternQuality) { - CHECK_RECEIVER(Context2d.GetPatternQuality); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); +Napi::Value +Context2d::GetPatternQuality(const Napi::CallbackInfo& info) { const char *quality; - switch (context->state->patternQuality) { + switch (state->patternQuality) { case CAIRO_FILTER_FAST: quality = "fast"; break; case CAIRO_FILTER_BEST: quality = "best"; break; case CAIRO_FILTER_NEAREST: quality = "nearest"; break; case CAIRO_FILTER_BILINEAR: quality = "bilinear"; break; default: quality = "good"; } - info.GetReturnValue().Set(Nan::New(quality).ToLocalChecked()); + return Napi::String::New(env, quality); } /* * Set ImageSmoothingEnabled value. */ -NAN_SETTER(Context2d::SetImageSmoothingEnabled) { - CHECK_RECEIVER(Context2d.SetImageSmoothingEnabled); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->imageSmoothingEnabled = Nan::To(value).FromMaybe(false); +void +Context2d::SetImageSmoothingEnabled(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::Boolean boolValue; + if (value.ToBoolean().UnwrapTo(&boolValue)) state->imageSmoothingEnabled = boolValue.Value(); } /* * Get pattern quality. */ -NAN_GETTER(Context2d::GetImageSmoothingEnabled) { - CHECK_RECEIVER(Context2d.GetImageSmoothingEnabled); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(context->state->imageSmoothingEnabled)); +Napi::Value +Context2d::GetImageSmoothingEnabled(const Napi::CallbackInfo& info) { + return Napi::Boolean::New(env, state->imageSmoothingEnabled); } /* * Set global composite operation. */ -NAN_SETTER(Context2d::SetGlobalCompositeOperation) { - CHECK_RECEIVER(Context2d.SetGlobalCompositeOperation); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); - Nan::Utf8String opStr(Nan::To(value).ToLocalChecked()); // Unlike CSS colors, this *is* case-sensitive - const std::map blendmodes = { - // composite modes: - {"clear", CAIRO_OPERATOR_CLEAR}, - {"copy", CAIRO_OPERATOR_SOURCE}, - {"destination", CAIRO_OPERATOR_DEST}, // this seems to have been omitted from the spec - {"source-over", CAIRO_OPERATOR_OVER}, - {"destination-over", CAIRO_OPERATOR_DEST_OVER}, - {"source-in", CAIRO_OPERATOR_IN}, - {"destination-in", CAIRO_OPERATOR_DEST_IN}, - {"source-out", CAIRO_OPERATOR_OUT}, - {"destination-out", CAIRO_OPERATOR_DEST_OUT}, - {"source-atop", CAIRO_OPERATOR_ATOP}, - {"destination-atop", CAIRO_OPERATOR_DEST_ATOP}, - {"xor", CAIRO_OPERATOR_XOR}, - {"lighter", CAIRO_OPERATOR_ADD}, - // blend modes: - {"normal", CAIRO_OPERATOR_OVER}, - {"multiply", CAIRO_OPERATOR_MULTIPLY}, - {"screen", CAIRO_OPERATOR_SCREEN}, - {"overlay", CAIRO_OPERATOR_OVERLAY}, - {"darken", CAIRO_OPERATOR_DARKEN}, - {"lighten", CAIRO_OPERATOR_LIGHTEN}, - {"color-dodge", CAIRO_OPERATOR_COLOR_DODGE}, - {"color-burn", CAIRO_OPERATOR_COLOR_BURN}, - {"hard-light", CAIRO_OPERATOR_HARD_LIGHT}, - {"soft-light", CAIRO_OPERATOR_SOFT_LIGHT}, - {"difference", CAIRO_OPERATOR_DIFFERENCE}, - {"exclusion", CAIRO_OPERATOR_EXCLUSION}, - {"hue", CAIRO_OPERATOR_HSL_HUE}, - {"saturation", CAIRO_OPERATOR_HSL_SATURATION}, - {"color", CAIRO_OPERATOR_HSL_COLOR}, - {"luminosity", CAIRO_OPERATOR_HSL_LUMINOSITY}, - // non-standard: - {"saturate", CAIRO_OPERATOR_SATURATE} - }; - auto op = blendmodes.find(*opStr); - if (op != blendmodes.end()) cairo_set_operator(ctx, op->second); +void +Context2d::SetGlobalCompositeOperation(const Napi::CallbackInfo& info, const Napi::Value& value) { + cairo_t *ctx = this->context(); + Napi::String opStr; + if (value.ToString().UnwrapTo(&opStr)) { // Unlike CSS colors, this *is* case-sensitive + const std::map blendmodes = { + // composite modes: + {"clear", CAIRO_OPERATOR_CLEAR}, + {"copy", CAIRO_OPERATOR_SOURCE}, + {"destination", CAIRO_OPERATOR_DEST}, // this seems to have been omitted from the spec + {"source-over", CAIRO_OPERATOR_OVER}, + {"destination-over", CAIRO_OPERATOR_DEST_OVER}, + {"source-in", CAIRO_OPERATOR_IN}, + {"destination-in", CAIRO_OPERATOR_DEST_IN}, + {"source-out", CAIRO_OPERATOR_OUT}, + {"destination-out", CAIRO_OPERATOR_DEST_OUT}, + {"source-atop", CAIRO_OPERATOR_ATOP}, + {"destination-atop", CAIRO_OPERATOR_DEST_ATOP}, + {"xor", CAIRO_OPERATOR_XOR}, + {"lighter", CAIRO_OPERATOR_ADD}, + // blend modes: + {"normal", CAIRO_OPERATOR_OVER}, + {"multiply", CAIRO_OPERATOR_MULTIPLY}, + {"screen", CAIRO_OPERATOR_SCREEN}, + {"overlay", CAIRO_OPERATOR_OVERLAY}, + {"darken", CAIRO_OPERATOR_DARKEN}, + {"lighten", CAIRO_OPERATOR_LIGHTEN}, + {"color-dodge", CAIRO_OPERATOR_COLOR_DODGE}, + {"color-burn", CAIRO_OPERATOR_COLOR_BURN}, + {"hard-light", CAIRO_OPERATOR_HARD_LIGHT}, + {"soft-light", CAIRO_OPERATOR_SOFT_LIGHT}, + {"difference", CAIRO_OPERATOR_DIFFERENCE}, + {"exclusion", CAIRO_OPERATOR_EXCLUSION}, + {"hue", CAIRO_OPERATOR_HSL_HUE}, + {"saturation", CAIRO_OPERATOR_HSL_SATURATION}, + {"color", CAIRO_OPERATOR_HSL_COLOR}, + {"luminosity", CAIRO_OPERATOR_HSL_LUMINOSITY}, + // non-standard: + {"saturate", CAIRO_OPERATOR_SATURATE} + }; + auto op = blendmodes.find(opStr.Utf8Value()); + if (op != blendmodes.end()) cairo_set_operator(ctx, op->second); + } } /* * Get shadow offset x. */ -NAN_GETTER(Context2d::GetShadowOffsetX) { - CHECK_RECEIVER(Context2d.GetShadowOffsetX); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(context->state->shadowOffsetX)); +Napi::Value +Context2d::GetShadowOffsetX(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, state->shadowOffsetX); } /* * Set shadow offset x. */ -NAN_SETTER(Context2d::SetShadowOffsetX) { - CHECK_RECEIVER(Context2d.SetShadowOffsetX); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->shadowOffsetX = Nan::To(value).FromMaybe(0); +void +Context2d::SetShadowOffsetX(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::Number numberValue; + if (value.ToNumber().UnwrapTo(&numberValue)) state->shadowOffsetX = numberValue.DoubleValue(); } /* * Get shadow offset y. */ -NAN_GETTER(Context2d::GetShadowOffsetY) { - CHECK_RECEIVER(Context2d.GetShadowOffsetY); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(context->state->shadowOffsetY)); +Napi::Value +Context2d::GetShadowOffsetY(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, state->shadowOffsetY); } /* * Set shadow offset y. */ -NAN_SETTER(Context2d::SetShadowOffsetY) { - CHECK_RECEIVER(Context2d.SetShadowOffsetY); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->shadowOffsetY = Nan::To(value).FromMaybe(0); +void +Context2d::SetShadowOffsetY(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::Number numberValue; + if (value.ToNumber().UnwrapTo(&numberValue)) state->shadowOffsetY = numberValue.DoubleValue(); } /* * Get shadow blur. */ -NAN_GETTER(Context2d::GetShadowBlur) { - CHECK_RECEIVER(Context2d.GetShadowBlur); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(context->state->shadowBlur)); +Napi::Value +Context2d::GetShadowBlur(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, state->shadowBlur); } /* * Set shadow blur. */ -NAN_SETTER(Context2d::SetShadowBlur) { - CHECK_RECEIVER(Context2d.SetShadowBlur); - int n = Nan::To(value).FromMaybe(0); - if (n >= 0) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->shadowBlur = n; +void +Context2d::SetShadowBlur(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::Number n; + if (value.ToNumber().UnwrapTo(&n)) { + double v = n.DoubleValue(); + if (v >= 0 && v <= std::numeric_limitsshadowBlur)>::max()) { + state->shadowBlur = v; + } } } @@ -1656,73 +1651,76 @@ NAN_SETTER(Context2d::SetShadowBlur) { * Get current antialiasing setting. */ -NAN_GETTER(Context2d::GetAntiAlias) { - CHECK_RECEIVER(Context2d.GetAntiAlias); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); +Napi::Value +Context2d::GetAntiAlias(const Napi::CallbackInfo& info) { const char *aa; - switch (cairo_get_antialias(context->context())) { + switch (cairo_get_antialias(context())) { case CAIRO_ANTIALIAS_NONE: aa = "none"; break; case CAIRO_ANTIALIAS_GRAY: aa = "gray"; break; case CAIRO_ANTIALIAS_SUBPIXEL: aa = "subpixel"; break; default: aa = "default"; } - info.GetReturnValue().Set(Nan::New(aa).ToLocalChecked()); + return Napi::String::New(env, aa); } /* * Set antialiasing. */ -NAN_SETTER(Context2d::SetAntiAlias) { - CHECK_RECEIVER(Context2d.SetAntiAlias); - Nan::Utf8String str(Nan::To(value).ToLocalChecked()); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); - cairo_antialias_t a; - if (0 == strcmp("none", *str)) { - a = CAIRO_ANTIALIAS_NONE; - } else if (0 == strcmp("default", *str)) { - a = CAIRO_ANTIALIAS_DEFAULT; - } else if (0 == strcmp("gray", *str)) { - a = CAIRO_ANTIALIAS_GRAY; - } else if (0 == strcmp("subpixel", *str)) { - a = CAIRO_ANTIALIAS_SUBPIXEL; - } else { - a = cairo_get_antialias(ctx); +void +Context2d::SetAntiAlias(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::String stringValue; + + if (value.ToString().UnwrapTo(&stringValue)) { + std::string str = stringValue.Utf8Value(); + cairo_t *ctx = context(); + cairo_antialias_t a; + if (str == "none") { + a = CAIRO_ANTIALIAS_NONE; + } else if (str == "default") { + a = CAIRO_ANTIALIAS_DEFAULT; + } else if (str == "gray") { + a = CAIRO_ANTIALIAS_GRAY; + } else if (str == "subpixel") { + a = CAIRO_ANTIALIAS_SUBPIXEL; + } else { + a = cairo_get_antialias(ctx); + } + cairo_set_antialias(ctx, a); } - cairo_set_antialias(ctx, a); } /* * Get text drawing mode. */ -NAN_GETTER(Context2d::GetTextDrawingMode) { - CHECK_RECEIVER(Context2d.GetTextDrawingMode); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); +Napi::Value +Context2d::GetTextDrawingMode(const Napi::CallbackInfo& info) { const char *mode; - if (context->state->textDrawingMode == TEXT_DRAW_PATHS) { + if (state->textDrawingMode == TEXT_DRAW_PATHS) { mode = "path"; - } else if (context->state->textDrawingMode == TEXT_DRAW_GLYPHS) { + } else if (state->textDrawingMode == TEXT_DRAW_GLYPHS) { mode = "glyph"; } else { mode = "unknown"; } - info.GetReturnValue().Set(Nan::New(mode).ToLocalChecked()); + return Napi::String::New(env, mode); } /* * Set text drawing mode. */ -NAN_SETTER(Context2d::SetTextDrawingMode) { - CHECK_RECEIVER(Context2d.SetTextDrawingMode); - Nan::Utf8String str(Nan::To(value).ToLocalChecked()); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - if (0 == strcmp("path", *str)) { - context->state->textDrawingMode = TEXT_DRAW_PATHS; - } else if (0 == strcmp("glyph", *str)) { - context->state->textDrawingMode = TEXT_DRAW_GLYPHS; +void +Context2d::SetTextDrawingMode(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::String stringValue; + if (value.ToString().UnwrapTo(&stringValue)) { + std::string str = stringValue.Utf8Value(); + if (str == "path") { + state->textDrawingMode = TEXT_DRAW_PATHS; + } else if (str == "glyph") { + state->textDrawingMode = TEXT_DRAW_GLYPHS; + } } } @@ -1730,79 +1728,77 @@ NAN_SETTER(Context2d::SetTextDrawingMode) { * Get filter. */ -NAN_GETTER(Context2d::GetQuality) { - CHECK_RECEIVER(Context2d.GetQuality); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); +Napi::Value +Context2d::GetQuality(const Napi::CallbackInfo& info) { const char *filter; - switch (cairo_pattern_get_filter(cairo_get_source(context->context()))) { + switch (cairo_pattern_get_filter(cairo_get_source(context()))) { case CAIRO_FILTER_FAST: filter = "fast"; break; case CAIRO_FILTER_BEST: filter = "best"; break; case CAIRO_FILTER_NEAREST: filter = "nearest"; break; case CAIRO_FILTER_BILINEAR: filter = "bilinear"; break; default: filter = "good"; } - info.GetReturnValue().Set(Nan::New(filter).ToLocalChecked()); + return Napi::String::New(env, filter); } /* * Set filter. */ -NAN_SETTER(Context2d::SetQuality) { - CHECK_RECEIVER(Context2d.SetQuality); - Nan::Utf8String str(Nan::To(value).ToLocalChecked()); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_filter_t filter; - if (0 == strcmp("fast", *str)) { - filter = CAIRO_FILTER_FAST; - } else if (0 == strcmp("best", *str)) { - filter = CAIRO_FILTER_BEST; - } else if (0 == strcmp("nearest", *str)) { - filter = CAIRO_FILTER_NEAREST; - } else if (0 == strcmp("bilinear", *str)) { - filter = CAIRO_FILTER_BILINEAR; - } else { - filter = CAIRO_FILTER_GOOD; +void +Context2d::SetQuality(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::String stringValue; + if (value.ToString().UnwrapTo(&stringValue)) { + std::string str = stringValue.Utf8Value(); + cairo_filter_t filter; + if (str == "fast") { + filter = CAIRO_FILTER_FAST; + } else if (str == "best") { + filter = CAIRO_FILTER_BEST; + } else if (str == "nearest") { + filter = CAIRO_FILTER_NEAREST; + } else if (str == "bilinear") { + filter = CAIRO_FILTER_BILINEAR; + } else { + filter = CAIRO_FILTER_GOOD; + } + cairo_pattern_set_filter(cairo_get_source(context()), filter); } - cairo_pattern_set_filter(cairo_get_source(context->context()), filter); } /* * Helper for get current transform matrix */ -Local -get_current_transform(Context2d *context) { - Isolate *iso = Isolate::GetCurrent(); - - Local arr = Float64Array::New(ArrayBuffer::New(iso, 48), 0, 6); - Nan::TypedArrayContents dest(arr); +Napi::Value +Context2d::get_current_transform() { + Napi::Float64Array arr = Napi::Float64Array::New(env, 6); + double *dest = arr.Data(); cairo_matrix_t matrix; - cairo_get_matrix(context->context(), &matrix); - (*dest)[0] = matrix.xx; - (*dest)[1] = matrix.yx; - (*dest)[2] = matrix.xy; - (*dest)[3] = matrix.yy; - (*dest)[4] = matrix.x0; - (*dest)[5] = matrix.y0; - - const int argc = 1; - Local argv[argc] = { arr }; - return Nan::NewInstance(context->_DOMMatrix.Get(iso), argc, argv).ToLocalChecked(); + cairo_get_matrix(context(), &matrix); + dest[0] = matrix.xx; + dest[1] = matrix.yx; + dest[2] = matrix.xy; + dest[3] = matrix.yy; + dest[4] = matrix.x0; + dest[5] = matrix.y0; + Napi::Maybe ret = env.GetInstanceData()->DOMMatrixCtor.Value().New({ arr }); + return ret.IsJust() ? ret.Unwrap() : env.Undefined(); } /* * Helper for get/set transform. */ -void parse_matrix_from_object(cairo_matrix_t &matrix, Local mat) { +void parse_matrix_from_object(cairo_matrix_t &matrix, Napi::Object mat) { + Napi::Value zero = Napi::Number::New(mat.Env(), 0); cairo_matrix_init(&matrix, - Nan::To(Nan::Get(mat, Nan::New("a").ToLocalChecked()).ToLocalChecked()).FromMaybe(0), - Nan::To(Nan::Get(mat, Nan::New("b").ToLocalChecked()).ToLocalChecked()).FromMaybe(0), - Nan::To(Nan::Get(mat, Nan::New("c").ToLocalChecked()).ToLocalChecked()).FromMaybe(0), - Nan::To(Nan::Get(mat, Nan::New("d").ToLocalChecked()).ToLocalChecked()).FromMaybe(0), - Nan::To(Nan::Get(mat, Nan::New("e").ToLocalChecked()).ToLocalChecked()).FromMaybe(0), - Nan::To(Nan::Get(mat, Nan::New("f").ToLocalChecked()).ToLocalChecked()).FromMaybe(0) + mat.Get("a").UnwrapOr(zero).As().DoubleValue(), + mat.Get("b").UnwrapOr(zero).As().DoubleValue(), + mat.Get("c").UnwrapOr(zero).As().DoubleValue(), + mat.Get("d").UnwrapOr(zero).As().DoubleValue(), + mat.Get("e").UnwrapOr(zero).As().DoubleValue(), + mat.Get("f").UnwrapOr(zero).As().DoubleValue() ); } @@ -1811,78 +1807,70 @@ void parse_matrix_from_object(cairo_matrix_t &matrix, Local mat) { * Get current transform. */ -NAN_GETTER(Context2d::GetCurrentTransform) { - CHECK_RECEIVER(Context2d.GetCurrentTransform); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Local instance = get_current_transform(context); - - info.GetReturnValue().Set(instance); +Napi::Value +Context2d::GetCurrentTransform(const Napi::CallbackInfo& info) { + return get_current_transform(); } /* * Set current transform. */ -NAN_SETTER(Context2d::SetCurrentTransform) { - CHECK_RECEIVER(Context2d.SetCurrentTransform); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Local ctx = Nan::GetCurrentContext(); - Local mat = Nan::To(value).ToLocalChecked(); +void +Context2d::SetCurrentTransform(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::Object mat; -#if NODE_MAJOR_VERSION >= 8 - if (!mat->InstanceOf(ctx, _DOMMatrix.Get(Isolate::GetCurrent())).ToChecked()) { - return Nan::ThrowTypeError("Expected DOMMatrix"); - } -#endif + if (value.ToObject().UnwrapTo(&mat)) { + if (!mat.InstanceOf(env.GetInstanceData()->DOMMatrixCtor.Value()).UnwrapOr(false)) { + if (!env.IsExceptionPending()) { + Napi::TypeError::New(env, "Expected DOMMatrix").ThrowAsJavaScriptException(); + } + return; + } - cairo_matrix_t matrix; - parse_matrix_from_object(matrix, mat); + cairo_matrix_t matrix; + parse_matrix_from_object(matrix, mat); - cairo_transform(context->context(), &matrix); + cairo_transform(context(), &matrix); + } } /* * Get current fill style. */ -NAN_GETTER(Context2d::GetFillStyle) { - CHECK_RECEIVER(Context2d.GetFillStyle); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Isolate *iso = Isolate::GetCurrent(); - Local style; +Napi::Value +Context2d::GetFillStyle(const Napi::CallbackInfo& info) { + Napi::Value style; - if (context->_fillStyle.IsEmpty()) - style = context->_getFillColor(); + if (_fillStyle.IsEmpty()) + style = _getFillColor(); else - style = context->_fillStyle.Get(iso); + style = _fillStyle.Value(); - info.GetReturnValue().Set(style); + return style; } /* * Set current fill style. */ -NAN_SETTER(Context2d::SetFillStyle) { - CHECK_RECEIVER(Context2d.SetFillStyle); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - - if (value->IsString()) { - MaybeLocal mstr = Nan::To(value); - if (mstr.IsEmpty()) return; - Local str = mstr.ToLocalChecked(); - context->_fillStyle.Reset(); - context->_setFillColor(str); - } else if (value->IsObject()) { - Local obj = Nan::To(value).ToLocalChecked(); - if (Nan::New(Gradient::constructor)->HasInstance(obj)) { - context->_fillStyle.Reset(value); - Gradient *grad = Nan::ObjectWrap::Unwrap(obj); - context->state->fillGradient = grad->pattern(); - } else if (Nan::New(Pattern::constructor)->HasInstance(obj)) { - context->_fillStyle.Reset(value); - Pattern *pattern = Nan::ObjectWrap::Unwrap(obj); - context->state->fillPattern = pattern->pattern(); +void +Context2d::SetFillStyle(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (value.IsString()) { + _fillStyle.Reset(); + _setFillColor(value.As()); + } else if (value.IsObject()) { + InstanceData *data = env.GetInstanceData(); + Napi::Object obj = value.As(); + if (obj.InstanceOf(data->CanvasGradientCtor.Value()).UnwrapOr(false)) { + _fillStyle.Reset(obj); + Gradient *grad = Gradient::Unwrap(obj); + state->fillGradient = grad->pattern(); + } else if (obj.InstanceOf(data->CanvasPatternCtor.Value()).UnwrapOr(false)) { + _fillStyle.Reset(obj); + Pattern *pattern = Pattern::Unwrap(obj); + state->fillPattern = pattern->pattern(); } } } @@ -1891,43 +1879,38 @@ NAN_SETTER(Context2d::SetFillStyle) { * Get current stroke style. */ -NAN_GETTER(Context2d::GetStrokeStyle) { - CHECK_RECEIVER(Context2d.GetStrokeStyle); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Local style; +Napi::Value +Context2d::GetStrokeStyle(const Napi::CallbackInfo& info) { + Napi::Value style; - if (context->_strokeStyle.IsEmpty()) - style = context->_getStrokeColor(); + if (_strokeStyle.IsEmpty()) + style = _getStrokeColor(); else - style = context->_strokeStyle.Get(Isolate::GetCurrent()); + style = _strokeStyle.Value(); - info.GetReturnValue().Set(style); + return style; } /* * Set current stroke style. */ -NAN_SETTER(Context2d::SetStrokeStyle) { - CHECK_RECEIVER(Context2d.SetStrokeStyle); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - - if (value->IsString()) { - MaybeLocal mstr = Nan::To(value); - if (mstr.IsEmpty()) return; - Local str = mstr.ToLocalChecked(); - context->_strokeStyle.Reset(); - context->_setStrokeColor(str); - } else if (value->IsObject()) { - Local obj = Nan::To(value).ToLocalChecked(); - if (Nan::New(Gradient::constructor)->HasInstance(obj)) { - context->_strokeStyle.Reset(value); - Gradient *grad = Nan::ObjectWrap::Unwrap(obj); - context->state->strokeGradient = grad->pattern(); - } else if (Nan::New(Pattern::constructor)->HasInstance(obj)) { - context->_strokeStyle.Reset(value); - Pattern *pattern = Nan::ObjectWrap::Unwrap(obj); - context->state->strokePattern = pattern->pattern(); +void +Context2d::SetStrokeStyle(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (value.IsString()) { + _strokeStyle.Reset(); + _setStrokeColor(value.As()); + } else if (value.IsObject()) { + InstanceData *data = env.GetInstanceData(); + Napi::Object obj = value.As(); + if (obj.InstanceOf(data->CanvasGradientCtor.Value()).UnwrapOr(false)) { + _strokeStyle.Reset(obj); + Gradient *grad = Gradient::Unwrap(obj); + state->strokeGradient = grad->pattern(); + } else if (obj.InstanceOf(data->CanvasPatternCtor.Value()).UnwrapOr(false)) { + _strokeStyle.Reset(value); + Pattern *pattern = Pattern::Unwrap(obj); + state->strokePattern = pattern->pattern(); } } } @@ -1936,22 +1919,21 @@ NAN_SETTER(Context2d::SetStrokeStyle) { * Get miter limit. */ -NAN_GETTER(Context2d::GetMiterLimit) { - CHECK_RECEIVER(Context2d.GetMiterLimit); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(cairo_get_miter_limit(context->context()))); +Napi::Value +Context2d::GetMiterLimit(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, cairo_get_miter_limit(context())); } /* * Set miter limit. */ -NAN_SETTER(Context2d::SetMiterLimit) { - CHECK_RECEIVER(Context2d.SetMiterLimit); - double n = Nan::To(value).FromMaybe(0); - if (n > 0) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_set_miter_limit(context->context(), n); +void +Context2d::SetMiterLimit(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::Maybe numberValue = value.ToNumber(); + if (numberValue.IsJust()) { + double n = numberValue.Unwrap().DoubleValue(); + if (n > 0) cairo_set_miter_limit(context(), n); } } @@ -1959,22 +1941,23 @@ NAN_SETTER(Context2d::SetMiterLimit) { * Get line width. */ -NAN_GETTER(Context2d::GetLineWidth) { - CHECK_RECEIVER(Context2d.GetLineWidth); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(cairo_get_line_width(context->context()))); +Napi::Value +Context2d::GetLineWidth(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, cairo_get_line_width(context())); } /* * Set line width. */ -NAN_SETTER(Context2d::SetLineWidth) { - CHECK_RECEIVER(Context2d.SetLineWidth); - double n = Nan::To(value).FromMaybe(0); - if (n > 0 && n != std::numeric_limits::infinity()) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_set_line_width(context->context(), n); +void +Context2d::SetLineWidth(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::Maybe numberValue = value.ToNumber(); + if (numberValue.IsJust()) { + double n = numberValue.Unwrap().DoubleValue(); + if (n > 0 && n != std::numeric_limits::infinity()) { + cairo_set_line_width(context(), n); + } } } @@ -1982,33 +1965,35 @@ NAN_SETTER(Context2d::SetLineWidth) { * Get line join. */ -NAN_GETTER(Context2d::GetLineJoin) { - CHECK_RECEIVER(Context2d.GetLineJoin); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); +Napi::Value +Context2d::GetLineJoin(const Napi::CallbackInfo& info) { const char *join; - switch (cairo_get_line_join(context->context())) { + switch (cairo_get_line_join(context())) { case CAIRO_LINE_JOIN_BEVEL: join = "bevel"; break; case CAIRO_LINE_JOIN_ROUND: join = "round"; break; default: join = "miter"; } - info.GetReturnValue().Set(Nan::New(join).ToLocalChecked()); + return Napi::String::New(env, join); } /* * Set line join. */ -NAN_SETTER(Context2d::SetLineJoin) { - CHECK_RECEIVER(Context2d.SetLineJoin); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); - Nan::Utf8String type(Nan::To(value).ToLocalChecked()); - if (0 == strcmp("round", *type)) { - cairo_set_line_join(ctx, CAIRO_LINE_JOIN_ROUND); - } else if (0 == strcmp("bevel", *type)) { - cairo_set_line_join(ctx, CAIRO_LINE_JOIN_BEVEL); - } else { - cairo_set_line_join(ctx, CAIRO_LINE_JOIN_MITER); +void +Context2d::SetLineJoin(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::Maybe stringValue = value.ToString(); + cairo_t *ctx = context(); + + if (stringValue.IsJust()) { + std::string type = stringValue.Unwrap().Utf8Value(); + if (type == "round") { + cairo_set_line_join(ctx, CAIRO_LINE_JOIN_ROUND); + } else if (type == "bevel") { + cairo_set_line_join(ctx, CAIRO_LINE_JOIN_BEVEL); + } else { + cairo_set_line_join(ctx, CAIRO_LINE_JOIN_MITER); + } } } @@ -2016,33 +2001,35 @@ NAN_SETTER(Context2d::SetLineJoin) { * Get line cap. */ -NAN_GETTER(Context2d::GetLineCap) { - CHECK_RECEIVER(Context2d.GetLineCap); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); +Napi::Value +Context2d::GetLineCap(const Napi::CallbackInfo& info) { const char *cap; - switch (cairo_get_line_cap(context->context())) { + switch (cairo_get_line_cap(context())) { case CAIRO_LINE_CAP_ROUND: cap = "round"; break; case CAIRO_LINE_CAP_SQUARE: cap = "square"; break; default: cap = "butt"; } - info.GetReturnValue().Set(Nan::New(cap).ToLocalChecked()); + return Napi::String::New(env, cap); } /* * Set line cap. */ -NAN_SETTER(Context2d::SetLineCap) { - CHECK_RECEIVER(Context2d.SetLineCap); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); - Nan::Utf8String type(Nan::To(value).ToLocalChecked()); - if (0 == strcmp("round", *type)) { - cairo_set_line_cap(ctx, CAIRO_LINE_CAP_ROUND); - } else if (0 == strcmp("square", *type)) { - cairo_set_line_cap(ctx, CAIRO_LINE_CAP_SQUARE); - } else { - cairo_set_line_cap(ctx, CAIRO_LINE_CAP_BUTT); +void +Context2d::SetLineCap(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::Maybe stringValue = value.ToString(); + cairo_t *ctx = context(); + + if (stringValue.IsJust()) { + std::string type = stringValue.Unwrap().Utf8Value(); + if (type == "round") { + cairo_set_line_cap(ctx, CAIRO_LINE_CAP_ROUND); + } else if (type == "square") { + cairo_set_line_cap(ctx, CAIRO_LINE_CAP_SQUARE); + } else { + cairo_set_line_cap(ctx, CAIRO_LINE_CAP_BUTT); + } } } @@ -2050,31 +2037,30 @@ NAN_SETTER(Context2d::SetLineCap) { * Check if the given point is within the current path. */ -NAN_METHOD(Context2d::IsPointInPath) { - if (info[0]->IsNumber() && info[1]->IsNumber()) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); - double x = Nan::To(info[0]).FromMaybe(0) - , y = Nan::To(info[1]).FromMaybe(0); - context->setFillRule(info[2]); - info.GetReturnValue().Set(Nan::New(cairo_in_fill(ctx, x, y) || cairo_in_stroke(ctx, x, y))); - return; +Napi::Value +Context2d::IsPointInPath(const Napi::CallbackInfo& info) { + if (info[0].IsNumber() && info[1].IsNumber()) { + cairo_t *ctx = context(); + double x = info[0].As(), y = info[1].As(); + setFillRule(info[2]); + return Napi::Boolean::New(env, cairo_in_fill(ctx, x, y) || cairo_in_stroke(ctx, x, y)); } - info.GetReturnValue().Set(Nan::False()); + return Napi::Boolean::New(env, false); } /* * Set shadow color. */ -NAN_SETTER(Context2d::SetShadowColor) { - CHECK_RECEIVER(Context2d.SetShadowColor); +void +Context2d::SetShadowColor(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::Maybe stringValue = value.ToString(); short ok; - Nan::Utf8String str(Nan::To(value).ToLocalChecked()); - uint32_t rgba = rgba_from_string(*str, &ok); - if (ok) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->shadow = rgba_create(rgba); + + if (stringValue.IsJust()) { + std::string str = stringValue.Unwrap().Utf8Value(); + uint32_t rgba = rgba_from_string(str.c_str(), &ok); + if (ok) state->shadow = rgba_create(rgba); } } @@ -2082,45 +2068,51 @@ NAN_SETTER(Context2d::SetShadowColor) { * Get shadow color. */ -NAN_GETTER(Context2d::GetShadowColor) { - CHECK_RECEIVER(Context2d.GetShadowColor); +Napi::Value +Context2d::GetShadowColor(const Napi::CallbackInfo& info) { char buf[64]; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - rgba_to_string(context->state->shadow, buf, sizeof(buf)); - info.GetReturnValue().Set(Nan::New(buf).ToLocalChecked()); + rgba_to_string(state->shadow, buf, sizeof(buf)); + return Napi::String::New(env, buf); } /* * Set fill color, used internally for fillStyle= */ -void Context2d::_setFillColor(Local arg) { +void +Context2d::_setFillColor(Napi::Value arg) { + Napi::Maybe stringValue = arg.ToString(); short ok; - Nan::Utf8String str(arg); - uint32_t rgba = rgba_from_string(*str, &ok); - if (!ok) return; - state->fillPattern = state->fillGradient = NULL; - state->fill = rgba_create(rgba); + + if (stringValue.IsJust()) { + std::string str = stringValue.Unwrap().Utf8Value(); + uint32_t rgba = rgba_from_string(str.c_str(), &ok); + if (!ok) return; + state->fillPattern = state->fillGradient = NULL; + state->fill = rgba_create(rgba); + } } /* * Get fill color. */ -Local Context2d::_getFillColor() { +Napi::Value +Context2d::_getFillColor() { char buf[64]; rgba_to_string(state->fill, buf, sizeof(buf)); - return Nan::New(buf).ToLocalChecked(); + return Napi::String::New(env, buf); } /* * Set stroke color, used internally for strokeStyle= */ -void Context2d::_setStrokeColor(Local arg) { +void +Context2d::_setStrokeColor(Napi::Value arg) { short ok; - Nan::Utf8String str(arg); - uint32_t rgba = rgba_from_string(*str, &ok); + std::string str = arg.As(); + uint32_t rgba = rgba_from_string(str.c_str(), &ok); if (!ok) return; state->strokePattern = state->strokeGradient = NULL; state->stroke = rgba_create(rgba); @@ -2130,59 +2122,46 @@ void Context2d::_setStrokeColor(Local arg) { * Get stroke color. */ -Local Context2d::_getStrokeColor() { +Napi::Value +Context2d::_getStrokeColor() { char buf[64]; rgba_to_string(state->stroke, buf, sizeof(buf)); - return Nan::New(buf).ToLocalChecked(); + return Napi::String::New(env, buf); } -NAN_METHOD(Context2d::CreatePattern) { - Local image = info[0]; - Local repetition = info[1]; - - if (!Nan::To(repetition).FromMaybe(false)) - repetition = Nan::New("repeat").ToLocalChecked(); - - const int argc = 2; - Local argv[argc] = { image, repetition }; - - Local ctor = Nan::GetFunction(Nan::New(Pattern::constructor)).ToLocalChecked(); - Local instance = Nan::NewInstance(ctor, argc, argv).ToLocalChecked(); - - info.GetReturnValue().Set(instance); +Napi::Value +Context2d::CreatePattern(const Napi::CallbackInfo& info) { + Napi::Function ctor = env.GetInstanceData()->CanvasPatternCtor.Value(); + Napi::Maybe ret = ctor.New({ info[0], info[1] }); + return ret.IsJust() ? ret.Unwrap() : env.Undefined(); } -NAN_METHOD(Context2d::CreateLinearGradient) { - const int argc = 4; - Local argv[argc] = { info[0], info[1], info[2], info[3] }; +Napi::Value +Context2d::CreateLinearGradient(const Napi::CallbackInfo& info) { + Napi::Function ctor = env.GetInstanceData()->CanvasGradientCtor.Value(); + Napi::Maybe ret = ctor.New({ info[0], info[1], info[2], info[3] }); + return ret.IsJust() ? ret.Unwrap() : env.Undefined(); - Local ctor = Nan::GetFunction(Nan::New(Gradient::constructor)).ToLocalChecked(); - Local instance = Nan::NewInstance(ctor, argc, argv).ToLocalChecked(); - - info.GetReturnValue().Set(instance); } -NAN_METHOD(Context2d::CreateRadialGradient) { - const int argc = 6; - Local argv[argc] = { info[0], info[1], info[2], info[3], info[4], info[5] }; - - Local ctor = Nan::GetFunction(Nan::New(Gradient::constructor)).ToLocalChecked(); - Local instance = Nan::NewInstance(ctor, argc, argv).ToLocalChecked(); - - info.GetReturnValue().Set(instance); +Napi::Value +Context2d::CreateRadialGradient(const Napi::CallbackInfo& info) { + Napi::Function ctor = env.GetInstanceData()->CanvasGradientCtor.Value(); + Napi::Maybe ret = ctor.New({ info[0], info[1], info[2], info[3], info[4], info[5] }); + return ret.IsJust() ? ret.Unwrap() : env.Undefined(); } /* * Bezier curve. */ -NAN_METHOD(Context2d::BezierCurveTo) { +void +Context2d::BezierCurveTo(const Napi::CallbackInfo& info) { double args[6]; if(!checkArgs(info, args, 6)) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_curve_to(context->context() + cairo_curve_to(context() , args[0] , args[1] , args[2] @@ -2195,13 +2174,13 @@ NAN_METHOD(Context2d::BezierCurveTo) { * Quadratic curve approximation from libsvg-cairo. */ -NAN_METHOD(Context2d::QuadraticCurveTo) { +void +Context2d::QuadraticCurveTo(const Napi::CallbackInfo& info) { double args[4]; if(!checkArgs(info, args, 4)) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); + cairo_t *ctx = context(); double x, y , x1 = args[0] @@ -2227,56 +2206,57 @@ NAN_METHOD(Context2d::QuadraticCurveTo) { * Save state. */ -NAN_METHOD(Context2d::Save) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->save(); +void +Context2d::Save(const Napi::CallbackInfo& info) { + save(); } /* * Restore state. */ -NAN_METHOD(Context2d::Restore) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->restore(); +void +Context2d::Restore(const Napi::CallbackInfo& info) { + restore(); } /* * Creates a new subpath. */ -NAN_METHOD(Context2d::BeginPath) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_new_path(context->context()); +void +Context2d::BeginPath(const Napi::CallbackInfo& info) { + cairo_new_path(context()); } /* * Marks the subpath as closed. */ -NAN_METHOD(Context2d::ClosePath) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_close_path(context->context()); +void +Context2d::ClosePath(const Napi::CallbackInfo& info) { + cairo_close_path(context()); } /* * Rotate transformation. */ -NAN_METHOD(Context2d::Rotate) { +void +Context2d::Rotate(const Napi::CallbackInfo& info) { double args[1]; if(!checkArgs(info, args, 1)) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_rotate(context->context(), args[0]); + cairo_rotate(context(), args[0]); } /* * Modify the CTM. */ -NAN_METHOD(Context2d::Transform) { +void +Context2d::Transform(const Napi::CallbackInfo& info) { double args[6]; if(!checkArgs(info, args, 6)) return; @@ -2290,52 +2270,49 @@ NAN_METHOD(Context2d::Transform) { , args[4] , args[5]); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_transform(context->context(), &matrix); + cairo_transform(context(), &matrix); } /* * Get the CTM */ -NAN_METHOD(Context2d::GetTransform) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Local instance = get_current_transform(context); - - info.GetReturnValue().Set(instance); +Napi::Value +Context2d::GetTransform(const Napi::CallbackInfo& info) { + return get_current_transform(); } /* * Reset the CTM, used internally by setTransform(). */ -NAN_METHOD(Context2d::ResetTransform) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_identity_matrix(context->context()); +void +Context2d::ResetTransform(const Napi::CallbackInfo& info) { + cairo_identity_matrix(context()); } /* * Reset transform matrix to identity, then apply the given args. */ -NAN_METHOD(Context2d::SetTransform) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - if (info.Length() == 1) { - Local mat = Nan::To(info[0]).ToLocalChecked(); +void +Context2d::SetTransform(const Napi::CallbackInfo& info) { + Napi::Object mat; - #if NODE_MAJOR_VERSION >= 8 - Local ctx = Nan::GetCurrentContext(); - if (!mat->InstanceOf(ctx, _DOMMatrix.Get(Isolate::GetCurrent())).ToChecked()) { - return Nan::ThrowTypeError("Expected DOMMatrix"); + if (info.Length() == 1 && info[0].ToObject().UnwrapTo(&mat)) { + if (!mat.InstanceOf(env.GetInstanceData()->DOMMatrixCtor.Value()).UnwrapOr(false)) { + if (!env.IsExceptionPending()) { + Napi::TypeError::New(env, "Expected DOMMatrix").ThrowAsJavaScriptException(); } - #endif + return; + } cairo_matrix_t matrix; parse_matrix_from_object(matrix, mat); - cairo_set_matrix(context->context(), &matrix); + cairo_set_matrix(context(), &matrix); } else { - cairo_identity_matrix(context->context()); + cairo_identity_matrix(context()); Context2d::Transform(info); } } @@ -2344,36 +2321,36 @@ NAN_METHOD(Context2d::SetTransform) { * Translate transformation. */ -NAN_METHOD(Context2d::Translate) { +void +Context2d::Translate(const Napi::CallbackInfo& info) { double args[2]; if(!checkArgs(info, args, 2)) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_translate(context->context(), args[0], args[1]); + cairo_translate(context(), args[0], args[1]); } /* * Scale transformation. */ -NAN_METHOD(Context2d::Scale) { +void +Context2d::Scale(const Napi::CallbackInfo& info) { double args[2]; if(!checkArgs(info, args, 2)) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_scale(context->context(), args[0], args[1]); + cairo_scale(context(), args[0], args[1]); } /* * Use path as clipping region. */ -NAN_METHOD(Context2d::Clip) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->setFillRule(info[0]); - cairo_t *ctx = context->context(); +void +Context2d::Clip(const Napi::CallbackInfo& info) { + setFillRule(info[0]); + cairo_t *ctx = context(); cairo_clip_preserve(ctx); } @@ -2381,19 +2358,19 @@ NAN_METHOD(Context2d::Clip) { * Fill the path. */ -NAN_METHOD(Context2d::Fill) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->setFillRule(info[0]); - context->fill(true); +void +Context2d::Fill(const Napi::CallbackInfo& info) { + setFillRule(info[0]); + fill(true); } /* * Stroke the path. */ -NAN_METHOD(Context2d::Stroke) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->stroke(true); +void +Context2d::Stroke(const Napi::CallbackInfo& info) { + stroke(true); } /* @@ -2414,44 +2391,47 @@ get_text_scale(PangoLayout *layout, double maxWidth) { } void -paintText(const Nan::FunctionCallbackInfo &info, bool stroke) { +Context2d::paintText(const Napi::CallbackInfo&info, bool stroke) { int argsNum = info.Length() >= 4 ? 3 : 2; - if (argsNum == 3 && info[3]->IsUndefined()) + if (argsNum == 3 && info[3].IsUndefined()) argsNum = 2; double args[3]; if(!checkArgs(info, args, argsNum, 1)) return; - Nan::Utf8String str(Nan::To(info[0]).ToLocalChecked()); + Napi::String strValue; + + if (!info[0].ToString().UnwrapTo(&strValue)) return; + + std::string str = strValue.Utf8Value(); double x = args[0]; double y = args[1]; double scaled_by = 1; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - PangoLayout *layout = context->layout(); + PangoLayout *layout = this->layout(); - pango_layout_set_text(layout, *str, -1); - pango_cairo_update_layout(context->context(), layout); + pango_layout_set_text(layout, str.c_str(), -1); + pango_cairo_update_layout(context(), layout); if (argsNum == 3) { scaled_by = get_text_scale(layout, args[2]); - cairo_save(context->context()); - cairo_scale(context->context(), scaled_by, 1); + cairo_save(context()); + cairo_scale(context(), scaled_by, 1); } - context->savePath(); - if (context->state->textDrawingMode == TEXT_DRAW_GLYPHS) { - if (stroke == true) { context->stroke(); } else { context->fill(); } - context->setTextPath(x / scaled_by, y); - } else if (context->state->textDrawingMode == TEXT_DRAW_PATHS) { - context->setTextPath(x / scaled_by, y); - if (stroke == true) { context->stroke(); } else { context->fill(); } + savePath(); + if (state->textDrawingMode == TEXT_DRAW_GLYPHS) { + if (stroke == true) { this->stroke(); } else { this->fill(); } + setTextPath(x / scaled_by, y); + } else if (state->textDrawingMode == TEXT_DRAW_PATHS) { + setTextPath(x / scaled_by, y); + if (stroke == true) { this->stroke(); } else { this->fill(); } } - context->restorePath(); + restorePath(); if (argsNum == 3) { - cairo_restore(context->context()); + cairo_restore(context()); } } @@ -2459,7 +2439,8 @@ paintText(const Nan::FunctionCallbackInfo &info, bool stroke) { * Fill text at (x, y). */ -NAN_METHOD(Context2d::FillText) { +void +Context2d::FillText(const Napi::CallbackInfo& info) { paintText(info, false); } @@ -2467,7 +2448,8 @@ NAN_METHOD(Context2d::FillText) { * Stroke text at (x ,y). */ -NAN_METHOD(Context2d::StrokeText) { +void +Context2d::StrokeText(const Napi::CallbackInfo& info) { paintText(info, true); } @@ -2532,37 +2514,35 @@ Context2d::setTextPath(double x, double y) { * Adds a point to the current subpath. */ -NAN_METHOD(Context2d::LineTo) { +void +Context2d::LineTo(const Napi::CallbackInfo& info) { double args[2]; if(!checkArgs(info, args, 2)) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_line_to(context->context(), args[0], args[1]); + cairo_line_to(context(), args[0], args[1]); } /* * Creates a new subpath at the given point. */ -NAN_METHOD(Context2d::MoveTo) { +void +Context2d::MoveTo(const Napi::CallbackInfo& info) { double args[2]; if(!checkArgs(info, args, 2)) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_move_to(context->context(), args[0], args[1]); + cairo_move_to(context(), args[0], args[1]); } /* * Get font. */ -NAN_GETTER(Context2d::GetFont) { - CHECK_RECEIVER(Context2d.GetFont); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - - info.GetReturnValue().Set(Nan::New(context->state->font).ToLocalChecked()); +Napi::Value +Context2d::GetFont(const Napi::CallbackInfo& info) { + return Napi::String::New(env, state->font); } /* @@ -2574,46 +2554,44 @@ NAN_GETTER(Context2d::GetFont) { * - family */ -NAN_SETTER(Context2d::SetFont) { - CHECK_RECEIVER(Context2d.SetFont); - if (!value->IsString()) return; +void +Context2d::SetFont(const Napi::CallbackInfo& info, const Napi::Value& value) { + InstanceData* data = env.GetInstanceData(); - Isolate *iso = Isolate::GetCurrent(); - Local ctx = Nan::GetCurrentContext(); + if (!value.IsString()) return; - Local str = Nan::To(value).ToLocalChecked(); - if (!str->Length()) return; + if (!value.As().Utf8Value().length()) return; - const int argc = 1; - Local argv[argc] = { value }; + Napi::Value mparsed; - Local mparsed = Nan::Call(_parseFont.Get(iso), ctx->Global(), argc, argv).ToLocalChecked(); // parseFont returns undefined for invalid CSS font strings - if (mparsed->IsUndefined()) return; - Local font = Nan::To(mparsed).ToLocalChecked(); + if (!data->parseFont.Call({ value }).UnwrapTo(&mparsed) || mparsed.IsUndefined()) return; - Nan::Utf8String weight(Nan::Get(font, Nan::New("weight").ToLocalChecked()).ToLocalChecked()); - Nan::Utf8String style(Nan::Get(font, Nan::New("style").ToLocalChecked()).ToLocalChecked()); - double size = Nan::To(Nan::Get(font, Nan::New("size").ToLocalChecked()).ToLocalChecked()).FromMaybe(0); - Nan::Utf8String unit(Nan::Get(font, Nan::New("unit").ToLocalChecked()).ToLocalChecked()); - Nan::Utf8String family(Nan::Get(font, Nan::New("family").ToLocalChecked()).ToLocalChecked()); + Napi::Object font = mparsed.As(); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); + Napi::String empty = Napi::String::New(env, ""); + Napi::Number zero = Napi::Number::New(env, 0); - PangoFontDescription *desc = pango_font_description_copy(context->state->fontDescription); - pango_font_description_free(context->state->fontDescription); + std::string weight = font.Get("weight").UnwrapOr(empty).ToString().UnwrapOr(empty).Utf8Value(); + std::string style = font.Get("style").UnwrapOr(empty).ToString().UnwrapOr(empty).Utf8Value(); + double size = font.Get("size").UnwrapOr(zero).ToNumber().UnwrapOr(zero).DoubleValue(); + std::string unit = font.Get("unit").UnwrapOr(empty).ToString().UnwrapOr(empty).Utf8Value(); + std::string family = font.Get("family").UnwrapOr(empty).ToString().UnwrapOr(empty).Utf8Value(); - pango_font_description_set_style(desc, Canvas::GetStyleFromCSSString(*style)); - pango_font_description_set_weight(desc, Canvas::GetWeightFromCSSString(*weight)); + PangoFontDescription *desc = pango_font_description_copy(state->fontDescription); + pango_font_description_free(state->fontDescription); - if (strlen(*family) > 0) { + pango_font_description_set_style(desc, Canvas::GetStyleFromCSSString(style.c_str())); + pango_font_description_set_weight(desc, Canvas::GetWeightFromCSSString(weight.c_str())); + + if (family.length() > 0) { // See #1643 - Pango understands "sans" whereas CSS uses "sans-serif" - std::string s1(*family); + std::string s1(family); std::string s2("sans-serif"); if (streq_casein(s1, s2)) { pango_font_description_set_family(desc, "sans"); } else { - pango_font_description_set_family(desc, *family); + pango_font_description_set_family(desc, family.c_str()); } } @@ -2622,21 +2600,20 @@ NAN_SETTER(Context2d::SetFont) { if (size > 0) pango_font_description_set_absolute_size(sys_desc, size * PANGO_SCALE); - context->state->fontDescription = sys_desc; - pango_layout_set_font_description(context->_layout, sys_desc); + state->fontDescription = sys_desc; + pango_layout_set_font_description(_layout, sys_desc); - context->state->font = *Nan::Utf8String(value); + state->font = value.As().Utf8Value().c_str(); } /* * Get text baseline. */ -NAN_GETTER(Context2d::GetTextBaseline) { - CHECK_RECEIVER(Context2d.GetTextBaseline); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); +Napi::Value +Context2d::GetTextBaseline(const Napi::CallbackInfo& info) { const char* baseline; - switch (context->state->textBaseline) { + switch (state->textBaseline) { default: case TEXT_BASELINE_ALPHABETIC: baseline = "alphabetic"; break; case TEXT_BASELINE_TOP: baseline = "top"; break; @@ -2645,18 +2622,18 @@ NAN_GETTER(Context2d::GetTextBaseline) { case TEXT_BASELINE_IDEOGRAPHIC: baseline = "ideographic"; break; case TEXT_BASELINE_HANGING: baseline = "hanging"; break; } - info.GetReturnValue().Set(Nan::New(baseline).ToLocalChecked()); + return Napi::String::New(env, baseline); } /* * Set text baseline. */ -NAN_SETTER(Context2d::SetTextBaseline) { - CHECK_RECEIVER(Context2d.SetTextBaseline); - if (!value->IsString()) return; +void +Context2d::SetTextBaseline(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (!value.IsString()) return; - Nan::Utf8String opStr(Nan::To(value).ToLocalChecked()); + std::string opStr = value.As(); const std::map modes = { {"alphabetic", TEXT_BASELINE_ALPHABETIC}, {"top", TEXT_BASELINE_TOP}, @@ -2665,22 +2642,20 @@ NAN_SETTER(Context2d::SetTextBaseline) { {"ideographic", TEXT_BASELINE_IDEOGRAPHIC}, {"hanging", TEXT_BASELINE_HANGING} }; - auto op = modes.find(*opStr); + auto op = modes.find(opStr); if (op == modes.end()) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->textBaseline = op->second; + state->textBaseline = op->second; } /* * Get text align. */ -NAN_GETTER(Context2d::GetTextAlign) { - CHECK_RECEIVER(Context2d.GetTextAlign); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); +Napi::Value +Context2d::GetTextAlign(const Napi::CallbackInfo& info) { const char* align; - switch (context->state->textAlignment) { + switch (state->textAlignment) { default: // TODO the default is supposed to be "start" case TEXT_ALIGNMENT_LEFT: align = "left"; break; @@ -2689,18 +2664,18 @@ NAN_GETTER(Context2d::GetTextAlign) { case TEXT_ALIGNMENT_RIGHT: align = "right"; break; case TEXT_ALIGNMENT_END: align = "end"; break; } - info.GetReturnValue().Set(Nan::New(align).ToLocalChecked()); + return Napi::String::New(env, align); } /* * Set text align. */ -NAN_SETTER(Context2d::SetTextAlign) { - CHECK_RECEIVER(Context2d.SetTextAlign); - if (!value->IsString()) return; +void +Context2d::SetTextAlign(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (!value.IsString()) return; - Nan::Utf8String opStr(Nan::To(value).ToLocalChecked()); + std::string opStr = value.As(); const std::map modes = { {"center", TEXT_ALIGNMENT_CENTER}, {"left", TEXT_ALIGNMENT_LEFT}, @@ -2708,11 +2683,10 @@ NAN_SETTER(Context2d::SetTextAlign) { {"right", TEXT_ALIGNMENT_RIGHT}, {"end", TEXT_ALIGNMENT_END} }; - auto op = modes.find(*opStr); + auto op = modes.find(opStr); if (op == modes.end()) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->textAlignment = op->second; + state->textAlignment = op->second; } /* @@ -2722,19 +2696,21 @@ NAN_SETTER(Context2d::SetTextAlign) { * fontBoundingBoxAscent, fontBoundingBoxDescent */ -NAN_METHOD(Context2d::MeasureText) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); +Napi::Value +Context2d::MeasureText(const Napi::CallbackInfo& info) { + cairo_t *ctx = this->context(); + + Napi::String str; + if (!info[0].ToString().UnwrapTo(&str)) return env.Undefined(); - Nan::Utf8String str(Nan::To(info[0]).ToLocalChecked()); - Local obj = Nan::New(); + Napi::Object obj = Napi::Object::New(env); PangoRectangle _ink_rect, _logical_rect; float_rectangle ink_rect, logical_rect; PangoFontMetrics *metrics; - PangoLayout *layout = context->layout(); + PangoLayout *layout = this->layout(); - pango_layout_set_text(layout, *str, -1); + pango_layout_set_text(layout, str.Utf8Value().c_str(), -1); pango_cairo_update_layout(ctx, layout); // Normally you could use pango_layout_get_pixel_extents and be done, or use @@ -2757,7 +2733,7 @@ NAN_METHOD(Context2d::MeasureText) { metrics = PANGO_LAYOUT_GET_METRICS(layout); double x_offset; - switch (context->state->textAlignment) { + switch (state->textAlignment) { case TEXT_ALIGNMENT_CENTER: x_offset = logical_rect.width / 2.; break; @@ -2773,36 +2749,20 @@ NAN_METHOD(Context2d::MeasureText) { cairo_matrix_t matrix; cairo_get_matrix(ctx, &matrix); - double y_offset = getBaselineAdjustment(layout, context->state->textBaseline); - - Nan::Set(obj, - Nan::New("width").ToLocalChecked(), - Nan::New(logical_rect.width)).Check(); - Nan::Set(obj, - Nan::New("actualBoundingBoxLeft").ToLocalChecked(), - Nan::New(PANGO_LBEARING(ink_rect) + x_offset)).Check(); - Nan::Set(obj, - Nan::New("actualBoundingBoxRight").ToLocalChecked(), - Nan::New(PANGO_RBEARING(ink_rect) - x_offset)).Check(); - Nan::Set(obj, - Nan::New("actualBoundingBoxAscent").ToLocalChecked(), - Nan::New(y_offset + PANGO_ASCENT(ink_rect))).Check(); - Nan::Set(obj, - Nan::New("actualBoundingBoxDescent").ToLocalChecked(), - Nan::New(PANGO_DESCENT(ink_rect) - y_offset)).Check(); - Nan::Set(obj, - Nan::New("emHeightAscent").ToLocalChecked(), - Nan::New(-(PANGO_ASCENT(logical_rect) - y_offset))).Check(); - Nan::Set(obj, - Nan::New("emHeightDescent").ToLocalChecked(), - Nan::New(PANGO_DESCENT(logical_rect) - y_offset)).Check(); - Nan::Set(obj, - Nan::New("alphabeticBaseline").ToLocalChecked(), - Nan::New(-(pango_font_metrics_get_ascent(metrics) * inverse_pango_scale - y_offset))).Check(); + double y_offset = getBaselineAdjustment(layout, state->textBaseline); + + obj.Set("width", Napi::Number::New(env, logical_rect.width)); + obj.Set("actualBoundingBoxLeft", Napi::Number::New(env, PANGO_LBEARING(ink_rect) + x_offset)); + obj.Set("actualBoundingBoxRight", Napi::Number::New(env, PANGO_RBEARING(ink_rect) - x_offset)); + obj.Set("actualBoundingBoxAscent", Napi::Number::New(env, y_offset + PANGO_ASCENT(ink_rect))); + obj.Set("actualBoundingBoxDescent", Napi::Number::New(env, PANGO_DESCENT(ink_rect) - y_offset)); + obj.Set("emHeightAscent", Napi::Number::New(env, -(PANGO_ASCENT(logical_rect) - y_offset))); + obj.Set("emHeightDescent", Napi::Number::New(env, PANGO_DESCENT(logical_rect) - y_offset)); + obj.Set("alphabeticBaseline", Napi::Number::New(env, -(pango_font_metrics_get_ascent(metrics) * inverse_pango_scale - y_offset))); pango_font_metrics_unref(metrics); - info.GetReturnValue().Set(obj); + return obj; } /* @@ -2810,22 +2770,22 @@ NAN_METHOD(Context2d::MeasureText) { * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash */ -NAN_METHOD(Context2d::SetLineDash) { - if (!info[0]->IsArray()) return; - Local dash = Local::Cast(info[0]); - uint32_t dashes = dash->Length() & 1 ? dash->Length() * 2 : dash->Length(); +void +Context2d::SetLineDash(const Napi::CallbackInfo& info) { + if (!info[0].IsArray()) return; + Napi::Array dash = info[0].As(); + uint32_t dashes = dash.Length() & 1 ? dash.Length() * 2 : dash.Length(); uint32_t zero_dashes = 0; std::vector a(dashes); for (uint32_t i=0; i d = Nan::Get(dash, i % dash->Length()).ToLocalChecked(); - if (!d->IsNumber()) return; - a[i] = Nan::To(d).FromMaybe(0); + Napi::Number d; + if (!dash.Get(i % dash.Length()).UnwrapTo(&d) || !d.IsNumber()) return; + a[i] = d.As().DoubleValue(); if (a[i] == 0) zero_dashes++; if (a[i] < 0 || !std::isfinite(a[i])) return; } - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); + cairo_t *ctx = this->context(); double offset; cairo_get_dash(ctx, NULL, &offset); if (zero_dashes == dashes) { @@ -2840,32 +2800,33 @@ NAN_METHOD(Context2d::SetLineDash) { * Get line dash * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash */ -NAN_METHOD(Context2d::GetLineDash) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); +Napi::Value +Context2d::GetLineDash(const Napi::CallbackInfo& info) { + cairo_t *ctx = this->context(); int dashes = cairo_get_dash_count(ctx); std::vector a(dashes); cairo_get_dash(ctx, a.data(), NULL); - Local dash = Nan::New(dashes); + Napi::Array dash = Napi::Array::New(env, dashes); for (int i=0; i(i), Nan::New(a[i])).Check(); + dash.Set(Napi::Number::New(env, i), Napi::Number::New(env, a[i])); } - info.GetReturnValue().Set(dash); + return dash; } /* * Set line dash offset * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash */ -NAN_SETTER(Context2d::SetLineDashOffset) { - CHECK_RECEIVER(Context2d.SetLineDashOffset); - double offset = Nan::To(value).FromMaybe(0); +void +Context2d::SetLineDashOffset(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::Number numberValue; + if (!value.ToNumber().UnwrapTo(&numberValue)) return; + double offset = numberValue.DoubleValue(); if (!std::isfinite(offset)) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); + cairo_t *ctx = this->context(); int dashes = cairo_get_dash_count(ctx); std::vector a(dashes); @@ -2877,61 +2838,60 @@ NAN_SETTER(Context2d::SetLineDashOffset) { * Get line dash offset * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash */ -NAN_GETTER(Context2d::GetLineDashOffset) { - CHECK_RECEIVER(Context2d.GetLineDashOffset); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); +Napi::Value +Context2d::GetLineDashOffset(const Napi::CallbackInfo& info) { + cairo_t *ctx = this->context(); double offset; cairo_get_dash(ctx, NULL, &offset); - info.GetReturnValue().Set(Nan::New(offset)); + return Napi::Number::New(env, offset); } /* * Fill the rectangle defined by x, y, width and height. */ -NAN_METHOD(Context2d::FillRect) { +void +Context2d::FillRect(const Napi::CallbackInfo& info) { RECT_ARGS; if (0 == width || 0 == height) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); - context->savePath(); + cairo_t *ctx = context(); + savePath(); cairo_rectangle(ctx, x, y, width, height); - context->fill(); - context->restorePath(); + fill(); + restorePath(); } /* * Stroke the rectangle defined by x, y, width and height. */ -NAN_METHOD(Context2d::StrokeRect) { +void +Context2d::StrokeRect(const Napi::CallbackInfo& info) { RECT_ARGS; if (0 == width && 0 == height) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); - context->savePath(); + cairo_t *ctx = context(); + savePath(); cairo_rectangle(ctx, x, y, width, height); - context->stroke(); - context->restorePath(); + stroke(); + restorePath(); } /* * Clears all pixels defined by x, y, width and height. */ -NAN_METHOD(Context2d::ClearRect) { +void +Context2d::ClearRect(const Napi::CallbackInfo& info) { RECT_ARGS; if (0 == width || 0 == height) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); + cairo_t *ctx = context(); cairo_save(ctx); - context->savePath(); + savePath(); cairo_rectangle(ctx, x, y, width, height); cairo_set_operator(ctx, CAIRO_OPERATOR_CLEAR); cairo_fill(ctx); - context->restorePath(); + restorePath(); cairo_restore(ctx); } @@ -2939,10 +2899,10 @@ NAN_METHOD(Context2d::ClearRect) { * Adds a rectangle subpath. */ -NAN_METHOD(Context2d::Rect) { +void +Context2d::Rect(const Napi::CallbackInfo& info) { RECT_ARGS; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); + cairo_t *ctx = context(); if (width == 0) { cairo_move_to(ctx, x, y); cairo_line_to(ctx, x, y + height); @@ -2972,29 +2932,34 @@ void elli_arc(cairo_t* ctx, double xc, double yc, double rx, double ry, double a } inline static -bool getRadius(Point& p, const Local& v) { - if (v->IsObject()) { // 5.1 DOMPointInit - auto rx = Nan::Get(v.As(), Nan::New("x").ToLocalChecked()).ToLocalChecked(); - auto ry = Nan::Get(v.As(), Nan::New("y").ToLocalChecked()).ToLocalChecked(); - if (rx->IsNumber() && ry->IsNumber()) { - auto rxv = Nan::To(rx).FromJust(); - auto ryv = Nan::To(ry).FromJust(); +bool getRadius(Point& p, const Napi::Value& v) { + Napi::Env env = v.Env(); + if (v.IsObject()) { // 5.1 DOMPointInit + Napi::Value rx; + Napi::Value ry; + auto rxMaybe = v.As().Get("x"); + auto ryMaybe = v.As().Get("y"); + if (rxMaybe.UnwrapTo(&rx) && rx.IsNumber() && ryMaybe.UnwrapTo(&ry) && ry.IsNumber()) { + auto rxv = rx.As().DoubleValue(); + auto ryv = ry.As().DoubleValue(); if (!std::isfinite(rxv) || !std::isfinite(ryv)) return true; if (rxv < 0 || ryv < 0) { - Nan::ThrowRangeError("radii must be positive."); + Napi::RangeError::New(env, "radii must be positive.").ThrowAsJavaScriptException(); + return true; } p.x = rxv; p.y = ryv; return false; } - } else if (v->IsNumber()) { // 5.2 unrestricted double - auto rv = Nan::To(v).FromJust(); + } else if (v.IsNumber()) { // 5.2 unrestricted double + auto rv = v.As().DoubleValue(); if (!std::isfinite(rv)) return true; if (rv < 0) { - Nan::ThrowRangeError("radii must be positive."); + Napi::RangeError::New(env, "radii must be positive.").ThrowAsJavaScriptException(); + return true; } p.x = p.y = rv; @@ -3007,30 +2972,30 @@ bool getRadius(Point& p, const Local& v) { * https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-roundrect * x, y, w, h, [radius|[radii]] */ -NAN_METHOD(Context2d::RoundRect) { +void +Context2d::RoundRect(const Napi::CallbackInfo& info) { RECT_ARGS; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); + cairo_t *ctx = this->context(); // 4. Let normalizedRadii be an empty list Point normalizedRadii[4]; size_t nRadii = 4; - if (info[4]->IsUndefined()) { + if (info[4].IsUndefined()) { for (size_t i = 0; i < 4; i++) normalizedRadii[i].x = normalizedRadii[i].y = 0.; - } else if (info[4]->IsArray()) { - auto radiiList = info[4].As(); - nRadii = radiiList->Length(); + } else if (info[4].IsArray()) { + auto radiiList = info[4].As(); + nRadii = radiiList.Length(); if (!(nRadii >= 1 && nRadii <= 4)) { - Nan::ThrowRangeError("radii must be a list of one, two, three or four radii."); + Napi::RangeError::New(env, "radii must be a list of one, two, three or four radii.").ThrowAsJavaScriptException(); return; } // 5. For each radius of radii for (size_t i = 0; i < nRadii; i++) { - auto r = Nan::Get(radiiList, i).ToLocalChecked(); - if (getRadius(normalizedRadii[i], r)) + Napi::Value r; + if (!radiiList.Get(i).UnwrapTo(&r) || getRadius(normalizedRadii[i], r)) return; } @@ -3177,7 +3142,8 @@ static double adjustEndAngle(double startAngle, double endAngle, bool counterclo * Adds an arc at x, y with the given radii and start/end angles. */ -NAN_METHOD(Context2d::Arc) { +void +Context2d::Arc(const Napi::CallbackInfo& info) { double args[5]; if(!checkArgs(info, args, 5)) return; @@ -3189,14 +3155,15 @@ NAN_METHOD(Context2d::Arc) { auto endAngle = args[4]; if (radius < 0) { - Nan::ThrowRangeError("The radius provided is negative."); + Napi::RangeError::New(env, "The radius provided is negative.").ThrowAsJavaScriptException(); return; } - bool counterclockwise = Nan::To(info[5]).FromMaybe(false); + Napi::Boolean counterclockwiseValue; + if (!info[5].ToBoolean().UnwrapTo(&counterclockwiseValue)) return; + bool counterclockwise = counterclockwiseValue.Value(); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); + cairo_t *ctx = context(); canonicalizeAngle(startAngle, endAngle); endAngle = adjustEndAngle(startAngle, endAngle, counterclockwise); @@ -3214,13 +3181,13 @@ NAN_METHOD(Context2d::Arc) { * Implementation influenced by WebKit. */ -NAN_METHOD(Context2d::ArcTo) { +void +Context2d::ArcTo(const Napi::CallbackInfo& info) { double args[5]; if(!checkArgs(info, args, 5)) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); + cairo_t *ctx = context(); // Current path point double x, y; @@ -3319,7 +3286,8 @@ NAN_METHOD(Context2d::ArcTo) { * going in the given direction by anticlockwise (defaulting to clockwise). */ -NAN_METHOD(Context2d::Ellipse) { +void +Context2d::Ellipse(const Napi::CallbackInfo& info) { double args[7]; if(!checkArgs(info, args, 7)) return; @@ -3334,10 +3302,12 @@ NAN_METHOD(Context2d::Ellipse) { double rotation = args[4]; double startAngle = args[5]; double endAngle = args[6]; - bool anticlockwise = Nan::To(info[7]).FromMaybe(false); + Napi::Boolean anticlockwiseValue; + + if (!info[7].ToBoolean().UnwrapTo(&anticlockwiseValue)) return; + bool anticlockwise = anticlockwiseValue.Value(); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); + cairo_t *ctx = context(); // See https://www.cairographics.org/cookbook/ellipses/ double xRatio = radiusX / radiusY; @@ -3365,5 +3335,3 @@ NAN_METHOD(Context2d::Ellipse) { } cairo_set_matrix(ctx, &save_matrix); } - -#undef CHECK_RECEIVER diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index 8ea4d60b8..745106e2d 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -5,7 +5,7 @@ #include "cairo.h" #include "Canvas.h" #include "color.h" -#include "nan.h" +#include "napi.h" #include #include @@ -81,108 +81,103 @@ typedef struct { float height; } float_rectangle; -class Context2d : public Nan::ObjectWrap { +class Context2d : public Napi::ObjectWrap { public: std::stack states; canvas_state_t *state; - Context2d(Canvas *canvas); - static Nan::Persistent _DOMMatrix; - static Nan::Persistent _parseFont; - static Nan::Persistent constructor; - static void Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target); - static NAN_METHOD(New); - static NAN_METHOD(SaveExternalModules); - static NAN_METHOD(DrawImage); - static NAN_METHOD(PutImageData); - static NAN_METHOD(Save); - static NAN_METHOD(Restore); - static NAN_METHOD(Rotate); - static NAN_METHOD(Translate); - static NAN_METHOD(Scale); - static NAN_METHOD(Transform); - static NAN_METHOD(GetTransform); - static NAN_METHOD(ResetTransform); - static NAN_METHOD(SetTransform); - static NAN_METHOD(IsPointInPath); - static NAN_METHOD(BeginPath); - static NAN_METHOD(ClosePath); - static NAN_METHOD(AddPage); - static NAN_METHOD(Clip); - static NAN_METHOD(Fill); - static NAN_METHOD(Stroke); - static NAN_METHOD(FillText); - static NAN_METHOD(StrokeText); - static NAN_METHOD(SetFont); - static NAN_METHOD(SetFillColor); - static NAN_METHOD(SetStrokeColor); - static NAN_METHOD(SetStrokePattern); - static NAN_METHOD(SetTextAlignment); - static NAN_METHOD(SetLineDash); - static NAN_METHOD(GetLineDash); - static NAN_METHOD(MeasureText); - static NAN_METHOD(BezierCurveTo); - static NAN_METHOD(QuadraticCurveTo); - static NAN_METHOD(LineTo); - static NAN_METHOD(MoveTo); - static NAN_METHOD(FillRect); - static NAN_METHOD(StrokeRect); - static NAN_METHOD(ClearRect); - static NAN_METHOD(Rect); - static NAN_METHOD(RoundRect); - static NAN_METHOD(Arc); - static NAN_METHOD(ArcTo); - static NAN_METHOD(Ellipse); - static NAN_METHOD(GetImageData); - static NAN_METHOD(CreateImageData); - static NAN_METHOD(GetStrokeColor); - static NAN_METHOD(CreatePattern); - static NAN_METHOD(CreateLinearGradient); - static NAN_METHOD(CreateRadialGradient); - static NAN_GETTER(GetFormat); - static NAN_GETTER(GetPatternQuality); - static NAN_GETTER(GetImageSmoothingEnabled); - static NAN_GETTER(GetGlobalCompositeOperation); - static NAN_GETTER(GetGlobalAlpha); - static NAN_GETTER(GetShadowColor); - static NAN_GETTER(GetMiterLimit); - static NAN_GETTER(GetLineCap); - static NAN_GETTER(GetLineJoin); - static NAN_GETTER(GetLineWidth); - static NAN_GETTER(GetLineDashOffset); - static NAN_GETTER(GetShadowOffsetX); - static NAN_GETTER(GetShadowOffsetY); - static NAN_GETTER(GetShadowBlur); - static NAN_GETTER(GetAntiAlias); - static NAN_GETTER(GetTextDrawingMode); - static NAN_GETTER(GetQuality); - static NAN_GETTER(GetCurrentTransform); - static NAN_GETTER(GetFillStyle); - static NAN_GETTER(GetStrokeStyle); - static NAN_GETTER(GetFont); - static NAN_GETTER(GetTextBaseline); - static NAN_GETTER(GetTextAlign); - static NAN_SETTER(SetPatternQuality); - static NAN_SETTER(SetImageSmoothingEnabled); - static NAN_SETTER(SetGlobalCompositeOperation); - static NAN_SETTER(SetGlobalAlpha); - static NAN_SETTER(SetShadowColor); - static NAN_SETTER(SetMiterLimit); - static NAN_SETTER(SetLineCap); - static NAN_SETTER(SetLineJoin); - static NAN_SETTER(SetLineWidth); - static NAN_SETTER(SetLineDashOffset); - static NAN_SETTER(SetShadowOffsetX); - static NAN_SETTER(SetShadowOffsetY); - static NAN_SETTER(SetShadowBlur); - static NAN_SETTER(SetAntiAlias); - static NAN_SETTER(SetTextDrawingMode); - static NAN_SETTER(SetQuality); - static NAN_SETTER(SetCurrentTransform); - static NAN_SETTER(SetFillStyle); - static NAN_SETTER(SetStrokeStyle); - static NAN_SETTER(SetFont); - static NAN_SETTER(SetTextBaseline); - static NAN_SETTER(SetTextAlign); + Context2d(const Napi::CallbackInfo& info); + static void Initialize(Napi::Env& env, Napi::Object& target); + void DrawImage(const Napi::CallbackInfo& info); + void PutImageData(const Napi::CallbackInfo& info); + void Save(const Napi::CallbackInfo& info); + void Restore(const Napi::CallbackInfo& info); + void Rotate(const Napi::CallbackInfo& info); + void Translate(const Napi::CallbackInfo& info); + void Scale(const Napi::CallbackInfo& info); + void Transform(const Napi::CallbackInfo& info); + Napi::Value GetTransform(const Napi::CallbackInfo& info); + void ResetTransform(const Napi::CallbackInfo& info); + void SetTransform(const Napi::CallbackInfo& info); + Napi::Value IsPointInPath(const Napi::CallbackInfo& info); + void BeginPath(const Napi::CallbackInfo& info); + void ClosePath(const Napi::CallbackInfo& info); + void AddPage(const Napi::CallbackInfo& info); + void Clip(const Napi::CallbackInfo& info); + void Fill(const Napi::CallbackInfo& info); + void Stroke(const Napi::CallbackInfo& info); + void FillText(const Napi::CallbackInfo& info); + void StrokeText(const Napi::CallbackInfo& info); + static Napi::Value SetFont(const Napi::CallbackInfo& info); + static Napi::Value SetFillColor(const Napi::CallbackInfo& info); + static Napi::Value SetStrokeColor(const Napi::CallbackInfo& info); + static Napi::Value SetStrokePattern(const Napi::CallbackInfo& info); + static Napi::Value SetTextAlignment(const Napi::CallbackInfo& info); + void SetLineDash(const Napi::CallbackInfo& info); + Napi::Value GetLineDash(const Napi::CallbackInfo& info); + Napi::Value MeasureText(const Napi::CallbackInfo& info); + void BezierCurveTo(const Napi::CallbackInfo& info); + void QuadraticCurveTo(const Napi::CallbackInfo& info); + void LineTo(const Napi::CallbackInfo& info); + void MoveTo(const Napi::CallbackInfo& info); + void FillRect(const Napi::CallbackInfo& info); + void StrokeRect(const Napi::CallbackInfo& info); + void ClearRect(const Napi::CallbackInfo& info); + void Rect(const Napi::CallbackInfo& info); + void RoundRect(const Napi::CallbackInfo& info); + void Arc(const Napi::CallbackInfo& info); + void ArcTo(const Napi::CallbackInfo& info); + void Ellipse(const Napi::CallbackInfo& info); + Napi::Value GetImageData(const Napi::CallbackInfo& info); + Napi::Value CreateImageData(const Napi::CallbackInfo& info); + static Napi::Value GetStrokeColor(const Napi::CallbackInfo& info); + Napi::Value CreatePattern(const Napi::CallbackInfo& info); + Napi::Value CreateLinearGradient(const Napi::CallbackInfo& info); + Napi::Value CreateRadialGradient(const Napi::CallbackInfo& info); + Napi::Value GetFormat(const Napi::CallbackInfo& info); + Napi::Value GetPatternQuality(const Napi::CallbackInfo& info); + Napi::Value GetImageSmoothingEnabled(const Napi::CallbackInfo& info); + Napi::Value GetGlobalCompositeOperation(const Napi::CallbackInfo& info); + Napi::Value GetGlobalAlpha(const Napi::CallbackInfo& info); + Napi::Value GetShadowColor(const Napi::CallbackInfo& info); + Napi::Value GetMiterLimit(const Napi::CallbackInfo& info); + Napi::Value GetLineCap(const Napi::CallbackInfo& info); + Napi::Value GetLineJoin(const Napi::CallbackInfo& info); + Napi::Value GetLineWidth(const Napi::CallbackInfo& info); + Napi::Value GetLineDashOffset(const Napi::CallbackInfo& info); + Napi::Value GetShadowOffsetX(const Napi::CallbackInfo& info); + Napi::Value GetShadowOffsetY(const Napi::CallbackInfo& info); + Napi::Value GetShadowBlur(const Napi::CallbackInfo& info); + Napi::Value GetAntiAlias(const Napi::CallbackInfo& info); + Napi::Value GetTextDrawingMode(const Napi::CallbackInfo& info); + Napi::Value GetQuality(const Napi::CallbackInfo& info); + Napi::Value GetCurrentTransform(const Napi::CallbackInfo& info); + Napi::Value GetFillStyle(const Napi::CallbackInfo& info); + Napi::Value GetStrokeStyle(const Napi::CallbackInfo& info); + Napi::Value GetFont(const Napi::CallbackInfo& info); + Napi::Value GetTextBaseline(const Napi::CallbackInfo& info); + Napi::Value GetTextAlign(const Napi::CallbackInfo& info); + void SetPatternQuality(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetImageSmoothingEnabled(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetGlobalCompositeOperation(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetGlobalAlpha(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetShadowColor(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetMiterLimit(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetLineCap(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetLineJoin(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetLineWidth(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetLineDashOffset(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetShadowOffsetX(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetShadowOffsetY(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetShadowBlur(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetAntiAlias(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetTextDrawingMode(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetQuality(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetCurrentTransform(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetFillStyle(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetStrokeStyle(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetFont(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetTextBaseline(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetTextAlign(const Napi::CallbackInfo& info, const Napi::Value& value); inline void setContext(cairo_t *ctx) { _context = ctx; } inline cairo_t *context(){ return _context; } inline Canvas *canvas(){ return _canvas; } @@ -198,7 +193,7 @@ class Context2d : public Nan::ObjectWrap { void restorePath(); void saveState(); void restoreState(); - void inline setFillRule(v8::Local value); + void inline setFillRule(Napi::Value value); void fill(bool preserve = false); void stroke(bool preserve = false); void save(); @@ -206,20 +201,23 @@ class Context2d : public Nan::ObjectWrap { void setFontFromState(); void resetState(); inline PangoLayout *layout(){ return _layout; } + ~Context2d(); + Napi::Env env; private: - ~Context2d(); void _resetPersistentHandles(); - v8::Local _getFillColor(); - v8::Local _getStrokeColor(); - void _setFillColor(v8::Local arg); - void _setFillPattern(v8::Local arg); - void _setStrokeColor(v8::Local arg); - void _setStrokePattern(v8::Local arg); - Nan::Persistent _fillStyle; - Nan::Persistent _strokeStyle; + Napi::Value _getFillColor(); + Napi::Value _getStrokeColor(); + Napi::Value get_current_transform(); + void _setFillColor(Napi::Value arg); + void _setFillPattern(Napi::Value arg); + void _setStrokeColor(Napi::Value arg); + void _setStrokePattern(Napi::Value arg); + void paintText(const Napi::CallbackInfo&, bool); + Napi::Reference _fillStyle; + Napi::Reference _strokeStyle; Canvas *_canvas; - cairo_t *_context; + cairo_t *_context = nullptr; cairo_path_t *_path; - PangoLayout *_layout; + PangoLayout *_layout = nullptr; }; diff --git a/src/Image.cc b/src/Image.cc index 301257769..a1f376136 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -1,6 +1,7 @@ // Copyright (c) 2010 LearnBoost #include "Image.h" +#include "InstanceData.h" #include "bmp/BMPParser.h" #include "Canvas.h" @@ -8,6 +9,7 @@ #include #include #include +#include /* Cairo limit: * https://lists.cairographics.org/archives/cairo/2010-December/021422.html @@ -36,98 +38,88 @@ struct canvas_jpeg_error_mgr: jpeg_error_mgr { */ typedef struct { + Napi::Env* env; unsigned len; uint8_t *buf; } read_closure_t; -using namespace v8; - -Nan::Persistent Image::constructor; - /* * Initialize Image. */ void -Image::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { - Nan::HandleScope scope; - - Local ctor = Nan::New(Image::New); - constructor.Reset(ctor); - ctor->InstanceTemplate()->SetInternalFieldCount(1); - ctor->SetClassName(Nan::New("Image").ToLocalChecked()); - - // Prototype - Local proto = ctor->PrototypeTemplate(); - Nan::SetAccessor(proto, Nan::New("complete").ToLocalChecked(), GetComplete); - Nan::SetAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, SetWidth); - Nan::SetAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, SetHeight); - Nan::SetAccessor(proto, Nan::New("naturalWidth").ToLocalChecked(), GetNaturalWidth); - Nan::SetAccessor(proto, Nan::New("naturalHeight").ToLocalChecked(), GetNaturalHeight); - Nan::SetAccessor(proto, Nan::New("dataMode").ToLocalChecked(), GetDataMode, SetDataMode); - - ctor->Set(Nan::New("MODE_IMAGE").ToLocalChecked(), Nan::New(DATA_IMAGE)); - ctor->Set(Nan::New("MODE_MIME").ToLocalChecked(), Nan::New(DATA_MIME)); - - Local ctx = Nan::GetCurrentContext(); - Nan::Set(target, Nan::New("Image").ToLocalChecked(), ctor->GetFunction(ctx).ToLocalChecked()); +Image::Initialize(Napi::Env& env, Napi::Object& exports) { + InstanceData *data = env.GetInstanceData(); + Napi::HandleScope scope(env); + + Napi::Function ctor = DefineClass(env, "Image", { + InstanceAccessor<&Image::GetComplete>("complete"), + InstanceAccessor<&Image::GetWidth, &Image::SetWidth>("width"), + InstanceAccessor<&Image::GetHeight, &Image::SetHeight>("height"), + InstanceAccessor<&Image::GetNaturalWidth>("naturalWidth"), + InstanceAccessor<&Image::GetNaturalHeight>("naturalHeight"), + InstanceAccessor<&Image::GetDataMode, &Image::SetDataMode>("dataMode"), + StaticValue("MODE_IMAGE", Napi::Number::New(env, DATA_IMAGE)), + StaticValue("MODE_MIME", Napi::Number::New(env, DATA_MIME)) + }); // Used internally in lib/image.js - NAN_EXPORT(target, GetSource); - NAN_EXPORT(target, SetSource); + exports.Set("GetSource", Napi::Function::New(env, &GetSource)); + exports.Set("SetSource", Napi::Function::New(env, &SetSource)); + + data->ImageCtor = Napi::Persistent(ctor); + exports.Set("Image", ctor); } /* * Initialize a new Image. */ -NAN_METHOD(Image::New) { - if (!info.IsConstructCall()) { - return Nan::ThrowTypeError("Class constructors cannot be invoked without 'new'"); - } - - Image *img = new Image; - img->data_mode = DATA_IMAGE; - img->Wrap(info.This()); - Nan::Set(info.This(), Nan::New("onload").ToLocalChecked(), Nan::Null()).Check(); - Nan::Set(info.This(), Nan::New("onerror").ToLocalChecked(), Nan::Null()).Check(); - info.GetReturnValue().Set(info.This()); +Image::Image(const Napi::CallbackInfo& info) : ObjectWrap(info), env(info.Env()) { + data_mode = DATA_IMAGE; + info.This().ToObject().Unwrap().Set("onload", env.Null()); + info.This().ToObject().Unwrap().Set("onerror", env.Null()); + filename = NULL; + _data = nullptr; + _data_len = 0; + _surface = NULL; + width = height = 0; + naturalWidth = naturalHeight = 0; + state = DEFAULT; +#ifdef HAVE_RSVG + _rsvg = NULL; + _is_svg = false; + _svg_last_width = _svg_last_height = 0; +#endif } /* * Get complete boolean. */ -NAN_GETTER(Image::GetComplete) { - info.GetReturnValue().Set(Nan::New(true)); +Napi::Value +Image::GetComplete(const Napi::CallbackInfo& info) { + return Napi::Boolean::New(env, true); } /* * Get dataMode. */ -NAN_GETTER(Image::GetDataMode) { - if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Image.GetDataMode called on incompatible receiver"); - return; - } - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(img->data_mode)); +Napi::Value +Image::GetDataMode(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, data_mode); } /* * Set dataMode. */ -NAN_SETTER(Image::SetDataMode) { - if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Image.SetDataMode called on incompatible receiver"); - return; - } - if (value->IsNumber()) { - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - int mode = Nan::To(value).FromMaybe(0); - img->data_mode = (data_mode_t) mode; +void +Image::SetDataMode(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (value.IsNumber()) { + int mode = value.As().Uint32Value(); + data_mode = (data_mode_t) mode; } } @@ -135,40 +127,28 @@ NAN_SETTER(Image::SetDataMode) { * Get natural width */ -NAN_GETTER(Image::GetNaturalWidth) { - if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Image.GetNaturalWidth called on incompatible receiver"); - return; - } - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(img->naturalWidth)); +Napi::Value +Image::GetNaturalWidth(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, naturalWidth); } /* * Get width. */ -NAN_GETTER(Image::GetWidth) { - if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Image.GetWidth called on incompatible receiver"); - return; - } - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(img->width)); +Napi::Value +Image::GetWidth(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, width); } /* * Set width. */ -NAN_SETTER(Image::SetWidth) { - if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Image.SetWidth called on incompatible receiver"); - return; - } - if (value->IsNumber()) { - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - img->width = Nan::To(value).FromMaybe(0); +void +Image::SetWidth(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (value.IsNumber()) { + width = value.As().Uint32Value(); } } @@ -176,40 +156,27 @@ NAN_SETTER(Image::SetWidth) { * Get natural height */ -NAN_GETTER(Image::GetNaturalHeight) { - if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Image.GetNaturalHeight called on incompatible receiver"); - return; - } - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(img->naturalHeight)); +Napi::Value +Image::GetNaturalHeight(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, naturalHeight); } /* * Get height. */ -NAN_GETTER(Image::GetHeight) { - if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Image.GetHeight called on incompatible receiver"); - return; - } - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(img->height)); +Napi::Value +Image::GetHeight(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, height); } /* * Set height. */ -NAN_SETTER(Image::SetHeight) { - if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - // #1534 - Nan::ThrowTypeError("Method Image.SetHeight called on incompatible receiver"); - return; - } - if (value->IsNumber()) { - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - img->height = Nan::To(value).FromMaybe(0); +void +Image::SetHeight(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (value.IsNumber()) { + height = value.As().Uint32Value(); } } @@ -217,14 +184,11 @@ NAN_SETTER(Image::SetHeight) { * Get src path. */ -NAN_METHOD(Image::GetSource){ - if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - // #1534 - Nan::ThrowTypeError("Method Image.GetSource called on incompatible receiver"); - return; - } - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(img->filename ? img->filename : "").ToLocalChecked()); +Napi::Value +Image::GetSource(const Napi::CallbackInfo& info){ + Napi::Env env = info.Env(); + Image *img = Image::Unwrap(info.This().As()); + return Napi::String::New(env, img->filename ? img->filename : ""); } /* @@ -235,7 +199,7 @@ void Image::clearData() { if (_surface) { cairo_surface_destroy(_surface); - Nan::AdjustExternalMemory(-_data_len); + Napi::MemoryManagement::AdjustExternalMemory(env, -_data_len); _data_len = 0; _surface = NULL; } @@ -262,55 +226,49 @@ Image::clearData() { * Set src path. */ -NAN_METHOD(Image::SetSource){ - if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - // #1534 - Nan::ThrowTypeError("Method Image.SetSource called on incompatible receiver"); - return; - } - Image *img = Nan::ObjectWrap::Unwrap(info.This()); +void +Image::SetSource(const Napi::CallbackInfo& info){ + Napi::Env env = info.Env(); + Napi::Object This = info.This().As(); + Image *img = Image::Unwrap(This); + cairo_status_t status = CAIRO_STATUS_READ_ERROR; - Local value = info[0]; + Napi::Value value = info[0]; img->clearData(); // Clear errno in case some unrelated previous syscall failed errno = 0; // url string - if (value->IsString()) { - Nan::Utf8String src(value); + if (value.IsString()) { + std::string src = value.As().Utf8Value(); if (img->filename) free(img->filename); - img->filename = strdup(*src); + img->filename = strdup(src.c_str()); status = img->load(); // Buffer - } else if (node::Buffer::HasInstance(value)) { - uint8_t *buf = (uint8_t *) node::Buffer::Data(Nan::To(value).ToLocalChecked()); - unsigned len = node::Buffer::Length(Nan::To(value).ToLocalChecked()); + } else if (value.IsBuffer()) { + uint8_t *buf = value.As>().Data(); + unsigned len = value.As>().Length(); status = img->loadFromBuffer(buf, len); } if (status) { - Local onerrorFn = Nan::Get(info.This(), Nan::New("onerror").ToLocalChecked()).ToLocalChecked(); - if (onerrorFn->IsFunction()) { - Local argv[1]; - CanvasError errorInfo = img->errorInfo; - if (errorInfo.cerrno) { - argv[0] = Nan::ErrnoException(errorInfo.cerrno, errorInfo.syscall.c_str(), errorInfo.message.c_str(), errorInfo.path.c_str()); - } else if (!errorInfo.message.empty()) { - argv[0] = Nan::Error(Nan::New(errorInfo.message).ToLocalChecked()); + Napi::Value onerrorFn; + if (This.Get("onerror").UnwrapTo(&onerrorFn) && onerrorFn.IsFunction()) { + Napi::Error arg; + if (img->errorInfo.empty()) { + arg = Napi::Error::New(env, Napi::String::New(env, cairo_status_to_string(status))); } else { - argv[0] = Nan::Error(Nan::New(cairo_status_to_string(status)).ToLocalChecked()); + arg = img->errorInfo.toError(env); } - Local ctx = Nan::GetCurrentContext(); - Nan::Call(onerrorFn.As(), ctx->Global(), 1, argv); + onerrorFn.As().Call({ arg.Value() }); } } else { img->loaded(); - Local onloadFn = Nan::Get(info.This(), Nan::New("onload").ToLocalChecked()).ToLocalChecked(); - if (onloadFn->IsFunction()) { - Local ctx = Nan::GetCurrentContext(); - Nan::Call(onloadFn.As(), ctx->Global(), 0, NULL); + Napi::Value onloadFn; + if (This.Get("onload").UnwrapTo(&onloadFn) && onloadFn.IsFunction()) { + onloadFn.As().Call({}); } } } @@ -380,6 +338,7 @@ Image::loadPNGFromBuffer(uint8_t *buf) { read_closure_t closure; closure.len = 0; closure.buf = buf; + closure.env = &env; _surface = cairo_image_surface_create_from_png_stream(readPNG, &closure); cairo_status_t status = cairo_surface_status(_surface); if (status) return status; @@ -398,25 +357,6 @@ Image::readPNG(void *c, uint8_t *data, unsigned int len) { return CAIRO_STATUS_SUCCESS; } -/* - * Initialize a new Image. - */ - -Image::Image() { - filename = NULL; - _data = nullptr; - _data_len = 0; - _surface = NULL; - width = height = 0; - naturalWidth = naturalHeight = 0; - state = DEFAULT; -#ifdef HAVE_RSVG - _rsvg = NULL; - _is_svg = false; - _svg_last_width = _svg_last_height = 0; -#endif -} - /* * Destroy image and associated surface. */ @@ -444,13 +384,13 @@ Image::load() { void Image::loaded() { - Nan::HandleScope scope; + Napi::HandleScope scope(env); state = COMPLETE; width = naturalWidth = cairo_image_surface_get_width(_surface); height = naturalHeight = cairo_image_surface_get_height(_surface); _data_len = naturalHeight * cairo_image_surface_get_stride(_surface); - Nan::AdjustExternalMemory(_data_len); + Napi::MemoryManagement::AdjustExternalMemory(env, _data_len); } /* @@ -467,7 +407,8 @@ cairo_surface_t *Image::surface() { cairo_status_t status = renderSVGToSurface(); if (status != CAIRO_STATUS_SUCCESS) { g_object_unref(_rsvg); - Nan::ThrowError(Canvas::Error(status)); + Napi::Error::New(env, cairo_status_to_string(status)).ThrowAsJavaScriptException(); + return NULL; } } @@ -1010,7 +951,8 @@ Image::decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len) { void clearMimeData(void *closure) { - Nan::AdjustExternalMemory( + Napi::MemoryManagement::AdjustExternalMemory( + *static_cast(closure)->env, -static_cast((static_cast(closure)->len))); free(static_cast(closure)->buf); free(closure); @@ -1039,10 +981,11 @@ Image::assignDataAsMime(uint8_t *data, int len, const char *mime_type) { memcpy(mime_data, data, len); + mime_closure->env = &env; mime_closure->buf = mime_data; mime_closure->len = len; - Nan::AdjustExternalMemory(len); + Napi::MemoryManagement::AdjustExternalMemory(env, len); return cairo_surface_set_mime_data(_surface , mime_type diff --git a/src/Image.h b/src/Image.h index 62bc3f13b..6b9b9593b 100644 --- a/src/Image.h +++ b/src/Image.h @@ -5,9 +5,8 @@ #include #include "CanvasError.h" #include -#include +#include #include // node < 7 uses libstdc++ on macOS which lacks complete c++11 -#include #ifdef HAVE_JPEG #include @@ -34,25 +33,26 @@ using JPEGDecodeL = std::function; -class Image: public Nan::ObjectWrap { +class Image : public Napi::ObjectWrap { public: char *filename; int width, height; int naturalWidth, naturalHeight; - static Nan::Persistent constructor; - static void Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target); - static NAN_METHOD(New); - static NAN_GETTER(GetComplete); - static NAN_GETTER(GetWidth); - static NAN_GETTER(GetHeight); - static NAN_GETTER(GetNaturalWidth); - static NAN_GETTER(GetNaturalHeight); - static NAN_GETTER(GetDataMode); - static NAN_SETTER(SetDataMode); - static NAN_SETTER(SetWidth); - static NAN_SETTER(SetHeight); - static NAN_METHOD(GetSource); - static NAN_METHOD(SetSource); + Napi::Env env; + static Napi::FunctionReference constructor; + static void Initialize(Napi::Env& env, Napi::Object& target); + Image(const Napi::CallbackInfo& info); + Napi::Value GetComplete(const Napi::CallbackInfo& info); + Napi::Value GetWidth(const Napi::CallbackInfo& info); + Napi::Value GetHeight(const Napi::CallbackInfo& info); + Napi::Value GetNaturalWidth(const Napi::CallbackInfo& info); + Napi::Value GetNaturalHeight(const Napi::CallbackInfo& info); + Napi::Value GetDataMode(const Napi::CallbackInfo& info); + void SetDataMode(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetWidth(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetHeight(const Napi::CallbackInfo& info, const Napi::Value& value); + static Napi::Value GetSource(const Napi::CallbackInfo& info); + static void SetSource(const Napi::CallbackInfo& info); inline uint8_t *data(){ return cairo_image_surface_get_data(_surface); } inline int stride(){ return cairo_image_surface_get_stride(_surface); } static int isPNG(uint8_t *data); @@ -90,7 +90,7 @@ class Image: public Nan::ObjectWrap { CanvasError errorInfo; void loaded(); cairo_status_t load(); - Image(); + ~Image(); enum { DEFAULT @@ -123,5 +123,4 @@ class Image: public Nan::ObjectWrap { int _svg_last_width; int _svg_last_height; #endif - ~Image(); }; diff --git a/src/ImageData.cc b/src/ImageData.cc index 03da2e270..b9f556bb3 100644 --- a/src/ImageData.cc +++ b/src/ImageData.cc @@ -1,146 +1,132 @@ // Copyright (c) 2010 LearnBoost #include "ImageData.h" - -using namespace v8; - -Nan::Persistent ImageData::constructor; +#include "InstanceData.h" /* * Initialize ImageData. */ void -ImageData::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { - Nan::HandleScope scope; - - // Constructor - Local ctor = Nan::New(ImageData::New); - constructor.Reset(ctor); - ctor->InstanceTemplate()->SetInternalFieldCount(1); - ctor->SetClassName(Nan::New("ImageData").ToLocalChecked()); - - // Prototype - Local proto = ctor->PrototypeTemplate(); - Nan::SetAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth); - Nan::SetAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight); - Local ctx = Nan::GetCurrentContext(); - Nan::Set(target, Nan::New("ImageData").ToLocalChecked(), ctor->GetFunction(ctx).ToLocalChecked()); +ImageData::Initialize(Napi::Env& env, Napi::Object& exports) { + Napi::HandleScope scope(env); + + InstanceData *data = env.GetInstanceData(); + + Napi::Function ctor = DefineClass(env, "ImageData", { + InstanceAccessor<&ImageData::GetWidth>("width"), + InstanceAccessor<&ImageData::GetHeight>("height") + }); + + exports.Set("ImageData", ctor); + data->ImageDataCtor = Napi::Persistent(ctor); } /* * Initialize a new ImageData object. */ -NAN_METHOD(ImageData::New) { - if (!info.IsConstructCall()) { - return Nan::ThrowTypeError("Class constructors cannot be invoked without 'new'"); - } - - Local dataArray; +ImageData::ImageData(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info), env(info.Env()) { + Napi::TypedArray dataArray; uint32_t width; uint32_t height; int length; - if (info[0]->IsUint32() && info[1]->IsUint32()) { - width = Nan::To(info[0]).FromMaybe(0); + if (info[0].IsNumber() && info[1].IsNumber()) { + width = info[0].As().Uint32Value(); if (width == 0) { - Nan::ThrowRangeError("The source width is zero."); + Napi::RangeError::New(env, "The source width is zero.").ThrowAsJavaScriptException(); return; } - height = Nan::To(info[1]).FromMaybe(0); + height = info[1].As().Uint32Value(); if (height == 0) { - Nan::ThrowRangeError("The source height is zero."); + Napi::RangeError::New(env, "The source height is zero.").ThrowAsJavaScriptException(); return; } length = width * height * 4; // ImageData(w, h) constructor assumes 4 BPP; documented. - dataArray = Uint8ClampedArray::New(ArrayBuffer::New(Isolate::GetCurrent(), length), 0, length); + dataArray = Napi::Uint8Array::New(env, length, napi_uint8_clamped_array); + } else if ( + info[0].IsTypedArray() && + info[0].As().TypedArrayType() == napi_uint8_clamped_array && + info[1].IsNumber() + ) { + dataArray = info[0].As(); - } else if (info[0]->IsUint8ClampedArray() && info[1]->IsUint32()) { - dataArray = info[0].As(); - - length = dataArray->Length(); + length = dataArray.ElementLength(); if (length == 0) { - Nan::ThrowRangeError("The input data has a zero byte length."); + Napi::RangeError::New(env, "The input data has a zero byte length.").ThrowAsJavaScriptException(); return; } // Don't assert that the ImageData length is a multiple of four because some // data formats are not 4 BPP. - width = Nan::To(info[1]).FromMaybe(0); + width = info[1].As().Uint32Value(); if (width == 0) { - Nan::ThrowRangeError("The source width is zero."); + Napi::RangeError::New(env, "The source width is zero.").ThrowAsJavaScriptException(); return; } // Don't assert that the byte length is a multiple of 4 * width, ditto. - if (info[2]->IsUint32()) { // Explicit height given - height = Nan::To(info[2]).FromMaybe(0); + if (info[2].IsNumber()) { // Explicit height given + height = info[2].As().Uint32Value(); } else { // Calculate height assuming 4 BPP int size = length / 4; height = size / width; } + } else if ( + info[0].IsTypedArray() && + info[0].As().TypedArrayType() == napi_uint16_array && + info[1].IsNumber() + ) { // Intended for RGB16_565 format + dataArray = info[0].As(); + + length = dataArray.ElementLength(); + if (length == 0) { + Napi::RangeError::New(env, "The input data has a zero byte length.").ThrowAsJavaScriptException(); + return; + } - } else if (info[0]->IsUint16Array() && info[1]->IsUint32()) { // Intended for RGB16_565 format - dataArray = info[0].As(); - - length = dataArray->Length(); - if (length == 0) { - Nan::ThrowRangeError("The input data has a zero byte length."); - return; - } - - width = Nan::To(info[1]).FromMaybe(0); - if (width == 0) { - Nan::ThrowRangeError("The source width is zero."); - return; - } - - if (info[2]->IsUint32()) { // Explicit height given - height = Nan::To(info[2]).FromMaybe(0); - } else { // Calculate height assuming 2 BPP - int size = length / 2; - height = size / width; - } + width = info[1].As().Uint32Value(); + if (width == 0) { + Napi::RangeError::New(env, "The source width is zero.").ThrowAsJavaScriptException(); + return; + } + if (info[2].IsNumber()) { // Explicit height given + height = info[2].As().Uint32Value(); + } else { // Calculate height assuming 2 BPP + int size = length / 2; + height = size / width; + } } else { - Nan::ThrowTypeError("Expected (Uint8ClampedArray, width[, height]), (Uint16Array, width[, height]) or (width, height)"); + Napi::TypeError::New(env, "Expected (Uint8ClampedArray, width[, height]), (Uint16Array, width[, height]) or (width, height)").ThrowAsJavaScriptException(); return; } - Nan::TypedArrayContents dataPtr(dataArray); + _width = width; + _height = height; + _data = dataArray.As().Data(); - ImageData *imageData = new ImageData(reinterpret_cast(*dataPtr), width, height); - imageData->Wrap(info.This()); - Nan::Set(info.This(), Nan::New("data").ToLocalChecked(), dataArray).Check(); - info.GetReturnValue().Set(info.This()); + info.This().As().Set("data", dataArray); } /* * Get width. */ -NAN_GETTER(ImageData::GetWidth) { - if (!ImageData::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method ImageData.GetWidth called on incompatible receiver"); - return; - } - ImageData *imageData = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(imageData->width())); +Napi::Value +ImageData::GetWidth(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, width()); } /* * Get height. */ -NAN_GETTER(ImageData::GetHeight) { - if (!ImageData::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method ImageData.GetHeight called on incompatible receiver"); - return; - } - ImageData *imageData = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(imageData->height())); +Napi::Value +ImageData::GetHeight(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, height()); } diff --git a/src/ImageData.h b/src/ImageData.h index 4832b37b2..32d6037d1 100644 --- a/src/ImageData.h +++ b/src/ImageData.h @@ -2,22 +2,21 @@ #pragma once -#include +#include #include // node < 7 uses libstdc++ on macOS which lacks complete c++11 -#include -class ImageData: public Nan::ObjectWrap { +class ImageData : public Napi::ObjectWrap { public: - static Nan::Persistent constructor; - static void Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target); - static NAN_METHOD(New); - static NAN_GETTER(GetWidth); - static NAN_GETTER(GetHeight); + static void Initialize(Napi::Env& env, Napi::Object& exports); + ImageData(const Napi::CallbackInfo& info); + Napi::Value GetWidth(const Napi::CallbackInfo& info); + Napi::Value GetHeight(const Napi::CallbackInfo& info); inline int width() { return _width; } inline int height() { return _height; } inline uint8_t *data() { return _data; } - ImageData(uint8_t *data, int width, int height) : _width(width), _height(height), _data(data) {} + + Napi::Env env; private: int _width; diff --git a/src/InstanceData.h b/src/InstanceData.h new file mode 100644 index 000000000..939f2a488 --- /dev/null +++ b/src/InstanceData.h @@ -0,0 +1,15 @@ +#include + +struct InstanceData { + Napi::FunctionReference ImageBackendCtor; + Napi::FunctionReference PdfBackendCtor; + Napi::FunctionReference SvgBackendCtor; + Napi::FunctionReference CanvasCtor; + Napi::FunctionReference CanvasGradientCtor; + Napi::FunctionReference DOMMatrixCtor; + Napi::FunctionReference ImageCtor; + Napi::FunctionReference parseFont; + Napi::FunctionReference Context2dCtor; + Napi::FunctionReference ImageDataCtor; + Napi::FunctionReference CanvasPatternCtor; +}; diff --git a/src/JPEGStream.h b/src/JPEGStream.h index b8efeed21..43c74f139 100644 --- a/src/JPEGStream.h +++ b/src/JPEGStream.h @@ -23,18 +23,15 @@ init_closure_destination(j_compress_ptr cinfo){ boolean empty_closure_output_buffer(j_compress_ptr cinfo){ - Nan::HandleScope scope; - Nan::AsyncResource async("canvas:empty_closure_output_buffer"); closure_destination_mgr *dest = (closure_destination_mgr *) cinfo->dest; + Napi::Env env = dest->closure->canvas->Env(); + Napi::HandleScope scope(env); + Napi::AsyncContext async(env, "canvas:empty_closure_output_buffer"); - v8::Local buf = Nan::NewBuffer((char *)dest->buffer, dest->bufsize).ToLocalChecked(); + Napi::Object buf = Napi::Buffer::New(env, (char *)dest->buffer, dest->bufsize); // emit "data" - v8::Local argv[2] = { - Nan::Null() - , buf - }; - dest->closure->cb.Call(sizeof argv / sizeof *argv, argv, &async); + dest->closure->cb.MakeCallback(env.Global(), {env.Null(), buf}, async); dest->buffer = (JOCTET *)malloc(dest->bufsize); cinfo->dest->next_output_byte = dest->buffer; @@ -44,25 +41,18 @@ empty_closure_output_buffer(j_compress_ptr cinfo){ void term_closure_destination(j_compress_ptr cinfo){ - Nan::HandleScope scope; - Nan::AsyncResource async("canvas:term_closure_destination"); closure_destination_mgr *dest = (closure_destination_mgr *) cinfo->dest; + Napi::Env env = dest->closure->canvas->Env(); + Napi::HandleScope scope(env); + Napi::AsyncContext async(env, "canvas:term_closure_destination"); /* emit remaining data */ - v8::Local buf = Nan::NewBuffer((char *)dest->buffer, dest->bufsize - dest->pub.free_in_buffer).ToLocalChecked(); + Napi::Object buf = Napi::Buffer::New(env, (char *)dest->buffer, dest->bufsize - dest->pub.free_in_buffer); - v8::Local data_argv[2] = { - Nan::Null() - , buf - }; - dest->closure->cb.Call(sizeof data_argv / sizeof *data_argv, data_argv, &async); + dest->closure->cb.MakeCallback(env.Global(), {env.Null(), buf}, async); // emit "end" - v8::Local end_argv[2] = { - Nan::Null() - , Nan::Null() - }; - dest->closure->cb.Call(sizeof end_argv / sizeof *end_argv, end_argv, &async); + dest->closure->cb.MakeCallback(env.Global(), {env.Null(), env.Null()}, async); } void diff --git a/src/backend/Backend.cc b/src/backend/Backend.cc index 9f2b39dd3..14d67e7b5 100644 --- a/src/backend/Backend.cc +++ b/src/backend/Backend.cc @@ -1,29 +1,21 @@ #include "Backend.h" #include +#include -Backend::Backend(std::string name, int width, int height) - : name(name) - , width(width) - , height(height) -{} +Backend::Backend(std::string name, Napi::CallbackInfo& info) : name(name), env(info.Env()) { + int width = 0; + int height = 0; + if (info[0].IsNumber()) width = info[0].As().Int32Value(); + if (info[1].IsNumber()) height = info[1].As().Int32Value(); + this->width = width; + this->height = height; +} Backend::~Backend() { Backend::destroySurface(); } -void Backend::init(const Nan::FunctionCallbackInfo &info) { - int width = 0; - int height = 0; - if (info[0]->IsNumber()) width = Nan::To(info[0]).FromMaybe(0); - if (info[1]->IsNumber()) height = Nan::To(info[1]).FromMaybe(0); - - Backend *backend = construct(width, height); - - backend->Wrap(info.This()); - info.GetReturnValue().Set(info.This()); -} - void Backend::setCanvas(Canvas* _canvas) { this->canvas = _canvas; diff --git a/src/backend/Backend.h b/src/backend/Backend.h index f8448c41a..d23573b6e 100644 --- a/src/backend/Backend.h +++ b/src/backend/Backend.h @@ -3,13 +3,12 @@ #include #include "../dll_visibility.h" #include -#include +#include #include -#include class Canvas; -class Backend : public Nan::ObjectWrap +class Backend { private: const std::string name; @@ -21,11 +20,11 @@ class Backend : public Nan::ObjectWrap cairo_surface_t* surface = nullptr; Canvas* canvas = nullptr; - Backend(std::string name, int width, int height); - static void init(const Nan::FunctionCallbackInfo &info); - static Backend *construct(int width, int height){ return nullptr; } + Backend(std::string name, Napi::CallbackInfo& info); public: + Napi::Env env; + virtual ~Backend(); void setCanvas(Canvas* canvas); diff --git a/src/backend/ImageBackend.cc b/src/backend/ImageBackend.cc index d354d92cc..682c56b18 100644 --- a/src/backend/ImageBackend.cc +++ b/src/backend/ImageBackend.cc @@ -1,16 +1,14 @@ #include "ImageBackend.h" +#include "../InstanceData.h" +#include +#include -using namespace v8; - -ImageBackend::ImageBackend(int width, int height) - : Backend("image", width, height) - {} - -Backend *ImageBackend::construct(int width, int height){ - return new ImageBackend(width, height); +ImageBackend::ImageBackend(Napi::CallbackInfo& info) : Napi::ObjectWrap(info), Backend("image", info) +{ } -// This returns an approximate value only, suitable for Nan::AdjustExternalMemory. +// This returns an approximate value only, suitable for +// Napi::MemoryManagement:: AdjustExternalMemory. // The formats that don't map to intrinsic types (RGB30, A1) round up. int32_t ImageBackend::approxBytesPerPixel() { switch (format) { @@ -35,7 +33,7 @@ cairo_surface_t* ImageBackend::createSurface() { assert(!surface); surface = cairo_image_surface_create(format, width, height); assert(surface); - Nan::AdjustExternalMemory(approxBytesPerPixel() * width * height); + Napi::MemoryManagement::AdjustExternalMemory(env, approxBytesPerPixel() * width * height); return surface; } @@ -43,7 +41,7 @@ void ImageBackend::destroySurface() { if (surface) { cairo_surface_destroy(surface); surface = nullptr; - Nan::AdjustExternalMemory(-approxBytesPerPixel() * width * height); + Napi::MemoryManagement::AdjustExternalMemory(env, -approxBytesPerPixel() * width * height); } } @@ -55,20 +53,11 @@ void ImageBackend::setFormat(cairo_format_t _format) { this->format = _format; } -Nan::Persistent ImageBackend::constructor; - -void ImageBackend::Initialize(Local target) { - Nan::HandleScope scope; - - Local ctor = Nan::New(ImageBackend::New); - ImageBackend::constructor.Reset(ctor); - ctor->InstanceTemplate()->SetInternalFieldCount(1); - ctor->SetClassName(Nan::New("ImageBackend").ToLocalChecked()); - Nan::Set(target, - Nan::New("ImageBackend").ToLocalChecked(), - Nan::GetFunction(ctor).ToLocalChecked()).Check(); -} +Napi::FunctionReference ImageBackend::constructor; -NAN_METHOD(ImageBackend::New) { - init(info); +void ImageBackend::Initialize(Napi::Object target) { + Napi::Env env = target.Env(); + Napi::Function ctor = DefineClass(env, "ImageBackend", {}); + InstanceData* data = env.GetInstanceData(); + data->ImageBackendCtor = Napi::Persistent(ctor); } diff --git a/src/backend/ImageBackend.h b/src/backend/ImageBackend.h index f68dacfdb..032907f0f 100644 --- a/src/backend/ImageBackend.h +++ b/src/backend/ImageBackend.h @@ -1,9 +1,9 @@ #pragma once #include "Backend.h" -#include +#include -class ImageBackend : public Backend +class ImageBackend : public Napi::ObjectWrap, public Backend { private: cairo_surface_t* createSurface(); @@ -11,16 +11,14 @@ class ImageBackend : public Backend cairo_format_t format = DEFAULT_FORMAT; public: - ImageBackend(int width, int height); - static Backend *construct(int width, int height); + ImageBackend(Napi::CallbackInfo& info); cairo_format_t getFormat(); void setFormat(cairo_format_t format); int32_t approxBytesPerPixel(); - static Nan::Persistent constructor; - static void Initialize(v8::Local target); - static NAN_METHOD(New); + static Napi::FunctionReference constructor; + static void Initialize(Napi::Object target); const static cairo_format_t DEFAULT_FORMAT = CAIRO_FORMAT_ARGB32; }; diff --git a/src/backend/PdfBackend.cc b/src/backend/PdfBackend.cc index fe831a68d..ce214a044 100644 --- a/src/backend/PdfBackend.cc +++ b/src/backend/PdfBackend.cc @@ -1,13 +1,11 @@ #include "PdfBackend.h" #include +#include "../InstanceData.h" #include "../Canvas.h" #include "../closure.h" -using namespace v8; - -PdfBackend::PdfBackend(int width, int height) - : Backend("pdf", width, height) { +PdfBackend::PdfBackend(Napi::CallbackInfo& info) : Napi::ObjectWrap(info), Backend("pdf", info) { PdfBackend::createSurface(); } @@ -17,10 +15,6 @@ PdfBackend::~PdfBackend() { destroySurface(); } -Backend *PdfBackend::construct(int width, int height){ - return new PdfBackend(width, height); -} - cairo_surface_t* PdfBackend::createSurface() { if (!_closure) _closure = new PdfSvgClosure(canvas); surface = cairo_pdf_surface_create_for_stream(PdfSvgClosure::writeVec, _closure, width, height); @@ -33,21 +27,10 @@ cairo_surface_t* PdfBackend::recreateSurface() { return surface; } - -Nan::Persistent PdfBackend::constructor; - -void PdfBackend::Initialize(Local target) { - Nan::HandleScope scope; - - Local ctor = Nan::New(PdfBackend::New); - PdfBackend::constructor.Reset(ctor); - ctor->InstanceTemplate()->SetInternalFieldCount(1); - ctor->SetClassName(Nan::New("PdfBackend").ToLocalChecked()); - Nan::Set(target, - Nan::New("PdfBackend").ToLocalChecked(), - Nan::GetFunction(ctor).ToLocalChecked()).Check(); -} - -NAN_METHOD(PdfBackend::New) { - init(info); +void +PdfBackend::Initialize(Napi::Object target) { + Napi::Env env = target.Env(); + InstanceData* data = env.GetInstanceData(); + Napi::Function ctor = DefineClass(env, "PdfBackend", {}); + data->PdfBackendCtor = Napi::Persistent(ctor); } diff --git a/src/backend/PdfBackend.h b/src/backend/PdfBackend.h index 03656f500..59aa0fedd 100644 --- a/src/backend/PdfBackend.h +++ b/src/backend/PdfBackend.h @@ -2,9 +2,9 @@ #include "Backend.h" #include "../closure.h" -#include +#include -class PdfBackend : public Backend +class PdfBackend : public Napi::ObjectWrap, public Backend { private: cairo_surface_t* createSurface(); @@ -14,11 +14,10 @@ class PdfBackend : public Backend PdfSvgClosure* _closure = NULL; inline PdfSvgClosure* closure() { return _closure; } - PdfBackend(int width, int height); + PdfBackend(Napi::CallbackInfo& info); ~PdfBackend(); - static Backend *construct(int width, int height); - static Nan::Persistent constructor; - static void Initialize(v8::Local target); - static NAN_METHOD(New); + static Napi::FunctionReference constructor; + static void Initialize(Napi::Object target); + static Napi::Value New(const Napi::CallbackInfo& info); }; diff --git a/src/backend/SvgBackend.cc b/src/backend/SvgBackend.cc index 7d4181fc2..530d0b571 100644 --- a/src/backend/SvgBackend.cc +++ b/src/backend/SvgBackend.cc @@ -1,14 +1,15 @@ #include "SvgBackend.h" #include +#include #include "../Canvas.h" #include "../closure.h" +#include "../InstanceData.h" #include -using namespace v8; +using namespace Napi; -SvgBackend::SvgBackend(int width, int height) - : Backend("svg", width, height) { +SvgBackend::SvgBackend(Napi::CallbackInfo& info) : Napi::ObjectWrap(info), Backend("svg", info) { SvgBackend::createSurface(); } @@ -21,10 +22,6 @@ SvgBackend::~SvgBackend() { destroySurface(); } -Backend *SvgBackend::construct(int width, int height){ - return new SvgBackend(width, height); -} - cairo_surface_t* SvgBackend::createSurface() { assert(!_closure); _closure = new PdfSvgClosure(canvas); @@ -42,20 +39,10 @@ cairo_surface_t* SvgBackend::recreateSurface() { } -Nan::Persistent SvgBackend::constructor; - -void SvgBackend::Initialize(Local target) { - Nan::HandleScope scope; - - Local ctor = Nan::New(SvgBackend::New); - SvgBackend::constructor.Reset(ctor); - ctor->InstanceTemplate()->SetInternalFieldCount(1); - ctor->SetClassName(Nan::New("SvgBackend").ToLocalChecked()); - Nan::Set(target, - Nan::New("SvgBackend").ToLocalChecked(), - Nan::GetFunction(ctor).ToLocalChecked()).Check(); -} - -NAN_METHOD(SvgBackend::New) { - init(info); +void +SvgBackend::Initialize(Napi::Object target) { + Napi::Env env = target.Env(); + Napi::Function ctor = DefineClass(env, "SvgBackend", {}); + InstanceData* data = env.GetInstanceData(); + data->SvgBackendCtor = Napi::Persistent(ctor); } diff --git a/src/backend/SvgBackend.h b/src/backend/SvgBackend.h index 6377b438b..301ec831c 100644 --- a/src/backend/SvgBackend.h +++ b/src/backend/SvgBackend.h @@ -2,9 +2,9 @@ #include "Backend.h" #include "../closure.h" -#include +#include -class SvgBackend : public Backend +class SvgBackend : public Napi::ObjectWrap, public Backend { private: cairo_surface_t* createSurface(); @@ -14,11 +14,8 @@ class SvgBackend : public Backend PdfSvgClosure* _closure = NULL; inline PdfSvgClosure* closure() { return _closure; } - SvgBackend(int width, int height); + SvgBackend(Napi::CallbackInfo& info); ~SvgBackend(); - static Backend *construct(int width, int height); - static Nan::Persistent constructor; - static void Initialize(v8::Local target); - static NAN_METHOD(New); + static void Initialize(Napi::Object target); }; diff --git a/src/closure.cc b/src/closure.cc index e821e7f22..3290db2e5 100644 --- a/src/closure.cc +++ b/src/closure.cc @@ -1,4 +1,5 @@ #include "closure.h" +#include "Canvas.h" #ifdef HAVE_JPEG void JpegClosure::init_destination(j_compress_ptr cinfo) { @@ -24,3 +25,28 @@ void JpegClosure::term_destination(j_compress_ptr cinfo) { } #endif +void +EncodingWorker::Init(void (*work_fn)(Closure*), Closure* closure) { + this->work_fn = work_fn; + this->closure = closure; +} + +void +EncodingWorker::Execute() { + this->work_fn(this->closure); +} + +void +EncodingWorker::OnWorkComplete(Napi::Env env, napi_status status) { + Napi::HandleScope scope(env); + + if (closure->status) { + closure->cb.Call({ closure->canvas->CairoError(closure->status).Value() }); + } else { + Napi::Object buf = Napi::Buffer::Copy(env, &closure->vec[0], closure->vec.size()); + closure->cb.Call({ env.Null(), buf }); + } + + closure->canvas->Unref(); + delete closure; +} diff --git a/src/closure.h b/src/closure.h index 3126114eb..ce5ec489c 100644 --- a/src/closure.h +++ b/src/closure.h @@ -8,7 +8,7 @@ #include #endif -#include +#include #include #include // node < 7 uses libstdc++ on macOS which lacks complete c++11 #include @@ -23,7 +23,7 @@ struct Closure { std::vector vec; - Nan::Callback cb; + Napi::FunctionReference cb; Canvas* canvas = nullptr; cairo_status_t status = CAIRO_STATUS_SUCCESS; @@ -79,3 +79,15 @@ struct JpegClosure : Closure { } }; #endif + +class EncodingWorker : public Napi::AsyncWorker { + public: + EncodingWorker(Napi::Env env): Napi::AsyncWorker(env) {}; + void Init(void (*work_fn)(Closure*), Closure* closure); + void Execute() override; + void OnWorkComplete(Napi::Env env, napi_status status) override; + + private: + void (*work_fn)(Closure*) = nullptr; + Closure* closure = nullptr; +}; diff --git a/src/init.cc b/src/init.cc index fd143973e..ad9207846 100644 --- a/src/init.cc +++ b/src/init.cc @@ -21,27 +21,47 @@ #include "CanvasRenderingContext2d.h" #include "Image.h" #include "ImageData.h" +#include "InstanceData.h" #include #include FT_FREETYPE_H -using namespace v8; +/* + * Save some external modules as private references. + */ + +static void +setDOMMatrix(const Napi::CallbackInfo& info) { + InstanceData* data = info.Env().GetInstanceData(); + data->DOMMatrixCtor = Napi::Persistent(info[0].As()); +} + +static void +setParseFont(const Napi::CallbackInfo& info) { + InstanceData* data = info.Env().GetInstanceData(); + data->parseFont = Napi::Persistent(info[0].As()); +} // Compatibility with Visual Studio versions prior to VS2015 #if defined(_MSC_VER) && _MSC_VER < 1900 #define snprintf _snprintf #endif -NAN_MODULE_INIT(init) { - Backends::Initialize(target); - Canvas::Initialize(target); - Image::Initialize(target); - ImageData::Initialize(target); - Context2d::Initialize(target); - Gradient::Initialize(target); - Pattern::Initialize(target); +Napi::Object init(Napi::Env env, Napi::Object exports) { + env.SetInstanceData(new InstanceData()); - Nan::Set(target, Nan::New("cairoVersion").ToLocalChecked(), Nan::New(cairo_version_string()).ToLocalChecked()).Check(); + Backends::Initialize(env, exports); + Canvas::Initialize(env, exports); + Image::Initialize(env, exports); + ImageData::Initialize(env, exports); + Context2d::Initialize(env, exports); + Gradient::Initialize(env, exports); + Pattern::Initialize(env, exports); + + exports.Set("setDOMMatrix", Napi::Function::New(env, &setDOMMatrix)); + exports.Set("setParseFont", Napi::Function::New(env, &setParseFont)); + + exports.Set("cairoVersion", Napi::String::New(env, cairo_version_string())); #ifdef HAVE_JPEG #ifndef JPEG_LIB_VERSION_MAJOR @@ -67,28 +87,30 @@ NAN_MODULE_INIT(init) { } else { snprintf(jpeg_version, 10, "%d", JPEG_LIB_VERSION_MAJOR); } - Nan::Set(target, Nan::New("jpegVersion").ToLocalChecked(), Nan::New(jpeg_version).ToLocalChecked()).Check(); + exports.Set("jpegVersion", Napi::String::New(env, jpeg_version)); #endif #ifdef HAVE_GIF #ifndef GIF_LIB_VERSION char gif_version[10]; snprintf(gif_version, 10, "%d.%d.%d", GIFLIB_MAJOR, GIFLIB_MINOR, GIFLIB_RELEASE); - Nan::Set(target, Nan::New("gifVersion").ToLocalChecked(), Nan::New(gif_version).ToLocalChecked()).Check(); + exports.Set("gifVersion", Napi::String::New(env, gif_version)); #else - Nan::Set(target, Nan::New("gifVersion").ToLocalChecked(), Nan::New(GIF_LIB_VERSION).ToLocalChecked()).Check(); + exports.Set("gifVersion", Napi::String::New(env, GIF_LIB_VERSION)); #endif #endif #ifdef HAVE_RSVG - Nan::Set(target, Nan::New("rsvgVersion").ToLocalChecked(), Nan::New(LIBRSVG_VERSION).ToLocalChecked()).Check(); + exports.Set("rsvgVersion", Napi::String::New(env, LIBRSVG_VERSION)); #endif - Nan::Set(target, Nan::New("pangoVersion").ToLocalChecked(), Nan::New(PANGO_VERSION_STRING).ToLocalChecked()).Check(); + exports.Set("pangoVersion", Napi::String::New(env, PANGO_VERSION_STRING)); char freetype_version[10]; snprintf(freetype_version, 10, "%d.%d.%d", FREETYPE_MAJOR, FREETYPE_MINOR, FREETYPE_PATCH); - Nan::Set(target, Nan::New("freetypeVersion").ToLocalChecked(), Nan::New(freetype_version).ToLocalChecked()).Check(); + exports.Set("freetypeVersion", Napi::String::New(env, freetype_version)); + + return exports; } -NODE_MODULE(canvas, init); +NODE_API_MODULE(canvas, init); diff --git a/test/canvas.test.js b/test/canvas.test.js index 9573688f5..c3b83b271 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -31,7 +31,7 @@ describe('Canvas', function () { it('Prototype and ctor are well-shaped, don\'t hit asserts on accessors (GH-803)', function () { const c = new Canvas(10, 10) - assert.throws(function () { Canvas.prototype.width }, /incompatible receiver/) + assert.throws(function () { Canvas.prototype.width }, /invalid argument/i) assert(!c.hasOwnProperty('width')) assert('width' in c) assert('width' in Canvas.prototype) diff --git a/test/image.test.js b/test/image.test.js index ec1631a10..a5d6f415c 100644 --- a/test/image.test.js +++ b/test/image.test.js @@ -24,7 +24,7 @@ const bmpDir = path.join(__dirname, '/fixtures/bmp') describe('Image', function () { it('Prototype and ctor are well-shaped, don\'t hit asserts on accessors (GH-803)', function () { const img = new Image() - assert.throws(function () { Image.prototype.width }, /incompatible receiver/) + assert.throws(function () { Image.prototype.width }, /invalid argument/i) assert(!img.hasOwnProperty('width')) assert('width' in img) assert(Image.prototype.hasOwnProperty('width')) @@ -182,7 +182,7 @@ describe('Image', function () { it('returns a nice, coded error for fopen failures', function (done) { const img = new Image() img.onerror = err => { - assert.equal(err.code, 'ENOENT') + assert.equal(err.message, 'No such file or directory') assert.equal(err.path, 'path/to/nothing') assert.equal(err.syscall, 'fopen') assert.strictEqual(img.complete, true) diff --git a/test/imageData.test.js b/test/imageData.test.js index 04b117b45..774bcf14e 100644 --- a/test/imageData.test.js +++ b/test/imageData.test.js @@ -9,7 +9,7 @@ const assert = require('assert') describe('ImageData', function () { it('Prototype and ctor are well-shaped, don\'t hit asserts on accessors (GH-803)', function () { - assert.throws(function () { ImageData.prototype.width }, /incompatible receiver/) + assert.throws(function () { ImageData.prototype.width }, /invalid argument/i) }) it('stringifies as [object ImageData]', function () { From c9969aa49e4290eefcbe6110e573103277865452 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Mon, 17 Apr 2023 11:03:23 -0400 Subject: [PATCH 399/474] optimize checkArgs this makes the lineTo benchmark (lineTo executes a very small number of operations, so it mostly measures the js<->C++ barrier) run about 50% faster --- src/CanvasRenderingContext2d.cc | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 9457122d0..e0edd7476 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -45,12 +45,28 @@ constexpr double twoPi = M_PI * 2.; pango_context_get_language(pango_layout_get_context(LAYOUT))) inline static bool checkArgs(const Napi::CallbackInfo&info, double *args, int argsNum, int offset = 0){ - Napi::Number zero = Napi::Number::New(info.Env(), 0); - int argsEnd = offset + argsNum; + Napi::Env env = info.Env(); + int argsEnd = std::min(9, offset + argsNum); bool areArgsValid = true; + napi_value argv[9]; + size_t argc = 9; + napi_get_cb_info(env, static_cast(info), &argc, argv, nullptr, nullptr); + for (int i = offset; i < argsEnd; i++) { - double val = info[i].ToNumber().UnwrapOr(zero).DoubleValue(); + napi_valuetype type; + double val = 0; + + napi_typeof(env, argv[i], &type); + if (type == napi_number) { + // fast path + napi_get_value_double(env, argv[i], &val); + } else { + napi_value num; + if (napi_coerce_to_number(env, argv[i], &num) == napi_ok) { + napi_get_value_double(env, num, &val); + } + } if (areArgsValid) { if (!std::isfinite(val)) { From 16c28ab73d70ffff49288a8a037c7b47a61437b7 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sat, 22 Apr 2023 14:58:11 -0400 Subject: [PATCH 400/474] optimize fillStyle to be 10% faster this is possibly a hot path, and avoiding the C++ string helps a little bit --- src/CanvasRenderingContext2d.cc | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index e0edd7476..56e68d899 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -2101,8 +2101,11 @@ Context2d::_setFillColor(Napi::Value arg) { short ok; if (stringValue.IsJust()) { - std::string str = stringValue.Unwrap().Utf8Value(); - uint32_t rgba = rgba_from_string(str.c_str(), &ok); + Napi::String str = stringValue.Unwrap(); + char buf[128] = {0}; + napi_status status = napi_get_value_string_utf8(env, str, buf, sizeof(buf) - 1, nullptr); + if (status != napi_ok) return; + uint32_t rgba = rgba_from_string(buf, &ok); if (!ok) return; state->fillPattern = state->fillGradient = NULL; state->fill = rgba_create(rgba); From 9b9be4f5deb9934db4781bb41eba4a9bfbaba880 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sun, 24 Sep 2023 21:44:12 -0400 Subject: [PATCH 401/474] fix broken font matching due to a use-after-free f3184ba9da2737dbe25275631678a7ef5924fe6b introduced a use-after- free bug. Pango does not copy the string when you use the _static version of pango_font_description_set_family. Font selection was not working for me at all. --- src/register_font.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/register_font.cc b/src/register_font.cc index 37182c0ac..cc0af52d7 100644 --- a/src/register_font.cc +++ b/src/register_font.cc @@ -303,7 +303,7 @@ get_pango_font_description(unsigned char* filepath) { return NULL; } - pango_font_description_set_family_static(desc, family); + pango_font_description_set_family(desc, family); free(family); pango_font_description_set_weight(desc, get_pango_weight(table->usWeightClass)); pango_font_description_set_stretch(desc, get_pango_stretch(table->usWidthClass)); From a5b379bbc241d2731c2a4f8d4410f71f123dd1ee Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Wed, 4 Oct 2023 10:55:18 -0700 Subject: [PATCH 402/474] v3.0.0 --- CHANGELOG.md | 11 ++++++++++- package.json | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97b2c2661..420079d7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,22 @@ project adheres to [Semantic Versioning](http://semver.org/). (Unreleased) ================== ### Changed +### Added +### Fixed + +3.0.0 +================== + +This release notably changes to using N-API. 🎉 + +### Changed +* Migrated to N-API (by way of node-addon-api) and removed libuv and v8 dependencies * Defer the initialization of the `op` variable to the `default` switch case to avoid a compiler warning. (#2229) * Use a `default` switch case with a null statement if some enum values aren't suppsed to be handled, this avoids a compiler warning. (#2229) * Migrate from librsvg's deprecated `rsvg_handle_get_dimensions()` and `rsvg_handle_render_cairo()` functions to the new `rsvg_handle_get_intrinsic_size_in_pixels()` and `rsvg_handle_render_document()` respectively. (#2229) * Avoid calling virtual methods in constructors/destructors to avoid bypassing virtual dispatch. (#2229) * Remove unused private field `backend` in the `Backend` class. (#2229) * Add Node.js v20 to CI. (#2237) -* Migrated to N-API (by way of node-addon-api) and removed libuv and v8 dependencies ### Added * Added string tags to support class detection ### Fixed diff --git a/package.json b/package.json index a240125ff..828d377b5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.11.2", + "version": "3.0.0", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 713208733b8a72821daa0189d7cfdd0a1aa0d6fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Tue, 28 Nov 2023 14:52:59 +0100 Subject: [PATCH 403/474] Replace dtslint with tsd --- .github/workflows/ci.yaml | 2 +- CHANGELOG.md | 1 + types/index.d.ts => index.d.ts | 0 types/test.ts => index.test-d.ts | 33 +++++++++++++++++++------------- package.json | 12 +++++++----- types/Readme.md | 3 --- types/tsconfig.json | 13 ------------- types/tslint.json | 7 ------- 8 files changed, 29 insertions(+), 42 deletions(-) rename types/index.d.ts => index.d.ts (100%) rename types/test.ts => index.test-d.ts (50%) delete mode 100644 types/Readme.md delete mode 100644 types/tsconfig.json delete mode 100644 types/tslint.json diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c21812da6..2d985cfc6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -86,4 +86,4 @@ jobs: - name: Lint run: npm run lint - name: Lint Types - run: npm run dtslint + run: npm run tsd diff --git a/CHANGELOG.md b/CHANGELOG.md index 420079d7b..03d9d2732 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ This release notably changes to using N-API. 🎉 * Avoid calling virtual methods in constructors/destructors to avoid bypassing virtual dispatch. (#2229) * Remove unused private field `backend` in the `Backend` class. (#2229) * Add Node.js v20 to CI. (#2237) +* Replaced `dtslint` with `tsd` (#2313) ### Added * Added string tags to support class detection ### Fixed diff --git a/types/index.d.ts b/index.d.ts similarity index 100% rename from types/index.d.ts rename to index.d.ts diff --git a/types/test.ts b/index.test-d.ts similarity index 50% rename from types/test.ts rename to index.test-d.ts index b48c78011..86e8dfc28 100644 --- a/types/test.ts +++ b/index.test-d.ts @@ -1,5 +1,8 @@ -import * as Canvas from 'canvas' -import * as path from "path"; +import { expectAssignable, expectType } from 'tsd' +import * as path from 'path' +import { Readable } from 'stream' + +import * as Canvas from './index' Canvas.registerFont(path.join(__dirname, '../pfennigFont/Pfennig.ttf'), {family: 'pfennigFont'}) @@ -13,34 +16,38 @@ canv.getContext('2d', {alpha: false}) // LHS is ImageData, not Canvas.ImageData const id = ctx.getImageData(0, 0, 10, 10) -const h: number = id.height +expectType(id.height) +expectType(id.width) ctx.currentTransform = ctx.getTransform() ctx.quality = 'best' ctx.textDrawingMode = 'glyph' -const grad: Canvas.CanvasGradient = ctx.createLinearGradient(0, 1, 2, 3) +const grad = ctx.createLinearGradient(0, 1, 2, 3) +expectType(grad) grad.addColorStop(0.1, 'red') const dm = new Canvas.DOMMatrix([1, 2, 3, 4, 5, 6]) -const a: number = dm.a +expectType(dm.a) -const b1: Buffer = canv.toBuffer() -canv.toBuffer("application/pdf") -canv.toBuffer((err, data) => {}, "image/png") -canv.createJPEGStream({quality: 0.5}) -canv.createPDFStream({author: "octocat"}) +expectType(canv.toBuffer()) +expectType(canv.toBuffer('application/pdf')) +canv.toBuffer((err, data) => {}, 'image/png') +expectAssignable(canv.createJPEGStream({ quality: 0.5 })) +expectAssignable(canv.createPDFStream({ author: 'octocat' })) canv.toDataURL() const img = new Canvas.Image() img.src = Buffer.alloc(0) img.dataMode = Canvas.Image.MODE_IMAGE | Canvas.Image.MODE_MIME img.onload = () => {} -img.onload = null; +img.onload = null -const id2: Canvas.ImageData = Canvas.createImageData(new Uint16Array(4), 1) +const id2 = Canvas.createImageData(new Uint16Array(4), 1) +expectType(id2) +ctx.putImageData(id2, 0, 0) ctx.drawImage(canv, 0, 0) -Canvas.deregisterAllFonts(); +Canvas.deregisterAllFonts() diff --git a/package.json b/package.json index 828d377b5..12b9be365 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", + "types": "index.d.ts", "contributors": [ "Nathan Rajlich ", "Rod Vagg ", @@ -32,7 +33,7 @@ "generate-wpt": "node ./test/wpt/generate.js", "test-wpt": "mocha test/wpt/generated/*.js", "install": "node-pre-gyp install --fallback-to-build --update-binary", - "dtslint": "dtslint types" + "tsd": "tsd" }, "binary": { "module_name": "canvas", @@ -43,12 +44,13 @@ }, "files": [ "binding.gyp", + "browser.js", + "index.d.ts", + "index.js", "lib/", "src/", - "util/", - "types/index.d.ts" + "util/" ], - "types": "types/index.d.ts", "dependencies": { "@mapbox/node-pre-gyp": "^1.0.0", "node-addon-api": "^7.0.0", @@ -57,12 +59,12 @@ "devDependencies": { "@types/node": "^10.12.18", "assert-rejects": "^1.0.0", - "dtslint": "^4.0.7", "express": "^4.16.3", "js-yaml": "^4.1.0", "mocha": "^5.2.0", "pixelmatch": "^4.0.2", "standard": "^12.0.1", + "tsd": "^0.29.0", "typescript": "^4.2.2" }, "engines": { diff --git a/types/Readme.md b/types/Readme.md deleted file mode 100644 index 4beb7f528..000000000 --- a/types/Readme.md +++ /dev/null @@ -1,3 +0,0 @@ -Notes: - -* `"unified-signatures": false` because of https://github.com/Microsoft/dtslint/issues/183 diff --git a/types/tsconfig.json b/types/tsconfig.json deleted file mode 100644 index 226482c23..000000000 --- a/types/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "lib": ["es6"], - "noImplicitAny": true, - "noImplicitThis": true, - "strictNullChecks": true, - "strictFunctionTypes": true, - "noEmit": true, - "baseUrl": ".", - "paths": { "canvas": ["."] } - } -} diff --git a/types/tslint.json b/types/tslint.json deleted file mode 100644 index 64e2a316f..000000000 --- a/types/tslint.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "dtslint/dtslint.json", - "rules": { - "semicolon": false, - "unified-signatures": false - } -} From ad793dab1fd2fd64bd8e9c55381679423b840236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Wed, 29 Nov 2023 10:53:03 +0100 Subject: [PATCH 404/474] Drop support for older versions of Node.js (#2310) * Drop support for older versions of Node.js * Install python setuptools on macOS * Temporarily disable tests on Windows + Node.js 20 --- .github/ISSUE_TEMPLATE.md | 2 +- .github/workflows/ci.yaml | 13 +++++++++---- .github/workflows/prebuild.yaml | 6 +++--- CHANGELOG.md | 2 ++ package.json | 2 +- 5 files changed, 16 insertions(+), 9 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 8828eac4d..10c0a04ab 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -14,4 +14,4 @@ var ctx = canvas.getContext('2d'); ## Your Environment * Version of node-canvas (output of `npm list canvas` or `yarn list canvas`): -* Environment (e.g. node 4.2.0 on Mac OS X 10.8): +* Environment (e.g. node 20.9.0 on macOS 14.1.1): diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2d985cfc6..f0606175d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [10, 12, 14, 16, 18, 20] + node: [18.12.0, 20.9.0] steps: - uses: actions/setup-node@v3 with: @@ -33,7 +33,11 @@ jobs: runs-on: windows-2019 strategy: matrix: - node: [10, 12, 14, 16, 18, 20] + # FIXME: Node.js 20.9.0 is currently broken on Windows, in the `registerFont` test: + # ENOENT: no such file or directory, lstat 'D:\a\node-canvas\node-canvas\examples\pfennigFont\pfennigMultiByte🚀.ttf' + # ref: https://github.com/nodejs/node/issues/48673 + # ref: https://github.com/nodejs/node/pull/50650 + node: [18.12.0] steps: - uses: actions/setup-node@v3 with: @@ -57,7 +61,7 @@ jobs: runs-on: macos-latest strategy: matrix: - node: [10, 12, 14, 16, 18, 20] + node: [18.12.0, 20.9.0] steps: - uses: actions/setup-node@v3 with: @@ -68,6 +72,7 @@ jobs: brew update brew install python3 || : # python doesn't need to be linked brew install pkg-config cairo pango libpng jpeg giflib librsvg + pip install setuptools - name: Install run: npm install --build-from-source - name: Test @@ -79,7 +84,7 @@ jobs: steps: - uses: actions/setup-node@v3 with: - node-version: 14 + node-version: 20.9.0 - uses: actions/checkout@v3 - name: Install run: npm install --ignore-scripts diff --git a/.github/workflows/prebuild.yaml b/.github/workflows/prebuild.yaml index 784069e06..d1d30960f 100644 --- a/.github/workflows/prebuild.yaml +++ b/.github/workflows/prebuild.yaml @@ -24,7 +24,7 @@ jobs: Linux: strategy: matrix: - node: [8, 9, 10, 11, 12, 13, 14, 16, 18, 20] + node: [18.12.0, 20.9.0] canvas_tag: [] # e.g. "v2.6.1" name: ${{ matrix.canvas_tag}}, Node.js ${{ matrix.node }}, Linux runs-on: ubuntu-latest @@ -97,7 +97,7 @@ jobs: macOS: strategy: matrix: - node: [8, 9, 10, 11, 12, 13, 14, 16, 18, 20] + node: [18.12.0, 20.9.0] canvas_tag: [] # e.g. "v2.6.1" name: ${{ matrix.canvas_tag}}, Node.js ${{ matrix.node }}, macOS runs-on: macos-latest @@ -163,7 +163,7 @@ jobs: Win: strategy: matrix: - node: [8, 9, 10, 11, 12, 13, 14, 16, 18, 20] + node: [18.12.0, 20.9.0] canvas_tag: [] # e.g. "v2.6.1" name: ${{ matrix.canvas_tag}}, Node.js ${{ matrix.node }}, Windows runs-on: windows-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 03d9d2732..52f66779b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ project adheres to [Semantic Versioning](http://semver.org/). This release notably changes to using N-API. 🎉 +### Breaking +* Dropped support for Node.js 16.x and below. ### Changed * Migrated to N-API (by way of node-addon-api) and removed libuv and v8 dependencies * Defer the initialization of the `op` variable to the `default` switch case to avoid a compiler warning. (#2229) diff --git a/package.json b/package.json index 12b9be365..d9c6526d2 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "typescript": "^4.2.2" }, "engines": { - "node": ">=10.20.0" + "node": "^18.12.0 || >= 20.9.0" }, "license": "MIT" } From 2c4d2a7dc61252913825cf9204730381051f0eba Mon Sep 17 00:00:00 2001 From: huan_kong <49610758+huankong233@users.noreply.github.com> Date: Fri, 8 Dec 2023 04:21:18 +0800 Subject: [PATCH 405/474] fix the wrong type of setTransform (#2322) * fix the wrong type of setTransform * Update CHANGELOG.md --- CHANGELOG.md | 1 + index.d.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52f66779b..4ccc8e7f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed +* fix the wrong type of setTransform 3.0.0 ================== diff --git a/index.d.ts b/index.d.ts index 8bcfd105e..73ad4cde9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -201,6 +201,7 @@ export class CanvasRenderingContext2D { getTransform(): DOMMatrix; resetTransform(): void; setTransform(transform?: DOMMatrix): void; + setTransform(a: number, b: number, c: number, d: number, e: number, f: number): void; isPointInPath(x: number, y: number, fillRule?: CanvasFillRule): boolean; scale(x: number, y: number): void; clip(fillRule?: CanvasFillRule): void; From 569e8fbbc0b39370b2db53812bc4a75e111c68e7 Mon Sep 17 00:00:00 2001 From: Dirk Stolle Date: Thu, 28 Dec 2023 02:22:24 +0100 Subject: [PATCH 406/474] Update actions in GitHub Actions CI The following updates are performed: * update actions/checkout to v4 * update actions/setup-node to v4 --- .github/workflows/ci.yaml | 16 ++++++++-------- .github/workflows/prebuild.yaml | 12 ++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f0606175d..615ad0699 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,10 +15,10 @@ jobs: matrix: node: [18.12.0, 20.9.0] steps: - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install Dependencies run: | sudo apt update @@ -39,10 +39,10 @@ jobs: # ref: https://github.com/nodejs/node/pull/50650 node: [18.12.0] steps: - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install Dependencies run: | Invoke-WebRequest "https://ftp-osl.osuosl.org/pub/gnome/binaries/win64/gtk+/2.22/gtk+-bundle_2.22.1-20101229_win64.zip" -OutFile "gtk.zip" @@ -63,10 +63,10 @@ jobs: matrix: node: [18.12.0, 20.9.0] steps: - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install Dependencies run: | brew update @@ -82,10 +82,10 @@ jobs: name: Lint runs-on: ubuntu-latest steps: - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: 20.9.0 - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install run: npm install --ignore-scripts - name: Lint diff --git a/.github/workflows/prebuild.yaml b/.github/workflows/prebuild.yaml index d1d30960f..036e115e3 100644 --- a/.github/workflows/prebuild.yaml +++ b/.github/workflows/prebuild.yaml @@ -33,11 +33,11 @@ jobs: env: CANVAS_VERSION_TO_BUILD: ${{ matrix.canvas_tag }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: ref: ${{ matrix.canvas_tag }} - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} @@ -104,11 +104,11 @@ jobs: env: CANVAS_VERSION_TO_BUILD: ${{ matrix.canvas_tag }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: ref: ${{ matrix.canvas_tag }} - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} @@ -176,11 +176,11 @@ jobs: update: true path-type: inherit - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: ref: ${{ matrix.canvas_tag }} From 41428ee2dfa2622b82f65e4fccd3adfefd1e0a72 Mon Sep 17 00:00:00 2001 From: Stepan Mikhailiuk Date: Thu, 28 Dec 2023 15:01:14 -0800 Subject: [PATCH 407/474] added deregisterAllFonts to readme (#2328) * added deregisterAllFonts to readme * Update Readme.md --- Readme.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/Readme.md b/Readme.md index c029e27cb..b84df72db 100644 --- a/Readme.md +++ b/Readme.md @@ -78,6 +78,8 @@ This project is an implementation of the Web Canvas API and implements that API * [createImageData()](#createimagedata) * [loadImage()](#loadimage) * [registerFont()](#registerfont) +* [deregisterAllFonts()](#deregisterAllFonts) + ### Non-standard APIs @@ -170,6 +172,35 @@ ctx.fillText('Everyone hates this font :(', 250, 10) The second argument is an object with properties that resemble the CSS properties that are specified in `@font-face` rules. You must specify at least `family`. `weight`, and `style` are optional and default to `'normal'`. +### deregisterAllFonts() + +> ```ts +> deregisterAllFonts() => void +> ``` + +Use `deregisterAllFonts` to unregister all fonts that have been previously registered. This method is useful when you want to remove all registered fonts, such as when using the canvas in tests + +```ts +const { registerFont, createCanvas, deregisterAllFonts } = require('canvas') + +describe('text rendering', () => { + afterEach(() => { + deregisterAllFonts(); + }) + it('should render text with Comic Sans', () => { + registerFont('comicsans.ttf', { family: 'Comic Sans' }) + + const canvas = createCanvas(500, 500) + const ctx = canvas.getContext('2d') + + ctx.font = '12px "Comic Sans"' + ctx.fillText('Everyone loves this font :)', 250, 10) + + // assertScreenshot() + }) +}) +``` + ### Image#src > ```ts From ff0f2abd0e3385f94da5057e9fd4bc1fb1c74b94 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 28 Dec 2023 14:59:49 -0800 Subject: [PATCH 408/474] Move a changelog entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ccc8e7f7..de18c3bef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,6 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed -* fix the wrong type of setTransform 3.0.0 ================== @@ -34,6 +33,7 @@ This release notably changes to using N-API. 🎉 * Fix a case of use-after-free. (#2229) * Fix usage of garbage value by filling the allocated memory entirely with zeros if it's not modified. (#2229) * Fix a potential memory leak. (#2229) +* Fix the wrong type of setTransform 2.11.2 ================== From 25fbac52c8f2d63468992d7c9f110aff6ef58dfc Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 28 Dec 2023 17:10:26 -0800 Subject: [PATCH 409/474] switch to prebuild-install --- CHANGELOG.md | 1 + package.json | 11 ++--------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de18c3bef..73ad0b00b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ This release notably changes to using N-API. 🎉 * Dropped support for Node.js 16.x and below. ### Changed * Migrated to N-API (by way of node-addon-api) and removed libuv and v8 dependencies +* Change from node-pre-gyp to prebuild-install * Defer the initialization of the `op` variable to the `default` switch case to avoid a compiler warning. (#2229) * Use a `default` switch case with a null statement if some enum values aren't suppsed to be handled, this avoids a compiler warning. (#2229) * Migrate from librsvg's deprecated `rsvg_handle_get_dimensions()` and `rsvg_handle_render_cairo()` functions to the new `rsvg_handle_get_intrinsic_size_in_pixels()` and `rsvg_handle_render_document()` respectively. (#2229) diff --git a/package.json b/package.json index d9c6526d2..c4e340972 100644 --- a/package.json +++ b/package.json @@ -32,16 +32,9 @@ "test-server": "node test/server.js", "generate-wpt": "node ./test/wpt/generate.js", "test-wpt": "mocha test/wpt/generated/*.js", - "install": "node-pre-gyp install --fallback-to-build --update-binary", + "install": "prebuild-install -r napi || node-gyp rebuild", "tsd": "tsd" }, - "binary": { - "module_name": "canvas", - "module_path": "build/Release", - "host": "https://github.com/Automattic/node-canvas/releases/download/", - "remote_path": "v{version}", - "package_name": "{module_name}-v{version}-{node_abi}-{platform}-{libc}-{arch}.tar.gz" - }, "files": [ "binding.gyp", "browser.js", @@ -52,8 +45,8 @@ "util/" ], "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.0", "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", "simple-get": "^3.0.3" }, "devDependencies": { From 4018095a9ec4d9277a927c6a1af4577f45f72fda Mon Sep 17 00:00:00 2001 From: Harlen Bains Date: Sat, 13 Apr 2024 22:13:07 -0700 Subject: [PATCH 410/474] change OS X to macOS in Readme changed OS X to macOS --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index b84df72db..cdf5c73ed 100644 --- a/Readme.md +++ b/Readme.md @@ -23,7 +23,7 @@ For detailed installation information, see the [wiki](https://github.com/Automat OS | Command ----- | ----- -OS X | Using [Homebrew](https://brew.sh/):
`brew install pkg-config cairo pango libpng jpeg giflib librsvg pixman` +macOS | Using [Homebrew](https://brew.sh/):
`brew install pkg-config cairo pango libpng jpeg giflib librsvg pixman` Ubuntu | `sudo apt-get install build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev` Fedora | `sudo yum install gcc-c++ cairo-devel pango-devel libjpeg-turbo-devel giflib-devel` Solaris | `pkgin install cairo pango pkg-config xproto renderproto kbproto xextproto` From f934f226fc01b2ccb4554d0056718a24279ac087 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 13 Apr 2024 22:31:47 -0700 Subject: [PATCH 411/474] remove use of designated initializers Added in #2229, this is a C++20 feature. For now we're on C++17 (by way of Node.js v18). --- src/Image.cc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Image.cc b/src/Image.cc index a1f376136..7a4831ae0 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -1178,10 +1178,10 @@ Image::renderSVGToSurface() { } RsvgRectangle viewport = { - .x = 0, - .y = 0, - .width = static_cast(width), - .height = static_cast(height), + 0, // x + 0, // y + static_cast(width), + static_cast(height) }; gboolean render_ok = rsvg_handle_render_document(_rsvg, cr, &viewport, nullptr); if (!render_ok) { From 3f3b2e62a999b8ecfb57034be016f8079988e96a Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Wed, 19 Jun 2024 14:14:23 -0700 Subject: [PATCH 412/474] rm prebuild files from master branch Prebuilds are built using the prebuilds branch. These files are unused and confusing. --- .github/workflows/prebuild.yaml | 233 +------------------------------- prebuild/Linux/Dockerfile | 43 ------ prebuild/Linux/binding.gyp | 53 -------- prebuild/Linux/bundle.sh | 5 - prebuild/Linux/preinstall.sh | 8 -- prebuild/Windows/binding.gyp | 79 ----------- prebuild/Windows/bundle.sh | 23 ---- prebuild/Windows/preinstall.sh | 38 ------ prebuild/macOS/binding.gyp | 51 ------- prebuild/macOS/bundle.sh | 4 - prebuild/macOS/preinstall.sh | 4 - prebuild/tarball.sh | 18 --- 12 files changed, 5 insertions(+), 554 deletions(-) delete mode 100644 prebuild/Linux/Dockerfile delete mode 100644 prebuild/Linux/binding.gyp delete mode 100644 prebuild/Linux/bundle.sh delete mode 100644 prebuild/Linux/preinstall.sh delete mode 100644 prebuild/Windows/binding.gyp delete mode 100644 prebuild/Windows/bundle.sh delete mode 100644 prebuild/Windows/preinstall.sh delete mode 100644 prebuild/macOS/binding.gyp delete mode 100644 prebuild/macOS/bundle.sh delete mode 100644 prebuild/macOS/preinstall.sh delete mode 100644 prebuild/tarball.sh diff --git a/.github/workflows/prebuild.yaml b/.github/workflows/prebuild.yaml index 036e115e3..88b6288bf 100644 --- a/.github/workflows/prebuild.yaml +++ b/.github/workflows/prebuild.yaml @@ -1,236 +1,13 @@ -# Triggering prebuilds: -# 1. Create a draft release manually using the GitHub UI. -# 2. Set the `jobs.*.strategy.matrix.node` arrays to the set of Node.js versions -# to build for. -# 3. Set the `jobs.*.strategy.matrix.canvas_tag` arrays to the set of Canvas -# tags to build. (Usually this is a single tag, but can be an array when a -# new version of Node.js is released and older versions of Canvas need to be -# built.) -# 4. Commit and push this file to master. -# 5. In the Actions tab, navigate to the "Make Prebuilds" workflow and click -# "Run workflow". -# 6. Once the builds succeed, promote the draft release to a full release. +# This is a dummy file so that this workflow shows up in the Actions tab. +# Prebuilds are actually run using the prebuilds branch. name: Make Prebuilds on: workflow_dispatch -# UPLOAD_TO can be specified to upload the release assets under a different tag -# name (e.g. for testing). If omitted, the assets are published under the same -# release tag as the canvas version being built. -# env: -# UPLOAD_TO: "v0.0.1" - jobs: Linux: - strategy: - matrix: - node: [18.12.0, 20.9.0] - canvas_tag: [] # e.g. "v2.6.1" - name: ${{ matrix.canvas_tag}}, Node.js ${{ matrix.node }}, Linux + name: Nothing runs-on: ubuntu-latest - container: - image: chearon/canvas-prebuilt:7 - env: - CANVAS_VERSION_TO_BUILD: ${{ matrix.canvas_tag }} - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ matrix.canvas_tag }} - - - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node }} - - - name: Build - run: | - npm install -g node-gyp - npm install --ignore-scripts - . prebuild/Linux/preinstall.sh - cp prebuild/Linux/binding.gyp binding.gyp - node-gyp rebuild -j 2 - . prebuild/Linux/bundle.sh - - - name: Test binary - run: | - cd /root/harfbuzz-* && make uninstall - cd /root/cairo-* && make uninstall - cd /root/pango-* && make uninstall - cd /root/libpng-* && make uninstall - cd /root/libjpeg-* && make uninstall - cd /root/giflib-* && make uninstall - cd $GITHUB_WORKSPACE && npm test - - - name: Make bundle - id: make_bundle - run: . prebuild/tarball.sh - - - name: Upload - uses: actions/github-script@0.9.0 - with: - script: | - const fs = require("fs"); - const assetName = "${{ steps.make_bundle.outputs.asset_name }}"; - const tagName = process.env.UPLOAD_TO || process.env.CANVAS_VERSION_TO_BUILD; - const [owner, repo] = process.env.GITHUB_REPOSITORY.split("/"); - - const releases = await github.repos.listReleases({owner, repo}); - const release = releases.data.find(r => r.tag_name === tagName); - if (!release) - throw new Error(`Tag ${tagName} not found. Did you make the GitHub release?`); - - const oldAsset = release.assets.find(a => a.name === assetName); - if (oldAsset) - await github.repos.deleteReleaseAsset({owner, repo, asset_id: oldAsset.id}); - - // (This is equivalent to actions/upload-release-asset. We're - // already in a script, so might as well do it here.) - const r = await github.repos.uploadReleaseAsset({ - url: release.upload_url, - headers: { - "content-type": "application/x-gzip", - "content-length": `${fs.statSync(assetName).size}` - }, - name: assetName, - data: fs.readFileSync(assetName) - }); - - macOS: - strategy: - matrix: - node: [18.12.0, 20.9.0] - canvas_tag: [] # e.g. "v2.6.1" - name: ${{ matrix.canvas_tag}}, Node.js ${{ matrix.node }}, macOS - runs-on: macos-latest - env: - CANVAS_VERSION_TO_BUILD: ${{ matrix.canvas_tag }} steps: - - uses: actions/checkout@v4 - with: - ref: ${{ matrix.canvas_tag }} - - - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node }} - - - name: Build - run: | - npm install -g node-gyp - npm install --ignore-scripts - . prebuild/macOS/preinstall.sh - cp prebuild/macOS/binding.gyp binding.gyp - node-gyp rebuild -j 2 - . prebuild/macOS/bundle.sh - - - name: Test binary - run: | - brew uninstall --force cairo pango librsvg giflib harfbuzz - npm test - - - name: Make bundle - id: make_bundle - run: . prebuild/tarball.sh - - - name: Upload - uses: actions/github-script@0.9.0 - with: - script: | - const fs = require("fs"); - const assetName = "${{ steps.make_bundle.outputs.asset_name }}"; - const tagName = process.env.UPLOAD_TO || process.env.CANVAS_VERSION_TO_BUILD; - const [owner, repo] = process.env.GITHUB_REPOSITORY.split("/"); - - const releases = await github.repos.listReleases({owner, repo}); - const release = releases.data.find(r => r.tag_name === tagName); - if (!release) - throw new Error(`Tag ${tagName} not found. Did you make the GitHub release?`); - - const oldAsset = release.assets.find(a => a.name === assetName); - if (oldAsset) - await github.repos.deleteReleaseAsset({owner, repo, asset_id: oldAsset.id}); - - // (This is equivalent to actions/upload-release-asset. We're - // already in a script, so might as well do it here.) - const r = await github.repos.uploadReleaseAsset({ - url: release.upload_url, - headers: { - "content-type": "application/x-gzip", - "content-length": `${fs.statSync(assetName).size}` - }, - name: assetName, - data: fs.readFileSync(assetName) - }); - - Win: - strategy: - matrix: - node: [18.12.0, 20.9.0] - canvas_tag: [] # e.g. "v2.6.1" - name: ${{ matrix.canvas_tag}}, Node.js ${{ matrix.node }}, Windows - runs-on: windows-latest - env: - CANVAS_VERSION_TO_BUILD: ${{ matrix.canvas_tag }} - steps: - # TODO drop when https://github.com/actions/virtual-environments/pull/632 lands - - uses: numworks/setup-msys2@v1 - with: - update: true - path-type: inherit - - - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node }} - - - uses: actions/checkout@v4 - with: - ref: ${{ matrix.canvas_tag }} - - - name: Build - run: | - npm install -g node-gyp - npm install --ignore-scripts - msys2do . prebuild/Windows/preinstall.sh - msys2do cp prebuild/Windows/binding.gyp binding.gyp - msys2do node-gyp configure - msys2do node-gyp rebuild -j 2 - - - name: Bundle - run: msys2do . prebuild/Windows/bundle.sh - - - name: Test binary - # By not running in msys2, this doesn't have access to the msys2 libs - run: npm test - - - name: Make asset - id: make_bundle - # I can't figure out why this isn't an env var already. It shows up with `env`. - run: msys2do UPLOAD_TO=${{ env.UPLOAD_TO }} CANVAS_VERSION_TO_BUILD=${{ env.CANVAS_VERSION_TO_BUILD}} . prebuild/tarball.sh - - - name: Upload - uses: actions/github-script@0.9.0 - with: - script: | - const fs = require("fs"); - const assetName = "${{ steps.make_bundle.outputs.asset_name }}"; - const tagName = process.env.UPLOAD_TO || process.env.CANVAS_VERSION_TO_BUILD; - const [owner, repo] = process.env.GITHUB_REPOSITORY.split("/"); - - const releases = await github.repos.listReleases({owner, repo}); - const release = releases.data.find(r => r.tag_name === tagName); - if (!release) - throw new Error(`Tag ${tagName} not found. Did you make the GitHub release?`); - - const oldAsset = release.assets.find(a => a.name === assetName); - if (oldAsset) - await github.repos.deleteReleaseAsset({owner, repo, asset_id: oldAsset.id}); - - // (This is equivalent to actions/upload-release-asset. We're - // already in a script, so might as well do it here.) - const r = await github.repos.uploadReleaseAsset({ - url: release.upload_url, - headers: { - "content-type": "application/x-gzip", - "content-length": `${fs.statSync(assetName).size}` - }, - name: assetName, - data: fs.readFileSync(assetName) - }); + - name: Nothing + run: echo "Nothing to do here" diff --git a/prebuild/Linux/Dockerfile b/prebuild/Linux/Dockerfile deleted file mode 100644 index be68e5b5e..000000000 --- a/prebuild/Linux/Dockerfile +++ /dev/null @@ -1,43 +0,0 @@ -FROM debian:stretch -RUN apt-get update && apt-get -y install curl git cmake make gcc g++ nasm wget gperf bzip2 meson uuid-dev perl libxml-parser-perl - -RUN bash -c 'cd; curl -LO https://pkg-config.freedesktop.org/releases/pkg-config-0.29.2.tar.gz; tar -xvf pkg-config-0.29.2.tar.gz; cd pkg-config-0.29.2; ./configure --with-internal-glib; make; make install' -RUN bash -c 'cd; curl -O https://zlib.net/fossils/zlib-1.2.11.tar.gz; tar -xvf zlib-1.2.11.tar.gz; cd zlib-1.2.11; ./configure; make; make install' -RUN bash -c 'cd; curl -LO https://github.com/libffi/libffi/releases/download/v3.3/libffi-3.3.tar.gz; tar -xvf libffi-3.3.tar.gz; cd libffi-3.3; ./configure; make; make install' -RUN bash -c 'cd; curl -O https://www.openssl.org/source/openssl-1.1.1i.tar.gz; tar -xvf openssl-1.1.1i.tar.gz; cd openssl-1.1.1i; ./config; make; make install' -RUN ldconfig -RUN bash -c 'cd; curl -O https://www.python.org/ftp/python/3.9.1/Python-3.9.1.tgz; tar -xvf Python-3.9.1.tgz; cd Python-3.9.1; ./configure --enable-shared --with-ensurepip=yes; make; make install' -RUN ldconfig -RUN pip3 install meson -RUN bash -c 'cd; curl -LO https://download.sourceforge.net/giflib/giflib-5.2.1.tar.gz; tar -xvf giflib-5.2.1.tar.gz; cd giflib-5.2.1; ./configure; make; make install' -RUN bash -c 'cd; curl -LO https://download.sourceforge.net/libpng/libpng-1.6.37.tar.gz; tar -xvf libpng-1.6.37.tar.gz; cd libpng-1.6.37; ./configure; make; make install' -RUN bash -c 'cd; curl -LO https://github.com/libjpeg-turbo/libjpeg-turbo/archive/2.0.6.tar.gz; tar -xvf 2.0.6.tar.gz; cd libjpeg-turbo-2.0.6; mkdir b; cd b; cmake -G"Unix Makefiles" -DCMAKE_INSTALL_PREFIX=/usr/local ..; make; make install' -RUN bash -c 'cd; curl -O https://ftp.pcre.org/pub/pcre/pcre-8.44.tar.bz2; tar -xvf pcre-8.44.tar.bz2; cd pcre-8.44; ./configure --enable-pcre16 --enable-pcre32 --enable-utf --enable-unicode-properties; make; make install ' -RUN ldconfig -RUN bash -c 'cd; curl -LO https://download.gnome.org/sources/glib/2.67/glib-2.67.1.tar.xz; tar -xvf glib-2.67.1.tar.xz; cd glib-2.67.1; meson _build; cd _build; ninja; ninja install' -RUN ldconfig -RUN bash -c 'cd; curl -LO https://download.sourceforge.net/freetype/freetype-2.10.4.tar.gz; tar -xvf freetype-2.10.4.tar.gz; cd freetype-2.10.4; ./configure; make; make install' -RUN bash -c 'cd; curl -LO https://github.com/harfbuzz/harfbuzz/releases/download/2.7.4/harfbuzz-2.7.4.tar.xz; tar -xvf harfbuzz-2.7.4.tar.xz; cd harfbuzz-2.7.4; ./configure; make; make install;' -RUN bash -c 'cd; curl -LO https://github.com/libexpat/libexpat/releases/download/R_2_2_10/expat-2.2.10.tar.gz; tar -xvf expat-2.2.10.tar.gz; cd expat-2.2.10; ./configure; make; make install' -RUN ldconfig -RUN ls -l /usr/include -RUN bash -c 'cd; curl -O https://www.freedesktop.org/software/fontconfig/release/fontconfig-2.13.1.tar.bz2; tar -xvf fontconfig-2.13.1.tar.bz2; cd fontconfig-2.13.1; UUID_LIBS="-L/lib/x86_64-linux-gnu -luuid" UUID_CFLAGS="-I/include" ./configure --enable-static --sysconfdir=/etc --localstatedir=/var; make; make install' -RUN bash -c 'cd; curl -O https://www.cairographics.org/releases/pixman-0.40.0.tar.gz; tar -xvf pixman-0.40.0.tar.gz; cd pixman-0.40.0; ./configure; make; make install' -RUN bash -c 'cd; curl -O https://cairographics.org/releases/cairo-1.16.0.tar.xz; tar -xvf cairo-1.16.0.tar.xz; cd cairo-1.16.0; ./configure; make; make install' -RUN bash -c 'cd; curl -LO https://github.com/fribidi/fribidi/releases/download/v1.0.10/fribidi-1.0.10.tar.xz; tar -xvf fribidi-1.0.10.tar.xz; cd fribidi-1.0.10; ./configure; make; make install' -RUN bash -c 'cd; curl -LO https://download.gnome.org/sources/pango/1.48/pango-1.48.0.tar.xz; tar -xvf pango-1.48.0.tar.xz; cd pango-1.48.0; meson -Dharfbuzz:docs=disabled -Dgtk_doc=false _build; cd _build; ninja; ninja install' -RUN ldconfig - -# librsvg -RUN bash -c 'curl https://sh.rustup.rs -sSf | sh -s -- -y'; -RUN bash -c 'curl -O http://xmlsoft.org/sources/libxml2-2.9.10.tar.gz; tar -xvf libxml2-2.9.10.tar.gz; cd libxml2-2.9.10; ./configure --without-python; make; make install' -RUN bash -c 'curl -O https://ftp.gnu.org/pub/gnu/gettext/gettext-0.21.tar.gz; tar -xvf gettext-0.21.tar.gz; cd gettext-0.21; ./configure; make; make install' -RUN ldconfig -RUN bash -c 'curl -LO https://launchpad.net/intltool/trunk/0.51.0/+download/intltool-0.51.0.tar.gz; tar -xvf intltool-0.51.0.tar.gz; cd intltool-0.51.0; ./configure; make; make install' -# using an old version of shared-mime-info because 2.1 has a ridiculous number of dependencies for what is essentially just a database -RUN bash -c 'curl -O https://people.freedesktop.org/~hadess/shared-mime-info-1.8.tar.xz; tar -xvf shared-mime-info-1.8.tar.xz; cd shared-mime-info-1.8; ./configure; make; make install' -RUN bash -c 'curl -LO https://download.gnome.org/sources/gdk-pixbuf/2.42/gdk-pixbuf-2.42.2.tar.xz; tar -xvf gdk-pixbuf-2.42.2.tar.xz; cd gdk-pixbuf-2.42.2; meson _build; cd _build; ninja install'; -RUN ldconfig -RUN bash -c 'curl -LO https://download.gnome.org/sources/libcroco/0.6/libcroco-0.6.13.tar.xz; tar -xvf libcroco-0.6.13.tar.xz; cd libcroco-0.6.13; ./configure; make; make install' -RUN bash -c 'cd; . .cargo/env; curl -LO https://download.gnome.org/sources/librsvg/2.50/librsvg-2.50.2.tar.xz; tar -xvf librsvg-2.50.2.tar.xz; cd librsvg-2.50.2; ./configure --enable-introspection=no; make; make install' -RUN ldconfig diff --git a/prebuild/Linux/binding.gyp b/prebuild/Linux/binding.gyp deleted file mode 100644 index 1a967667d..000000000 --- a/prebuild/Linux/binding.gyp +++ /dev/null @@ -1,53 +0,0 @@ -{ - 'targets': [ - { - 'target_name': 'canvas', - 'sources': [ - 'src/backend/Backend.cc', - 'src/backend/ImageBackend.cc', - 'src/backend/PdfBackend.cc', - 'src/backend/SvgBackend.cc', - 'src/bmp/BMPParser.cc', - 'src/Backends.cc', - 'src/Canvas.cc', - 'src/CanvasGradient.cc', - 'src/CanvasPattern.cc', - 'src/CanvasRenderingContext2d.cc', - 'src/closure.cc', - 'src/color.cc', - 'src/Image.cc', - 'src/ImageData.cc', - 'src/init.cc', - 'src/register_font.cc' - ], - 'defines': [ - 'HAVE_GIF', - 'HAVE_JPEG', - 'HAVE_RSVG' - ], - 'libraries': [ - ' /dev/null 2>&1 || { - echo "could not find lib$lib.dll, have to skip "; - continue; - } - - dlltool -d lib$lib.def -l /mingw64/lib/lib$lib.lib > /dev/null 2>&1 || { - echo "could not create dll for lib$lib.dll"; - continue; - } - - echo "created lib$lib.lib from lib$lib.dll"; - - rm lib$lib.def -done diff --git a/prebuild/macOS/binding.gyp b/prebuild/macOS/binding.gyp deleted file mode 100644 index 00ae2ccbc..000000000 --- a/prebuild/macOS/binding.gyp +++ /dev/null @@ -1,51 +0,0 @@ -{ - 'targets': [ - { - 'target_name': 'canvas', - 'sources': [ - 'src/backend/Backend.cc', - 'src/backend/ImageBackend.cc', - 'src/backend/PdfBackend.cc', - 'src/backend/SvgBackend.cc', - 'src/bmp/BMPParser.cc', - 'src/Backends.cc', - 'src/Canvas.cc', - 'src/CanvasGradient.cc', - 'src/CanvasPattern.cc', - 'src/CanvasRenderingContext2d.cc', - 'src/closure.cc', - 'src/color.cc', - 'src/Image.cc', - 'src/ImageData.cc', - 'src/init.cc', - 'src/register_font.cc' - ], - 'defines': [ - 'HAVE_GIF', - 'HAVE_JPEG', - 'HAVE_RSVG' - ], - 'libraries': [ - ' Date: Wed, 19 Jun 2024 16:08:07 -0700 Subject: [PATCH 413/474] update installation info in readme --- Readme.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Readme.md b/Readme.md index cdf5c73ed..1bd9c634e 100644 --- a/Readme.md +++ b/Readme.md @@ -11,9 +11,14 @@ node-canvas is a [Cairo](http://cairographics.org/)-backed Canvas implementation $ npm install canvas ``` -By default, binaries for macOS, Linux and Windows will be downloaded. If you want to build from source, use `npm install --build-from-source` and see the **Compiling** section below. +By default, pre-built binaries will be downloaded if you're on one of the following platforms: +- macOS x86/64 (*not* Apple silicon) +- Linux x86/64 (glibc only) +- Windows x86/64 -The minimum version of Node.js required is **10.20.0**. +If you want to build from source, use `npm install --build-from-source` and see the **Compiling** section below. + +The minimum version of Node.js required is **18.12.0**. ### Compiling From 130785fa1db9464e558755ff2a3bf60606ec7b8a Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Wed, 19 Jun 2024 16:42:51 -0700 Subject: [PATCH 414/474] publish v3.0.0-rc2 -rc1 never really existed, but I created the tag and don't want to delete it in case someone is installing from github. --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index c4e340972..371b0767b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "3.0.0", + "version": "3.0.0-rc2", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", @@ -63,5 +63,8 @@ "engines": { "node": "^18.12.0 || >= 20.9.0" }, + "binary": { + "napi_versions": [7] + }, "license": "MIT" } From 3b04bde706f6c034c9e460ec28b8c2f5ca1cdf1f Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Wed, 19 Jun 2024 17:29:55 -0700 Subject: [PATCH 415/474] add note about v3.0.0-rd2 --- Readme.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Readme.md b/Readme.md index 1bd9c634e..1a38bdaea 100644 --- a/Readme.md +++ b/Readme.md @@ -5,6 +5,13 @@ node-canvas is a [Cairo](http://cairographics.org/)-backed Canvas implementation for [Node.js](http://nodejs.org). +> [!TIP] +> **v3.0.0-rc2 is now available for testing on Linux and Windows!** It's the first version +> to use N-API and prebuild-install. Please give it a try and let us know if you run into any issues. +> ```sh +> npm install canvas@next +> ``` + ## Installation ```bash From 2de0f8b36dbb271c9dc1bdb211812c5dabca5129 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Wed, 19 Jun 2024 17:39:15 -0700 Subject: [PATCH 416/474] Add python-setuptools to MacOS compilation info --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 1a38bdaea..4a38e037b 100644 --- a/Readme.md +++ b/Readme.md @@ -35,7 +35,7 @@ For detailed installation information, see the [wiki](https://github.com/Automat OS | Command ----- | ----- -macOS | Using [Homebrew](https://brew.sh/):
`brew install pkg-config cairo pango libpng jpeg giflib librsvg pixman` +macOS | Using [Homebrew](https://brew.sh/):
`brew install pkg-config cairo pango libpng jpeg giflib librsvg pixman python-setuptools` Ubuntu | `sudo apt-get install build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev` Fedora | `sudo yum install gcc-c++ cairo-devel pango-devel libjpeg-turbo-devel giflib-devel` Solaris | `pkgin install cairo pango pkg-config xproto renderproto kbproto xextproto` From 20c98827223e242f6db4ee5eb99ce42569a11eec Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 22 Jun 2024 21:51:06 -0700 Subject: [PATCH 417/474] update prebuild status in readme --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 4a38e037b..7e7b88ba1 100644 --- a/Readme.md +++ b/Readme.md @@ -6,7 +6,7 @@ node-canvas is a [Cairo](http://cairographics.org/)-backed Canvas implementation for [Node.js](http://nodejs.org). > [!TIP] -> **v3.0.0-rc2 is now available for testing on Linux and Windows!** It's the first version +> **v3.0.0-rc2 is now available for testing on Linux (x64 glibc), macOS (x64) and Windows (x64)!** It's the first version > to use N-API and prebuild-install. Please give it a try and let us know if you run into any issues. > ```sh > npm install canvas@next From 7726ea5e2f60aeae041498fdd52aadfe1c78530c Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 22 Jun 2024 21:52:29 -0700 Subject: [PATCH 418/474] run CI on macos-12 --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 615ad0699..cb6010cda 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -58,7 +58,7 @@ jobs: macOS: name: Test on macOS - runs-on: macos-latest + runs-on: macos-12 strategy: matrix: node: [18.12.0, 20.9.0] From e6d55d88c0b72f5742af2fe6f101e6149f0e672c Mon Sep 17 00:00:00 2001 From: Pranav Sharma Date: Sat, 18 May 2024 13:08:48 +0530 Subject: [PATCH 419/474] Add fix for alpha '%' parsing --- src/color.cc | 11 ++++++++++- test/canvas.test.js | 11 +++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/color.cc b/src/color.cc index 230fb8dbe..8368f7b92 100644 --- a/src/color.cc +++ b/src/color.cc @@ -226,7 +226,16 @@ parse_clipped_percentage(const char** pStr, float *pFraction) { #define ALPHA(NAME) \ if (*str >= '1' && *str <= '9') { \ - NAME = 1; \ + NAME = 0; \ + float n = .1f; \ + while(*str >='0' && *str <= '9') { \ + NAME += (*str - '0') * n; \ + str++; \ + } \ + while(*str == ' ')str++; \ + if(*str != '%') { \ + NAME = 1; \ + } \ } else { \ if ('0' == *str) { \ NAME = 0; \ diff --git a/test/canvas.test.js b/test/canvas.test.js index c3b83b271..8a8fa05b7 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -213,6 +213,17 @@ describe('Canvas', function () { ctx.fillStyle = 'rgba(124, 58, 26, 0)'; assert.equal('rgba(124, 58, 26, 0.00)', ctx.fillStyle); + ctx.fillStyle = 'rgba( 255, 200, 90, 40%)' + assert.equal('rgba(255, 200, 90, 0.40)', ctx.fillStyle) + + ctx.fillStyle = 'rgba( 255, 200, 90, 50 %)' + assert.equal('rgba(255, 200, 90, 0.50)', ctx.fillStyle) + + ctx.fillStyle = 'rgba( 255, 200, 90, 10%)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) + + ctx.fillStyle = 'rgba( 255, 200, 90, 10 %)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) // hsl / hsla tests ctx.fillStyle = 'hsl(0, 0%, 0%)' From b3ecff1b6bc2bd7dc483756980ffd120256114a0 Mon Sep 17 00:00:00 2001 From: Pranav Sharma Date: Sat, 18 May 2024 14:02:05 +0530 Subject: [PATCH 420/474] Add support for '/' in rgba --- src/color.cc | 5 ++++- test/canvas.test.js | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/color.cc b/src/color.cc index 8368f7b92..33b75e8c2 100644 --- a/src/color.cc +++ b/src/color.cc @@ -210,6 +210,9 @@ parse_clipped_percentage(const char** pStr, float *pFraction) { #define WHITESPACE_OR_COMMA \ while (' ' == *str || ',' == *str) ++str; +#define WHITESPACE_OR_COMMA_OR_SLASH \ + while (' ' == *str || ',' == *str || '/' == *str) ++str; + #define CHANNEL(NAME) \ if (!parse_rgb_channel(&str, &NAME)) \ return 0; \ @@ -649,7 +652,7 @@ rgba_from_rgba_string(const char *str, short *ok) { CHANNEL(g); WHITESPACE_OR_COMMA; CHANNEL(b); - WHITESPACE_OR_COMMA; + WHITESPACE_OR_COMMA_OR_SLASH; ALPHA(a); WHITESPACE; return *ok = 1, rgba_from_rgba(r, g, b, (int) (a * 255)); diff --git a/test/canvas.test.js b/test/canvas.test.js index 8a8fa05b7..25d6b54f6 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -224,6 +224,25 @@ describe('Canvas', function () { ctx.fillStyle = 'rgba( 255, 200, 90, 10 %)' assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) + + ctx.fillStyle = 'rgba( 255, 200, 90 / 40%)' + assert.equal('rgba(255, 200, 90, 0.40)', ctx.fillStyle) + + ctx.fillStyle = 'rgba( 255, 200, 90 / 0.5)' + assert.equal('rgba(255, 200, 90, 0.50)', ctx.fillStyle) + + ctx.fillStyle = 'rgba( 255, 200, 90 / 10%)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) + + ctx.fillStyle = 'rgba( 255, 200, 90 / 0.1)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) + + ctx.fillStyle = 'rgba( 255 200 90 / 10%)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) + + ctx.fillStyle = 'rgba( 255 200 90 0.1)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) + // hsl / hsla tests ctx.fillStyle = 'hsl(0, 0%, 0%)' From 3e0b75cebf62655328bdd6c65fd8be13bf834eda Mon Sep 17 00:00:00 2001 From: Pranav Sharma Date: Sat, 18 May 2024 19:59:13 +0530 Subject: [PATCH 421/474] Fix parsing of alpha in RGBA --- src/color.cc | 6 ++++-- test/canvas.test.js | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/color.cc b/src/color.cc index 33b75e8c2..86cfb73c8 100644 --- a/src/color.cc +++ b/src/color.cc @@ -625,13 +625,15 @@ rgba_from_rgb_string(const char *str, short *ok) { str += 4; WHITESPACE; uint8_t r = 0, g = 0, b = 0; + float a=1.f; CHANNEL(r); WHITESPACE_OR_COMMA; CHANNEL(g); WHITESPACE_OR_COMMA; CHANNEL(b); - WHITESPACE; - return *ok = 1, rgba_from_rgb(r, g, b); + WHITESPACE_OR_COMMA_OR_SLASH; + ALPHA(a); + return *ok = 1, rgba_from_rgba(r, g, b, (int) (255 * a)); } return *ok = 0; } diff --git a/test/canvas.test.js b/test/canvas.test.js index 25d6b54f6..a4a8a5b77 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -243,6 +243,46 @@ describe('Canvas', function () { ctx.fillStyle = 'rgba( 255 200 90 0.1)' assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) + ctx.fillStyle = 'rgb(0, 0, 0, 42.42)' + assert.equal('#000000', ctx.fillStyle) + + ctx.fillStyle = 'rgb(255, 250, 255)'; + assert.equal('#fffaff', ctx.fillStyle); + + ctx.fillStyle = 'rgb(124, 58, 26, 0)'; + assert.equal('rgba(124, 58, 26, 0.00)', ctx.fillStyle); + + ctx.fillStyle = 'rgb( 255, 200, 90, 40%)' + assert.equal('rgba(255, 200, 90, 0.40)', ctx.fillStyle) + + ctx.fillStyle = 'rgb( 255, 200, 90, 50 %)' + assert.equal('rgba(255, 200, 90, 0.50)', ctx.fillStyle) + + ctx.fillStyle = 'rgb( 255, 200, 90, 10%)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) + + ctx.fillStyle = 'rgb( 255, 200, 90, 10 %)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) + + ctx.fillStyle = 'rgb( 255, 200, 90 / 40%)' + assert.equal('rgba(255, 200, 90, 0.40)', ctx.fillStyle) + + ctx.fillStyle = 'rgb( 255, 200, 90 / 0.5)' + assert.equal('rgba(255, 200, 90, 0.50)', ctx.fillStyle) + + ctx.fillStyle = 'rgb( 255, 200, 90 / 10%)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) + + ctx.fillStyle = 'rgb( 255, 200, 90 / 0.1)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) + + ctx.fillStyle = 'rgb( 255 200 90 / 10%)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) + + ctx.fillStyle = 'rgb( 255 200 90 0.1)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) + + // hsl / hsla tests ctx.fillStyle = 'hsl(0, 0%, 0%)' From f138b3a667c50935d3e1ffa69ed0fb56f0f2877c Mon Sep 17 00:00:00 2001 From: Pranav Sharma Date: Sat, 18 May 2024 20:09:08 +0530 Subject: [PATCH 422/474] update changelog for 3.0.0 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73ad0b00b..0e8ba2150 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ This release notably changes to using N-API. 🎉 * Fix usage of garbage value by filling the allocated memory entirely with zeros if it's not modified. (#2229) * Fix a potential memory leak. (#2229) * Fix the wrong type of setTransform +* Fix the improper parsing of rgb functions issue. (#2300) 2.11.2 ================== From 83a51260b0eb10f05311f049ce1531914cfcabe2 Mon Sep 17 00:00:00 2001 From: Pranav Sharma <43780292+pranav1344@users.noreply.github.com> Date: Mon, 15 Jul 2024 09:08:43 +0530 Subject: [PATCH 423/474] Fixes related to parsing of colors in RGB functions (#2398) * Fix leading whitespace in color string issue * Add parsing support for floating point numbers in rbg function * Update CHANGELOG.md --- CHANGELOG.md | 3 +++ src/color.cc | 7 +++++-- test/canvas.test.js | 23 +++++++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e8ba2150..77cc5db8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Added ### Fixed + 3.0.0 ================== @@ -36,6 +37,8 @@ This release notably changes to using N-API. 🎉 * Fix a potential memory leak. (#2229) * Fix the wrong type of setTransform * Fix the improper parsing of rgb functions issue. (#2300) +* Fix issue related to improper parsing of leading and trailing whitespaces in CSS color. (#2301) +* RGB functions should support real numbers now instead of just integers. (#2339) 2.11.2 ================== diff --git a/src/color.cc b/src/color.cc index 86cfb73c8..f82629460 100644 --- a/src/color.cc +++ b/src/color.cc @@ -159,8 +159,9 @@ wrap_float(T value, T limit) { static bool parse_rgb_channel(const char** pStr, uint8_t *pChannel) { - int channel; - if (parse_integer(pStr, &channel)) { + float f_channel; + if (parse_css_number(pStr, &f_channel)) { + int channel = (int) ceil(f_channel); *pChannel = clip(channel, 0, 255); return true; } @@ -739,6 +740,7 @@ rgba_from_hex_string(const char *str, short *ok) { static int32_t rgba_from_name_string(const char *str, short *ok) { + WHITESPACE; std::string lowered(str); std::transform(lowered.begin(), lowered.end(), lowered.begin(), tolower); auto color = named_colors.find(lowered); @@ -765,6 +767,7 @@ rgba_from_name_string(const char *str, short *ok) { int32_t rgba_from_string(const char *str, short *ok) { + WHITESPACE; if ('#' == str[0]) return rgba_from_hex_string(++str, ok); if (str == strstr(str, "rgba")) diff --git a/test/canvas.test.js b/test/canvas.test.js index a4a8a5b77..1de5134a7 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -163,6 +163,13 @@ describe('Canvas', function () { ctx.fillStyle = '#FGG' assert.equal('#ff0000', ctx.fillStyle) + ctx.fillStyle = ' #FCA' + assert.equal('#ffccaa', ctx.fillStyle) + + ctx.fillStyle = ' #ffccaa' + assert.equal('#ffccaa', ctx.fillStyle) + + ctx.fillStyle = '#fff' ctx.fillStyle = 'afasdfasdf' assert.equal('#ffffff', ctx.fillStyle) @@ -282,7 +289,20 @@ describe('Canvas', function () { ctx.fillStyle = 'rgb( 255 200 90 0.1)' assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) + ctx.fillStyle = ' rgb( 255 100 90 0.1)' + assert.equal('rgba(255, 100, 90, 0.10)', ctx.fillStyle) + + ctx.fillStyle = 'rgb(124.00, 58, 26, 0)'; + assert.equal('rgba(124, 58, 26, 0.00)', ctx.fillStyle); + + ctx.fillStyle = 'rgb( 255, 200.09, 90, 40%)' + assert.equal('rgba(255, 201, 90, 0.40)', ctx.fillStyle) + + ctx.fillStyle = 'rgb( 255.00, 199.03, 90, 50 %)' + assert.equal('rgba(255, 200, 90, 0.50)', ctx.fillStyle) + ctx.fillStyle = 'rgb( 255, 300.09, 90, 40%)' + assert.equal('rgba(255, 255, 90, 0.40)', ctx.fillStyle) // hsl / hsla tests ctx.fillStyle = 'hsl(0, 0%, 0%)' @@ -306,6 +326,9 @@ describe('Canvas', function () { ctx.fillStyle = 'hsl(237, 76%, 25%)' assert.equal('#0f1470', ctx.fillStyle) + ctx.fillStyle = ' hsl(0, 150%, 150%)' + assert.equal('#ffffff', ctx.fillStyle) + ctx.fillStyle = 'hsl(240, 73%, 25%)' assert.equal('#11116e', ctx.fillStyle) From ae1aacb72a292f20cacdbe324d04524da8e01a6c Mon Sep 17 00:00:00 2001 From: Danko Aleksejevs Date: Fri, 24 May 2024 05:26:53 +0300 Subject: [PATCH 424/474] Allow quotes within font-family names Add fix to the changelog Use regexp syntax for "string" Co-authored-by: Caleb Hearon --- CHANGELOG.md | 1 + lib/parse-font.js | 13 +++++++++++-- test/canvas.test.js | 4 +++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77cc5db8e..0505b04e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ This release notably changes to using N-API. 🎉 * Fix the improper parsing of rgb functions issue. (#2300) * Fix issue related to improper parsing of leading and trailing whitespaces in CSS color. (#2301) * RGB functions should support real numbers now instead of just integers. (#2339) +* Allow alternate or properly escaped quotes *within* font-family names 2.11.2 ================== diff --git a/lib/parse-font.js b/lib/parse-font.js index 713db5082..a18f05e51 100644 --- a/lib/parse-font.js +++ b/lib/parse-font.js @@ -9,7 +9,7 @@ const styles = 'italic|oblique' const variants = 'small-caps' const stretches = 'ultra-condensed|extra-condensed|condensed|semi-condensed|semi-expanded|expanded|extra-expanded|ultra-expanded' const units = 'px|pt|pc|in|cm|mm|%|em|ex|ch|rem|q' -const string = '\'([^\']+)\'|"([^"]+)"|[\\w\\s-]+' +const string = /'((\\'|[^'])+)'|"((\\"|[^"])+)"|[\w\s-]+/.source // [ [ <‘font-style’> || || <‘font-weight’> || <‘font-stretch’> ]? // <‘font-size’> [ / <‘line-height’> ]? <‘font-family’> ] @@ -18,6 +18,9 @@ const weightRe = new RegExp(`(${weights}) +`, 'i') const styleRe = new RegExp(`(${styles}) +`, 'i') const variantRe = new RegExp(`(${variants}) +`, 'i') const stretchRe = new RegExp(`(${stretches}) +`, 'i') +const familyRe = new RegExp(string, 'g') +const unquoteRe = /^['"](.*)['"]$/ +const unescapeRe = /\\(['"])/g const sizeFamilyRe = new RegExp( `([\\d\\.]+)(${units}) *((?:${string})( *, *(?:${string}))*)`) @@ -46,6 +49,12 @@ module.exports = str => { const sizeFamily = sizeFamilyRe.exec(str) if (!sizeFamily) return // invalid + const names = sizeFamily[3] + .match(familyRe) + // remove actual bounding quotes, if any, unescape any remaining quotes inside + .map(s => s.trim().replace(unquoteRe, '$1').replace(unescapeRe, '$1')) + .filter(s => !!s) + // Default values and required properties const font = { weight: 'normal', @@ -54,7 +63,7 @@ module.exports = str => { variant: 'normal', size: parseFloat(sizeFamily[1]), unit: sizeFamily[2], - family: sizeFamily[3].replace(/["']/g, '').replace(/ *, */g, ',') + family: names.join(',') } // Optional, unordered properties. diff --git a/test/canvas.test.js b/test/canvas.test.js index 1de5134a7..d0feff0d1 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -88,7 +88,9 @@ describe('Canvas', function () { '20px "new century schoolbook", serif', { size: 20, unit: 'px', family: 'new century schoolbook,serif' }, '20px "Arial bold 300"', // synthetic case with weight keyword inside family - { size: 20, unit: 'px', family: 'Arial bold 300', variant: 'normal' } + { size: 20, unit: 'px', family: 'Arial bold 300', variant: 'normal' }, + `50px "Helvetica 'Neue'", "foo \\"bar\\" baz" , "Someone's weird \\'edge\\' case", sans-serif`, + { size: 50, unit: 'px', family: `Helvetica 'Neue',foo "bar" baz,Someone's weird 'edge' case,sans-serif` } ] for (let i = 0, len = tests.length; i < len; ++i) { From 7dfeb0443a16f1566365b27d60635c5e3920a817 Mon Sep 17 00:00:00 2001 From: musou1500 Date: Wed, 10 Jul 2024 00:13:31 +0900 Subject: [PATCH 425/474] add TextMetrics properties update CHANGELOG fix changelog --- CHANGELOG.md | 1 + index.d.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0505b04e4..349e1c6f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ This release notably changes to using N-API. 🎉 * Fix issue related to improper parsing of leading and trailing whitespaces in CSS color. (#2301) * RGB functions should support real numbers now instead of just integers. (#2339) * Allow alternate or properly escaped quotes *within* font-family names +* Fix TextMetrics type to include alphabeticBaseline, emHeightAscent, and emHeightDescent properties 2.11.2 ================== diff --git a/index.d.ts b/index.d.ts index 73ad4cde9..49636f396 100644 --- a/index.d.ts +++ b/index.d.ts @@ -128,10 +128,13 @@ export class Canvas { } export interface TextMetrics { + readonly alphabeticBaseline: number; readonly actualBoundingBoxAscent: number; readonly actualBoundingBoxDescent: number; readonly actualBoundingBoxLeft: number; readonly actualBoundingBoxRight: number; + readonly emHeightAscent: number; + readonly emHeightDescent: number; readonly fontBoundingBoxAscent: number; readonly fontBoundingBoxDescent: number; readonly width: number; From ba896869eca6efbdd92b1d05d9d2af8f06a7cb4a Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sun, 11 Aug 2024 16:12:18 -0400 Subject: [PATCH 426/474] remove unnecessary call to cairo_get_matrix --- src/CanvasRenderingContext2d.cc | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 56e68d899..9c4f6af9c 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -2766,8 +2766,6 @@ Context2d::MeasureText(const Napi::CallbackInfo& info) { x_offset = 0.0; } - cairo_matrix_t matrix; - cairo_get_matrix(ctx, &matrix); double y_offset = getBaselineAdjustment(layout, state->textBaseline); obj.Set("width", Napi::Number::New(env, logical_rect.width)); From 2db6f49b98aad47a8614aa33535596d1124c8d35 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sun, 22 Sep 2024 15:15:11 -0400 Subject: [PATCH 427/474] add exif browser tests incorrect now, correct after we merge #2296 --- test/fixtures/exif-orientation-f1.jpg | Bin 0 -> 3076 bytes test/fixtures/exif-orientation-f2.jpg | Bin 0 -> 3076 bytes test/fixtures/exif-orientation-f3.jpg | Bin 0 -> 3076 bytes test/fixtures/exif-orientation-f4.jpg | Bin 0 -> 3076 bytes test/fixtures/exif-orientation-f5.jpg | Bin 0 -> 3076 bytes test/fixtures/exif-orientation-f6.jpg | Bin 0 -> 3076 bytes test/fixtures/exif-orientation-f7.jpg | Bin 0 -> 3076 bytes test/fixtures/exif-orientation-f8.jpg | Bin 0 -> 3076 bytes test/fixtures/exif-orientation-fi.jpg | Bin 0 -> 3076 bytes test/fixtures/exif-orientation-fm.jpg | Bin 0 -> 3088 bytes test/fixtures/exif-orientation-fn.jpg | Bin 0 -> 3040 bytes test/public/tests.js | 39 ++++++++++++++++++++++++++ 12 files changed, 39 insertions(+) create mode 100644 test/fixtures/exif-orientation-f1.jpg create mode 100644 test/fixtures/exif-orientation-f2.jpg create mode 100644 test/fixtures/exif-orientation-f3.jpg create mode 100644 test/fixtures/exif-orientation-f4.jpg create mode 100644 test/fixtures/exif-orientation-f5.jpg create mode 100644 test/fixtures/exif-orientation-f6.jpg create mode 100644 test/fixtures/exif-orientation-f7.jpg create mode 100644 test/fixtures/exif-orientation-f8.jpg create mode 100644 test/fixtures/exif-orientation-fi.jpg create mode 100644 test/fixtures/exif-orientation-fm.jpg create mode 100644 test/fixtures/exif-orientation-fn.jpg diff --git a/test/fixtures/exif-orientation-f1.jpg b/test/fixtures/exif-orientation-f1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..64847b1d2ddf5750128985a3d0f8cc937fc6c269 GIT binary patch literal 3076 zcmc&$2~ZPR8h%X@ZiE0H2-pg^;58@#Q9RW zfQteuDk_5%kwaM|AUF&l$`L?tAS50G2tzU;w5HQ z|M!2d;ZrCBHtvETJ^AL-$$?CQDmKS5Myn1r(V8EsVj?Vlr4PCKIh5L+gOaW|_`fv|8J2^A|cc2hE*g z&s^5ET~kmwc}tVRj{ z78RG=y;u66s`}ygH9yqWHMdAw+uEOYbjswFUioUmP^Eaf;P7UvuViL8YE8XNzL; z?yx5PDR$1X*-8zyab_P!Ebc#I_JP>nc|8MbQDJ}IfC%LGA)vQm7hnPd*a-|77!nhM z4Y+`&9l#~67=7@M6Bjn3=ZxnNJZl-PP&^r!s&b(G)wyK>GVAafp%v+V+5L9EaI)g% z;Hj+d`Wx$`LifxRl}Tbn39H)mRo5*K-?TDYh1Z2r9zUtO+0n_Wc8yALMGVZ^Q@&cPQsZg5HlQIY1L$iYr3I)OD{>HwX&;x($LB>p?&ixC3(xHcxcgK@y+n5%EU$M?ye;+RLScF~(%VKH6Y51RgmAO7+Hg2x@x4Z$)x_A?QgG zE7tgsbHoE#5U>`i)617r;p)u%<<^R-icDLHW0{Y9j*Hqx>HIpVFgI85A~@V>z>o2q z8yCO_senE&mfaBZL*FgQkL8h(%aI%#i~z2X@b0*(t$Zu;sb!fcTdxvOBKJFXT%b?Q z>+z+np8#Tb?W!4xJ<=qpvCww=E*`Z)DUFYg49Iz$S~8Ytq+EGI9_3|u({?zZxa@9| zWB!%m%ynYHw~zX4J%?|l)NYzor_ojG1SHLB!oy7mPRa!>c7vz<*kMO6&U2_8s%f{d zjKN1=OW#tZ5LBEHlP>{&;64Od_4$#9=h3?>uemTPc0*Ur^KzELV zv`+wn_q(9CFGg#VmP!L!&ss*X1*)E!2dAwBaXzOYn7#n_S0+~F>k88RqIUQPY}^sN zPx2;X`skayK}ky7l#4Z{$+p{=34l%$%g&1T67LqD?trLPhV4~iny=A#!bWup1ihWy zeGpI_-XsX#Ubw`dEtZWUsb>eOk;WAb(aTVI55%mnufz>$t7y)h`z^F3#Z`D#y*BcY zee%WtX9~8iQC!PVOE$mV{A2gCKsRBQQET-utV2JdY)hS#}gSFy0&Hzi|ot zql7<^*L)nxgl_Xu-!k6H^($SX+2m%GJH=6YRzCSNt=&l5Q45uM$$j)(1usdS9%8vu zv_3l{BtM`ey_^4yw|N*yzWHN(V*;RBAV_3DP-#i@&5#1FVrxF}%PZ7OUm~k3q0bB| z&?3ui41!Mr86%dX2i0j1pmKGX$@0XNLmfFX-f?;owWzSog=eyZ+|8S{eA z>_Xnga3A~9HcHe=wM64M6GM%^*6#;gxg$=_Pg76QP_9NsT#R)0|BN*Ld5AU|R+kZh zIO*_&GHdZrXG0Ja;`yL6r}~-nYuvO9U(p^19;YAh)6kSt#+&ol5)x+WF{8r{ylM8^L~)D{u4@u{ew zMB;qk!{CDDQ-iq$335H#`L{MCX9wzhLsZ!d>mV2j!S6soTjVDaR6`>&Uc(I4JP4W! zg-_7##V@ypU|924;1oHKkG8KS6&C_oIvEMUqb}sowNjB-ZRt6nY59a;nG^yJ`mgw( zL1X>2vBEX3d`9nUnG}%R2i-b~cRPI!r%#QixL3(wPp{lmqwug&{@42g_LNLDWcVzM Xf0PpA#0hZ5B#k;yV~4*b#g4xL5)i2L literal 0 HcmV?d00001 diff --git a/test/fixtures/exif-orientation-f2.jpg b/test/fixtures/exif-orientation-f2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..75064ea1cdff738cde2ae13094ad7ece9ca2b231 GIT binary patch literal 3076 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 X|0pHKi4)+ANg8#a#twf=iXDFgHNU9$ literal 0 HcmV?d00001 diff --git a/test/fixtures/exif-orientation-f5.jpg b/test/fixtures/exif-orientation-f5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ebdcf4db7e12d3564f543278981942af4dbe79ad GIT binary patch literal 3076 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 X|0pHKnH%7ONgH&b)((G5itT>`O^K-a literal 0 HcmV?d00001 diff --git a/test/fixtures/exif-orientation-f7.jpg b/test/fixtures/exif-orientation-f7.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2d91716b7e31165679b3f0904f7679bbbd2aba16 GIT binary patch literal 3076 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 AfdBvi literal 0 HcmV?d00001 diff --git a/test/public/tests.js b/test/public/tests.js index d24202602..c904cde7e 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -2752,3 +2752,42 @@ tests['transformed drawimage'] = function (ctx) { ctx.transform(1.2, 1, 1.8, 1.3, 0, 0) ctx.drawImage(ctx.canvas, 0, 0) } + +// https://github.com/noell/jpg-exif-test-images +for (let n = 1; n <= 8; n++) { + tests[`exif orientation ${n}`] = function (ctx, done) { + const img = new Image() + img.onload = function () { + ctx.drawImage(img, 0, 0) + done() + } + img.src = imageSrc(`exif-orientation-f${n}.jpg`) + } +} + +tests['invalid exif orientation 9'] = function (ctx, done) { + const img = new Image() + img.onload = function () { + ctx.drawImage(img, 0, 0) + done() + } + img.src = imageSrc(`exif-orientation-fi.jpg`) +} + +tests['two exif orientations, value 1 and value 2'] = function (ctx, done) { + const img = new Image() + img.onload = function () { + ctx.drawImage(img, 0, 0) + done() + } + img.src = imageSrc(`exif-orientation-fm.jpg`) +} + +tests['no exif orientation'] = function (ctx, done) { + const img = new Image() + img.onload = function () { + ctx.drawImage(img, 0, 0) + done() + } + img.src = imageSrc(`exif-orientation-fn.jpg`) +} From efcde935516e31fb0c707bddff67f4accfe4fb54 Mon Sep 17 00:00:00 2001 From: Fred Cox Date: Fri, 27 Sep 2024 09:22:49 +0300 Subject: [PATCH 428/474] Fix class properties should have defaults as standard js classes would have Changed PNG consts to static properties --- CHANGELOG.md | 2 + index.d.ts | 14 ++-- index.test-d.ts | 2 +- src/Canvas.cc | 34 ++++----- src/CanvasGradient.cc | 2 +- src/CanvasPattern.cc | 2 +- src/CanvasRenderingContext2d.cc | 126 ++++++++++++++++---------------- src/Image.cc | 16 ++-- src/ImageData.cc | 4 +- test/canvas.test.js | 5 ++ 10 files changed, 107 insertions(+), 100 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 349e1c6f7..0ca6def96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ This release notably changes to using N-API. 🎉 * Remove unused private field `backend` in the `Backend` class. (#2229) * Add Node.js v20 to CI. (#2237) * Replaced `dtslint` with `tsd` (#2313) +* Changed PNG consts to static properties of Canvas class ### Added * Added string tags to support class detection ### Fixed @@ -41,6 +42,7 @@ This release notably changes to using N-API. 🎉 * RGB functions should support real numbers now instead of just integers. (#2339) * Allow alternate or properly escaped quotes *within* font-family names * Fix TextMetrics type to include alphabeticBaseline, emHeightAscent, and emHeightDescent properties +* Fix class properties should have defaults as standard js classes (#2390) 2.11.2 ================== diff --git a/index.d.ts b/index.d.ts index 49636f396..97fe03962 100644 --- a/index.d.ts +++ b/index.d.ts @@ -63,19 +63,19 @@ export class Canvas { readonly stride: number; /** Constant used in PNG encoding methods. */ - readonly PNG_NO_FILTERS: number + static readonly PNG_NO_FILTERS: number /** Constant used in PNG encoding methods. */ - readonly PNG_ALL_FILTERS: number + static readonly PNG_ALL_FILTERS: number /** Constant used in PNG encoding methods. */ - readonly PNG_FILTER_NONE: number + static readonly PNG_FILTER_NONE: number /** Constant used in PNG encoding methods. */ - readonly PNG_FILTER_SUB: number + static readonly PNG_FILTER_SUB: number /** Constant used in PNG encoding methods. */ - readonly PNG_FILTER_UP: number + static readonly PNG_FILTER_UP: number /** Constant used in PNG encoding methods. */ - readonly PNG_FILTER_AVG: number + static readonly PNG_FILTER_AVG: number /** Constant used in PNG encoding methods. */ - readonly PNG_FILTER_PAETH: number + static readonly PNG_FILTER_PAETH: number constructor(width: number, height: number, type?: 'image'|'pdf'|'svg') diff --git a/index.test-d.ts b/index.test-d.ts index 86e8dfc28..f898f2d58 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -33,7 +33,7 @@ expectType(dm.a) expectType(canv.toBuffer()) expectType(canv.toBuffer('application/pdf')) -canv.toBuffer((err, data) => {}, 'image/png') +canv.toBuffer((err, data) => {}, 'image/png', {filters: Canvas.Canvas.PNG_ALL_FILTERS}) expectAssignable(canv.createJPEGStream({ quality: 0.5 })) expectAssignable(canv.createPDFStream({ author: 'octocat' })) canv.toDataURL() diff --git a/src/Canvas.cc b/src/Canvas.cc index 2555605f9..ee79915be 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -50,25 +50,25 @@ Canvas::Initialize(Napi::Env& env, Napi::Object& exports) { // Constructor Napi::Function ctor = DefineClass(env, "Canvas", { - InstanceMethod<&Canvas::ToBuffer>("toBuffer"), - InstanceMethod<&Canvas::StreamPNGSync>("streamPNGSync"), - InstanceMethod<&Canvas::StreamPDFSync>("streamPDFSync"), + InstanceMethod<&Canvas::ToBuffer>("toBuffer", napi_default_method), + InstanceMethod<&Canvas::StreamPNGSync>("streamPNGSync", napi_default_method), + InstanceMethod<&Canvas::StreamPDFSync>("streamPDFSync", napi_default_method), #ifdef HAVE_JPEG - InstanceMethod<&Canvas::StreamJPEGSync>("streamJPEGSync"), + InstanceMethod<&Canvas::StreamJPEGSync>("streamJPEGSync", napi_default_method), #endif - InstanceAccessor<&Canvas::GetType>("type"), - InstanceAccessor<&Canvas::GetStride>("stride"), - InstanceAccessor<&Canvas::GetWidth, &Canvas::SetWidth>("width"), - InstanceAccessor<&Canvas::GetHeight, &Canvas::SetHeight>("height"), - InstanceValue("PNG_NO_FILTERS", Napi::Number::New(env, PNG_NO_FILTERS)), - InstanceValue("PNG_FILTER_NONE", Napi::Number::New(env, PNG_FILTER_NONE)), - InstanceValue("PNG_FILTER_SUB", Napi::Number::New(env, PNG_FILTER_SUB)), - InstanceValue("PNG_FILTER_UP", Napi::Number::New(env, PNG_FILTER_UP)), - InstanceValue("PNG_FILTER_AVG", Napi::Number::New(env, PNG_FILTER_AVG)), - InstanceValue("PNG_FILTER_PAETH", Napi::Number::New(env, PNG_FILTER_PAETH)), - InstanceValue("PNG_ALL_FILTERS", Napi::Number::New(env, PNG_ALL_FILTERS)), - StaticMethod<&Canvas::RegisterFont>("_registerFont"), - StaticMethod<&Canvas::DeregisterAllFonts>("_deregisterAllFonts") + InstanceAccessor<&Canvas::GetType>("type", napi_default_jsproperty), + InstanceAccessor<&Canvas::GetStride>("stride", napi_default_jsproperty), + InstanceAccessor<&Canvas::GetWidth, &Canvas::SetWidth>("width", napi_default_jsproperty), + InstanceAccessor<&Canvas::GetHeight, &Canvas::SetHeight>("height", napi_default_jsproperty), + StaticValue("PNG_NO_FILTERS", Napi::Number::New(env, PNG_NO_FILTERS), napi_default_jsproperty), + StaticValue("PNG_FILTER_NONE", Napi::Number::New(env, PNG_FILTER_NONE), napi_default_jsproperty), + StaticValue("PNG_FILTER_SUB", Napi::Number::New(env, PNG_FILTER_SUB), napi_default_jsproperty), + StaticValue("PNG_FILTER_UP", Napi::Number::New(env, PNG_FILTER_UP), napi_default_jsproperty), + StaticValue("PNG_FILTER_AVG", Napi::Number::New(env, PNG_FILTER_AVG), napi_default_jsproperty), + StaticValue("PNG_FILTER_PAETH", Napi::Number::New(env, PNG_FILTER_PAETH), napi_default_jsproperty), + StaticValue("PNG_ALL_FILTERS", Napi::Number::New(env, PNG_ALL_FILTERS), napi_default_jsproperty), + StaticMethod<&Canvas::RegisterFont>("_registerFont", napi_default_method), + StaticMethod<&Canvas::DeregisterAllFonts>("_deregisterAllFonts", napi_default_method) }); data->CanvasCtor = Napi::Persistent(ctor); diff --git a/src/CanvasGradient.cc b/src/CanvasGradient.cc index 9c2d42360..ceb0e5054 100644 --- a/src/CanvasGradient.cc +++ b/src/CanvasGradient.cc @@ -18,7 +18,7 @@ Gradient::Initialize(Napi::Env& env, Napi::Object& exports) { InstanceData* data = env.GetInstanceData(); Napi::Function ctor = DefineClass(env, "CanvasGradient", { - InstanceMethod<&Gradient::AddColorStop>("addColorStop") + InstanceMethod<&Gradient::AddColorStop>("addColorStop", napi_default_method) }); exports.Set("CanvasGradient", ctor); diff --git a/src/CanvasPattern.cc b/src/CanvasPattern.cc index 55b8bb7fb..ec30b6f09 100644 --- a/src/CanvasPattern.cc +++ b/src/CanvasPattern.cc @@ -21,7 +21,7 @@ Pattern::Initialize(Napi::Env& env, Napi::Object& exports) { // Constructor Napi::Function ctor = DefineClass(env, "CanvasPattern", { - InstanceMethod<&Pattern::setTransform>("setTransform") + InstanceMethod<&Pattern::setTransform>("setTransform", napi_default_method) }); // Prototype diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 9c4f6af9c..d0966e299 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -94,69 +94,69 @@ Context2d::Initialize(Napi::Env& env, Napi::Object& exports) { InstanceData* data = env.GetInstanceData(); Napi::Function ctor = DefineClass(env, "CanvasRenderingContext2D", { - InstanceMethod<&Context2d::DrawImage>("drawImage"), - InstanceMethod<&Context2d::PutImageData>("putImageData"), - InstanceMethod<&Context2d::GetImageData>("getImageData"), - InstanceMethod<&Context2d::CreateImageData>("createImageData"), - InstanceMethod<&Context2d::AddPage>("addPage"), - InstanceMethod<&Context2d::Save>("save"), - InstanceMethod<&Context2d::Restore>("restore"), - InstanceMethod<&Context2d::Rotate>("rotate"), - InstanceMethod<&Context2d::Translate>("translate"), - InstanceMethod<&Context2d::Transform>("transform"), - InstanceMethod<&Context2d::GetTransform>("getTransform"), - InstanceMethod<&Context2d::ResetTransform>("resetTransform"), - InstanceMethod<&Context2d::SetTransform>("setTransform"), - InstanceMethod<&Context2d::IsPointInPath>("isPointInPath"), - InstanceMethod<&Context2d::Scale>("scale"), - InstanceMethod<&Context2d::Clip>("clip"), - InstanceMethod<&Context2d::Fill>("fill"), - InstanceMethod<&Context2d::Stroke>("stroke"), - InstanceMethod<&Context2d::FillText>("fillText"), - InstanceMethod<&Context2d::StrokeText>("strokeText"), - InstanceMethod<&Context2d::FillRect>("fillRect"), - InstanceMethod<&Context2d::StrokeRect>("strokeRect"), - InstanceMethod<&Context2d::ClearRect>("clearRect"), - InstanceMethod<&Context2d::Rect>("rect"), - InstanceMethod<&Context2d::RoundRect>("roundRect"), - InstanceMethod<&Context2d::MeasureText>("measureText"), - InstanceMethod<&Context2d::MoveTo>("moveTo"), - InstanceMethod<&Context2d::LineTo>("lineTo"), - InstanceMethod<&Context2d::BezierCurveTo>("bezierCurveTo"), - InstanceMethod<&Context2d::QuadraticCurveTo>("quadraticCurveTo"), - InstanceMethod<&Context2d::BeginPath>("beginPath"), - InstanceMethod<&Context2d::ClosePath>("closePath"), - InstanceMethod<&Context2d::Arc>("arc"), - InstanceMethod<&Context2d::ArcTo>("arcTo"), - InstanceMethod<&Context2d::Ellipse>("ellipse"), - InstanceMethod<&Context2d::SetLineDash>("setLineDash"), - InstanceMethod<&Context2d::GetLineDash>("getLineDash"), - InstanceMethod<&Context2d::CreatePattern>("createPattern"), - InstanceMethod<&Context2d::CreateLinearGradient>("createLinearGradient"), - InstanceMethod<&Context2d::CreateRadialGradient>("createRadialGradient"), - InstanceAccessor<&Context2d::GetFormat>("pixelFormat"), - InstanceAccessor<&Context2d::GetPatternQuality, &Context2d::SetPatternQuality>("patternQuality"), - InstanceAccessor<&Context2d::GetImageSmoothingEnabled, &Context2d::SetImageSmoothingEnabled>("imageSmoothingEnabled"), - InstanceAccessor<&Context2d::GetGlobalCompositeOperation, &Context2d::SetGlobalCompositeOperation>("globalCompositeOperation"), - InstanceAccessor<&Context2d::GetGlobalAlpha, &Context2d::SetGlobalAlpha>("globalAlpha"), - InstanceAccessor<&Context2d::GetShadowColor, &Context2d::SetShadowColor>("shadowColor"), - InstanceAccessor<&Context2d::GetMiterLimit, &Context2d::SetMiterLimit>("miterLimit"), - InstanceAccessor<&Context2d::GetLineWidth, &Context2d::SetLineWidth>("lineWidth"), - InstanceAccessor<&Context2d::GetLineCap, &Context2d::SetLineCap>("lineCap"), - InstanceAccessor<&Context2d::GetLineJoin, &Context2d::SetLineJoin>("lineJoin"), - InstanceAccessor<&Context2d::GetLineDashOffset, &Context2d::SetLineDashOffset>("lineDashOffset"), - InstanceAccessor<&Context2d::GetShadowOffsetX, &Context2d::SetShadowOffsetX>("shadowOffsetX"), - InstanceAccessor<&Context2d::GetShadowOffsetY, &Context2d::SetShadowOffsetY>("shadowOffsetY"), - InstanceAccessor<&Context2d::GetShadowBlur, &Context2d::SetShadowBlur>("shadowBlur"), - InstanceAccessor<&Context2d::GetAntiAlias, &Context2d::SetAntiAlias>("antialias"), - InstanceAccessor<&Context2d::GetTextDrawingMode, &Context2d::SetTextDrawingMode>("textDrawingMode"), - InstanceAccessor<&Context2d::GetQuality, &Context2d::SetQuality>("quality"), - InstanceAccessor<&Context2d::GetCurrentTransform, &Context2d::SetCurrentTransform>("currentTransform"), - InstanceAccessor<&Context2d::GetFillStyle, &Context2d::SetFillStyle>("fillStyle"), - InstanceAccessor<&Context2d::GetStrokeStyle, &Context2d::SetStrokeStyle>("strokeStyle"), - InstanceAccessor<&Context2d::GetFont, &Context2d::SetFont>("font"), - InstanceAccessor<&Context2d::GetTextBaseline, &Context2d::SetTextBaseline>("textBaseline"), - InstanceAccessor<&Context2d::GetTextAlign, &Context2d::SetTextAlign>("textAlign") + InstanceMethod<&Context2d::DrawImage>("drawImage", napi_default_method), + InstanceMethod<&Context2d::PutImageData>("putImageData", napi_default_method), + InstanceMethod<&Context2d::GetImageData>("getImageData", napi_default_method), + InstanceMethod<&Context2d::CreateImageData>("createImageData", napi_default_method), + InstanceMethod<&Context2d::AddPage>("addPage", napi_default_method), + InstanceMethod<&Context2d::Save>("save", napi_default_method), + InstanceMethod<&Context2d::Restore>("restore", napi_default_method), + InstanceMethod<&Context2d::Rotate>("rotate", napi_default_method), + InstanceMethod<&Context2d::Translate>("translate", napi_default_method), + InstanceMethod<&Context2d::Transform>("transform", napi_default_method), + InstanceMethod<&Context2d::GetTransform>("getTransform", napi_default_method), + InstanceMethod<&Context2d::ResetTransform>("resetTransform", napi_default_method), + InstanceMethod<&Context2d::SetTransform>("setTransform", napi_default_method), + InstanceMethod<&Context2d::IsPointInPath>("isPointInPath", napi_default_method), + InstanceMethod<&Context2d::Scale>("scale", napi_default_method), + InstanceMethod<&Context2d::Clip>("clip", napi_default_method), + InstanceMethod<&Context2d::Fill>("fill", napi_default_method), + InstanceMethod<&Context2d::Stroke>("stroke", napi_default_method), + InstanceMethod<&Context2d::FillText>("fillText", napi_default_method), + InstanceMethod<&Context2d::StrokeText>("strokeText", napi_default_method), + InstanceMethod<&Context2d::FillRect>("fillRect", napi_default_method), + InstanceMethod<&Context2d::StrokeRect>("strokeRect", napi_default_method), + InstanceMethod<&Context2d::ClearRect>("clearRect", napi_default_method), + InstanceMethod<&Context2d::Rect>("rect", napi_default_method), + InstanceMethod<&Context2d::RoundRect>("roundRect", napi_default_method), + InstanceMethod<&Context2d::MeasureText>("measureText", napi_default_method), + InstanceMethod<&Context2d::MoveTo>("moveTo", napi_default_method), + InstanceMethod<&Context2d::LineTo>("lineTo", napi_default_method), + InstanceMethod<&Context2d::BezierCurveTo>("bezierCurveTo", napi_default_method), + InstanceMethod<&Context2d::QuadraticCurveTo>("quadraticCurveTo", napi_default_method), + InstanceMethod<&Context2d::BeginPath>("beginPath", napi_default_method), + InstanceMethod<&Context2d::ClosePath>("closePath", napi_default_method), + InstanceMethod<&Context2d::Arc>("arc", napi_default_method), + InstanceMethod<&Context2d::ArcTo>("arcTo", napi_default_method), + InstanceMethod<&Context2d::Ellipse>("ellipse", napi_default_method), + InstanceMethod<&Context2d::SetLineDash>("setLineDash", napi_default_method), + InstanceMethod<&Context2d::GetLineDash>("getLineDash", napi_default_method), + InstanceMethod<&Context2d::CreatePattern>("createPattern", napi_default_method), + InstanceMethod<&Context2d::CreateLinearGradient>("createLinearGradient", napi_default_method), + InstanceMethod<&Context2d::CreateRadialGradient>("createRadialGradient", napi_default_method), + InstanceAccessor<&Context2d::GetFormat>("pixelFormat", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetPatternQuality, &Context2d::SetPatternQuality>("patternQuality", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetImageSmoothingEnabled, &Context2d::SetImageSmoothingEnabled>("imageSmoothingEnabled", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetGlobalCompositeOperation, &Context2d::SetGlobalCompositeOperation>("globalCompositeOperation", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetGlobalAlpha, &Context2d::SetGlobalAlpha>("globalAlpha", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetShadowColor, &Context2d::SetShadowColor>("shadowColor", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetMiterLimit, &Context2d::SetMiterLimit>("miterLimit", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetLineWidth, &Context2d::SetLineWidth>("lineWidth", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetLineCap, &Context2d::SetLineCap>("lineCap", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetLineJoin, &Context2d::SetLineJoin>("lineJoin", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetLineDashOffset, &Context2d::SetLineDashOffset>("lineDashOffset", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetShadowOffsetX, &Context2d::SetShadowOffsetX>("shadowOffsetX", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetShadowOffsetY, &Context2d::SetShadowOffsetY>("shadowOffsetY", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetShadowBlur, &Context2d::SetShadowBlur>("shadowBlur", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetAntiAlias, &Context2d::SetAntiAlias>("antialias", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetTextDrawingMode, &Context2d::SetTextDrawingMode>("textDrawingMode", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetQuality, &Context2d::SetQuality>("quality", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetCurrentTransform, &Context2d::SetCurrentTransform>("currentTransform", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetFillStyle, &Context2d::SetFillStyle>("fillStyle", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetStrokeStyle, &Context2d::SetStrokeStyle>("strokeStyle", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetFont, &Context2d::SetFont>("font", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetTextBaseline, &Context2d::SetTextBaseline>("textBaseline", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetTextAlign, &Context2d::SetTextAlign>("textAlign", napi_default_jsproperty) }); exports.Set("CanvasRenderingContext2d", ctor); diff --git a/src/Image.cc b/src/Image.cc index 7a4831ae0..fcf4b3add 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -53,14 +53,14 @@ Image::Initialize(Napi::Env& env, Napi::Object& exports) { Napi::HandleScope scope(env); Napi::Function ctor = DefineClass(env, "Image", { - InstanceAccessor<&Image::GetComplete>("complete"), - InstanceAccessor<&Image::GetWidth, &Image::SetWidth>("width"), - InstanceAccessor<&Image::GetHeight, &Image::SetHeight>("height"), - InstanceAccessor<&Image::GetNaturalWidth>("naturalWidth"), - InstanceAccessor<&Image::GetNaturalHeight>("naturalHeight"), - InstanceAccessor<&Image::GetDataMode, &Image::SetDataMode>("dataMode"), - StaticValue("MODE_IMAGE", Napi::Number::New(env, DATA_IMAGE)), - StaticValue("MODE_MIME", Napi::Number::New(env, DATA_MIME)) + InstanceAccessor<&Image::GetComplete>("complete", napi_default_jsproperty), + InstanceAccessor<&Image::GetWidth, &Image::SetWidth>("width", napi_default_jsproperty), + InstanceAccessor<&Image::GetHeight, &Image::SetHeight>("height", napi_default_jsproperty), + InstanceAccessor<&Image::GetNaturalWidth>("naturalWidth", napi_default_jsproperty), + InstanceAccessor<&Image::GetNaturalHeight>("naturalHeight", napi_default_jsproperty), + InstanceAccessor<&Image::GetDataMode, &Image::SetDataMode>("dataMode", napi_default_jsproperty), + StaticValue("MODE_IMAGE", Napi::Number::New(env, DATA_IMAGE), napi_default_jsproperty), + StaticValue("MODE_MIME", Napi::Number::New(env, DATA_MIME), napi_default_jsproperty) }); // Used internally in lib/image.js diff --git a/src/ImageData.cc b/src/ImageData.cc index b9f556bb3..d334ca894 100644 --- a/src/ImageData.cc +++ b/src/ImageData.cc @@ -14,8 +14,8 @@ ImageData::Initialize(Napi::Env& env, Napi::Object& exports) { InstanceData *data = env.GetInstanceData(); Napi::Function ctor = DefineClass(env, "ImageData", { - InstanceAccessor<&ImageData::GetWidth>("width"), - InstanceAccessor<&ImageData::GetHeight>("height") + InstanceAccessor<&ImageData::GetWidth>("width", napi_default_jsproperty), + InstanceAccessor<&ImageData::GetHeight>("height", napi_default_jsproperty) }); exports.Set("ImageData", ctor); diff --git a/test/canvas.test.js b/test/canvas.test.js index d0feff0d1..1a75ac031 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -694,6 +694,11 @@ describe('Canvas', function () { assert.equal('PNG', buf.slice(1, 4).toString()) }) + it('Canvas#toBuffer("image/png", {filters: PNG_ALL_FILTERS})', function () { + const buf = createCanvas(200, 200).toBuffer('image/png', { filters: Canvas.PNG_ALL_FILTERS }) + assert.equal('PNG', buf.slice(1, 4).toString()) + }) + it('Canvas#toBuffer("image/jpeg")', function () { const buf = createCanvas(200, 200).toBuffer('image/jpeg') assert.equal(buf[0], 0xff) From a2e10e61413a0d158174a7a869c16aa13e5d3575 Mon Sep 17 00:00:00 2001 From: Fred Cox Date: Thu, 18 Jul 2024 12:47:28 +0100 Subject: [PATCH 429/474] Throw surface errors in toBuffer --- CHANGELOG.md | 1 + src/Canvas.cc | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ca6def96..e253d1e15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ This release notably changes to using N-API. 🎉 * Changed PNG consts to static properties of Canvas class ### Added * Added string tags to support class detection +* Throw Cairo errors in canvas.toBuffer() ### Fixed * Fix a case of use-after-free. (#2229) * Fix usage of garbage value by filling the allocated memory entirely with zeros if it's not modified. (#2229) diff --git a/src/Canvas.cc b/src/Canvas.cc index ee79915be..6ba312008 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -382,7 +382,15 @@ Canvas::ToBuffer(const Napi::CallbackInfo& info) { closure = static_cast(backend())->closure(); } - cairo_surface_finish(surface()); + cairo_surface_t *surf = surface(); + cairo_surface_finish(surf); + + cairo_status_t status = cairo_surface_status(surf); + if (status != CAIRO_STATUS_SUCCESS) { + Napi::Error::New(env, cairo_status_to_string(status)).ThrowAsJavaScriptException(); + return env.Undefined(); + } + return Napi::Buffer::Copy(env, &closure->vec[0], closure->vec.size()); } From 77f0b99d10dd11a911607aa8abca9e4022adb8c6 Mon Sep 17 00:00:00 2001 From: prewett-toptal <47159039+prewett-toptal@users.noreply.github.com> Date: Wed, 13 Nov 2024 23:07:33 -0500 Subject: [PATCH 430/474] Handle Exif orientations for JPEG images (fixes #1670) (#2296) * Handle Exif orientation for JPEG images * Updated CHANGELOG.md * Changes for PR --------- Co-authored-by: Geoffrey Prewett --- CHANGELOG.md | 1 + src/Image.cc | 348 ++++++++++++++++++++++++++++++++++++++++++++++++++- src/Image.h | 22 +++- 3 files changed, 366 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e253d1e15..240fa2605 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ This release notably changes to using N-API. 🎉 * Allow alternate or properly escaped quotes *within* font-family names * Fix TextMetrics type to include alphabeticBaseline, emHeightAscent, and emHeightDescent properties * Fix class properties should have defaults as standard js classes (#2390) +* Fixed Exif orientation in JPEG files being ignored (#1670) 2.11.2 ================== diff --git a/src/Image.cc b/src/Image.cc index fcf4b3add..970cd2e28 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -766,6 +766,52 @@ static void jpeg_mem_src (j_decompress_ptr cinfo, void* buffer, long nbytes) { #endif +class BufferReader : public Image::Reader { +public: + BufferReader(uint8_t* buf, unsigned len) : _buf(buf), _len(len), _idx(0) {} + + bool hasBytes(unsigned n) const override { return (_idx + n - 1 < _len); } + + uint8_t getNext() override { + if (_idx < _len) { + return _buf[_idx++]; + } + } + + void skipBytes(unsigned n) override { _idx += n; } + +private: + uint8_t* _buf; // we do not own this + unsigned _len; + unsigned _idx; +}; + +class StreamReader : public Image::Reader { +public: + StreamReader(FILE *stream) : _stream(stream), _len(0), _idx(0) { + fseeko(_stream, 0, SEEK_END); + _len = ftello(_stream); + fseeko(_stream, 0, SEEK_SET); + } + + bool hasBytes(unsigned n) const override { return (_idx + n - 1 < _len); } + + uint8_t getNext() override { + ++_idx; + return getc(_stream); + } + + void skipBytes(unsigned n) override { + _idx += n; + fseeko(_stream, _idx, SEEK_SET); + } + +private: + FILE* _stream; + off_t _len; + off_t _idx; +}; + void Image::jpegToARGB(jpeg_decompress_struct* args, uint8_t* data, uint8_t* src, JPEGDecodeL decode) { int stride = naturalWidth * 4; for (int y = 0; y < naturalHeight; ++y) { @@ -784,10 +830,11 @@ void Image::jpegToARGB(jpeg_decompress_struct* args, uint8_t* data, uint8_t* src */ cairo_status_t -Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args) { +Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args, Orientation orientation) { + const int channels = 4; cairo_status_t status = CAIRO_STATUS_SUCCESS; - uint8_t *data = new uint8_t[naturalWidth * naturalHeight * 4]; + uint8_t *data = new uint8_t[naturalWidth * naturalHeight * channels]; if (!data) { jpeg_abort_decompress(args); jpeg_destroy_decompress(args); @@ -834,6 +881,8 @@ Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args) { break; } + updateDimensionsForOrientation(orientation); + if (!status) { _surface = cairo_image_surface_create_for_data( data @@ -847,6 +896,8 @@ Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args) { jpeg_destroy_decompress(args); status = cairo_surface_status(_surface); + rotatePixels(data, naturalWidth, naturalHeight, channels, orientation); + delete[] src; if (status) { @@ -922,6 +973,10 @@ Image::decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len) { return CAIRO_STATUS_NO_MEMORY; } + BufferReader reader(buf, len); + Orientation orientation = getExifOrientation(reader); + updateDimensionsForOrientation(orientation); + // New image surface _surface = cairo_image_surface_create_for_data( data @@ -940,6 +995,8 @@ Image::decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len) { return status; } + rotatePixels(data, naturalWidth, naturalHeight, 1, orientation); + _data = data; return assignDataAsMime(buf, len, CAIRO_MIME_TYPE_JPEG); @@ -1001,6 +1058,9 @@ Image::assignDataAsMime(uint8_t *data, int len, const char *mime_type) { cairo_status_t Image::loadJPEGFromBuffer(uint8_t *buf, unsigned len) { + BufferReader reader(buf, len); + Orientation orientation = getExifOrientation(reader); + // TODO: remove this duplicate logic // JPEG setup struct jpeg_decompress_struct args; @@ -1028,7 +1088,7 @@ Image::loadJPEGFromBuffer(uint8_t *buf, unsigned len) { width = naturalWidth = args.output_width; height = naturalHeight = args.output_height; - return decodeJPEGIntoSurface(&args); + return decodeJPEGIntoSurface(&args, orientation); } /* @@ -1044,6 +1104,13 @@ Image::loadJPEG(FILE *stream) { #else if (data_mode == DATA_IMAGE) { // Can lazily read in the JPEG. #endif + Orientation orientation = NORMAL; + { + StreamReader reader(stream); + orientation = getExifOrientation(reader); + rewind(stream); + } + // JPEG setup struct jpeg_decompress_struct args; struct canvas_jpeg_error_mgr err; @@ -1076,7 +1143,7 @@ Image::loadJPEG(FILE *stream) { width = naturalWidth = args.output_width; height = naturalHeight = args.output_height; - status = decodeJPEGIntoSurface(&args); + status = decodeJPEGIntoSurface(&args, orientation); fclose(stream); } else { // We'll need the actual source jpeg data, so read fully. uint8_t *buf; @@ -1116,6 +1183,279 @@ Image::loadJPEG(FILE *stream) { return status; } +/* + * Returns the Exif orientation if one exists, otherwise returns NORMAL + */ + +Image::Orientation +Image::getExifOrientation(Reader& jpeg) { + static const char kJpegStartOfImage = (char)0xd8; + static const char kJpegStartOfFrameBaseline = (char)0xc0; + static const char kJpegStartOfFrameProgressive = (char)0xc2; + static const char kJpegHuffmanTable = (char)0xc4; + static const char kJpegQuantizationTable = (char)0xdb; + static const char kJpegRestartInterval = (char)0xdd; + static const char kJpegComment = (char)0xfe; + static const char kJpegStartOfScan = (char)0xda; + static const char kJpegApp0 = (char)0xe0; + static const char kJpegApp1 = (char)0xe1; + + // Find the Exif tag (if it exists) + int exif_len = 0; + bool done = false; + while (!done && jpeg.hasBytes(1)) { + while (jpeg.hasBytes(1) && jpeg.getNext() != 0xff) { + // noop + } + if (jpeg.hasBytes(1)) { + char tag = jpeg.getNext(); + switch (tag) { + case kJpegStartOfImage: + break; // beginning of file, no extra bytes + case kJpegRestartInterval: + jpeg.skipBytes(4); + break; + case kJpegStartOfFrameBaseline: + case kJpegStartOfFrameProgressive: + case kJpegHuffmanTable: + case kJpegQuantizationTable: + case kJpegComment: + case kJpegApp0: + case kJpegApp1: { + if (jpeg.hasBytes(2)) { + uint16_t tag_len = 0; + tag_len |= jpeg.getNext() << 8; + tag_len |= jpeg.getNext(); + // The tag length includes the two bytes for the length + uint16_t tag_content_len = std::max(0, tag_len - 2); + if (tag != kJpegApp1 || !jpeg.hasBytes(tag_content_len)) { + jpeg.skipBytes(tag_content_len); // skip JPEG tags we ignore. + } else if (!jpeg.hasBytes(6)) { + jpeg.skipBytes(tag_content_len); // too short to have "Exif\0\0" + } else { + if (jpeg.getNext() == 'E' && jpeg.getNext() == 'x' && + jpeg.getNext() == 'i' && jpeg.getNext() == 'f' && + jpeg.getNext() == '\0' && jpeg.getNext() == '\0') { + exif_len = tag_content_len - 6; + done = true; + } else { + jpeg.skipBytes(tag_content_len); // too short to have "Exif\0\0" + } + } + } else { + done = true; // shouldn't happen: corrupt file or we have a bug + } + break; + } + case kJpegStartOfScan: + default: + done = true; // got to the image, apparently no exif tags here + break; + } + } + } + + // Parse exif if it exists. If it does, we have already checked that jpeglen + // is longer than exifStart + exifLen, so we can safely index the data + if (exif_len > 0) { + // The first two bytes of TIFF header are "II" if little-endian ("Intel") + // and "MM" if big-endian ("Motorola") + const bool isLE = (jpeg.getNext() == 'I'); + jpeg.skipBytes(3); // +1 for the other I/M, +2 for 0x002a + + auto readUint16Little = [](Reader &jpeg) -> uint32_t { + uint16_t val = uint16_t(jpeg.getNext()); + val |= uint16_t(jpeg.getNext()) << 8; + return val; + }; + auto readUint32Little = [](Reader &jpeg) -> uint32_t { + uint32_t val = uint32_t(jpeg.getNext()); + val |= uint32_t(jpeg.getNext()) << 8; + val |= uint32_t(jpeg.getNext()) << 16; + val |= uint32_t(jpeg.getNext()) << 24; + return val; + }; + auto readUint16Big = [](Reader &jpeg) -> uint32_t { + uint16_t val = uint16_t(jpeg.getNext()) << 8; + val |= uint16_t(jpeg.getNext()); + return val; + }; + auto readUint32Big = [](Reader &jpeg) -> uint32_t { + uint32_t val = uint32_t(jpeg.getNext()) << 24; + val |= uint32_t(jpeg.getNext()) << 16; + val |= uint32_t(jpeg.getNext()) << 8; + val |= uint32_t(jpeg.getNext()); + return val; + }; + // The first two bytes of TIFF header are "II" if little-endian ("Intel") + // and "MM" if big-endian ("Motorola") + auto readUint32 = (isLE ? readUint32Little : readUint32Big); + auto readUint16 = (isLE ? readUint16Little : readUint16Big); + // offset to the IFD0 (offset from beginning of TIFF header, II/MM, + // which is 8 bytes before where we are after reading the uint32) + jpeg.skipBytes(readUint32(jpeg) - 8); + + // Read the IFD0 ("Image File Directory 0") + // | NN | n entries in directory (2 bytes) + // | TT | tt | nnnn | vvvv | entry: tag (2b), data type (2b), + // n components (4b), value/offset (4b) + if (jpeg.hasBytes(2)) { + uint16_t nEntries = readUint16(jpeg); + for (uint16_t i = 0; i < nEntries && jpeg.hasBytes(2); ++i) { + uint16_t tag = readUint16(jpeg); + // The entry is 12 bytes. We already read the 2 bytes for the tag. + jpeg.skipBytes(6); // skip 2 for the data type, skip 4 n components. + if (tag == 0x112) { + switch (readUint16(jpeg)) { // orientation tag is always one uint16 + case 1: return NORMAL; + case 2: return MIRROR_HORIZ; + case 3: return ROTATE_180; + case 4: return MIRROR_VERT; + case 5: return MIRROR_HORIZ_AND_ROTATE_270_CW; + case 6: return ROTATE_90_CW; + case 7: return MIRROR_HORIZ_AND_ROTATE_90_CW; + case 8: return ROTATE_270_CW; + default: return NORMAL; + } + } else { + jpeg.skipBytes(4); // skip the four bytes for the value + } + } + } + } + + return NORMAL; +} + +/* + * Updates the dimensions of the bitmap according to the orientation + */ + +void Image::updateDimensionsForOrientation(Orientation orientation) { + switch (orientation) { + case ROTATE_90_CW: + case ROTATE_270_CW: + case MIRROR_HORIZ_AND_ROTATE_90_CW: + case MIRROR_HORIZ_AND_ROTATE_270_CW: { + int tmp = naturalWidth; + naturalWidth = naturalHeight; + naturalHeight = tmp; + tmp = width; + width = height; + height = tmp; + break; + } + case NORMAL: + case MIRROR_HORIZ: + case MIRROR_VERT: + case ROTATE_180: + default: { + break; + } + } +} + +/* + * Rotates the pixels to the correct orientation. + */ + +void +Image::rotatePixels(uint8_t* pixels, int width, int height, int channels, + Orientation orientation) { + auto swapPixel = [channels](uint8_t* pixels, int src_idx, int dst_idx) { + uint8_t tmp; + for (int i = 0; i < channels; ++i) { + tmp = pixels[src_idx + i]; + pixels[src_idx + i] = pixels[dst_idx + i]; + pixels[dst_idx + i] = tmp; + } + }; + + auto mirrorHoriz = [swapPixel](uint8_t* pixels, int width, int height, int channels) { + int midX = width / 2; // ok to truncate if odd, since we don't swap a center pixel + for (int y = 0; y < height; ++y) { + for (int x = 0; x < midX; ++x) { + int orig_idx = (y * width + x) * channels; + int new_idx = (y * width + width - 1 - x) * channels; + swapPixel(pixels, orig_idx, new_idx); + } + } + }; + + auto mirrorVert = [swapPixel](uint8_t* pixels, int width, int height, int channels) { + int midY = height / 2; // ok to truncate if odd, since we don't swap a center pixel + for (int y = 0; y < midY; ++y) { + for (int x = 0; x < width; ++x) { + int orig_idx = (y * width + x) * channels; + int new_idx = ((height - y - 1) * width + x) * channels; + swapPixel(pixels, orig_idx, new_idx); + } + } + }; + + auto rotate90 = [](uint8_t* pixels, int width, int height, int channels) { + const int n_bytes = width * height * channels; + uint8_t *unrotated = new uint8_t[n_bytes]; + if (!unrotated) { + return; + } + std::memcpy(unrotated, pixels, n_bytes); + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width ; ++x) { + int orig_idx = (y * width + x) * channels; + int new_idx = (x * height + height - 1 - y) * channels; + std::memcpy(pixels + new_idx, unrotated + orig_idx, channels); + } + } + }; + + auto rotate270 = [](uint8_t* pixels, int width, int height, int channels) { + const int n_bytes = width * height * channels; + uint8_t *unrotated = new uint8_t[n_bytes]; + if (!unrotated) { + return; + } + std::memcpy(unrotated, pixels, n_bytes); + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width ; ++x) { + int orig_idx = (y * width + x) * channels; + int new_idx = ((width - 1 - x) * height + y) * channels; + std::memcpy(pixels + new_idx, unrotated + orig_idx, channels); + } + } + }; + + switch (orientation) { + case MIRROR_HORIZ: + mirrorHoriz(pixels, width, height, channels); + break; + case MIRROR_VERT: + mirrorVert(pixels, width, height, channels); + break; + case ROTATE_180: + mirrorHoriz(pixels, width, height, channels); + mirrorVert(pixels, width, height, channels); + break; + case ROTATE_90_CW: + rotate90(pixels, height, width, channels); // swap w/h because we need orig w/h + break; + case ROTATE_270_CW: + rotate270(pixels, height, width, channels); // swap w/h because we need orig w/h + break; + case MIRROR_HORIZ_AND_ROTATE_90_CW: + mirrorHoriz(pixels, height, width, channels); // swap w/h because we need orig w/h + rotate90(pixels, height, width, channels); + break; + case MIRROR_HORIZ_AND_ROTATE_270_CW: + mirrorHoriz(pixels, height, width, channels); // swap w/h because we need orig w/h + rotate270(pixels, height, width, channels); + break; + case NORMAL: + default: + break; + } +} + #endif /* HAVE_JPEG */ #ifdef HAVE_RSVG diff --git a/src/Image.h b/src/Image.h index 6b9b9593b..6ead16fdb 100644 --- a/src/Image.h +++ b/src/Image.h @@ -78,12 +78,32 @@ class Image : public Napi::ObjectWrap { cairo_status_t loadGIF(FILE *stream); #endif #ifdef HAVE_JPEG + enum Orientation { + NORMAL, + MIRROR_HORIZ, + MIRROR_VERT, + ROTATE_180, + ROTATE_90_CW, + ROTATE_270_CW, + MIRROR_HORIZ_AND_ROTATE_90_CW, + MIRROR_HORIZ_AND_ROTATE_270_CW + }; cairo_status_t loadJPEGFromBuffer(uint8_t *buf, unsigned len); cairo_status_t loadJPEG(FILE *stream); void jpegToARGB(jpeg_decompress_struct* args, uint8_t* data, uint8_t* src, JPEGDecodeL decode); - cairo_status_t decodeJPEGIntoSurface(jpeg_decompress_struct *info); + cairo_status_t decodeJPEGIntoSurface(jpeg_decompress_struct *info, Orientation orientation); cairo_status_t decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len); cairo_status_t assignDataAsMime(uint8_t *data, int len, const char *mime_type); + + class Reader { + public: + virtual bool hasBytes(unsigned n) const = 0; + virtual uint8_t getNext() = 0; + virtual void skipBytes(unsigned n) = 0; + }; + Orientation getExifOrientation(Reader& jpeg); + void updateDimensionsForOrientation(Orientation orientation); + void rotatePixels(uint8_t* pixels, int width, int height, int channels, Orientation orientation); #endif cairo_status_t loadBMPFromBuffer(uint8_t *buf, unsigned len); cairo_status_t loadBMP(FILE *stream); From 8983c4ac2a4cf59c4091c3492db9d251e28ea93a Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Thu, 5 Dec 2024 08:38:40 -0500 Subject: [PATCH 431/474] fix windows build (#2458) broke in #2296 because we don't run tests on PRs for some reason - getNext didn't return in all paths. Check wasn't necessary. - Windows doesn't have fseeko or ftello. I highly doubt we'll ever need 64 bits to address images. - Lambda functions get their own anonymous types and C++ standards don't require them to be interchangeable. --- src/Image.cc | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/Image.cc b/src/Image.cc index 970cd2e28..559f8a36c 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -773,9 +773,7 @@ class BufferReader : public Image::Reader { bool hasBytes(unsigned n) const override { return (_idx + n - 1 < _len); } uint8_t getNext() override { - if (_idx < _len) { - return _buf[_idx++]; - } + return _buf[_idx++]; } void skipBytes(unsigned n) override { _idx += n; } @@ -789,9 +787,9 @@ class BufferReader : public Image::Reader { class StreamReader : public Image::Reader { public: StreamReader(FILE *stream) : _stream(stream), _len(0), _idx(0) { - fseeko(_stream, 0, SEEK_END); - _len = ftello(_stream); - fseeko(_stream, 0, SEEK_SET); + fseek(_stream, 0, SEEK_END); + _len = ftell(_stream); + fseek(_stream, 0, SEEK_SET); } bool hasBytes(unsigned n) const override { return (_idx + n - 1 < _len); } @@ -803,13 +801,13 @@ class StreamReader : public Image::Reader { void skipBytes(unsigned n) override { _idx += n; - fseeko(_stream, _idx, SEEK_SET); + fseek(_stream, _idx, SEEK_SET); } private: FILE* _stream; - off_t _len; - off_t _idx; + unsigned _len; + unsigned _idx; }; void Image::jpegToARGB(jpeg_decompress_struct* args, uint8_t* data, uint8_t* src, JPEGDecodeL decode) { @@ -1289,8 +1287,12 @@ Image::getExifOrientation(Reader& jpeg) { }; // The first two bytes of TIFF header are "II" if little-endian ("Intel") // and "MM" if big-endian ("Motorola") - auto readUint32 = (isLE ? readUint32Little : readUint32Big); - auto readUint16 = (isLE ? readUint16Little : readUint16Big); + auto readUint32 = [readUint32Little, readUint32Big, isLE](Reader &jpeg) -> uint32_t { + return isLE ? readUint32Little(jpeg) : readUint32Big(jpeg); + }; + auto readUint16 = [readUint16Little, readUint16Big, isLE](Reader &jpeg) -> uint32_t { + return isLE ? readUint16Little(jpeg) : readUint16Big(jpeg); + }; // offset to the IFD0 (offset from beginning of TIFF header, II/MM, // which is 8 bytes before where we are after reading the uint32) jpeg.skipBytes(readUint32(jpeg) - 8); From d1ea3f82003773fd06a27644faf1b7b86113e505 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Thu, 5 Dec 2024 19:47:07 -0500 Subject: [PATCH 432/474] fix windows ci config (url went dead) --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cb6010cda..5607331c0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -45,7 +45,7 @@ jobs: - uses: actions/checkout@v4 - name: Install Dependencies run: | - Invoke-WebRequest "https://ftp-osl.osuosl.org/pub/gnome/binaries/win64/gtk+/2.22/gtk+-bundle_2.22.1-20101229_win64.zip" -OutFile "gtk.zip" + Invoke-WebRequest "https://ftp.gnome.org/pub/GNOME/binaries/win64/gtk+/2.22/gtk+-bundle_2.22.1-20101229_win64.zip" -OutFile "gtk.zip" Expand-Archive gtk.zip -DestinationPath "C:\GTK" Invoke-WebRequest "https://downloads.sourceforge.net/project/libjpeg-turbo/2.0.4/libjpeg-turbo-2.0.4-vc64.exe" -OutFile "libjpeg.exe" -UserAgent NativeHost .\libjpeg.exe /S From 22820e1dacc420524af462bde14f0bf88be8c7c4 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Thu, 5 Dec 2024 19:50:41 -0500 Subject: [PATCH 433/474] fix macos ci config (macos-12 was deleted) --- .github/workflows/ci.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5607331c0..333288a3c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -58,7 +58,7 @@ jobs: macOS: name: Test on macOS - runs-on: macos-12 + runs-on: macos-15 strategy: matrix: node: [18.12.0, 20.9.0] @@ -70,9 +70,7 @@ jobs: - name: Install Dependencies run: | brew update - brew install python3 || : # python doesn't need to be linked - brew install pkg-config cairo pango libpng jpeg giflib librsvg - pip install setuptools + brew install python-setuptools pkg-config cairo pango libpng jpeg giflib librsvg - name: Install run: npm install --build-from-source - name: Test From 19a33287c4c571264f1d062e92d311bafb1685aa Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sat, 7 Dec 2024 10:53:40 -0500 Subject: [PATCH 434/474] v3.0.0-rc3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 371b0767b..ae5df9f42 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "3.0.0-rc2", + "version": "3.0.0-rc3", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From d0278832f70e0b0a06e1e3441596d402b75a7fe8 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Tue, 10 Dec 2024 20:47:04 -0500 Subject: [PATCH 435/474] update README wrt macOS/aarch64 --- Readme.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 7e7b88ba1..d7a7c65cc 100644 --- a/Readme.md +++ b/Readme.md @@ -19,7 +19,8 @@ $ npm install canvas ``` By default, pre-built binaries will be downloaded if you're on one of the following platforms: -- macOS x86/64 (*not* Apple silicon) +- macOS x86/64 +- macOS aarch64 (aka Apple silicon) - Linux x86/64 (glibc only) - Windows x86/64 From b6b2dc760bfe983ebf0ac8e19d09b90ab03c3ed2 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Thu, 5 Dec 2024 20:41:46 -0500 Subject: [PATCH 436/474] modernize node version matrix These are the currently supported versions. Luckily the Windows file path issue was backported to node 20. --- .github/workflows/ci.yaml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 333288a3c..83ddb105b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [18.12.0, 20.9.0] + node: [18.20.5, 20.18.1, 22.12.0, 23.3.0] steps: - uses: actions/setup-node@v4 with: @@ -33,11 +33,7 @@ jobs: runs-on: windows-2019 strategy: matrix: - # FIXME: Node.js 20.9.0 is currently broken on Windows, in the `registerFont` test: - # ENOENT: no such file or directory, lstat 'D:\a\node-canvas\node-canvas\examples\pfennigFont\pfennigMultiByte🚀.ttf' - # ref: https://github.com/nodejs/node/issues/48673 - # ref: https://github.com/nodejs/node/pull/50650 - node: [18.12.0] + node: [18.20.5, 20.18.1, 22.12.0, 23.3.0] steps: - uses: actions/setup-node@v4 with: @@ -61,7 +57,7 @@ jobs: runs-on: macos-15 strategy: matrix: - node: [18.12.0, 20.9.0] + node: [18.20.5, 20.18.1, 22.12.0, 23.3.0] steps: - uses: actions/setup-node@v4 with: From f5894dbe8e2da7cb7d0eb763a7ca55bb0e0274fc Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sat, 31 Aug 2024 09:56:12 +0300 Subject: [PATCH 437/474] fix(DOMMatrix/DOMPoint): spec compatibility --- CHANGELOG.md | 1 + index.d.ts | 10 ++++++- lib/DOMMatrix.js | 56 ++++++++++++++++++++++++++++++++++++ test/dommatrix.test.js | 64 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 130 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 240fa2605..68e0b2614 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ This release notably changes to using N-API. 🎉 * Fix TextMetrics type to include alphabeticBaseline, emHeightAscent, and emHeightDescent properties * Fix class properties should have defaults as standard js classes (#2390) * Fixed Exif orientation in JPEG files being ignored (#1670) +* Align DOMMatrix/DOMPoint to spec by adding missing methods 2.11.2 ================== diff --git a/index.d.ts b/index.d.ts index 97fe03962..52db720d2 100644 --- a/index.d.ts +++ b/index.d.ts @@ -400,10 +400,13 @@ export class DOMPoint { x: number; y: number; z: number; + matrixTransform(matrix?: DOMMatrixInit): DOMPoint; + toJSON(): any; + static fromPoint(other?: DOMPointInit): DOMPoint; } export class DOMMatrix { - constructor(init: string | number[]); + constructor(init?: string | number[]); toString(): string; multiply(other?: DOMMatrix): DOMMatrix; multiplySelf(other?: DOMMatrix): DOMMatrix; @@ -414,6 +417,10 @@ export class DOMMatrix { scale3d(scale?: number, originX?: number, originY?: number, originZ?: number): DOMMatrix; scale3dSelf(scale?: number, originX?: number, originY?: number, originZ?: number): DOMMatrix; scaleSelf(scaleX?: number, scaleY?: number, scaleZ?: number, originX?: number, originY?: number, originZ?: number): DOMMatrix; + /** + * @deprecated + */ + scaleNonUniform(scaleX?: number, scaleY?: number): DOMMatrix; rotateFromVector(x?: number, y?: number): DOMMatrix; rotateFromVectorSelf(x?: number, y?: number): DOMMatrix; rotate(rotX?: number, rotY?: number, rotZ?: number): DOMMatrix; @@ -430,6 +437,7 @@ export class DOMMatrix { invertSelf(): DOMMatrix; setMatrixValue(transformList: string): DOMMatrix; transformPoint(point?: DOMPoint): DOMPoint; + toJSON(): any; toFloat32Array(): Float32Array; toFloat64Array(): Float64Array; readonly is2D: boolean; diff --git a/lib/DOMMatrix.js b/lib/DOMMatrix.js index 479e7e6e5..20a41c0c8 100644 --- a/lib/DOMMatrix.js +++ b/lib/DOMMatrix.js @@ -17,6 +17,24 @@ class DOMPoint { this.z = typeof z === 'number' ? z : 0 this.w = typeof w === 'number' ? w : 1 } + + matrixTransform(init) { + const m = init instanceof DOMMatrix ? init : new DOMMatrix(init) + return m.transformPoint(this) + } + + toJSON() { + return { + x: this.x, + y: this.y, + z: this.z, + w: this.w + } + } + + static fromPoint(other) { + return new this(other.x, other.y, other.z, other.w) + } } // Constants to index into _values (col-major) @@ -163,6 +181,13 @@ class DOMMatrix { return this.scaleSelf(scale, scale, scale, originX, originY, originZ) } + /** + * @deprecated + */ + scaleNonUniform(scaleX, scaleY) { + return this.scale(scaleX, scaleY) + } + scaleSelf (scaleX, scaleY, scaleZ, originX, originY, originZ) { // Not redundant with translate's checks because we need to negate the values later. if (typeof originX !== 'number') originX = 0 @@ -587,6 +612,37 @@ Object.defineProperties(DOMMatrix.prototype, { values[M31] === 0 && values[M32] === 0 && values[M33] === 1 && values[M34] === 0 && values[M41] === 0 && values[M42] === 0 && values[M43] === 0 && values[M44] === 1) } + }, + + toJSON: { + value() { + return { + a: this.a, + b: this.b, + c: this.c, + d: this.d, + e: this.e, + f: this.f, + m11: this.m11, + m12: this.m12, + m13: this.m13, + m14: this.m14, + m21: this.m21, + m22: this.m22, + m23: this.m23, + m23: this.m23, + m31: this.m31, + m32: this.m32, + m33: this.m33, + m34: this.m34, + m41: this.m41, + m42: this.m42, + m43: this.m43, + m44: this.m44, + is2D: this.is2D, + isIdentity: this.isIdentity, + } + } } }) diff --git a/test/dommatrix.test.js b/test/dommatrix.test.js index 2c29e73eb..71be6d59e 100644 --- a/test/dommatrix.test.js +++ b/test/dommatrix.test.js @@ -586,4 +586,68 @@ describe('DOMMatrix', function () { 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1)') }) }) + + describe('toJSON', function () { + it('works, 2d', function () { + const x = new DOMMatrix() + assert.deepStrictEqual(x.toJSON(), { + a: 1, + b: 0, + c: 0, + d: 1, + e: 0, + f: 0, + m11: 1, + m12: 0, + m13: 0, + m14: 0, + m21: 0, + m22: 1, + m23: 0, + m23: 0, + m31: 0, + m32: 0, + m33: 1, + m34: 0, + m41: 0, + m42: 0, + m43: 0, + m44: 1, + is2D: true, + isIdentity: true, + }) + }) + + it('works, 3d', function () { + const x = new DOMMatrix() + x.m31 = 1 + assert.equal(x.is2D, false) + assert.deepStrictEqual(x.toJSON(), { + a: 1, + b: 0, + c: 0, + d: 1, + e: 0, + f: 0, + m11: 1, + m12: 0, + m13: 0, + m14: 0, + m21: 0, + m22: 1, + m23: 0, + m23: 0, + m31: 1, + m32: 0, + m33: 1, + m34: 0, + m41: 0, + m42: 0, + m43: 0, + m44: 1, + is2D: false, + isIdentity: false, + }) + }) + }) }) From a8c035f6280e3de0682ff3fb3d43ce617db6e0fb Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Mon, 23 Dec 2024 11:33:53 -0500 Subject: [PATCH 438/474] Revert "select fonts via postscript name on Linux" This reverts commit ddce10f478a7fe15a312e17dd41d1225efcead6d. --- CHANGELOG.md | 2 ++ src/register_font.cc | 69 ++++---------------------------------------- 2 files changed, 8 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68e0b2614..c835d3739 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ This release notably changes to using N-API. 🎉 * Add Node.js v20 to CI. (#2237) * Replaced `dtslint` with `tsd` (#2313) * Changed PNG consts to static properties of Canvas class +* Reverted improved font matching on Linux (#1572) because it doesn't work if fonts are installed. If you experience degraded font selection, please file an issue and use v3.0.0-rc3 in the meantime. + ### Added * Added string tags to support class detection * Throw Cairo errors in canvas.toBuffer() diff --git a/src/register_font.cc b/src/register_font.cc index cc0af52d7..ae2ece584 100644 --- a/src/register_font.cc +++ b/src/register_font.cc @@ -1,6 +1,5 @@ #include "register_font.h" -#include #include #include #include @@ -12,7 +11,6 @@ #include #else #include -#include #endif #include @@ -35,29 +33,11 @@ #define PREFERRED_ENCODING_ID TT_MS_ID_UNICODE_CS #endif -// With PangoFcFontMaps (the pango font module on Linux) we're able to add a -// hook that lets us get perfect matching. Tie the conditions for enabling that -// feature to one variable -#if !defined(__APPLE__) && !defined(_WIN32) && PANGO_VERSION_CHECK(1, 47, 0) -#define PERFECT_MATCHES_ENABLED -#endif - #define IS_PREFERRED_ENC(X) \ X.platform_id == PREFERRED_PLATFORM_ID && X.encoding_id == PREFERRED_ENCODING_ID -#ifdef PERFECT_MATCHES_ENABLED -// On Linux-like OSes using FontConfig, the PostScript name ranks higher than -// preferred family and family name since we'll use it to get perfect font -// matching (see fc_font_map_substitute_hook) -#define GET_NAME_RANK(X) \ - ((IS_PREFERRED_ENC(X) ? 1 : 0) << 2) | \ - ((X.name_id == TT_NAME_ID_PS_NAME ? 1 : 0) << 1) | \ - (X.name_id == TT_NAME_ID_PREFERRED_FAMILY ? 1 : 0) -#else #define GET_NAME_RANK(X) \ - ((IS_PREFERRED_ENC(X) ? 1 : 0) << 1) | \ - (X.name_id == TT_NAME_ID_PREFERRED_FAMILY ? 1 : 0) -#endif + (IS_PREFERRED_ENC(X) ? 1 : 0) + (X.name_id == TT_NAME_ID_PREFERRED_FAMILY ? 1 : 0) /* * Return a UTF-8 encoded string given a TrueType name buf+len @@ -125,31 +105,15 @@ get_family_name(FT_Face face) { for (unsigned i = 0; i < FT_Get_Sfnt_Name_Count(face); ++i) { FT_Get_Sfnt_Name(face, i, &name); - if ( - name.name_id == TT_NAME_ID_FONT_FAMILY || -#ifdef PERFECT_MATCHES_ENABLED - name.name_id == TT_NAME_ID_PS_NAME || -#endif - name.name_id == TT_NAME_ID_PREFERRED_FAMILY - ) { - int rank = GET_NAME_RANK(name); + if (name.name_id == TT_NAME_ID_FONT_FAMILY || name.name_id == TT_NAME_ID_PREFERRED_FAMILY) { + char *buf = to_utf8(name.string, name.string_len, name.platform_id, name.encoding_id); - if (rank > best_rank) { - char *buf = to_utf8(name.string, name.string_len, name.platform_id, name.encoding_id); - if (buf) { + if (buf) { + int rank = GET_NAME_RANK(name); + if (rank > best_rank) { best_rank = rank; if (best_buf) free(best_buf); best_buf = buf; - -#ifdef PERFECT_MATCHES_ENABLED - // Prepend an '@' to the postscript name - if (name.name_id == TT_NAME_ID_PS_NAME) { - std::string best_buf_modified = "@"; - best_buf_modified += best_buf; - free(best_buf); - best_buf = strdup(best_buf_modified.c_str()); - } -#endif } else { free(buf); } @@ -320,21 +284,6 @@ get_pango_font_description(unsigned char* filepath) { return NULL; } -#ifdef PERFECT_MATCHES_ENABLED -static void -fc_font_map_substitute_hook(FcPattern *pat, gpointer data) { - FcChar8 *family; - - for (int i = 0; FcPatternGetString(pat, FC_FAMILY, i, &family) == FcResultMatch; i++) { - if (family[0] == '@') { - FcPatternAddString(pat, FC_POSTSCRIPT_NAME, (FcChar8 *)family + 1); - FcPatternRemove(pat, FC_FAMILY, i); - i -= 1; - } - } -} -#endif - /* * Register font with the OS */ @@ -365,12 +314,6 @@ register_font(unsigned char *filepath) { // font families. pango_cairo_font_map_set_default(NULL); -#ifdef PERFECT_MATCHES_ENABLED - PangoFontMap* map = pango_cairo_font_map_get_default(); - PangoFcFontMap* fc_map = PANGO_FC_FONT_MAP(map); - pango_fc_font_map_set_default_substitute(fc_map, fc_font_map_substitute_hook, NULL, NULL); -#endif - return true; } From 834651230003e8ea63d5945f4bd1ef4371ec3c63 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Mon, 23 Dec 2024 12:22:36 -0500 Subject: [PATCH 439/474] v3.0.0 --- Readme.md | 7 ------- package.json | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/Readme.md b/Readme.md index d7a7c65cc..73c65d369 100644 --- a/Readme.md +++ b/Readme.md @@ -5,13 +5,6 @@ node-canvas is a [Cairo](http://cairographics.org/)-backed Canvas implementation for [Node.js](http://nodejs.org). -> [!TIP] -> **v3.0.0-rc2 is now available for testing on Linux (x64 glibc), macOS (x64) and Windows (x64)!** It's the first version -> to use N-API and prebuild-install. Please give it a try and let us know if you run into any issues. -> ```sh -> npm install canvas@next -> ``` - ## Installation ```bash diff --git a/package.json b/package.json index ae5df9f42..2f305fbd6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "3.0.0-rc3", + "version": "3.0.0", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 181710970861fd65cec7b10f2aa1036b8682ad3e Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Tue, 31 Dec 2024 13:44:42 -0500 Subject: [PATCH 440/474] add missing DOMMatrixInit and DOMPointInit types this was accidentally relying on people importing ambient DOM declarations --- CHANGELOG.md | 1 + index.d.ts | 10 ++++++++++ lib/DOMMatrix.js | 2 ++ 3 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c835d3739..32d861b0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed +* Fixed accidental depenency on ambient DOM types 3.0.0 diff --git a/index.d.ts b/index.d.ts index 52db720d2..ce21cef26 100644 --- a/index.d.ts +++ b/index.d.ts @@ -395,6 +395,16 @@ export class JPEGStream extends Readable {} /** This class must not be constructed directly; use `canvas.createPDFStream()`. */ export class PDFStream extends Readable {} +// TODO: this is wrong. See matrixTransform in lib/DOMMatrix.js +type DOMMatrixInit = DOMMatrix | string | number[]; + +interface DOMPointInit { + w?: number; + x?: number; + y?: number; + z?: number; +} + export class DOMPoint { w: number; x: number; diff --git a/lib/DOMMatrix.js b/lib/DOMMatrix.js index 20a41c0c8..97015adcf 100644 --- a/lib/DOMMatrix.js +++ b/lib/DOMMatrix.js @@ -19,6 +19,8 @@ class DOMPoint { } matrixTransform(init) { + // TODO: this next line is wrong. matrixTransform is supposed to only take + // an object with the DOMMatrix properties called DOMMatrixInit const m = init instanceof DOMMatrix ? init : new DOMMatrix(init) return m.transformPoint(this) } From 80e94ea7644b8f0c879b6e4ba899e50e6289e09a Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Tue, 31 Dec 2024 16:17:30 -0500 Subject: [PATCH 441/474] v3.0.1 --- CHANGELOG.md | 5 ++++- package.json | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32d861b0c..67cf68da0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,11 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed -* Fixed accidental depenency on ambient DOM types +3.0.1 +================== +### Fixed +* Fixed accidental depenency on ambient DOM types 3.0.0 ================== diff --git a/package.json b/package.json index 2f305fbd6..476f0ab24 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "3.0.0", + "version": "3.0.1", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 1d956b7246dd516cff9810db19a2915bc5598420 Mon Sep 17 00:00:00 2001 From: yumiura Date: Mon, 27 Nov 2023 17:49:17 +0900 Subject: [PATCH 442/474] use fetch api --- CHANGELOG.md | 1 + lib/image.js | 27 ++++++++++++--------------- package.json | 3 +-- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67cf68da0..d4a0cea10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ project adheres to [Semantic Versioning](http://semver.org/). (Unreleased) ================== ### Changed +* Replaced `simple-get ` with ` Node.js builtin` `fetch` (#2309) ### Added ### Fixed diff --git a/lib/image.js b/lib/image.js index 4a37849ee..9ffa3c794 100644 --- a/lib/image.js +++ b/lib/image.js @@ -14,9 +14,6 @@ const bindings = require('./bindings') const Image = module.exports = bindings.Image const util = require('util') -// Lazily loaded simple-get -let get - const { GetSource, SetSource } = bindings Object.defineProperty(Image.prototype, 'src', { @@ -47,20 +44,20 @@ Object.defineProperty(Image.prototype, 'src', { } } - if (!get) get = require('simple-get') - - get.concat({ - url: val, + fetch(val, { + method: 'GET', headers: { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36' } - }, (err, res, data) => { - if (err) return onerror(err) - - if (res.statusCode < 200 || res.statusCode >= 300) { - return onerror(new Error(`Server responded with ${res.statusCode}`)) - } - - setSource(this, data) }) + .then(res => { + if (!res.ok) { + throw new Error(`Server responded with ${res.statusCode}`) + } + return res.arrayBuffer() + }) + .then(data => { + setSource(this, Buffer.from(data)) + }) + .catch(onerror) } else { // local file path assumed setSource(this, val) } diff --git a/package.json b/package.json index 476f0ab24..8d4133042 100644 --- a/package.json +++ b/package.json @@ -46,8 +46,7 @@ ], "dependencies": { "node-addon-api": "^7.0.0", - "prebuild-install": "^7.1.1", - "simple-get": "^3.0.3" + "prebuild-install": "^7.1.1" }, "devDependencies": { "@types/node": "^10.12.18", From 7ed0a96b91735d3c6f1df0ceb827a9646b998c9a Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sun, 15 Sep 2024 16:58:18 -0400 Subject: [PATCH 443/474] add font setter benchmarks --- benchmarks/run.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/benchmarks/run.js b/benchmarks/run.js index 14f4db379..5a5c9d507 100644 --- a/benchmarks/run.js +++ b/benchmarks/run.js @@ -64,6 +64,22 @@ function done (benchmark, times, start, isAsync) { // node-canvas +function fontName () { + return String.fromCharCode(0x61 + Math.floor(Math.random() * 26)) + + String.fromCharCode(0x61 + Math.floor(Math.random() * 26)) + + String.fromCharCode(0x61 + Math.floor(Math.random() * 26)) + + String.fromCharCode(0x61 + Math.floor(Math.random() * 26)) +} + +bm('font setter', function () { + ctx.font = `12px ${fontName()}` + ctx.font = `400 6px ${fontName()}` + ctx.font = `1px ${fontName()}` + ctx.font = `normal normal bold 12cm ${fontName()}` + ctx.font = `italic 9mm ${fontName}, "Times New Roman", "Apple Color Emoji", "Comic Sans"` + ctx.font = `small-caps oblique 44px/44px ${fontName()}, "The Quick Brown", "Fox Jumped", "Over", "The", "Lazy Dog"` +}) + bm('save/restore', function () { for (let i = 0; i < 1000; i++) { const max = i & 15 From 728e76cc80da2748961ef973e9bb646f83f2c69e Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Thu, 26 Dec 2024 15:26:35 -0500 Subject: [PATCH 444/474] add C++ parser for the font shorthand This is the first part needed for the new font stack, which will look like the FontFace-based API the browsers have. FontFace uses a parser for font-family, font-size, etc., so I will need deeper control over the parser. FontFace will be implemented in C++ and I didn't want to carry over the awkward (and slow) switching between JS and C++. So here it is. I used Claude to generate initial classes and busy work, but it's been heavily examined and heavily modified. Caching aside, this is 3x faster in the benchmarks, which use random names to bypass the cache, and still a full 2x as fast when the JS version has a cached value. Those results were a bit inconsistent, so I'm not sure how much I trust them, but I expect this parser to have a stable performance profile nonetheless, so I'm not going to add any caching. It's also far more correct than what we had! --- CHANGELOG.md | 2 + binding.gyp | 3 +- index.js | 3 - lib/parse-font.js | 110 ------ src/Canvas.cc | 39 +- src/Canvas.h | 1 + src/CanvasRenderingContext2d.cc | 40 +-- src/CharData.h | 231 ++++++++++++ src/FontParser.cc | 605 ++++++++++++++++++++++++++++++++ src/FontParser.h | 115 ++++++ test/canvas.test.js | 73 ---- test/fontParser.test.js | 118 +++++++ 12 files changed, 1130 insertions(+), 210 deletions(-) delete mode 100644 lib/parse-font.js create mode 100644 src/CharData.h create mode 100644 src/FontParser.cc create mode 100644 src/FontParser.h create mode 100644 test/fontParser.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index d4a0cea10..0e9e0ca08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ project adheres to [Semantic Versioning](http://semver.org/). ================== ### Changed * Replaced `simple-get ` with ` Node.js builtin` `fetch` (#2309) +* `ctx.font` has a new C++ parser and is 2x-400x faster. Please file an issue if you experience different results, as caching has been removed. + ### Added ### Fixed diff --git a/binding.gyp b/binding.gyp index 166842641..bf647f7d1 100644 --- a/binding.gyp +++ b/binding.gyp @@ -75,7 +75,8 @@ 'src/Image.cc', 'src/ImageData.cc', 'src/init.cc', - 'src/register_font.cc' + 'src/register_font.cc', + 'src/FontParser.cc' ], 'conditions': [ ['OS=="win"', { diff --git a/index.js b/index.js index 89f2daabc..adde4da12 100644 --- a/index.js +++ b/index.js @@ -2,7 +2,6 @@ const Canvas = require('./lib/canvas') const Image = require('./lib/image') const CanvasRenderingContext2D = require('./lib/context2d') const CanvasPattern = require('./lib/pattern') -const parseFont = require('./lib/parse-font') const packageJson = require('./package.json') const bindings = require('./lib/bindings') const fs = require('fs') @@ -12,7 +11,6 @@ const JPEGStream = require('./lib/jpegstream') const { DOMPoint, DOMMatrix } = require('./lib/DOMMatrix') bindings.setDOMMatrix(DOMMatrix) -bindings.setParseFont(parseFont) function createCanvas (width, height, type) { return new Canvas(width, height, type) @@ -73,7 +71,6 @@ exports.DOMPoint = DOMPoint exports.registerFont = registerFont exports.deregisterAllFonts = deregisterAllFonts -exports.parseFont = parseFont exports.createCanvas = createCanvas exports.createImageData = createImageData diff --git a/lib/parse-font.js b/lib/parse-font.js deleted file mode 100644 index a18f05e51..000000000 --- a/lib/parse-font.js +++ /dev/null @@ -1,110 +0,0 @@ -'use strict' - -/** - * Font RegExp helpers. - */ - -const weights = 'bold|bolder|lighter|[1-9]00' -const styles = 'italic|oblique' -const variants = 'small-caps' -const stretches = 'ultra-condensed|extra-condensed|condensed|semi-condensed|semi-expanded|expanded|extra-expanded|ultra-expanded' -const units = 'px|pt|pc|in|cm|mm|%|em|ex|ch|rem|q' -const string = /'((\\'|[^'])+)'|"((\\"|[^"])+)"|[\w\s-]+/.source - -// [ [ <‘font-style’> || || <‘font-weight’> || <‘font-stretch’> ]? -// <‘font-size’> [ / <‘line-height’> ]? <‘font-family’> ] -// https://drafts.csswg.org/css-fonts-3/#font-prop -const weightRe = new RegExp(`(${weights}) +`, 'i') -const styleRe = new RegExp(`(${styles}) +`, 'i') -const variantRe = new RegExp(`(${variants}) +`, 'i') -const stretchRe = new RegExp(`(${stretches}) +`, 'i') -const familyRe = new RegExp(string, 'g') -const unquoteRe = /^['"](.*)['"]$/ -const unescapeRe = /\\(['"])/g -const sizeFamilyRe = new RegExp( - `([\\d\\.]+)(${units}) *((?:${string})( *, *(?:${string}))*)`) - -/** - * Cache font parsing. - */ - -const cache = {} - -const defaultHeight = 16 // pt, common browser default - -/** - * Parse font `str`. - * - * @param {String} str - * @return {Object} Parsed font. `size` is in device units. `unit` is the unit - * appearing in the input string. - * @api private - */ - -module.exports = str => { - // Cached - if (cache[str]) return cache[str] - - // Try for required properties first. - const sizeFamily = sizeFamilyRe.exec(str) - if (!sizeFamily) return // invalid - - const names = sizeFamily[3] - .match(familyRe) - // remove actual bounding quotes, if any, unescape any remaining quotes inside - .map(s => s.trim().replace(unquoteRe, '$1').replace(unescapeRe, '$1')) - .filter(s => !!s) - - // Default values and required properties - const font = { - weight: 'normal', - style: 'normal', - stretch: 'normal', - variant: 'normal', - size: parseFloat(sizeFamily[1]), - unit: sizeFamily[2], - family: names.join(',') - } - - // Optional, unordered properties. - let weight, style, variant, stretch - // Stop search at `sizeFamily.index` - const substr = str.substring(0, sizeFamily.index) - if ((weight = weightRe.exec(substr))) font.weight = weight[1] - if ((style = styleRe.exec(substr))) font.style = style[1] - if ((variant = variantRe.exec(substr))) font.variant = variant[1] - if ((stretch = stretchRe.exec(substr))) font.stretch = stretch[1] - - // Convert to device units. (`font.unit` is the original unit) - // TODO: ch, ex - switch (font.unit) { - case 'pt': - font.size /= 0.75 - break - case 'pc': - font.size *= 16 - break - case 'in': - font.size *= 96 - break - case 'cm': - font.size *= 96.0 / 2.54 - break - case 'mm': - font.size *= 96.0 / 25.4 - break - case '%': - // TODO disabled because existing unit tests assume 100 - // font.size *= defaultHeight / 100 / 0.75 - break - case 'em': - case 'rem': - font.size *= defaultHeight / 0.75 - break - case 'q': - font.size *= 96 / 25.4 / 4 - break - } - - return (cache[str] = font) -} diff --git a/src/Canvas.cc b/src/Canvas.cc index 6ba312008..7b208bec2 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -21,6 +21,7 @@ #include "Util.h" #include #include "node_buffer.h" +#include "FontParser.h" #ifdef HAVE_JPEG #include "JPEGStream.h" @@ -68,7 +69,8 @@ Canvas::Initialize(Napi::Env& env, Napi::Object& exports) { StaticValue("PNG_FILTER_PAETH", Napi::Number::New(env, PNG_FILTER_PAETH), napi_default_jsproperty), StaticValue("PNG_ALL_FILTERS", Napi::Number::New(env, PNG_ALL_FILTERS), napi_default_jsproperty), StaticMethod<&Canvas::RegisterFont>("_registerFont", napi_default_method), - StaticMethod<&Canvas::DeregisterAllFonts>("_deregisterAllFonts", napi_default_method) + StaticMethod<&Canvas::DeregisterAllFonts>("_deregisterAllFonts", napi_default_method), + StaticMethod<&Canvas::ParseFont>("parseFont", napi_default_method) }); data->CanvasCtor = Napi::Persistent(ctor); @@ -694,6 +696,7 @@ Canvas::RegisterFont(const Napi::CallbackInfo& info) { // now check the attrs, there are many ways to be wrong Napi::Object js_user_desc = info[1].As(); + // TODO: use FontParser on these values just like the FontFace API works char *family = str_value(js_user_desc.Get("family"), NULL, false); char *weight = str_value(js_user_desc.Get("weight"), "normal", true); char *style = str_value(js_user_desc.Get("style"), "normal", false); @@ -749,6 +752,40 @@ Canvas::DeregisterAllFonts(const Napi::CallbackInfo& info) { if (!success) Napi::Error::New(env, "Could not deregister one or more fonts").ThrowAsJavaScriptException(); } +/* + * Do not use! This is only exported for testing + */ +Napi::Value +Canvas::ParseFont(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + + if (info.Length() != 1) return env.Undefined(); + + Napi::String str; + if (!info[0].ToString().UnwrapTo(&str)) return env.Undefined(); + + bool ok; + auto props = FontParser::parse(str, &ok); + if (!ok) return env.Undefined(); + + Napi::Object obj = Napi::Object::New(env); + obj.Set("size", Napi::Number::New(env, props.fontSize)); + Napi::Array families = Napi::Array::New(env); + obj.Set("families", families); + + unsigned int index = 0; + + for (auto& family : props.fontFamily) { + families[index++] = Napi::String::New(env, family); + } + + obj.Set("weight", Napi::Number::New(env, props.fontWeight)); + obj.Set("variant", Napi::Number::New(env, static_cast(props.fontVariant))); + obj.Set("style", Napi::Number::New(env, static_cast(props.fontStyle))); + + return obj; +} + /* * Get a PangoStyle from a CSS string (like "italic") */ diff --git a/src/Canvas.h b/src/Canvas.h index 5f35b356b..5b039539a 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -68,6 +68,7 @@ class Canvas : public Napi::ObjectWrap { void StreamJPEGSync(const Napi::CallbackInfo& info); static void RegisterFont(const Napi::CallbackInfo& info); static void DeregisterAllFonts(const Napi::CallbackInfo& info); + static Napi::Value ParseFont(const Napi::CallbackInfo& info); Napi::Error CairoError(cairo_status_t status); static void ToPngBufferAsync(Closure* closure); static void ToJpegBufferAsync(Closure* closure); diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index d0966e299..1597d089a 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -9,6 +9,7 @@ #include "CanvasGradient.h" #include "CanvasPattern.h" #include "InstanceData.h" +#include "FontParser.h" #include #include #include "Image.h" @@ -2575,34 +2576,29 @@ Context2d::GetFont(const Napi::CallbackInfo& info) { void Context2d::SetFont(const Napi::CallbackInfo& info, const Napi::Value& value) { - InstanceData* data = env.GetInstanceData(); - if (!value.IsString()) return; - if (!value.As().Utf8Value().length()) return; - - Napi::Value mparsed; + std::string str = value.As().Utf8Value(); + if (!str.length()) return; - // parseFont returns undefined for invalid CSS font strings - if (!data->parseFont.Call({ value }).UnwrapTo(&mparsed) || mparsed.IsUndefined()) return; - - Napi::Object font = mparsed.As(); - - Napi::String empty = Napi::String::New(env, ""); - Napi::Number zero = Napi::Number::New(env, 0); - - std::string weight = font.Get("weight").UnwrapOr(empty).ToString().UnwrapOr(empty).Utf8Value(); - std::string style = font.Get("style").UnwrapOr(empty).ToString().UnwrapOr(empty).Utf8Value(); - double size = font.Get("size").UnwrapOr(zero).ToNumber().UnwrapOr(zero).DoubleValue(); - std::string unit = font.Get("unit").UnwrapOr(empty).ToString().UnwrapOr(empty).Utf8Value(); - std::string family = font.Get("family").UnwrapOr(empty).ToString().UnwrapOr(empty).Utf8Value(); + bool success; + auto props = FontParser::parse(str, &success); + if (!success) return; PangoFontDescription *desc = pango_font_description_copy(state->fontDescription); pango_font_description_free(state->fontDescription); - pango_font_description_set_style(desc, Canvas::GetStyleFromCSSString(style.c_str())); - pango_font_description_set_weight(desc, Canvas::GetWeightFromCSSString(weight.c_str())); + PangoStyle style = props.fontStyle == FontStyle::Italic ? PANGO_STYLE_ITALIC + : props.fontStyle == FontStyle::Oblique ? PANGO_STYLE_OBLIQUE + : PANGO_STYLE_NORMAL; + pango_font_description_set_style(desc, style); + pango_font_description_set_weight(desc, static_cast(props.fontWeight)); + + std::string family = props.fontFamily.empty() ? "" : props.fontFamily[0]; + for (size_t i = 1; i < props.fontFamily.size(); i++) { + family += "," + props.fontFamily[i]; + } if (family.length() > 0) { // See #1643 - Pango understands "sans" whereas CSS uses "sans-serif" std::string s1(family); @@ -2617,12 +2613,12 @@ Context2d::SetFont(const Napi::CallbackInfo& info, const Napi::Value& value) { PangoFontDescription *sys_desc = Canvas::ResolveFontDescription(desc); pango_font_description_free(desc); - if (size > 0) pango_font_description_set_absolute_size(sys_desc, size * PANGO_SCALE); + if (props.fontSize > 0) pango_font_description_set_absolute_size(sys_desc, props.fontSize * PANGO_SCALE); state->fontDescription = sys_desc; pango_layout_set_font_description(_layout, sys_desc); - state->font = value.As().Utf8Value().c_str(); + state->font = str; } /* diff --git a/src/CharData.h b/src/CharData.h new file mode 100644 index 000000000..ebc2dd5e1 --- /dev/null +++ b/src/CharData.h @@ -0,0 +1,231 @@ +// This is used for classifying characters according to the definition of tokens +// in the CSS standards, but could be extended for any other future uses + +#pragma once + +namespace CharData { + static constexpr uint8_t Whitespace = 0x1; + static constexpr uint8_t Newline = 0x2; + static constexpr uint8_t Hex = 0x4; + static constexpr uint8_t Nmstart = 0x8; + static constexpr uint8_t Nmchar = 0x10; + static constexpr uint8_t Sign = 0x20; + static constexpr uint8_t Digit = 0x40; + static constexpr uint8_t NumStart = 0x80; +}; + +using namespace CharData; + +constexpr const uint8_t charData[256] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-8 + Whitespace, // 9 (HT) + Whitespace | Newline, // 10 (LF) + 0, // 11 (VT) + Whitespace | Newline, // 12 (FF) + Whitespace | Newline, // 13 (CR) + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 14-31 + Whitespace, // 32 (Space) + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 33-42 + Sign | NumStart, // 43 (+) + 0, // 44 + Nmchar | Sign | NumStart, // 45 (-) + 0, 0, // 46-47 + Nmchar | Digit | NumStart | Hex, // 48 (0) + Nmchar | Digit | NumStart | Hex, // 49 (1) + Nmchar | Digit | NumStart | Hex, // 50 (2) + Nmchar | Digit | NumStart | Hex, // 51 (3) + Nmchar | Digit | NumStart | Hex, // 52 (4) + Nmchar | Digit | NumStart | Hex, // 53 (5) + Nmchar | Digit | NumStart | Hex, // 54 (6) + Nmchar | Digit | NumStart | Hex, // 55 (7) + Nmchar | Digit | NumStart | Hex, // 56 (8) + Nmchar | Digit | NumStart | Hex, // 57 (9) + 0, 0, 0, 0, 0, 0, 0, // 58-64 + Nmstart | Nmchar | Hex, // 65 (A) + Nmstart | Nmchar | Hex, // 66 (B) + Nmstart | Nmchar | Hex, // 67 (C) + Nmstart | Nmchar | Hex, // 68 (D) + Nmstart | Nmchar | Hex, // 69 (E) + Nmstart | Nmchar | Hex, // 70 (F) + Nmstart | Nmchar, // 71 (G) + Nmstart | Nmchar, // 72 (H) + Nmstart | Nmchar, // 73 (I) + Nmstart | Nmchar, // 74 (J) + Nmstart | Nmchar, // 75 (K) + Nmstart | Nmchar, // 76 (L) + Nmstart | Nmchar, // 77 (M) + Nmstart | Nmchar, // 78 (N) + Nmstart | Nmchar, // 79 (O) + Nmstart | Nmchar, // 80 (P) + Nmstart | Nmchar, // 81 (Q) + Nmstart | Nmchar, // 82 (R) + Nmstart | Nmchar, // 83 (S) + Nmstart | Nmchar, // 84 (T) + Nmstart | Nmchar, // 85 (U) + Nmstart | Nmchar, // 86 (V) + Nmstart | Nmchar, // 87 (W) + Nmstart | Nmchar, // 88 (X) + Nmstart | Nmchar, // 89 (Y) + Nmstart | Nmchar, // 90 (Z) + 0, // 91 + Nmstart, // 92 (\) + 0, 0, // 93-94 + Nmstart | Nmchar, // 95 (_) + 0, // 96 + Nmstart | Nmchar | Hex, // 97 (a) + Nmstart | Nmchar | Hex, // 98 (b) + Nmstart | Nmchar | Hex, // 99 (c) + Nmstart | Nmchar | Hex, // 100 (d) + Nmstart | Nmchar | Hex, // 101 (e) + Nmstart | Nmchar | Hex, // 102 (f) + Nmstart | Nmchar, // 103 (g) + Nmstart | Nmchar, // 104 (h) + Nmstart | Nmchar, // 105 (i) + Nmstart | Nmchar, // 106 (j) + Nmstart | Nmchar, // 107 (k) + Nmstart | Nmchar, // 108 (l) + Nmstart | Nmchar, // 109 (m) + Nmstart | Nmchar, // 110 (n) + Nmstart | Nmchar, // 111 (o) + Nmstart | Nmchar, // 112 (p) + Nmstart | Nmchar, // 113 (q) + Nmstart | Nmchar, // 114 (r) + Nmstart | Nmchar, // 115 (s) + Nmstart | Nmchar, // 116 (t) + Nmstart | Nmchar, // 117 (u) + Nmstart | Nmchar, // 118 (v) + Nmstart | Nmchar, // 119 (w) + Nmstart | Nmchar, // 120 (x) + Nmstart | Nmchar, // 121 (y) + Nmstart | Nmchar, // 122 (z) + 0, 0, 0, 0, 0, // 123-127 + // Non-ASCII + Nmstart | Nmchar, // 128 + Nmstart | Nmchar, // 129 + Nmstart | Nmchar, // 130 + Nmstart | Nmchar, // 131 + Nmstart | Nmchar, // 132 + Nmstart | Nmchar, // 133 + Nmstart | Nmchar, // 134 + Nmstart | Nmchar, // 135 + Nmstart | Nmchar, // 136 + Nmstart | Nmchar, // 137 + Nmstart | Nmchar, // 138 + Nmstart | Nmchar, // 139 + Nmstart | Nmchar, // 140 + Nmstart | Nmchar, // 141 + Nmstart | Nmchar, // 142 + Nmstart | Nmchar, // 143 + Nmstart | Nmchar, // 144 + Nmstart | Nmchar, // 145 + Nmstart | Nmchar, // 146 + Nmstart | Nmchar, // 147 + Nmstart | Nmchar, // 148 + Nmstart | Nmchar, // 149 + Nmstart | Nmchar, // 150 + Nmstart | Nmchar, // 151 + Nmstart | Nmchar, // 152 + Nmstart | Nmchar, // 153 + Nmstart | Nmchar, // 154 + Nmstart | Nmchar, // 155 + Nmstart | Nmchar, // 156 + Nmstart | Nmchar, // 157 + Nmstart | Nmchar, // 158 + Nmstart | Nmchar, // 159 + Nmstart | Nmchar, // 160 + Nmstart | Nmchar, // 161 + Nmstart | Nmchar, // 162 + Nmstart | Nmchar, // 163 + Nmstart | Nmchar, // 164 + Nmstart | Nmchar, // 165 + Nmstart | Nmchar, // 166 + Nmstart | Nmchar, // 167 + Nmstart | Nmchar, // 168 + Nmstart | Nmchar, // 169 + Nmstart | Nmchar, // 170 + Nmstart | Nmchar, // 171 + Nmstart | Nmchar, // 172 + Nmstart | Nmchar, // 173 + Nmstart | Nmchar, // 174 + Nmstart | Nmchar, // 175 + Nmstart | Nmchar, // 176 + Nmstart | Nmchar, // 177 + Nmstart | Nmchar, // 178 + Nmstart | Nmchar, // 179 + Nmstart | Nmchar, // 180 + Nmstart | Nmchar, // 181 + Nmstart | Nmchar, // 182 + Nmstart | Nmchar, // 183 + Nmstart | Nmchar, // 184 + Nmstart | Nmchar, // 185 + Nmstart | Nmchar, // 186 + Nmstart | Nmchar, // 187 + Nmstart | Nmchar, // 188 + Nmstart | Nmchar, // 189 + Nmstart | Nmchar, // 190 + Nmstart | Nmchar, // 191 + Nmstart | Nmchar, // 192 + Nmstart | Nmchar, // 193 + Nmstart | Nmchar, // 194 + Nmstart | Nmchar, // 195 + Nmstart | Nmchar, // 196 + Nmstart | Nmchar, // 197 + Nmstart | Nmchar, // 198 + Nmstart | Nmchar, // 199 + Nmstart | Nmchar, // 200 + Nmstart | Nmchar, // 201 + Nmstart | Nmchar, // 202 + Nmstart | Nmchar, // 203 + Nmstart | Nmchar, // 204 + Nmstart | Nmchar, // 205 + Nmstart | Nmchar, // 206 + Nmstart | Nmchar, // 207 + Nmstart | Nmchar, // 208 + Nmstart | Nmchar, // 209 + Nmstart | Nmchar, // 210 + Nmstart | Nmchar, // 211 + Nmstart | Nmchar, // 212 + Nmstart | Nmchar, // 213 + Nmstart | Nmchar, // 214 + Nmstart | Nmchar, // 215 + Nmstart | Nmchar, // 216 + Nmstart | Nmchar, // 217 + Nmstart | Nmchar, // 218 + Nmstart | Nmchar, // 219 + Nmstart | Nmchar, // 220 + Nmstart | Nmchar, // 221 + Nmstart | Nmchar, // 222 + Nmstart | Nmchar, // 223 + Nmstart | Nmchar, // 224 + Nmstart | Nmchar, // 225 + Nmstart | Nmchar, // 226 + Nmstart | Nmchar, // 227 + Nmstart | Nmchar, // 228 + Nmstart | Nmchar, // 229 + Nmstart | Nmchar, // 230 + Nmstart | Nmchar, // 231 + Nmstart | Nmchar, // 232 + Nmstart | Nmchar, // 233 + Nmstart | Nmchar, // 234 + Nmstart | Nmchar, // 235 + Nmstart | Nmchar, // 236 + Nmstart | Nmchar, // 237 + Nmstart | Nmchar, // 238 + Nmstart | Nmchar, // 239 + Nmstart | Nmchar, // 240 + Nmstart | Nmchar, // 241 + Nmstart | Nmchar, // 242 + Nmstart | Nmchar, // 243 + Nmstart | Nmchar, // 244 + Nmstart | Nmchar, // 245 + Nmstart | Nmchar, // 246 + Nmstart | Nmchar, // 247 + Nmstart | Nmchar, // 248 + Nmstart | Nmchar, // 249 + Nmstart | Nmchar, // 250 + Nmstart | Nmchar, // 251 + Nmstart | Nmchar, // 252 + Nmstart | Nmchar, // 253 + Nmstart | Nmchar, // 254 + Nmstart | Nmchar // 255 +}; diff --git a/src/FontParser.cc b/src/FontParser.cc new file mode 100644 index 000000000..773502cb3 --- /dev/null +++ b/src/FontParser.cc @@ -0,0 +1,605 @@ +// This is written to exactly parse the `font` shorthand in CSS2: +// https://www.w3.org/TR/CSS22/fonts.html#font-shorthand +// https://www.w3.org/TR/CSS22/syndata.html#tokenization +// +// We may want to update it for CSS 3 (e.g. font-stretch, or updated +// tokenization) but I've only ever seen one or two issues filed in node-canvas +// due to parsing in my 8 years on the project + +#include "FontParser.h" +#include "CharData.h" +#include +#include + +Token::Token(Type type, std::string value) : type_(type), value_(std::move(value)) {} + +Token::Token(Type type, double value) : type_(type), value_(value) {} + +Token::Token(Type type) : type_(type), value_(std::string{}) {} + +const std::string& +Token::getString() const { + static const std::string empty; + auto* str = std::get_if(&value_); + return str ? *str : empty; +} + +double +Token::getNumber() const { + auto* num = std::get_if(&value_); + return num ? *num : 0.0f; +} + +Tokenizer::Tokenizer(std::string_view input) : input_(input) {} + +std::string +Tokenizer::utf8Encode(uint32_t codepoint) { + std::string result; + + if (codepoint < 0x80) { + result += static_cast(codepoint); + } else if (codepoint < 0x800) { + result += static_cast((codepoint >> 6) | 0xc0); + result += static_cast((codepoint & 0x3f) | 0x80); + } else if (codepoint < 0x10000) { + result += static_cast((codepoint >> 12) | 0xe0); + result += static_cast(((codepoint >> 6) & 0x3f) | 0x80); + result += static_cast((codepoint & 0x3f) | 0x80); + } else { + result += static_cast((codepoint >> 18) | 0xf0); + result += static_cast(((codepoint >> 12) & 0x3f) | 0x80); + result += static_cast(((codepoint >> 6) & 0x3f) | 0x80); + result += static_cast((codepoint & 0x3f) | 0x80); + } + + return result; +} + +char +Tokenizer::peek() const { + return position_ < input_.length() ? input_[position_] : '\0'; +} + +char +Tokenizer::advance() { + return position_ < input_.length() ? input_[position_++] : '\0'; +} + +Token +Tokenizer::parseNumber() { + enum class State { + Start, + AfterSign, + Digits, + AfterDecimal, + AfterE, + AfterESign, + ExponentDigits + }; + + size_t start = position_; + size_t ePosition = 0; + State state = State::Start; + bool valid = false; + + while (position_ < input_.length()) { + char c = peek(); + uint8_t flags = charData[static_cast(c)]; + + switch (state) { + case State::Start: + if (flags & CharData::Sign) { + position_++; + state = State::AfterSign; + } else if (flags & CharData::Digit) { + position_++; + state = State::Digits; + valid = true; + } else if (c == '.') { + position_++; + state = State::AfterDecimal; + } else { + goto done; + } + break; + + case State::AfterSign: + if (flags & CharData::Digit) { + position_++; + state = State::Digits; + valid = true; + } else if (c == '.') { + position_++; + state = State::AfterDecimal; + } else { + goto done; + } + break; + + case State::Digits: + if (flags & CharData::Digit) { + position_++; + } else if (c == '.') { + position_++; + state = State::AfterDecimal; + } else if (c == 'e' || c == 'E') { + ePosition = position_; + position_++; + state = State::AfterE; + valid = false; + } else { + goto done; + } + break; + + case State::AfterDecimal: + if (flags & CharData::Digit) { + position_++; + valid = true; + state = State::Digits; + } else { + goto done; + } + break; + + case State::AfterE: + if (flags & CharData::Sign) { + position_++; + state = State::AfterESign; + } else if (flags & CharData::Digit) { + position_++; + valid = true; + state = State::ExponentDigits; + } else { + position_ = ePosition; + valid = true; + goto done; + } + break; + + case State::AfterESign: + if (flags & CharData::Digit) { + position_++; + valid = true; + state = State::ExponentDigits; + } else { + position_ = ePosition; + valid = true; + goto done; + } + break; + + case State::ExponentDigits: + if (flags & CharData::Digit) { + position_++; + } else { + goto done; + } + break; + } + } + +done: + if (!valid) { + position_ = start; + return Token(Token::Type::Invalid); + } + + std::string number_str(input_.substr(start, position_ - start)); + double value = std::stod(number_str); + return Token(Token::Type::Number, value); +} + +// Note that identifiers are always lower-case. This helps us make easier/more +// efficient comparisons, but means that font-families specified as identifiers +// will be lower-cased. Since font selection isn't case sensitive, this +// shouldn't ever be a problem. +Token +Tokenizer::parseIdentifier() { + std::string identifier; + auto flags = CharData::Nmstart; + auto start = position_; + + while (position_ < input_.length()) { + char c = peek(); + + if (c == '\\') { + advance(); + if (!parseEscape(identifier)) { + position_ = start; + return Token(Token::Type::Invalid); + } + flags = CharData::Nmchar; + } else if (charData[static_cast(c)] & flags) { + identifier += advance() + (c >= 'A' && c <= 'Z' ? 32 : 0); + flags = CharData::Nmchar; + } else { + break; + } + } + + return Token(Token::Type::Identifier, identifier); +} + +uint32_t +Tokenizer::parseUnicode() { + uint32_t value = 0; + size_t count = 0; + + while (position_ < input_.length() && count < 6) { + char c = peek(); + uint32_t digit; + + if (c >= '0' && c <= '9') { + digit = c - '0'; + } else if (c >= 'a' && c <= 'f') { + digit = c - 'a' + 10; + } else if (c >= 'A' && c <= 'F') { + digit = c - 'A' + 10; + } else { + break; + } + + value = value * 16 + digit; + advance(); + count++; + } + + // Optional whitespace after hex escape + char c = peek(); + if (c == '\r') { + advance(); + if (peek() == '\n') advance(); + } else if (isWhitespace(c)) { + advance(); + } + + return value; +} + +bool +Tokenizer::parseEscape(std::string& str) { + char c = peek(); + auto flags = charData[static_cast(c)]; + + if (flags & CharData::Hex) { + str += utf8Encode(parseUnicode()); + return true; + } else if (!(flags & CharData::Newline) && !(flags & CharData::Hex)) { + str += advance(); + return true; + } + + return false; +} + +Token +Tokenizer::parseString(char quote) { + advance(); + std::string value; + auto start = position_; + + while (position_ < input_.length()) { + char c = peek(); + + if (c == quote) { + advance(); + return Token(Token::Type::QuotedString, value); + } else if (c == '\\') { + advance(); + c = peek(); + if (c == '\r') { + advance(); + if (peek() == '\n') advance(); + } else if (isNewline(c)) { + advance(); + } else { + if (!parseEscape(value)) { + position_ = start; + return Token(Token::Type::Invalid); + } + } + } else { + value += advance(); + } + } + + position_ = start; + return Token(Token::Type::Invalid); +} + +Token +Tokenizer::nextToken() { + if (position_ >= input_.length()) { + return Token(Token::Type::EndOfInput); + } + + char c = peek(); + auto flags = charData[static_cast(c)]; + + if (isWhitespace(c)) { + std::string whitespace; + while (position_ < input_.length() && isWhitespace(peek())) { + whitespace += advance(); + } + return Token(Token::Type::Whitespace, whitespace); + } + + if (flags & CharData::NumStart) { + Token token = parseNumber(); + if (token.type() != Token::Type::Invalid) return token; + } + + if (flags & CharData::Nmstart) { + Token token = parseIdentifier(); + if (token.type() != Token::Type::Invalid) return token; + } + + if (c == '"') { + Token token = parseString('"'); + if (token.type() != Token::Type::Invalid) return token; + } + + if (c == '\'') { + Token token = parseString('\''); + if (token.type() != Token::Type::Invalid) return token; + } + + switch (advance()) { + case '/': return Token(Token::Type::Slash); + case ',': return Token(Token::Type::Comma); + case '%': return Token(Token::Type::Percent); + default: return Token(Token::Type::Invalid); + } +} + +FontParser::FontParser(std::string_view input) + : tokenizer_(input) + , currentToken_(tokenizer_.nextToken()) + , nextToken_(tokenizer_.nextToken()) {} + +const std::unordered_map FontParser::weightMap = { + {"normal", 400}, + {"bold", 700}, + {"lighter", 100}, + {"bolder", 700} +}; + +const std::unordered_map FontParser::unitMap = { + {"cm", 37.8f}, + {"mm", 3.78f}, + {"in", 96.0f}, + {"pt", 96.0f / 72.0f}, + {"pc", 96.0f / 6.0f}, + {"em", 16.0f}, + {"px", 1.0f} +}; + +void +FontParser::advance() { + currentToken_ = nextToken_; + nextToken_ = tokenizer_.nextToken(); +} + +void +FontParser::skipWs() { + while (currentToken_.type() == Token::Type::Whitespace) advance(); +} + +bool +FontParser::check(Token::Type type) const { + return currentToken_.type() == type; +} + +bool +FontParser::checkWs() const { + return nextToken_.type() == Token::Type::Whitespace + || nextToken_.type() == Token::Type::EndOfInput; +} + +bool +FontParser::parseFontStyle(FontProperties& props) { + if (check(Token::Type::Identifier)) { + const auto& value = currentToken_.getString(); + if (value == "italic") { + props.fontStyle = FontStyle::Italic; + advance(); + return true; + } else if (value == "oblique") { + props.fontStyle = FontStyle::Oblique; + advance(); + return true; + } else if (value == "normal") { + props.fontStyle = FontStyle::Normal; + advance(); + return true; + } + } + + return false; +} + +bool +FontParser::parseFontVariant(FontProperties& props) { + if (check(Token::Type::Identifier)) { + const auto& value = currentToken_.getString(); + if (value == "small-caps") { + props.fontVariant = FontVariant::SmallCaps; + advance(); + return true; + } else if (value == "normal") { + props.fontVariant = FontVariant::Normal; + advance(); + return true; + } + } + + return false; +} + +bool +FontParser::parseFontWeight(FontProperties& props) { + if (check(Token::Type::Number)) { + double weightFloat = currentToken_.getNumber(); + int weight = static_cast(weightFloat); + if (weight < 1 || weight > 1000) return false; + props.fontWeight = static_cast(weight); + advance(); + return true; + } else if (check(Token::Type::Identifier)) { + const auto& value = currentToken_.getString(); + + if (auto it = weightMap.find(value); it != weightMap.end()) { + props.fontWeight = it->second; + advance(); + return true; + } + } + + return false; +} + +bool +FontParser::parseFontSize(FontProperties& props) { + if (!check(Token::Type::Number)) return false; + + props.fontSize = currentToken_.getNumber(); + advance(); + + double multiplier = 1.0f; + if (check(Token::Type::Identifier)) { + const auto& unit = currentToken_.getString(); + + if (auto it = unitMap.find(unit); it != unitMap.end()) { + multiplier = it->second; + advance(); + } else { + return false; + } + } else if (check(Token::Type::Percent)) { + multiplier = 16.0f / 100.0f; + advance(); + } else { + return false; + } + + // Technically if we consumed some tokens but couldn't parse the font-size, + // we should rewind the tokenizer, but I don't think the grammar allows for + // any valid alternates in this specific case + + props.fontSize *= multiplier; + return true; +} + +// line-height is not used by canvas ever, but should still parse +bool +FontParser::parseLineHeight(FontProperties& props) { + if (check(Token::Type::Slash)) { + advance(); + skipWs(); + if (check(Token::Type::Number)) { + advance(); + if (check(Token::Type::Percent)) { + advance(); + } else if (check(Token::Type::Identifier)) { + auto identifier = currentToken_.getString(); + if (auto it = unitMap.find(identifier); it != unitMap.end()) { + advance(); + } else { + return false; + } + } else { + return false; + } + } else if (check(Token::Type::Identifier) && currentToken_.getString() == "normal") { + advance(); + } else { + return false; + } + } + + return true; +} + +bool +FontParser::parseFontFamily(FontProperties& props) { + while (!check(Token::Type::EndOfInput)) { + std::string family = ""; + std::string trailingWs = ""; + bool found = false; + + while ( + check(Token::Type::QuotedString) || + check(Token::Type::Identifier) || + check(Token::Type::Whitespace) + ) { + if (check(Token::Type::Whitespace)) { + if (found) trailingWs += currentToken_.getString(); + } else { // Identifier, QuotedString + if (found) { + family += trailingWs; + trailingWs.clear(); + } + + family += currentToken_.getString(); + found = true; + } + + advance(); + } + + if (!found) return false; // only whitespace or non-id/string found + + props.fontFamily.push_back(family); + + if (check(Token::Type::Comma)) advance(); + } + + return true; +} + +FontProperties +FontParser::parse(const std::string& fontString, bool* success) { + FontParser parser(fontString); + auto result = parser.parseFont(); + if (success) *success = !parser.hasError_; + return result; +} + +FontProperties +FontParser::parseFont() { + FontProperties props; + uint8_t state = 0b111; + + skipWs(); + + for (size_t i = 0; i < 3 && checkWs(); i++) { + if ((state & 0b001) && parseFontStyle(props)) { + state &= 0b110; + goto match; + } + + if ((state & 0b010) && parseFontVariant(props)) { + state &= 0b101; + goto match; + } + + if ((state & 0b100) && parseFontWeight(props)) { + state &= 0b011; + goto match; + } + + break; // all attempts exhausted + match: skipWs(); // success: move to the next non-ws token + } + + if (parseFontSize(props)) { + skipWs(); + if (parseLineHeight(props) && parseFontFamily(props)) { + return props; + } + } + + hasError_ = true; + return props; +} diff --git a/src/FontParser.h b/src/FontParser.h new file mode 100644 index 000000000..c88802109 --- /dev/null +++ b/src/FontParser.h @@ -0,0 +1,115 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include "CharData.h" + +enum class FontStyle { + Normal, + Italic, + Oblique +}; + +enum class FontVariant { + Normal, + SmallCaps +}; + +struct FontProperties { + double fontSize{16.0f}; + std::vector fontFamily; + uint16_t fontWeight{400}; + FontVariant fontVariant{FontVariant::Normal}; + FontStyle fontStyle{FontStyle::Normal}; +}; + +class Token { + public: + enum class Type { + Invalid, + Number, + Percent, + Identifier, + Slash, + Comma, + QuotedString, + Whitespace, + EndOfInput + }; + + Token(Type type, std::string value); + Token(Type type, double value); + Token(Type type); + + Type type() const { return type_; } + + const std::string& getString() const; + double getNumber() const; + + private: + Type type_; + std::variant value_; +}; + +class Tokenizer { + public: + Tokenizer(std::string_view input); + Token nextToken(); + + private: + std::string_view input_; + size_t position_{0}; + + // Util + std::string utf8Encode(uint32_t codepoint); + inline bool isWhitespace(char c) const { + return charData[static_cast(c)] & CharData::Whitespace; + } + inline bool isNewline(char c) const { + return charData[static_cast(c)] & CharData::Newline; + } + + // Moving through the string + char peek() const; + char advance(); + + // Tokenize + Token parseNumber(); + Token parseIdentifier(); + uint32_t parseUnicode(); + bool parseEscape(std::string& str); + Token parseString(char quote); +}; + +class FontParser { + public: + static FontProperties parse(const std::string& fontString, bool* success = nullptr); + + private: + static const std::unordered_map weightMap; + static const std::unordered_map unitMap; + + FontParser(std::string_view input); + + void advance(); + void skipWs(); + bool check(Token::Type type) const; + bool checkWs() const; + + bool parseFontStyle(FontProperties& props); + bool parseFontVariant(FontProperties& props); + bool parseFontWeight(FontProperties& props); + bool parseFontSize(FontProperties& props); + bool parseLineHeight(FontProperties& props); + bool parseFontFamily(FontProperties& props); + FontProperties parseFont(); + + Tokenizer tokenizer_; + Token currentToken_; + Token nextToken_; + bool hasError_{false}; +}; diff --git a/test/canvas.test.js b/test/canvas.test.js index 1a75ac031..75f15ed5a 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -14,7 +14,6 @@ const { createCanvas, createImageData, loadImage, - parseFont, registerFont, Canvas, deregisterAllFonts @@ -37,78 +36,6 @@ describe('Canvas', function () { assert('width' in Canvas.prototype) }) - it('.parseFont()', function () { - const tests = [ - '20px Arial', - { size: 20, unit: 'px', family: 'Arial' }, - '20pt Arial', - { size: 26.666666666666668, unit: 'pt', family: 'Arial' }, - '20.5pt Arial', - { size: 27.333333333333332, unit: 'pt', family: 'Arial' }, - '20% Arial', - { size: 20, unit: '%', family: 'Arial' }, // TODO I think this is a bad assertion - ZB 23-Jul-2017 - '20mm Arial', - { size: 75.59055118110237, unit: 'mm', family: 'Arial' }, - '20px serif', - { size: 20, unit: 'px', family: 'serif' }, - '20px sans-serif', - { size: 20, unit: 'px', family: 'sans-serif' }, - '20px monospace', - { size: 20, unit: 'px', family: 'monospace' }, - '50px Arial, sans-serif', - { size: 50, unit: 'px', family: 'Arial,sans-serif' }, - 'bold italic 50px Arial, sans-serif', - { style: 'italic', weight: 'bold', size: 50, unit: 'px', family: 'Arial,sans-serif' }, - '50px Helvetica , Arial, sans-serif', - { size: 50, unit: 'px', family: 'Helvetica,Arial,sans-serif' }, - '50px "Helvetica Neue", sans-serif', - { size: 50, unit: 'px', family: 'Helvetica Neue,sans-serif' }, - '50px "Helvetica Neue", "foo bar baz" , sans-serif', - { size: 50, unit: 'px', family: 'Helvetica Neue,foo bar baz,sans-serif' }, - "50px 'Helvetica Neue'", - { size: 50, unit: 'px', family: 'Helvetica Neue' }, - 'italic 20px Arial', - { size: 20, unit: 'px', style: 'italic', family: 'Arial' }, - 'oblique 20px Arial', - { size: 20, unit: 'px', style: 'oblique', family: 'Arial' }, - 'normal 20px Arial', - { size: 20, unit: 'px', style: 'normal', family: 'Arial' }, - '300 20px Arial', - { size: 20, unit: 'px', weight: '300', family: 'Arial' }, - '800 20px Arial', - { size: 20, unit: 'px', weight: '800', family: 'Arial' }, - 'bolder 20px Arial', - { size: 20, unit: 'px', weight: 'bolder', family: 'Arial' }, - 'lighter 20px Arial', - { size: 20, unit: 'px', weight: 'lighter', family: 'Arial' }, - 'normal normal normal 16px Impact', - { size: 16, unit: 'px', weight: 'normal', family: 'Impact', style: 'normal', variant: 'normal' }, - 'italic small-caps bolder 16px cursive', - { size: 16, unit: 'px', style: 'italic', variant: 'small-caps', weight: 'bolder', family: 'cursive' }, - '20px "new century schoolbook", serif', - { size: 20, unit: 'px', family: 'new century schoolbook,serif' }, - '20px "Arial bold 300"', // synthetic case with weight keyword inside family - { size: 20, unit: 'px', family: 'Arial bold 300', variant: 'normal' }, - `50px "Helvetica 'Neue'", "foo \\"bar\\" baz" , "Someone's weird \\'edge\\' case", sans-serif`, - { size: 50, unit: 'px', family: `Helvetica 'Neue',foo "bar" baz,Someone's weird 'edge' case,sans-serif` } - ] - - for (let i = 0, len = tests.length; i < len; ++i) { - const str = tests[i++] - const expected = tests[i] - const actual = parseFont(str) - - if (!expected.style) expected.style = 'normal' - if (!expected.weight) expected.weight = 'normal' - if (!expected.stretch) expected.stretch = 'normal' - if (!expected.variant) expected.variant = 'normal' - - assert.deepEqual(actual, expected, 'Failed to parse: ' + str) - } - - assert.strictEqual(parseFont('Helvetica, sans'), undefined) - }) - it('registerFont', function () { // Minimal test to make sure nothing is thrown registerFont('./examples/pfennigFont/Pfennig.ttf', { family: 'Pfennig' }) diff --git a/test/fontParser.test.js b/test/fontParser.test.js new file mode 100644 index 000000000..0302466b9 --- /dev/null +++ b/test/fontParser.test.js @@ -0,0 +1,118 @@ +/* eslint-env mocha */ + +'use strict' + +/** + * Module dependencies. + */ +const assert = require('assert') +const {Canvas} = require('..'); + +const tests = [ + '20px Arial', + { size: 20, families: ['arial'] }, + '20pt Arial', + { size: 26.666667461395264, families: ['arial'] }, + '20.5pt Arial', + { size: 27.333334147930145, families: ['arial'] }, + '20% Arial', + { size: 3.1999999284744263, families: ['arial'] }, + '20mm Arial', + { size: 75.59999942779541, families: ['arial'] }, + '20px serif', + { size: 20, families: ['serif'] }, + '20px sans-serif', + { size: 20, families: ['sans-serif'] }, + '20px monospace', + { size: 20, families: ['monospace'] }, + '50px Arial, sans-serif', + { size: 50, families: ['arial', 'sans-serif'] }, + 'bold italic 50px Arial, sans-serif', + { style: 1, weight: 700, size: 50, families: ['arial', 'sans-serif'] }, + '50px Helvetica , Arial, sans-serif', + { size: 50, families: ['helvetica', 'arial', 'sans-serif'] }, + '50px "Helvetica Neue", sans-serif', + { size: 50, families: ['Helvetica Neue', 'sans-serif'] }, + '50px "Helvetica Neue", "foo bar baz" , sans-serif', + { size: 50, families: ['Helvetica Neue', 'foo bar baz', 'sans-serif'] }, + "50px 'Helvetica Neue'", + { size: 50, families: ['Helvetica Neue'] }, + 'italic 20px Arial', + { size: 20, style: 1, families: ['arial'] }, + 'oblique 20px Arial', + { size: 20, style: 2, families: ['arial'] }, + 'normal 20px Arial', + { size: 20, families: ['arial'] }, + '300 20px Arial', + { size: 20, weight: 300, families: ['arial'] }, + '800 20px Arial', + { size: 20, weight: 800, families: ['arial'] }, + 'bolder 20px Arial', + { size: 20, weight: 700, families: ['arial'] }, + 'lighter 20px Arial', + { size: 20, weight: 100, families: ['arial'] }, + 'normal normal normal 16px Impact', + { size: 16, families: ['impact'] }, + 'italic small-caps bolder 16px cursive', + { size: 16, style: 1, variant: 1, weight: 700, families: ['cursive'] }, + '20px "new century schoolbook", serif', + { size: 20, families: ['new century schoolbook', 'serif'] }, + '20px "Arial bold 300"', // synthetic case with weight keyword inside family + { size: 20, families: ['Arial bold 300'] }, + `50px "Helvetica 'Neue'", "foo \\"bar\\" baz" , "Someone's weird \\'edge\\' case", sans-serif`, + { size: 50, families: [`Helvetica 'Neue'`, 'foo "bar" baz', `Someone's weird 'edge' case`, 'sans-serif'] }, + 'Helvetica, sans', + undefined, + '123px thefont/123abc', + undefined, + '123px /\tnormal thefont', + {size: 123, families: ['thefont']}, + '12px/1.2whoops arial', + undefined, + 'bold bold 12px thefont', + undefined, + 'italic italic 12px Arial', + undefined, + 'small-caps bold italic small-caps 12px Arial', + undefined, + 'small-caps bold oblique 12px \'A\'ri\\61l', + {size: 12, style: 2, weight: 700, variant: 1, families: ['Arial']}, + '12px/34% "The\\\n Word"', + {size: 12, families: ['The Word']}, + '', + undefined, + 'normal normal normal 1%/normal a , \'b\'', + {size: 0.1599999964237213, families: ['a', 'b']}, + 'normalnormalnormal 1px/normal a', + undefined, + '12px _the_font', + {size: 12, families: ['_the_font']}, + '9px 7 birds', + undefined, + '2em "Courier', + undefined, + `2em \\'Courier\\"`, + {size: 32, families: ['\'courier"']}, + '1px \\10abcde', + {size: 1, families: [String.fromCodePoint(parseInt('10abcd', 16)) + 'e']}, + '3E+2 1e-1px yay', + {weight: 300, size: 0.1, families: ['yay']} +]; + +describe('Font parser', function () { + for (let i = 0; i < tests.length; i++) { + const str = tests[i++] + it(str, function () { + const expected = tests[i] + const actual = Canvas.parseFont(str) + + if (expected) { + if (expected.style == null) expected.style = 0 + if (expected.weight == null) expected.weight = 400 + if (expected.variant == null) expected.variant = 0 + } + + assert.deepEqual(actual, expected) + }) + } +}) From da33bbed88946188385af6dc10368410ffede365 Mon Sep 17 00:00:00 2001 From: Fred Cox Date: Thu, 18 Jul 2024 12:44:34 +0100 Subject: [PATCH 445/474] Add link tags for pdfs Co-Authored-By: Caleb Hearon --- .gitignore | 1 + CHANGELOG.md | 2 ++ Readme.md | 20 ++++++++++++ examples/pdf-link.js | 20 ++++++++++++ index.d.ts | 2 ++ src/CanvasRenderingContext2d.cc | 56 ++++++++++++++++++++++++++++++++- src/CanvasRenderingContext2d.h | 4 +++ test/canvas.test.js | 37 ++++++++++++++++++++++ 8 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 examples/pdf-link.js diff --git a/.gitignore b/.gitignore index ff66b1103..4fd0b5eda 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ build test/images/*.png examples/*.png examples/*.jpg +examples/*.pdf testing out.png out.pdf diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e9e0ca08..11d8a039d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ project adheres to [Semantic Versioning](http://semver.org/). * `ctx.font` has a new C++ parser and is 2x-400x faster. Please file an issue if you experience different results, as caching has been removed. ### Added +* Support for accessibility and links in PDFs + ### Fixed 3.0.1 diff --git a/Readme.md b/Readme.md index 73c65d369..4cb17701c 100644 --- a/Readme.md +++ b/Readme.md @@ -515,6 +515,26 @@ ctx.addPage(400, 800) ctx.fillText('Hello World 2', 50, 80) ``` +It is possible to add hyperlinks using `.beginTag()` and `.endTag()`: + +```js +ctx.beginTag('Link', "uri='https://google.com'") +ctx.font = '22px Helvetica' +ctx.fillText('Hello World', 50, 80) +ctx.endTag('Link') +``` + +Or with a defined rectangle: + +```js +ctx.beginTag('Link', "uri='https://google.com' rect=[50 80 100 20]") +ctx.endTag('Link') +``` + +Note that the syntax for attributes is unique to Cairo. See [cairo_tag_begin](https://www.cairographics.org/manual/cairo-Tags-and-Links.html#cairo-tag-begin) for the full documentation. + +You can create areas on the canvas using the "cairo.dest" tag, and then link to them using the "Link" tag with the `dest=` attribute. You can also define PDF structure for accessibility by using tag names like "P", "H1", and "TABLE". The standard tags are defined in §14.8.4 of the [PDF 1.7](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf) specification. + See also: * [Image#dataMode](#imagedatamode) for embedding JPEGs in PDFs diff --git a/examples/pdf-link.js b/examples/pdf-link.js new file mode 100644 index 000000000..f6e40291b --- /dev/null +++ b/examples/pdf-link.js @@ -0,0 +1,20 @@ +const fs = require('fs') +const path = require('path') +const Canvas = require('..') + +const canvas = Canvas.createCanvas(400, 300, 'pdf') +const ctx = canvas.getContext('2d') + +ctx.beginTag('Link', 'uri=\'https://google.com\'') +ctx.font = '22px Helvetica' +ctx.fillText('Text link to Google', 110, 50) +ctx.endTag('Link') + +ctx.fillText('Rect link to node-canvas below!', 40, 180) + +ctx.beginTag('Link', 'uri=\'https://github.com/Automattic/node-canvas\' rect=[0 200 400 100]') +ctx.endTag('Link') + +fs.writeFile(path.join(__dirname, 'pdf-link.pdf'), canvas.toBuffer(), function (err) { + if (err) throw err +}) diff --git a/index.d.ts b/index.d.ts index ce21cef26..6458bc132 100644 --- a/index.d.ts +++ b/index.d.ts @@ -232,6 +232,8 @@ export class CanvasRenderingContext2D { createPattern(image: Canvas|Image, repetition: 'repeat' | 'repeat-x' | 'repeat-y' | 'no-repeat' | '' | null): CanvasPattern createLinearGradient(x0: number, y0: number, x1: number, y1: number): CanvasGradient; createRadialGradient(x0: number, y0: number, r0: number, x1: number, y1: number, r1: number): CanvasGradient; + beginTag(tagName: string, attributes?: string): void; + endTag(tagName: string): void; /** * _Non-standard_. Defaults to 'good'. Affects pattern (gradient, image, * etc.) rendering quality. diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 1597d089a..f8f217ad9 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -135,6 +135,10 @@ Context2d::Initialize(Napi::Env& env, Napi::Object& exports) { InstanceMethod<&Context2d::CreatePattern>("createPattern", napi_default_method), InstanceMethod<&Context2d::CreateLinearGradient>("createLinearGradient", napi_default_method), InstanceMethod<&Context2d::CreateRadialGradient>("createRadialGradient", napi_default_method), + #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0) + InstanceMethod<&Context2d::BeginTag>("beginTag", napi_default_method), + InstanceMethod<&Context2d::EndTag>("endTag", napi_default_method), + #endif InstanceAccessor<&Context2d::GetFormat>("pixelFormat", napi_default_jsproperty), InstanceAccessor<&Context2d::GetPatternQuality, &Context2d::SetPatternQuality>("patternQuality", napi_default_jsproperty), InstanceAccessor<&Context2d::GetImageSmoothingEnabled, &Context2d::SetImageSmoothingEnabled>("imageSmoothingEnabled", napi_default_jsproperty), @@ -419,7 +423,7 @@ Context2d::fill(bool preserve) { width = cairo_image_surface_get_width(patternSurface); height = y2 - y1; } - + cairo_new_path(_context); cairo_rectangle(_context, 0, 0, width, height); cairo_clip(_context); @@ -3348,3 +3352,53 @@ Context2d::Ellipse(const Napi::CallbackInfo& info) { } cairo_set_matrix(ctx, &save_matrix); } + +#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0) + +void +Context2d::BeginTag(const Napi::CallbackInfo& info) { + std::string tagName = ""; + std::string attributes = ""; + + if (info.Length() == 0) { + Napi::TypeError::New(env, "Tag name is required").ThrowAsJavaScriptException(); + return; + } else { + if (!info[0].IsString()) { + Napi::TypeError::New(env, "Tag name must be a string.").ThrowAsJavaScriptException(); + return; + } else { + tagName = info[0].As().Utf8Value(); + } + + if (info.Length() > 1) { + if (!info[1].IsString()) { + Napi::TypeError::New(env, "Attributes must be a string matching Cairo's attribute format").ThrowAsJavaScriptException(); + return; + } else { + attributes = info[1].As().Utf8Value(); + } + } + } + + cairo_tag_begin(_context, tagName.c_str(), attributes.c_str()); +} + +void +Context2d::EndTag(const Napi::CallbackInfo& info) { + if (info.Length() == 0) { + Napi::TypeError::New(env, "Tag name is required").ThrowAsJavaScriptException(); + return; + } + + if (!info[0].IsString()) { + Napi::TypeError::New(env, "Tag name must be a string.").ThrowAsJavaScriptException(); + return; + } + + std::string tagName = info[0].As().Utf8Value(); + + cairo_tag_end(_context, tagName.c_str()); +} + +#endif diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index 745106e2d..a78788451 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -178,6 +178,10 @@ class Context2d : public Napi::ObjectWrap { void SetFont(const Napi::CallbackInfo& info, const Napi::Value& value); void SetTextBaseline(const Napi::CallbackInfo& info, const Napi::Value& value); void SetTextAlign(const Napi::CallbackInfo& info, const Napi::Value& value); + #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0) + void BeginTag(const Napi::CallbackInfo& info); + void EndTag(const Napi::CallbackInfo& info); + #endif inline void setContext(cairo_t *ctx) { _context = ctx; } inline cairo_t *context(){ return _context; } inline Canvas *canvas(){ return _canvas; } diff --git a/test/canvas.test.js b/test/canvas.test.js index 75f15ed5a..ecadbc7ec 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -755,6 +755,11 @@ describe('Canvas', function () { assertPixel(0xffff0000, 5, 0, 'first red pixel') }) }) + + it('Canvas#toBuffer("application/pdf")', function () { + const buf = createCanvas(200, 200, 'pdf').toBuffer('application/pdf') + assert.equal('PDF', buf.slice(1, 4).toString()) + }) }) describe('#toDataURL()', function () { @@ -2000,4 +2005,36 @@ describe('Canvas', function () { }) } }) + + describe('Context2d#beingTag()/endTag()', function () { + before(function () { + const canvas = createCanvas(20, 20, 'pdf') + const ctx = canvas.getContext('2d') + if (!('beginTag' in ctx)) { + this.skip() + } + }) + + it('generates a pdf', function () { + const canvas = createCanvas(20, 20, 'pdf') + const ctx = canvas.getContext('2d') + ctx.beginTag('Link', "uri='http://example.com'") + ctx.strokeText('hello', 0, 0) + ctx.endTag('Link') + const buf = canvas.toBuffer('application/pdf') + assert.equal('PDF', buf.slice(1, 4).toString()) + }) + + it('requires tag argument', function () { + const canvas = createCanvas(20, 20, 'pdf') + const ctx = canvas.getContext('2d') + assert.throws(() => { ctx.beginTag() }) + }) + + it('requires attributes to be a string', function () { + const canvas = createCanvas(20, 20, 'pdf') + const ctx = canvas.getContext('2d') + assert.throws(() => { ctx.beginTag('Link', {}) }) + }) + }) }) From a0c80314687ed278803d3143d9a7f88c8575837f Mon Sep 17 00:00:00 2001 From: Philippe Plantier Date: Wed, 26 Oct 2022 11:47:40 +0200 Subject: [PATCH 446/474] getImageData fixes when rectangle is outside of canvas fix a crash in getImageData if the rectangle is outside the canvas return transparent black pixels when getting image data outside the canvas remove dead code, add comments Fixes #2024 Fixes #1849 --- CHANGELOG.md | 2 + src/CanvasRenderingContext2d.cc | 50 ++-- test/canvas.test.js | 478 ++++++++++++++++++++++++++++++++ test/public/tests.js | 22 ++ 4 files changed, 533 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11d8a039d..ed1b43c00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ project adheres to [Semantic Versioning](http://semver.org/). * Support for accessibility and links in PDFs ### Fixed +* Fix a crash in `getImageData` when the rectangle is entirely outside the canvas. ([#2024](https://github.com/Automattic/node-canvas/issues/2024)) +* Fix `getImageData` cropping the resulting `ImageData` when the given rectangle is partly outside the canvas. ([#1849](https://github.com/Automattic/node-canvas/issues/1849)) 3.0.1 ================== diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index f8f217ad9..dfbcc17a0 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -1011,21 +1011,26 @@ Context2d::GetImageData(const Napi::CallbackInfo& info) { sh = -sh; } - if (sx + sw > width) sw = width - sx; - if (sy + sh > height) sh = height - sy; - - // WebKit/moz functionality. node-canvas used to return in either case. - if (sw <= 0) sw = 1; - if (sh <= 0) sh = 1; - - // Non-compliant. "Pixels outside the canvas must be returned as transparent - // black." This instead clips the returned array to the canvas area. + // Width and height to actually copy + int cw = sw; + int ch = sh; + // Offsets in the destination image + int ox = 0; + int oy = 0; + + // Clamp the copy width and height if the copy would go outside the image + if (sx + sw > width) cw = width - sx; + if (sy + sh > height) ch = height - sy; + + // Clamp the copy origin if the copy would go outside the image if (sx < 0) { - sw += sx; + ox = -sx; + cw += sx; sx = 0; } if (sy < 0) { - sh += sy; + oy = -sy; + ch += sy; sy = 0; } @@ -1047,13 +1052,16 @@ Context2d::GetImageData(const Napi::CallbackInfo& info) { uint8_t *dst = (uint8_t *)buffer.Data(); + if (!(cw > 0 && ch > 0)) goto return_empty; + switch (canvas->backend()->getFormat()) { case CAIRO_FORMAT_ARGB32: { + dst += oy * dstStride + ox * 4; // Rearrange alpha (argb -> rgba), undo alpha pre-multiplication, // and store in big-endian format - for (int y = 0; y < sh; ++y) { + for (int y = 0; y < ch; ++y) { uint32_t *row = (uint32_t *)(src + srcStride * (y + sy)); - for (int x = 0; x < sw; ++x) { + for (int x = 0; x < cw; ++x) { int bx = x * 4; uint32_t *pixel = row + x + sx; uint8_t a = *pixel >> 24; @@ -1082,10 +1090,11 @@ Context2d::GetImageData(const Napi::CallbackInfo& info) { break; } case CAIRO_FORMAT_RGB24: { + dst += oy * dstStride + ox * 4; // Rearrange alpha (argb -> rgba) and store in big-endian format - for (int y = 0; y < sh; ++y) { + for (int y = 0; y < ch; ++y) { uint32_t *row = (uint32_t *)(src + srcStride * (y + sy)); - for (int x = 0; x < sw; ++x) { + for (int x = 0; x < cw; ++x) { int bx = x * 4; uint32_t *pixel = row + x + sx; uint8_t r = *pixel >> 16; @@ -1102,9 +1111,10 @@ Context2d::GetImageData(const Napi::CallbackInfo& info) { break; } case CAIRO_FORMAT_A8: { - for (int y = 0; y < sh; ++y) { + dst += oy * dstStride + ox; + for (int y = 0; y < ch; ++y) { uint8_t *row = (uint8_t *)(src + srcStride * (y + sy)); - memcpy(dst, row + sx, dstStride); + memcpy(dst, row + sx, cw); dst += dstStride; } break; @@ -1116,9 +1126,10 @@ Context2d::GetImageData(const Napi::CallbackInfo& info) { break; } case CAIRO_FORMAT_RGB16_565: { - for (int y = 0; y < sh; ++y) { + dst += oy * dstStride + ox * 2; + for (int y = 0; y < ch; ++y) { uint16_t *row = (uint16_t *)(src + srcStride * (y + sy)); - memcpy(dst, row + sx, dstStride); + memcpy(dst, row + sx, cw * 2); dst += dstStride; } break; @@ -1138,6 +1149,7 @@ Context2d::GetImageData(const Napi::CallbackInfo& info) { } } +return_empty: Napi::Number swHandle = Napi::Number::New(env, sw); Napi::Number shHandle = Napi::Number::New(env, sh); Napi::Function ctor = env.GetInstanceData()->ImageDataCtor.Value(); diff --git a/test/canvas.test.js b/test/canvas.test.js index ecadbc7ec..48abb57b8 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -1251,6 +1251,430 @@ describe('Canvas', function () { it('works, slice, RGB30') + describe('slice partially outside the canvas', function () { + describe('left', function () { + if('works, RGBA32', function () { + const ctx = createTestCanvas() + const imageData = ctx.getImageData(-1, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + + assert.equal(255, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + }) + + it('works, RGB24', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB24' }) + const imageData = ctx.getImageData(-1, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + + assert.equal(255, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + }) + + it('works, RGB16_565', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB16_565' }) + const imageData = ctx.getImageData(-1, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(2, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal((255 & 0b11111) << 11, imageData.data[1]) + }) + + it('works, A8', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + const imageData = ctx.getImageData(-1, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(2, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(63, imageData.data[1]) + }) + }) + + describe('right', function () { + it('works, RGBA32', function () { + const ctx = createTestCanvas() + const imageData = ctx.getImageData(2, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(255, imageData.data[2]) + assert.equal(255, imageData.data[3]) + + assert.equal(0, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(0, imageData.data[7]) + }) + + it('works, RGB24', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB24' }) + const imageData = ctx.getImageData(2, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(255, imageData.data[2]) + assert.equal(255, imageData.data[3]) + + assert.equal(0, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(0, imageData.data[7]) + }) + + it('works, RGB16_565', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB16_565' }) + const imageData = ctx.getImageData(2, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(2, imageData.data.length) + + assert.equal((255 & 0b11111), imageData.data[0]) + assert.equal(0, imageData.data[1]) + }) + + it('works, A8', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + const imageData = ctx.getImageData(2, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(2, imageData.data.length) + + assert.equal(191, imageData.data[0]) + assert.equal(0, imageData.data[1]) + }) + }) + + describe('left and right', function () { + it('works, RGBA32', function () { + const ctx = createTestCanvas() + const imageData = ctx.getImageData(-1, 0, 5, 1) + assert.equal(5, imageData.width) + assert.equal(1, imageData.height) + assert.equal(20, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + + assert.equal(255, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + + assert.equal(0, imageData.data[8]) + assert.equal(255, imageData.data[9]) + assert.equal(0, imageData.data[10]) + assert.equal(255, imageData.data[11]) + + assert.equal(0, imageData.data[12]) + assert.equal(0, imageData.data[13]) + assert.equal(255, imageData.data[14]) + assert.equal(255, imageData.data[15]) + + assert.equal(0, imageData.data[16]) + assert.equal(0, imageData.data[17]) + assert.equal(0, imageData.data[18]) + assert.equal(0, imageData.data[19]) + }) + + it('works, RGB24', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB24' }) + const imageData = ctx.getImageData(-1, 0, 5, 1) + assert.equal(5, imageData.width) + assert.equal(1, imageData.height) + assert.equal(20, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + + assert.equal(255, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + + assert.equal(0, imageData.data[8]) + assert.equal(255, imageData.data[9]) + assert.equal(0, imageData.data[10]) + assert.equal(255, imageData.data[11]) + + assert.equal(0, imageData.data[12]) + assert.equal(0, imageData.data[13]) + assert.equal(255, imageData.data[14]) + assert.equal(255, imageData.data[15]) + + assert.equal(0, imageData.data[16]) + assert.equal(0, imageData.data[17]) + assert.equal(0, imageData.data[18]) + assert.equal(0, imageData.data[19]) + }) + + it('works, RGB16_565', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB16_565' }) + const imageData = ctx.getImageData(-1, 0, 5, 1) + assert.equal(5, imageData.width) + assert.equal(1, imageData.height) + assert.equal(5, imageData.data.length) + + assert.equal(0, imageData.data[0]) + + assert.equal((255 & 0b11111) << 11, imageData.data[1]) + assert.equal((255 & 0b111111) << 5, imageData.data[2]) + assert.equal((255 & 0b11111), imageData.data[3]) + + assert.equal(0, imageData.data[4]) + }) + + it('works, A8', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + const imageData = ctx.getImageData(-1, 0, 5, 1) + assert.equal(5, imageData.width) + assert.equal(1, imageData.height) + assert.equal(5, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(63, imageData.data[1]) + assert.equal(127, imageData.data[2]) + assert.equal(191, imageData.data[3]) + assert.equal(0, imageData.data[4]) + }) + }) + + describe('top', function () { + it('works, RGBA32', function () { + const ctx = createTestCanvas() + const imageData = ctx.getImageData(0, -1, 1, 2) + assert.equal(1, imageData.width) + assert.equal(2, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + + assert.equal(255, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + }) + + it('works, RGB24', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB24' }) + const imageData = ctx.getImageData(0, -1, 1, 2) + assert.equal(1, imageData.width) + assert.equal(2, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + + assert.equal(255, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + }) + + it('works, RGB16_565', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB16_565' }) + const imageData = ctx.getImageData(0, -1, 1, 2) + assert.equal(1, imageData.width) + assert.equal(2, imageData.height) + assert.equal(2, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal((255 & 0b11111) << 11, imageData.data[1]) + }) + + it('works, A8', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + const imageData = ctx.getImageData(0, -1, 1, 2) + assert.equal(1, imageData.width) + assert.equal(2, imageData.height) + assert.equal(2, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(63, imageData.data[1]) + }) + }) + + describe('bottom', function () { + it('works, RGBA32', function () { + const ctx = createTestCanvas() + const imageData = ctx.getImageData(0, 5, 1, 2) + assert.equal(1, imageData.width) + assert.equal(2, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(255, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(255, imageData.data[3]) + + assert.equal(0, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(0, imageData.data[7]) + }) + + it('works, RGB24', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB24' }) + const imageData = ctx.getImageData(0, 5, 1, 2) + assert.equal(1, imageData.width) + assert.equal(2, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(255, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(255, imageData.data[3]) + + assert.equal(0, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(0, imageData.data[7]) + }) + + it('works, RGB16_565', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB16_565' }) + const imageData = ctx.getImageData(0, 5, 1, 2) + assert.equal(1, imageData.width) + assert.equal(2, imageData.height) + assert.equal(2, imageData.data.length) + + assert.equal((255 & 0b11111) << 11, imageData.data[0]) + assert.equal(0, imageData.data[1]) + }) + + it('works, A8', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + const imageData = ctx.getImageData(0, 5, 1, 2) + assert.equal(1, imageData.width) + assert.equal(2, imageData.height) + assert.equal(2, imageData.data.length) + + assert.equal(63, imageData.data[0]) + assert.equal(0, imageData.data[1]) + }) + }) + + describe('top to bottom', function () { + it('works, RGBA32', function () { + const ctx = createTestCanvas() + const imageData = ctx.getImageData(0, -1, 1, 8) + assert.equal(1, imageData.width) + assert.equal(8, imageData.height) + assert.equal(32, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + + assert.equal(255, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + + assert.equal(255, imageData.data[24]) + assert.equal(0, imageData.data[25]) + assert.equal(0, imageData.data[26]) + assert.equal(255, imageData.data[27]) + + assert.equal(0, imageData.data[28]) + assert.equal(0, imageData.data[29]) + assert.equal(0, imageData.data[30]) + assert.equal(0, imageData.data[31]) + }) + + it('works, RGB24', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB24' }) + const imageData = ctx.getImageData(0, -1, 1, 8) + assert.equal(1, imageData.width) + assert.equal(8, imageData.height) + assert.equal(32, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + + assert.equal(255, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + + assert.equal(255, imageData.data[24]) + assert.equal(0, imageData.data[25]) + assert.equal(0, imageData.data[26]) + assert.equal(255, imageData.data[27]) + + assert.equal(0, imageData.data[28]) + assert.equal(0, imageData.data[29]) + assert.equal(0, imageData.data[30]) + assert.equal(0, imageData.data[31]) + }) + + it('works, RGB16_565', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB16_565' }) + const imageData = ctx.getImageData(0, -1, 1, 8) + assert.equal(1, imageData.width) + assert.equal(8, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal((255 & 0b11111) << 11, imageData.data[1]) + assert.equal((255 & 0b11111) << 11, imageData.data[6]) + assert.equal(0, imageData.data[7]) + }) + + it('works, A8', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + const imageData = ctx.getImageData(0, -1, 1, 8) + assert.equal(1, imageData.width) + assert.equal(8, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(63, imageData.data[1]) + assert.equal(63, imageData.data[6]) + assert.equal(0, imageData.data[7]) + }) + }) + }) + it('works, assignment', function () { const ctx = createTestCanvas() const data = ctx.getImageData(0, 0, 5, 5).data @@ -1274,6 +1698,60 @@ describe('Canvas', function () { ctx.getImageData(0, 0, 3, 6) }) }) + + describe('does not throw if rectangle is outside the canvas (#2024)', function () { + it('on the left', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + + const imageData = ctx.getImageData(-11, 0, 2, 2); + assert.equal(2, imageData.width) + assert.equal(2, imageData.height) + assert.equal(4, imageData.data.length) + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + }) + + it('on the right', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + + const imageData = ctx.getImageData(98, 0, 2, 2); + assert.equal(2, imageData.width) + assert.equal(2, imageData.height) + assert.equal(4, imageData.data.length) + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + }) + + it('on the top', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + + const imageData = ctx.getImageData(0, -12, 2, 2); + assert.equal(2, imageData.width) + assert.equal(2, imageData.height) + assert.equal(4, imageData.data.length) + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + }) + + it('on the bottom', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + + const imageData = ctx.getImageData(0, 98, 2, 2); + assert.equal(2, imageData.width) + assert.equal(2, imageData.height) + assert.equal(4, imageData.data.length) + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + }) + }) }) it('Context2d#createPattern(Canvas)', function () { diff --git a/test/public/tests.js b/test/public/tests.js index c904cde7e..89d5ba67e 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -2378,6 +2378,28 @@ tests['putImageData() 10'] = function (ctx) { ctx.putImageData(data, 20, 120) } +tests['putImageData() 11'] = function (ctx) { + for (let i = 0; i < 8; i++) { + for (let j = 0; j < 8; j++) { + ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' + ctx.fillRect(j * 25, i * 25, 25, 25) + } + } + const data = ctx.getImageData(-25, -25, 50, 50) + ctx.putImageData(data, 10, 10) +} + +tests['putImageData() 12'] = function (ctx) { + for (let i = 0; i < 8; i++) { + for (let j = 0; j < 8; j++) { + ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' + ctx.fillRect(j * 25, i * 25, 25, 25) + } + } + const data = ctx.getImageData(175, 175, 50, 50) + ctx.putImageData(data, 10, 10) +} + tests['putImageData() alpha'] = function (ctx) { ctx.fillStyle = 'rgba(255,0,0,0.5)' ctx.fillRect(0, 0, 50, 100) From 0b2edc1ba91303087dcd3584e97dfa90581b375d Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sun, 12 Jan 2025 13:50:26 -0500 Subject: [PATCH 447/474] remove reference to old JS parseFont --- browser.js | 4 ---- index.d.ts | 3 --- 2 files changed, 7 deletions(-) diff --git a/browser.js b/browser.js index 0267a75a2..df6b3e533 100644 --- a/browser.js +++ b/browser.js @@ -1,9 +1,5 @@ /* globals document, ImageData */ -const parseFont = require('./lib/parse-font') - -exports.parseFont = parseFont - exports.createCanvas = function (width, height) { return Object.assign(document.createElement('canvas'), { width: width, height: height }) } diff --git a/index.d.ts b/index.d.ts index 6458bc132..4cce92c3e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -489,9 +489,6 @@ export class ImageData { readonly width: number; } -// This is marked private, but is exported... -// export function parseFont(description: string): object - // Not documented: backends /** Library version. */ From 52330b89b70ac1cdaf6fb3c8331d675b65aaa0cf Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Mon, 13 Jan 2025 22:45:21 -0500 Subject: [PATCH 448/474] support ctx.direction and textAlign start/end --- CHANGELOG.md | 2 ++ index.d.ts | 1 + src/Canvas.h | 1 - src/CanvasRenderingContext2d.cc | 46 ++++++++++++++++++++++++++++----- src/CanvasRenderingContext2d.h | 5 +++- test/canvas.test.js | 2 -- 6 files changed, 47 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed1b43c00..8aa4a39e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Added * Support for accessibility and links in PDFs +* `ctx.direction` is implemented: `'rtl'` or `'ltr'` set the base direction of text +* `ctx.textAlign` `'start'` and `'end'` are now `'right'` and `'left'` when `ctx.direction === 'rtl'` ### Fixed * Fix a crash in `getImageData` when the rectangle is entirely outside the canvas. ([#2024](https://github.com/Automattic/node-canvas/issues/2024)) diff --git a/index.d.ts b/index.d.ts index 4cce92c3e..43ff107d0 100644 --- a/index.d.ts +++ b/index.d.ts @@ -290,6 +290,7 @@ export class CanvasRenderingContext2D { textBaseline: CanvasTextBaseline; textAlign: CanvasTextAlign; canvas: Canvas; + direction: 'ltr' | 'rtl'; } export class CanvasGradient { diff --git a/src/Canvas.h b/src/Canvas.h index 5b039539a..eb19843e5 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -37,7 +37,6 @@ enum text_align_t : int8_t { TEXT_ALIGNMENT_LEFT = -1, TEXT_ALIGNMENT_CENTER = 0, TEXT_ALIGNMENT_RIGHT = 1, - // Currently same as LEFT and RIGHT without RTL support: TEXT_ALIGNMENT_START = -2, TEXT_ALIGNMENT_END = 2 }; diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index dfbcc17a0..d294a4b35 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -161,7 +161,8 @@ Context2d::Initialize(Napi::Env& env, Napi::Object& exports) { InstanceAccessor<&Context2d::GetStrokeStyle, &Context2d::SetStrokeStyle>("strokeStyle", napi_default_jsproperty), InstanceAccessor<&Context2d::GetFont, &Context2d::SetFont>("font", napi_default_jsproperty), InstanceAccessor<&Context2d::GetTextBaseline, &Context2d::SetTextBaseline>("textBaseline", napi_default_jsproperty), - InstanceAccessor<&Context2d::GetTextAlign, &Context2d::SetTextAlign>("textAlign", napi_default_jsproperty) + InstanceAccessor<&Context2d::GetTextAlign, &Context2d::SetTextAlign>("textAlign", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetDirection, &Context2d::SetDirection>("direction", napi_default_jsproperty) }); exports.Set("CanvasRenderingContext2d", ctor); @@ -230,6 +231,8 @@ Context2d::Context2d(const Napi::CallbackInfo& info) : Napi::ObjectWrapfontDescription); @@ -762,6 +765,27 @@ Context2d::AddPage(const Napi::CallbackInfo& info) { cairo_pdf_surface_set_size(canvas()->surface(), width, height); } +/* + * Get text direction. + */ +Napi::Value +Context2d::GetDirection(const Napi::CallbackInfo& info) { + return Napi::String::New(env, state->direction); +} + +/* + * Set text direction. + */ +void +Context2d::SetDirection(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (!value.IsString()) return; + + std::string dir = value.As(); + if (dir != "ltr" && dir != "rtl") return; + + state->direction = dir; +} + /* * Put image data. * @@ -2451,6 +2475,9 @@ Context2d::paintText(const Napi::CallbackInfo&info, bool stroke) { pango_layout_set_text(layout, str.c_str(), -1); pango_cairo_update_layout(context(), layout); + PangoDirection pango_dir = state->direction == "ltr" ? PANGO_DIRECTION_LTR : PANGO_DIRECTION_RTL; + pango_context_set_base_dir(pango_layout_get_context(_layout), pango_dir); + if (argsNum == 3) { scaled_by = get_text_scale(layout, args[2]); cairo_save(context()); @@ -2522,18 +2549,26 @@ inline double getBaselineAdjustment(PangoLayout* layout, short baseline) { void Context2d::setTextPath(double x, double y) { PangoRectangle logical_rect; + text_align_t alignment = state->textAlignment; - switch (state->textAlignment) { + // Convert start/end to left/right based on direction + if (alignment == TEXT_ALIGNMENT_START) { + alignment = (state->direction == "rtl") ? TEXT_ALIGNMENT_RIGHT : TEXT_ALIGNMENT_LEFT; + } else if (alignment == TEXT_ALIGNMENT_END) { + alignment = (state->direction == "rtl") ? TEXT_ALIGNMENT_LEFT : TEXT_ALIGNMENT_RIGHT; + } + + switch (alignment) { case TEXT_ALIGNMENT_CENTER: pango_layout_get_pixel_extents(_layout, NULL, &logical_rect); x -= logical_rect.width / 2; break; - case TEXT_ALIGNMENT_END: case TEXT_ALIGNMENT_RIGHT: pango_layout_get_pixel_extents(_layout, NULL, &logical_rect); x -= logical_rect.width; break; - default: ; + default: // TEXT_ALIGNMENT_LEFT + break; } y -= getBaselineAdjustment(_layout, state->textBaseline); @@ -2687,13 +2722,12 @@ Napi::Value Context2d::GetTextAlign(const Napi::CallbackInfo& info) { const char* align; switch (state->textAlignment) { - default: - // TODO the default is supposed to be "start" case TEXT_ALIGNMENT_LEFT: align = "left"; break; case TEXT_ALIGNMENT_START: align = "start"; break; case TEXT_ALIGNMENT_CENTER: align = "center"; break; case TEXT_ALIGNMENT_RIGHT: align = "right"; break; case TEXT_ALIGNMENT_END: align = "end"; break; + default: align = "start"; } return Napi::String::New(env, align); } diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index a78788451..16cd42d34 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -31,10 +31,11 @@ struct canvas_state_t { cairo_filter_t patternQuality = CAIRO_FILTER_GOOD; float globalAlpha = 1.f; int shadowBlur = 0; - text_align_t textAlignment = TEXT_ALIGNMENT_LEFT; // TODO default is supposed to be START + text_align_t textAlignment = TEXT_ALIGNMENT_START; text_baseline_t textBaseline = TEXT_BASELINE_ALPHABETIC; canvas_draw_mode_t textDrawingMode = TEXT_DRAW_PATHS; bool imageSmoothingEnabled = true; + std::string direction = "ltr"; canvas_state_t() { fontDescription = pango_font_description_from_string("sans"); @@ -182,6 +183,8 @@ class Context2d : public Napi::ObjectWrap { void BeginTag(const Napi::CallbackInfo& info); void EndTag(const Napi::CallbackInfo& info); #endif + Napi::Value GetDirection(const Napi::CallbackInfo& info); + void SetDirection(const Napi::CallbackInfo& info, const Napi::Value& value); inline void setContext(cairo_t *ctx) { _context = ctx; } inline cairo_t *context(){ return _context; } inline Canvas *canvas(){ return _canvas; } diff --git a/test/canvas.test.js b/test/canvas.test.js index 48abb57b8..d33bda63f 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -569,8 +569,6 @@ describe('Canvas', function () { const canvas = createCanvas(200, 200) const ctx = canvas.getContext('2d') - assert.equal('left', ctx.textAlign) // default TODO wrong default - ctx.textAlign = 'start' assert.equal('start', ctx.textAlign) ctx.textAlign = 'center' assert.equal('center', ctx.textAlign) From 88e965709234c5b3ebcedfad7405c56da658df88 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sat, 18 Jan 2025 11:17:16 -0500 Subject: [PATCH 449/474] allow registerFont after a canvas has been created (#2483) Fixes #1921 --- CHANGELOG.md | 1 + Readme.md | 2 +- src/Canvas.cc | 7 ++++++- src/Canvas.h | 2 ++ src/CanvasRenderingContext2d.cc | 20 +++++++++++++++++++- src/CanvasRenderingContext2d.h | 2 ++ 6 files changed, 31 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8aa4a39e4..33c2b1cd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed * Replaced `simple-get ` with ` Node.js builtin` `fetch` (#2309) * `ctx.font` has a new C++ parser and is 2x-400x faster. Please file an issue if you experience different results, as caching has been removed. +* The restriction of registering fonts before a canvas is created has been removed. You can now register a font as late as right before the `fillText` call ([#1921](https://github.com/Automattic/node-canvas/issues/1921)) ### Added * Support for accessibility and links in PDFs diff --git a/Readme.md b/Readme.md index 4cb17701c..d0429c520 100644 --- a/Readme.md +++ b/Readme.md @@ -163,7 +163,7 @@ const myimg = await loadImage('http://server.com/image.png') > registerFont(path: string, { family: string, weight?: string, style?: string }) => void > ``` -To use a font file that is not installed as a system font, use `registerFont()` to register the font with Canvas. *This must be done before the Canvas is created.* +To use a font file that is not installed as a system font, use `registerFont()` to register the font with Canvas. ```js const { registerFont, createCanvas } = require('canvas') diff --git a/src/Canvas.cc b/src/Canvas.cc index 7b208bec2..b4fe25120 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -38,7 +38,10 @@ using namespace std; -std::vector font_face_list; +std::vector Canvas::font_face_list; + +// Increases each time a font is (de)registered +int Canvas::fontSerial = 1; /* * Initialize Canvas. @@ -734,6 +737,7 @@ Canvas::RegisterFont(const Napi::CallbackInfo& info) { free(family); free(weight); free(style); + fontSerial++; } void @@ -749,6 +753,7 @@ Canvas::DeregisterAllFonts(const Napi::CallbackInfo& info) { }); font_face_list.clear(); + fontSerial++; if (!success) Napi::Error::New(env, "Could not deregister one or more fonts").ThrowAsJavaScriptException(); } diff --git a/src/Canvas.h b/src/Canvas.h index eb19843e5..3883c5137 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -91,9 +91,11 @@ class Canvas : public Napi::ObjectWrap { void resurface(Napi::Object This); Napi::Env env; + static int fontSerial; private: Backend* _backend; Napi::ObjectReference _jsBackend; Napi::FunctionReference ctor; + static std::vector font_face_list; }; diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index d294a4b35..449f5c07f 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -2450,8 +2450,24 @@ get_text_scale(PangoLayout *layout, double maxWidth) { } } +/* + * Make sure the layout's font list is up-to-date + */ +void +Context2d::checkFonts() { + // If fonts have been registered, the PangoContext is using an outdated FontMap + if (canvas()->fontSerial != fontSerial) { + pango_context_set_font_map( + pango_layout_get_context(_layout), + pango_cairo_font_map_get_default() + ); + + fontSerial = canvas()->fontSerial; + } +} + void -Context2d::paintText(const Napi::CallbackInfo&info, bool stroke) { +Context2d::paintText(const Napi::CallbackInfo& info, bool stroke) { int argsNum = info.Length() >= 4 ? 3 : 2; if (argsNum == 3 && info[3].IsUndefined()) @@ -2472,6 +2488,7 @@ Context2d::paintText(const Napi::CallbackInfo&info, bool stroke) { PangoLayout *layout = this->layout(); + checkFonts(); pango_layout_set_text(layout, str.c_str(), -1); pango_cairo_update_layout(context(), layout); @@ -2775,6 +2792,7 @@ Context2d::MeasureText(const Napi::CallbackInfo& info) { PangoFontMetrics *metrics; PangoLayout *layout = this->layout(); + checkFonts(); pango_layout_set_text(layout, str.Utf8Value().c_str(), -1); pango_cairo_update_layout(ctx, layout); diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index 16cd42d34..6b29b60f0 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -220,6 +220,7 @@ class Context2d : public Napi::ObjectWrap { void _setFillPattern(Napi::Value arg); void _setStrokeColor(Napi::Value arg); void _setStrokePattern(Napi::Value arg); + void checkFonts(); void paintText(const Napi::CallbackInfo&, bool); Napi::Reference _fillStyle; Napi::Reference _strokeStyle; @@ -227,4 +228,5 @@ class Context2d : public Napi::ObjectWrap { cairo_t *_context = nullptr; cairo_path_t *_path; PangoLayout *_layout = nullptr; + int fontSerial = 1; }; From 61e474e299b04babd4b5348bc15ba71bee42099e Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Tue, 21 Jan 2025 21:54:46 -0500 Subject: [PATCH 450/474] 3.1.0 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33c2b1cd0..b0383e8c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ project adheres to [Semantic Versioning](http://semver.org/). (Unreleased) ================== ### Changed +### Added +### Fixed + +3.1.0 +================== * Replaced `simple-get ` with ` Node.js builtin` `fetch` (#2309) * `ctx.font` has a new C++ parser and is 2x-400x faster. Please file an issue if you experience different results, as caching has been removed. * The restriction of registering fonts before a canvas is created has been removed. You can now register a font as late as right before the `fillText` call ([#1921](https://github.com/Automattic/node-canvas/issues/1921)) diff --git a/package.json b/package.json index 8d4133042..22b5f3f49 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "3.0.1", + "version": "3.1.0", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 2f9de7c913e3903c6e9b3caefa6d352cd403bb62 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sat, 22 Feb 2025 20:51:44 -0500 Subject: [PATCH 451/474] remove unused and un-freed GError docs say passing null is allowed --- src/Image.cc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Image.cc b/src/Image.cc index 559f8a36c..e6af93474 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -1471,9 +1471,8 @@ Image::loadSVGFromBuffer(uint8_t *buf, unsigned len) { _is_svg = true; cairo_status_t status; - GError *gerr = NULL; - if (NULL == (_rsvg = rsvg_handle_new_from_data(buf, len, &gerr))) { + if (NULL == (_rsvg = rsvg_handle_new_from_data(buf, len, nullptr))) { return CAIRO_STATUS_READ_ERROR; } From d24a127b0c3529f8593fe06191a1e72423ee2a69 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sat, 22 Feb 2025 20:53:08 -0500 Subject: [PATCH 452/474] double-free via g_object_unref that could crash Fixes #2486 --- CHANGELOG.md | 2 ++ src/Image.cc | 10 +--------- test/canvas.test.js | 9 +++++++++ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0383e8c1..08d3d458a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,9 @@ project adheres to [Semantic Versioning](http://semver.org/). ================== ### Changed ### Added + ### Fixed +* Fix a crash when SVGs without width or height are loaded (#2486) 3.1.0 ================== diff --git a/src/Image.cc b/src/Image.cc index e6af93474..a70f67566 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -1470,8 +1470,6 @@ cairo_status_t Image::loadSVGFromBuffer(uint8_t *buf, unsigned len) { _is_svg = true; - cairo_status_t status; - if (NULL == (_rsvg = rsvg_handle_new_from_data(buf, len, nullptr))) { return CAIRO_STATUS_READ_ERROR; } @@ -1484,13 +1482,7 @@ Image::loadSVGFromBuffer(uint8_t *buf, unsigned len) { width = naturalWidth = d_width; height = naturalHeight = d_height; - status = renderSVGToSurface(); - if (status != CAIRO_STATUS_SUCCESS) { - g_object_unref(_rsvg); - return status; - } - - return CAIRO_STATUS_SUCCESS; + return renderSVGToSurface(); } /* diff --git a/test/canvas.test.js b/test/canvas.test.js index d33bda63f..9dfe4d5a8 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -2513,4 +2513,13 @@ describe('Canvas', function () { assert.throws(() => { ctx.beginTag('Link', {}) }) }) }) + + describe('loadImage', function () { + it('doesn\'t crash when you don\'t specify width and height', async function () { + await assert.rejects(async () => { + const svg = ``; + await loadImage(Buffer.from(svg)); + }); + }); + }); }) From 9d5f104cb52644f878eb3bac4c29198f73f5f956 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sat, 22 Feb 2025 21:11:59 -0500 Subject: [PATCH 453/474] nicer error reporting when svg width/height missing --- src/Image.cc | 5 +++++ test/canvas.test.js | 9 ++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Image.cc b/src/Image.cc index a70f67566..ce0b51996 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -1482,6 +1482,11 @@ Image::loadSVGFromBuffer(uint8_t *buf, unsigned len) { width = naturalWidth = d_width; height = naturalHeight = d_height; + if (width <= 0 || height <= 0) { + this->errorInfo.set("Width and height must be set on the svg element"); + return CAIRO_STATUS_READ_ERROR; + } + return renderSVGToSurface(); } diff --git a/test/canvas.test.js b/test/canvas.test.js index 9dfe4d5a8..b45382a2a 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -2516,10 +2516,17 @@ describe('Canvas', function () { describe('loadImage', function () { it('doesn\'t crash when you don\'t specify width and height', async function () { + const err = {name: "Error"}; + + // TODO: remove this when we have a static build or something + if (os.platform() !== 'win32') { + err.message = "Width and height must be set on the svg element"; + } + await assert.rejects(async () => { const svg = ``; await loadImage(Buffer.from(svg)); - }); + }, err); }); }); }) From 367af8c01a9290102b3b59ae43a31a41d13f7927 Mon Sep 17 00:00:00 2001 From: Anton Gilgur Date: Sat, 29 Mar 2025 01:37:04 -0400 Subject: [PATCH 454/474] fix(deps): update `prebuild-install` to work on Node 22.14+ This installation error on Node 22.14+: ``` npm error prebuild-install warn This package does not support N-API version undefined npm error prebuild-install warn install No prebuilt binaries found (target=undefined runtime=napi arch=x64 libc= platform=linux) ``` is resolved by updating to newer `prebuild-install` 7.1.3, which has a newer version of `napi-build-utils` (2.0.0) with a bugfix for newer N-API version comparison --- CHANGELOG.md | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08d3d458a..88bff24f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Fixed * Fix a crash when SVGs without width or height are loaded (#2486) +* Fix fetching prebuilds during installation on certain newer versions of Node (#2497) 3.1.0 ================== diff --git a/package.json b/package.json index 22b5f3f49..2754cd381 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ ], "dependencies": { "node-addon-api": "^7.0.0", - "prebuild-install": "^7.1.1" + "prebuild-install": "^7.1.3" }, "devDependencies": { "@types/node": "^10.12.18", From 494035d3d67b6117f5602ac57ce4aa271be9c4cb Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Thu, 3 Apr 2025 21:44:54 -0400 Subject: [PATCH 455/474] leak in sync toBuffer Claude found it right away Fixes #2490 --- src/Canvas.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Canvas.cc b/src/Canvas.cc index b4fe25120..cc5c4f96e 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -368,7 +368,6 @@ static void setPdfMetadata(Canvas* canvas, Napi::Object opts) { Napi::Value Canvas::ToBuffer(const Napi::CallbackInfo& info) { - EncodingWorker *worker = new EncodingWorker(info.Env()); cairo_status_t status; // Vector canvases, sync only @@ -434,7 +433,6 @@ Canvas::ToBuffer(const Napi::CallbackInfo& info) { CairoError(ex).ThrowAsJavaScriptException(); } catch (const char* ex) { Napi::Error::New(env, ex).ThrowAsJavaScriptException(); - } return env.Undefined(); @@ -461,6 +459,7 @@ Canvas::ToBuffer(const Napi::CallbackInfo& info) { // Make sure the surface exists since we won't have an isolate context in the async block: surface(); + EncodingWorker* worker = new EncodingWorker(env); worker->Init(&ToPngBufferAsync, closure); worker->Queue(); @@ -498,6 +497,7 @@ Canvas::ToBuffer(const Napi::CallbackInfo& info) { // Make sure the surface exists since we won't have an isolate context in the async block: surface(); + EncodingWorker* worker = new EncodingWorker(env); worker->Init(&ToJpegBufferAsync, closure); worker->Queue(); return env.Undefined(); From b50b392d569fd9fc3c564d99fe16ef803db70e94 Mon Sep 17 00:00:00 2001 From: truman126 Date: Mon, 17 Mar 2025 14:10:10 -0300 Subject: [PATCH 456/474] added a check to make sure maxWidth > 0 Fixes #2171 --- CHANGELOG.md | 1 + src/CanvasRenderingContext2d.cc | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88bff24f7..cd13a7f2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Fixed * Fix a crash when SVGs without width or height are loaded (#2486) * Fix fetching prebuilds during installation on certain newer versions of Node (#2497) +* Fixed issue with fillText that was breaking subsequent fillText calls (#2171) 3.1.0 ================== diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 449f5c07f..06c31df75 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -2496,6 +2496,7 @@ Context2d::paintText(const Napi::CallbackInfo& info, bool stroke) { pango_context_set_base_dir(pango_layout_get_context(_layout), pango_dir); if (argsNum == 3) { + if (args[2] <= 0) return; scaled_by = get_text_scale(layout, args[2]); cairo_save(context()); cairo_scale(context(), scaled_by, 1); From c9363611daf7768130f7d0a4d491defa8bb459b5 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Fri, 11 Apr 2025 18:43:42 -0400 Subject: [PATCH 457/474] add visual test for #2498 --- test/public/tests.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/public/tests.js b/test/public/tests.js index 89d5ba67e..165d847e6 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -3,6 +3,8 @@ let Image let imageSrc const tests = {} +/* global btoa */ + if (typeof module !== 'undefined' && module.exports) { module.exports = tests Image = require('../../').Image @@ -2813,3 +2815,20 @@ tests['no exif orientation'] = function (ctx, done) { } img.src = imageSrc(`exif-orientation-fn.jpg`) } + +tests['scaling SVGs'] = function (ctx, done) { + const img = new Image() + + img.onload = function () { + img.width = 200 + img.height = 200 + ctx.drawImage(img, 0, 0, 200, 200) + done() + } + + img.src = 'data:image/svg+xml;base64,' + btoa(` + + + + `) +} From 1234a86f65b2318906334ce0790cd12401c67a25 Mon Sep 17 00:00:00 2001 From: Mike Gilfillan Date: Thu, 3 Apr 2025 12:51:41 +0100 Subject: [PATCH 458/474] Render at natural dimensions to fix scaling issues Fixes #2498 --- CHANGELOG.md | 1 + src/Image.cc | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd13a7f2a..4cb978ce9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * Fix a crash when SVGs without width or height are loaded (#2486) * Fix fetching prebuilds during installation on certain newer versions of Node (#2497) * Fixed issue with fillText that was breaking subsequent fillText calls (#2171) +* Fix svg rendering when the image is resized (#2498) 3.1.0 ================== diff --git a/src/Image.cc b/src/Image.cc index ce0b51996..973736505 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -1506,9 +1506,6 @@ Image::renderSVGToSurface() { } cairo_t *cr = cairo_create(_surface); - cairo_scale(cr, - (double)width / (double)naturalWidth, - (double)height / (double)naturalHeight); status = cairo_status(cr); if (status != CAIRO_STATUS_SUCCESS) { g_object_unref(_rsvg); From 772c464a4d6eaa8e9c9a06a69ba92e67d75d116a Mon Sep 17 00:00:00 2001 From: Shachar <34343793+ShaMan123@users.noreply.github.com> Date: Mon, 16 Jun 2025 03:31:52 +0300 Subject: [PATCH 459/474] fix(TextMetrics): rtl direction + start/end textAlign (#2510) * fix(TextMetrics): rtl direction + start/end textAlign * changelog * update tests * expose resolveTextAlignment * jest test --- CHANGELOG.md | 1 + src/CanvasRenderingContext2d.cc | 29 ++++++++++++++++++----------- src/CanvasRenderingContext2d.h | 1 + test/canvas.test.js | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cb978ce9..af2da3fe6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * Fix fetching prebuilds during installation on certain newer versions of Node (#2497) * Fixed issue with fillText that was breaking subsequent fillText calls (#2171) * Fix svg rendering when the image is resized (#2498) +* Fix measureText with direction rtl textAlign start/end 3.1.0 ================== diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 06c31df75..15c5c80f5 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -2557,6 +2557,20 @@ inline double getBaselineAdjustment(PangoLayout* layout, short baseline) { } } +text_align_t +Context2d::resolveTextAlignment() { + text_align_t alignment = state->textAlignment; + + // Convert start/end to left/right based on direction + if (alignment == TEXT_ALIGNMENT_START) { + return (state->direction == "rtl") ? TEXT_ALIGNMENT_RIGHT : TEXT_ALIGNMENT_LEFT; + } else if (alignment == TEXT_ALIGNMENT_END) { + return (state->direction == "rtl") ? TEXT_ALIGNMENT_LEFT : TEXT_ALIGNMENT_RIGHT; + } + + return alignment; +} + /* * Set text path for the string in the layout at (x, y). * This function is called by paintText and won't behave correctly @@ -2567,14 +2581,7 @@ inline double getBaselineAdjustment(PangoLayout* layout, short baseline) { void Context2d::setTextPath(double x, double y) { PangoRectangle logical_rect; - text_align_t alignment = state->textAlignment; - - // Convert start/end to left/right based on direction - if (alignment == TEXT_ALIGNMENT_START) { - alignment = (state->direction == "rtl") ? TEXT_ALIGNMENT_RIGHT : TEXT_ALIGNMENT_LEFT; - } else if (alignment == TEXT_ALIGNMENT_END) { - alignment = (state->direction == "rtl") ? TEXT_ALIGNMENT_LEFT : TEXT_ALIGNMENT_RIGHT; - } + text_align_t alignment = resolveTextAlignment(); switch (alignment) { case TEXT_ALIGNMENT_CENTER: @@ -2816,16 +2823,16 @@ Context2d::MeasureText(const Napi::CallbackInfo& info) { metrics = PANGO_LAYOUT_GET_METRICS(layout); + text_align_t alignment = resolveTextAlignment(); + double x_offset; - switch (state->textAlignment) { + switch (alignment) { case TEXT_ALIGNMENT_CENTER: x_offset = logical_rect.width / 2.; break; - case TEXT_ALIGNMENT_END: case TEXT_ALIGNMENT_RIGHT: x_offset = logical_rect.width; break; - case TEXT_ALIGNMENT_START: case TEXT_ALIGNMENT_LEFT: default: x_offset = 0.0; diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index 6b29b60f0..341e5936d 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -222,6 +222,7 @@ class Context2d : public Napi::ObjectWrap { void _setStrokePattern(Napi::Value arg); void checkFonts(); void paintText(const Napi::CallbackInfo&, bool); + text_align_t resolveTextAlignment(); Napi::Reference _fillStyle; Napi::Reference _strokeStyle; Canvas *_canvas; diff --git a/test/canvas.test.js b/test/canvas.test.js index b45382a2a..01156d089 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -1026,6 +1026,39 @@ describe('Canvas', function () { assertApprox(rm.actualBoundingBoxLeft, 19, 6) assertApprox(rm.actualBoundingBoxRight, 1, 6) }) + + it('resolves text alignment wrt Context2d#direction #2508', function () { + const canvas = createCanvas(0, 0) + const ctx = canvas.getContext('2d') + + ctx.textAlign = "left"; + const leftMetrics = ctx.measureText('hello'); + assert(leftMetrics.actualBoundingBoxLeft < leftMetrics.actualBoundingBoxRight, "leftMetrics.actualBoundingBoxLeft < leftMetrics.actualBoundingBoxRight"); + + ctx.textAlign = "right"; + const rightMetrics = ctx.measureText('hello'); + assert(rightMetrics.actualBoundingBoxLeft > rightMetrics.actualBoundingBoxRight, "metrics.actualBoundingBoxLeft > metrics.actualBoundingBoxRight"); + + ctx.textAlign = "start"; + + ctx.direction = "ltr"; + const ltrStartMetrics = ctx.measureText('hello'); + assert.deepStrictEqual(ltrStartMetrics, leftMetrics, "ltr start metrics should equal left metrics"); + + ctx.direction = "rtl"; + const rtlStartMetrics = ctx.measureText('hello'); + assert.deepStrictEqual(rtlStartMetrics, rightMetrics, "rtl start metrics should equal right metrics"); + + ctx.textAlign = "end"; + + ctx.direction = "ltr"; + const ltrEndMetrics = ctx.measureText('hello'); + assert.deepStrictEqual(ltrEndMetrics, rightMetrics, "ltr end metrics should equal right metrics"); + + ctx.direction = "rtl"; + const rtlEndMetrics = ctx.measureText('hello'); + assert.deepStrictEqual(rtlEndMetrics, leftMetrics, "rtl end metrics should equal left metrics"); + }) }) it('Context2d#fillText()', function () { From 05a53d83a7c55fd8d0dc4c42914f44d1a6dc78b0 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sun, 15 Jun 2025 22:10:17 -0400 Subject: [PATCH 460/474] add node 24 to ci --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 83ddb105b..b523a3e33 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [18.20.5, 20.18.1, 22.12.0, 23.3.0] + node: [18.20.5, 20.18.1, 22.12.0, 23.3.0, 24.2.0] steps: - uses: actions/setup-node@v4 with: @@ -33,7 +33,7 @@ jobs: runs-on: windows-2019 strategy: matrix: - node: [18.20.5, 20.18.1, 22.12.0, 23.3.0] + node: [18.20.5, 20.18.1, 22.12.0, 23.3.0, 24.2.0] steps: - uses: actions/setup-node@v4 with: @@ -57,7 +57,7 @@ jobs: runs-on: macos-15 strategy: matrix: - node: [18.20.5, 20.18.1, 22.12.0, 23.3.0] + node: [18.20.5, 20.18.1, 22.12.0, 23.3.0, 24.2.0] steps: - uses: actions/setup-node@v4 with: From f5f103abb81783bba8390b09d7fdb42703e9f877 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sun, 15 Jun 2025 22:39:08 -0400 Subject: [PATCH 461/474] report correct freed size when resurfacing A little kludgey to force 2 calls in canvas.cc but 2 calls are necessary when changing size anyways, so it's the more general usage. TODO remove backends anyways Fixes #2514 --- src/Canvas.cc | 3 ++- src/backend/Backend.cc | 13 ++++--------- src/backend/Backend.h | 1 - 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/Canvas.cc b/src/Canvas.cc index cc5c4f96e..3d7984fdc 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -907,7 +907,8 @@ Canvas::resurface(Napi::Object This) { Napi::Value context; if (This.Get("context").UnwrapTo(&context) && context.IsObject()) { - backend()->recreateSurface(); + backend()->destroySurface(); + backend()->createSurface(); // Reset context Context2d *context2d = Context2d::Unwrap(context.As()); cairo_t *prev = context2d->context(); diff --git a/src/backend/Backend.cc b/src/backend/Backend.cc index 14d67e7b5..1d2cff54b 100644 --- a/src/backend/Backend.cc +++ b/src/backend/Backend.cc @@ -22,13 +22,6 @@ void Backend::setCanvas(Canvas* _canvas) } -cairo_surface_t* Backend::recreateSurface() -{ - this->destroySurface(); - - return this->createSurface(); -} - DLL_PUBLIC cairo_surface_t* Backend::getSurface() { if (!surface) createSurface(); return surface; @@ -55,8 +48,9 @@ int Backend::getWidth() } void Backend::setWidth(int width_) { + this->destroySurface(); this->width = width_; - this->recreateSurface(); + this->createSurface(); } int Backend::getHeight() @@ -65,8 +59,9 @@ int Backend::getHeight() } void Backend::setHeight(int height_) { + this->destroySurface(); this->height = height_; - this->recreateSurface(); + this->createSurface(); } bool Backend::isSurfaceValid(){ diff --git a/src/backend/Backend.h b/src/backend/Backend.h index d23573b6e..0b3f92c15 100644 --- a/src/backend/Backend.h +++ b/src/backend/Backend.h @@ -30,7 +30,6 @@ class Backend void setCanvas(Canvas* canvas); virtual cairo_surface_t* createSurface() = 0; - virtual cairo_surface_t* recreateSurface(); DLL_PUBLIC cairo_surface_t* getSurface(); virtual void destroySurface(); From 3dca82adc782239fb2191f041fbae69591ba0aa7 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Tue, 17 Jun 2025 21:01:21 -0400 Subject: [PATCH 462/474] fix build / refactor backends 0. Most importantly this fixes the dumb mistake I made in the previous commit: not all backends implemented destroySurface. 1. Both Backend and Pdf/SvgBackend were cleaning up memory on exit. This responsibility should not be unclear; let's just do it all in the child class, since PdfBackend and SvgBackend have other stuff to cleanup. This is all still less code. 2. The only reason to destroy the surface is if it's dirty from e.g. setWidth (this is referring to isSurfaceValid) 3. Make createSurface idempotent. This allows us to merge it with getSurface() and makes it safer. Call it ensureSurface. --- src/Canvas.cc | 6 +++--- src/Canvas.h | 2 +- src/backend/Backend.cc | 29 ++--------------------------- src/backend/Backend.h | 9 ++------- src/backend/ImageBackend.cc | 20 ++++++++++++-------- src/backend/ImageBackend.h | 4 +++- src/backend/PdfBackend.cc | 27 +++++++++++++++------------ src/backend/PdfBackend.h | 5 +++-- src/backend/SvgBackend.cc | 36 ++++++++++++++++-------------------- src/backend/SvgBackend.h | 5 +++-- 10 files changed, 60 insertions(+), 83 deletions(-) diff --git a/src/Canvas.cc b/src/Canvas.cc index 3d7984fdc..bc790add5 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -131,13 +131,13 @@ Canvas::Canvas(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info), if (instance.IsJust()) backend = ImageBackend::Unwrap(jsBackend = instance.Unwrap()); } + backend->setCanvas(this); + if (!backend->isSurfaceValid()) { Napi::Error::New(env, backend->getError()).ThrowAsJavaScriptException(); return; } - backend->setCanvas(this); - // Note: the backend gets destroyed when the jsBackend is GC'd. The cleaner // way would be to only store the jsBackend and unwrap it when the c++ ref is // needed, but that's slower and a burden. The _backend might be null if we @@ -908,7 +908,7 @@ Canvas::resurface(Napi::Object This) { if (This.Get("context").UnwrapTo(&context) && context.IsObject()) { backend()->destroySurface(); - backend()->createSurface(); + backend()->ensureSurface(); // Reset context Context2d *context2d = Context2d::Unwrap(context.As()); cairo_t *prev = context2d->context(); diff --git a/src/Canvas.h b/src/Canvas.h index 3883c5137..7d0bc9d6d 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -76,7 +76,7 @@ class Canvas : public Napi::ObjectWrap { static PangoFontDescription *ResolveFontDescription(const PangoFontDescription *desc); DLL_PUBLIC inline Backend* backend() { return _backend; } - DLL_PUBLIC inline cairo_surface_t* surface(){ return backend()->getSurface(); } + DLL_PUBLIC inline cairo_surface_t* surface(){ return backend()->ensureSurface(); } cairo_t* createCairoContext(); DLL_PUBLIC inline uint8_t *data(){ return cairo_image_surface_get_data(surface()); } diff --git a/src/backend/Backend.cc b/src/backend/Backend.cc index 1d2cff54b..4607fb646 100644 --- a/src/backend/Backend.cc +++ b/src/backend/Backend.cc @@ -11,31 +11,12 @@ Backend::Backend(std::string name, Napi::CallbackInfo& info) : name(name), env(i this->height = height; } -Backend::~Backend() -{ - Backend::destroySurface(); -} - void Backend::setCanvas(Canvas* _canvas) { this->canvas = _canvas; } -DLL_PUBLIC cairo_surface_t* Backend::getSurface() { - if (!surface) createSurface(); - return surface; -} - -void Backend::destroySurface() -{ - if(this->surface) - { - cairo_surface_destroy(this->surface); - this->surface = NULL; - } -} - std::string Backend::getName() { @@ -50,7 +31,6 @@ void Backend::setWidth(int width_) { this->destroySurface(); this->width = width_; - this->createSurface(); } int Backend::getHeight() @@ -61,23 +41,18 @@ void Backend::setHeight(int height_) { this->destroySurface(); this->height = height_; - this->createSurface(); } -bool Backend::isSurfaceValid(){ - bool hadSurface = surface != NULL; +bool Backend::isSurfaceValid() { bool isValid = true; - cairo_status_t status = cairo_surface_status(getSurface()); + cairo_status_t status = cairo_surface_status(ensureSurface()); if (status != CAIRO_STATUS_SUCCESS) { error = cairo_status_to_string(status); isValid = false; } - if (!hadSurface) - destroySurface(); - return isValid; } diff --git a/src/backend/Backend.h b/src/backend/Backend.h index 0b3f92c15..d51eb7601 100644 --- a/src/backend/Backend.h +++ b/src/backend/Backend.h @@ -17,7 +17,6 @@ class Backend protected: int width; int height; - cairo_surface_t* surface = nullptr; Canvas* canvas = nullptr; Backend(std::string name, Napi::CallbackInfo& info); @@ -25,14 +24,10 @@ class Backend public: Napi::Env env; - virtual ~Backend(); - void setCanvas(Canvas* canvas); - virtual cairo_surface_t* createSurface() = 0; - - DLL_PUBLIC cairo_surface_t* getSurface(); - virtual void destroySurface(); + virtual cairo_surface_t* ensureSurface() = 0; + virtual void destroySurface() = 0; DLL_PUBLIC std::string getName(); diff --git a/src/backend/ImageBackend.cc b/src/backend/ImageBackend.cc index 682c56b18..1fede0736 100644 --- a/src/backend/ImageBackend.cc +++ b/src/backend/ImageBackend.cc @@ -3,8 +3,10 @@ #include #include -ImageBackend::ImageBackend(Napi::CallbackInfo& info) : Napi::ObjectWrap(info), Backend("image", info) -{ +ImageBackend::ImageBackend(Napi::CallbackInfo& info) : Napi::ObjectWrap(info), Backend("image", info) {} + +ImageBackend::~ImageBackend() { + destroySurface(); } // This returns an approximate value only, suitable for @@ -29,11 +31,12 @@ int32_t ImageBackend::approxBytesPerPixel() { } } -cairo_surface_t* ImageBackend::createSurface() { - assert(!surface); - surface = cairo_image_surface_create(format, width, height); - assert(surface); - Napi::MemoryManagement::AdjustExternalMemory(env, approxBytesPerPixel() * width * height); +cairo_surface_t* ImageBackend::ensureSurface() { + if (!surface) { + surface = cairo_image_surface_create(format, width, height); + assert(surface); + Napi::MemoryManagement::AdjustExternalMemory(env, approxBytesPerPixel() * width * height); + } return surface; } @@ -50,7 +53,8 @@ cairo_format_t ImageBackend::getFormat() { } void ImageBackend::setFormat(cairo_format_t _format) { - this->format = _format; + this->destroySurface(); + this->format = _format; } Napi::FunctionReference ImageBackend::constructor; diff --git a/src/backend/ImageBackend.h b/src/backend/ImageBackend.h index 032907f0f..14946c7b9 100644 --- a/src/backend/ImageBackend.h +++ b/src/backend/ImageBackend.h @@ -6,12 +6,14 @@ class ImageBackend : public Napi::ObjectWrap, public Backend { private: - cairo_surface_t* createSurface(); + cairo_surface_t* ensureSurface(); void destroySurface(); cairo_format_t format = DEFAULT_FORMAT; + cairo_surface_t* surface = nullptr; public: ImageBackend(Napi::CallbackInfo& info); + ~ImageBackend(); cairo_format_t getFormat(); void setFormat(cairo_format_t format); diff --git a/src/backend/PdfBackend.cc b/src/backend/PdfBackend.cc index ce214a044..b9e7c3665 100644 --- a/src/backend/PdfBackend.cc +++ b/src/backend/PdfBackend.cc @@ -5,26 +5,29 @@ #include "../Canvas.h" #include "../closure.h" -PdfBackend::PdfBackend(Napi::CallbackInfo& info) : Napi::ObjectWrap(info), Backend("pdf", info) { - PdfBackend::createSurface(); -} +PdfBackend::PdfBackend(Napi::CallbackInfo& info) : Napi::ObjectWrap(info), Backend("pdf", info) {} PdfBackend::~PdfBackend() { - cairo_surface_finish(surface); - if (_closure) delete _closure; destroySurface(); } -cairo_surface_t* PdfBackend::createSurface() { - if (!_closure) _closure = new PdfSvgClosure(canvas); - surface = cairo_pdf_surface_create_for_stream(PdfSvgClosure::writeVec, _closure, width, height); +cairo_surface_t* PdfBackend::ensureSurface() { + if (!surface) { + _closure = new PdfSvgClosure(canvas); + surface = cairo_pdf_surface_create_for_stream(PdfSvgClosure::writeVec, _closure, width, height); + } return surface; } -cairo_surface_t* PdfBackend::recreateSurface() { - cairo_pdf_surface_set_size(surface, width, height); - - return surface; +void PdfBackend::destroySurface() { + if (surface) { + cairo_surface_destroy(surface); + surface = nullptr; + if (_closure) { + delete _closure; + _closure = nullptr; + } + } } void diff --git a/src/backend/PdfBackend.h b/src/backend/PdfBackend.h index 59aa0fedd..6ae8415c8 100644 --- a/src/backend/PdfBackend.h +++ b/src/backend/PdfBackend.h @@ -7,8 +7,9 @@ class PdfBackend : public Napi::ObjectWrap, public Backend { private: - cairo_surface_t* createSurface(); - cairo_surface_t* recreateSurface(); + cairo_surface_t* ensureSurface(); + void destroySurface(); + cairo_surface_t* surface = nullptr; public: PdfSvgClosure* _closure = NULL; diff --git a/src/backend/SvgBackend.cc b/src/backend/SvgBackend.cc index 530d0b571..e1f0b8d0e 100644 --- a/src/backend/SvgBackend.cc +++ b/src/backend/SvgBackend.cc @@ -9,36 +9,32 @@ using namespace Napi; -SvgBackend::SvgBackend(Napi::CallbackInfo& info) : Napi::ObjectWrap(info), Backend("svg", info) { - SvgBackend::createSurface(); -} +SvgBackend::SvgBackend(Napi::CallbackInfo& info) : Napi::ObjectWrap(info), Backend("svg", info) {} SvgBackend::~SvgBackend() { - cairo_surface_finish(surface); - if (_closure) { - delete _closure; - _closure = nullptr; - } destroySurface(); } -cairo_surface_t* SvgBackend::createSurface() { - assert(!_closure); - _closure = new PdfSvgClosure(canvas); - surface = cairo_svg_surface_create_for_stream(PdfSvgClosure::writeVec, _closure, width, height); +cairo_surface_t* SvgBackend::ensureSurface() { + if (!surface) { + assert(!_closure); + _closure = new PdfSvgClosure(canvas); + surface = cairo_svg_surface_create_for_stream(PdfSvgClosure::writeVec, _closure, width, height); + } return surface; } -cairo_surface_t* SvgBackend::recreateSurface() { - cairo_surface_finish(surface); - delete _closure; - _closure = nullptr; - cairo_surface_destroy(surface); - - return createSurface(); +void SvgBackend::destroySurface() { + if (surface) { + cairo_surface_destroy(surface); + surface = nullptr; + if (_closure) { + delete _closure; + _closure = nullptr; + } + } } - void SvgBackend::Initialize(Napi::Object target) { Napi::Env env = target.Env(); diff --git a/src/backend/SvgBackend.h b/src/backend/SvgBackend.h index 301ec831c..f44842690 100644 --- a/src/backend/SvgBackend.h +++ b/src/backend/SvgBackend.h @@ -7,8 +7,9 @@ class SvgBackend : public Napi::ObjectWrap, public Backend { private: - cairo_surface_t* createSurface(); - cairo_surface_t* recreateSurface(); + cairo_surface_t* ensureSurface(); + void destroySurface(); + cairo_surface_t* surface = nullptr; public: PdfSvgClosure* _closure = NULL; From 627c6018fb47066a5015ce4d4693fed16b0bd4a1 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Tue, 17 Jun 2025 21:30:34 -0400 Subject: [PATCH 463/474] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index af2da3fe6..2c8bbb470 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * Fixed issue with fillText that was breaking subsequent fillText calls (#2171) * Fix svg rendering when the image is resized (#2498) * Fix measureText with direction rtl textAlign start/end +* Fix a crash in Node 24, due to external memory API change (#2514) 3.1.0 ================== From 48ecd5a6249712a2a23cf778f1c40ced9fda1ecd Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Tue, 17 Jun 2025 21:38:02 -0400 Subject: [PATCH 464/474] further backend cleanup, add asserts --- src/backend/PdfBackend.cc | 8 ++++---- src/backend/SvgBackend.cc | 7 +++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/backend/PdfBackend.cc b/src/backend/PdfBackend.cc index b9e7c3665..5c1e8fa5b 100644 --- a/src/backend/PdfBackend.cc +++ b/src/backend/PdfBackend.cc @@ -1,6 +1,7 @@ #include "PdfBackend.h" #include +#include #include "../InstanceData.h" #include "../Canvas.h" #include "../closure.h" @@ -23,10 +24,9 @@ void PdfBackend::destroySurface() { if (surface) { cairo_surface_destroy(surface); surface = nullptr; - if (_closure) { - delete _closure; - _closure = nullptr; - } + assert(_closure); + delete _closure; + _closure = nullptr; } } diff --git a/src/backend/SvgBackend.cc b/src/backend/SvgBackend.cc index e1f0b8d0e..f4bf2b9c3 100644 --- a/src/backend/SvgBackend.cc +++ b/src/backend/SvgBackend.cc @@ -28,10 +28,9 @@ void SvgBackend::destroySurface() { if (surface) { cairo_surface_destroy(surface); surface = nullptr; - if (_closure) { - delete _closure; - _closure = nullptr; - } + assert(_closure); + delete _closure; + _closure = nullptr; } } From 7a942d484fe10544432a3a9a21034f3e811e7995 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Thu, 19 Jun 2025 08:33:10 -0400 Subject: [PATCH 465/474] 3.1.1 --- CHANGELOG.md | 4 ++++ package.json | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c8bbb470..341358def 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,10 @@ project adheres to [Semantic Versioning](http://semver.org/). ================== ### Changed ### Added +### Fixed +3.1.1 +================== ### Fixed * Fix a crash when SVGs without width or height are loaded (#2486) * Fix fetching prebuilds during installation on certain newer versions of Node (#2497) @@ -20,6 +23,7 @@ project adheres to [Semantic Versioning](http://semver.org/). 3.1.0 ================== +### Changed * Replaced `simple-get ` with ` Node.js builtin` `fetch` (#2309) * `ctx.font` has a new C++ parser and is 2x-400x faster. Please file an issue if you experience different results, as caching has been removed. * The restriction of registering fonts before a canvas is created has been removed. You can now register a font as late as right before the `fillText` call ([#1921](https://github.com/Automattic/node-canvas/issues/1921)) diff --git a/package.json b/package.json index 2754cd381..bf4eaaf9b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "3.1.0", + "version": "3.1.1", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", @@ -63,7 +63,9 @@ "node": "^18.12.0 || >= 20.9.0" }, "binary": { - "napi_versions": [7] + "napi_versions": [ + 7 + ] }, "license": "MIT" } From 2738a707afe7a3f0f6daf95c0f50d326cda2a288 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Wed, 25 Jun 2025 21:41:16 -0400 Subject: [PATCH 466/474] fix resurface crash with png, svg canvases Regressed in 3dca82adc782239fb2191f041fbae69591ba0aa7 This was originally added in cfc6dfd714772af87e814a687230ae16b4690182, but I don't think that assessment is right at least for the current bug: cairo_destroy, called after the backend's SetWidth, still holds a reference to the surface and calls the closure (presumably for leading or something?). I can't get it to happen consistently enough to write a test, sadly. Fixes #2520 --- src/backend/PdfBackend.cc | 1 + src/backend/SvgBackend.cc | 1 + 2 files changed, 2 insertions(+) diff --git a/src/backend/PdfBackend.cc b/src/backend/PdfBackend.cc index 5c1e8fa5b..4eb46168c 100644 --- a/src/backend/PdfBackend.cc +++ b/src/backend/PdfBackend.cc @@ -22,6 +22,7 @@ cairo_surface_t* PdfBackend::ensureSurface() { void PdfBackend::destroySurface() { if (surface) { + cairo_surface_finish(surface); cairo_surface_destroy(surface); surface = nullptr; assert(_closure); diff --git a/src/backend/SvgBackend.cc b/src/backend/SvgBackend.cc index f4bf2b9c3..475c07dea 100644 --- a/src/backend/SvgBackend.cc +++ b/src/backend/SvgBackend.cc @@ -26,6 +26,7 @@ cairo_surface_t* SvgBackend::ensureSurface() { void SvgBackend::destroySurface() { if (surface) { + cairo_surface_finish(surface); cairo_surface_destroy(surface); surface = nullptr; assert(_closure); From a862af8040c03593bd9376fe2464a73867a0924d Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Wed, 25 Jun 2025 22:43:14 -0400 Subject: [PATCH 467/474] 3.1.2 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 341358def..98cba4534 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Added ### Fixed +3.1.2 +================== +### Fixed +* Fix crash when setting width/height on PDF, SVG canvas (#2520) + 3.1.1 ================== ### Fixed diff --git a/package.json b/package.json index bf4eaaf9b..5ad174990 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "3.1.1", + "version": "3.1.2", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 6353deb01147e37d76a6b4a01400e179db84776f Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sat, 16 Aug 2025 12:09:45 -0400 Subject: [PATCH 468/474] ci: windows-2025 image (2019 was retired) --- .github/workflows/ci.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b523a3e33..963d21721 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -30,7 +30,7 @@ jobs: Windows: name: Test on Windows - runs-on: windows-2019 + runs-on: windows-2025 strategy: matrix: node: [18.20.5, 20.18.1, 22.12.0, 23.3.0, 24.2.0] @@ -45,6 +45,7 @@ jobs: Expand-Archive gtk.zip -DestinationPath "C:\GTK" Invoke-WebRequest "https://downloads.sourceforge.net/project/libjpeg-turbo/2.0.4/libjpeg-turbo-2.0.4-vc64.exe" -OutFile "libjpeg.exe" -UserAgent NativeHost .\libjpeg.exe /S + winget install --accept-source-agreements --id=Microsoft.VCRedist.2010.x64 -e npm install -g node-gyp@8 npm prefix -g | % {npm config set node_gyp "$_\node_modules\node-gyp\bin\node-gyp.js"} - name: Install From d548caadb12ad4023fe689e541f0d66da0cf45cb Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Mon, 11 Aug 2025 21:26:43 -0400 Subject: [PATCH 469/474] use memcpy to avoid misaligned pointer UB compiles to the same thing before/after at any optimization level --- src/bmp/BMPParser.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bmp/BMPParser.cc b/src/bmp/BMPParser.cc index d2c2ddbb3..9058be8af 100644 --- a/src/bmp/BMPParser.cc +++ b/src/bmp/BMPParser.cc @@ -1,6 +1,7 @@ #include "BMPParser.h" #include +#include using namespace std; using namespace BMPParser; @@ -384,7 +385,8 @@ string Parser::getErrMsg() const{ template inline T Parser::get(){ if(check) CHECK_OVERRUN(ptr, sizeof(T), T); - T val = *(T*)ptr; + T val; + std::memcpy(&val, ptr, sizeof(T)); ptr += sizeof(T); return val; } From 6ce963d278d535665e671c4c5a5761565e1bf4da Mon Sep 17 00:00:00 2001 From: Nick Doiron Date: Sun, 17 Aug 2025 13:57:44 -0500 Subject: [PATCH 470/474] Set pango language through ctx.lang (#2526) Co-authored-by: Caleb Hearon --- CHANGELOG.md | 1 + index.d.ts | 1 + src/CanvasRenderingContext2d.cc | 30 ++++++++++++++++++++++++++++-- src/CanvasRenderingContext2d.h | 4 ++++ test/canvas.test.js | 3 ++- 5 files changed, 36 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98cba4534..79119fecd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ================== ### Changed ### Added +* Added `ctx.lang` to set the ISO language code for text ### Fixed 3.1.2 diff --git a/index.d.ts b/index.d.ts index 43ff107d0..27ab0c341 100644 --- a/index.d.ts +++ b/index.d.ts @@ -291,6 +291,7 @@ export class CanvasRenderingContext2D { textAlign: CanvasTextAlign; canvas: Canvas; direction: 'ltr' | 'rtl'; + lang: string; } export class CanvasGradient { diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 15c5c80f5..2342a57e5 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -43,7 +43,7 @@ constexpr double twoPi = M_PI * 2.; #define PANGO_LAYOUT_GET_METRICS(LAYOUT) pango_context_get_metrics( \ pango_layout_get_context(LAYOUT), \ pango_layout_get_font_description(LAYOUT), \ - pango_context_get_language(pango_layout_get_context(LAYOUT))) + pango_language_from_string(state->lang.c_str())) inline static bool checkArgs(const Napi::CallbackInfo&info, double *args, int argsNum, int offset = 0){ Napi::Env env = info.Env(); @@ -162,7 +162,8 @@ Context2d::Initialize(Napi::Env& env, Napi::Object& exports) { InstanceAccessor<&Context2d::GetFont, &Context2d::SetFont>("font", napi_default_jsproperty), InstanceAccessor<&Context2d::GetTextBaseline, &Context2d::SetTextBaseline>("textBaseline", napi_default_jsproperty), InstanceAccessor<&Context2d::GetTextAlign, &Context2d::SetTextAlign>("textAlign", napi_default_jsproperty), - InstanceAccessor<&Context2d::GetDirection, &Context2d::SetDirection>("direction", napi_default_jsproperty) + InstanceAccessor<&Context2d::GetDirection, &Context2d::SetDirection>("direction", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetLanguage, &Context2d::SetLanguage>("lang", napi_default_jsproperty) }); exports.Set("CanvasRenderingContext2d", ctor); @@ -786,6 +787,25 @@ Context2d::SetDirection(const Napi::CallbackInfo& info, const Napi::Value& value state->direction = dir; } +/* + * Get language. + */ +Napi::Value +Context2d::GetLanguage(const Napi::CallbackInfo& info) { + return Napi::String::New(env, state->lang); +} + +/* + * Set language. + */ +void +Context2d::SetLanguage(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (!value.IsString()) return; + + std::string lang = value.As(); + state->lang = lang; +} + /* * Put image data. * @@ -2490,6 +2510,9 @@ Context2d::paintText(const Napi::CallbackInfo& info, bool stroke) { checkFonts(); pango_layout_set_text(layout, str.c_str(), -1); + if (state->lang != "") { + pango_context_set_language(pango_layout_get_context(_layout), pango_language_from_string(state->lang.c_str())); + } pango_cairo_update_layout(context(), layout); PangoDirection pango_dir = state->direction == "ltr" ? PANGO_DIRECTION_LTR : PANGO_DIRECTION_RTL; @@ -2802,6 +2825,9 @@ Context2d::MeasureText(const Napi::CallbackInfo& info) { checkFonts(); pango_layout_set_text(layout, str.Utf8Value().c_str(), -1); + if (state->lang != "") { + pango_context_set_language(pango_layout_get_context(_layout), pango_language_from_string(state->lang.c_str())); + } pango_cairo_update_layout(ctx, layout); // Normally you could use pango_layout_get_pixel_extents and be done, or use diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index 341e5936d..1d9548895 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -36,6 +36,7 @@ struct canvas_state_t { canvas_draw_mode_t textDrawingMode = TEXT_DRAW_PATHS; bool imageSmoothingEnabled = true; std::string direction = "ltr"; + std::string lang = ""; canvas_state_t() { fontDescription = pango_font_description_from_string("sans"); @@ -61,6 +62,7 @@ struct canvas_state_t { fontDescription = pango_font_description_copy(other.fontDescription); font = other.font; imageSmoothingEnabled = other.imageSmoothingEnabled; + lang = other.lang; } ~canvas_state_t() { @@ -157,6 +159,7 @@ class Context2d : public Napi::ObjectWrap { Napi::Value GetFont(const Napi::CallbackInfo& info); Napi::Value GetTextBaseline(const Napi::CallbackInfo& info); Napi::Value GetTextAlign(const Napi::CallbackInfo& info); + Napi::Value GetLanguage(const Napi::CallbackInfo& info); void SetPatternQuality(const Napi::CallbackInfo& info, const Napi::Value& value); void SetImageSmoothingEnabled(const Napi::CallbackInfo& info, const Napi::Value& value); void SetGlobalCompositeOperation(const Napi::CallbackInfo& info, const Napi::Value& value); @@ -179,6 +182,7 @@ class Context2d : public Napi::ObjectWrap { void SetFont(const Napi::CallbackInfo& info, const Napi::Value& value); void SetTextBaseline(const Napi::CallbackInfo& info, const Napi::Value& value); void SetTextAlign(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetLanguage(const Napi::CallbackInfo& info, const Napi::Value& value); #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0) void BeginTag(const Napi::CallbackInfo& info); void EndTag(const Napi::CallbackInfo& info); diff --git a/test/canvas.test.js b/test/canvas.test.js index 01156d089..4655c31c7 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -2490,7 +2490,8 @@ describe('Canvas', function () { ['patternQuality', 'best'], // ['quality', 'best'], // doesn't do anything, TODO remove ['textDrawingMode', 'glyph'], - ['antialias', 'gray'] + ['antialias', 'gray'], + ['lang', 'eu'] ] for (const [k, v] of state) { From 9bcf3631b41c422ad832118186ee9f02bbde2810 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sun, 17 Aug 2025 15:22:38 -0400 Subject: [PATCH 471/474] 3.2.0 --- CHANGELOG.md | 6 +++++- package.json | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79119fecd..940894033 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,13 @@ project adheres to [Semantic Versioning](http://semver.org/). ================== ### Changed ### Added -* Added `ctx.lang` to set the ISO language code for text ### Fixed +3.2.0 +================== +### Added +* Added `ctx.lang` to set the ISO language code for text + 3.1.2 ================== ### Fixed diff --git a/package.json b/package.json index 5ad174990..3dd7e1329 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "3.1.2", + "version": "3.2.0", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 418f555e1645a2d0fc7e0a9e86265c69c7ddbfde Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 6 Sep 2025 21:44:30 -0700 Subject: [PATCH 472/474] bug: incorrect roundRect() with large radii Fixes #2400 --- CHANGELOG.md | 1 + src/CanvasRenderingContext2d.cc | 6 +++--- test/public/tests.js | 5 +++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 940894033..b16096f69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed +* `roundRect()` shape incorrect when radii were large relative to rectangle size (#2400) 3.2.0 ================== diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 2342a57e5..3f52c1fdd 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -3175,10 +3175,10 @@ Context2d::RoundRect(const Napi::CallbackInfo& info) { upperLeft.x *= scale; upperLeft.y *= scale; upperRight.x *= scale; - upperRight.x *= scale; - lowerLeft.y *= scale; + upperRight.y *= scale; + lowerLeft.x *= scale; lowerLeft.y *= scale; - lowerRight.y *= scale; + lowerRight.x *= scale; lowerRight.y *= scale; } } diff --git a/test/public/tests.js b/test/public/tests.js index 165d847e6..582c0ce28 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -132,6 +132,11 @@ tests['roundRect()'] = function (ctx) { ctx.roundRect(135, 70, 60, 60, [{ x: 30, y: 10 }, { x: 5, y: 20 }]) ctx.fillStyle = 'darkseagreen' ctx.fill() + + ctx.beginPath() + ctx.roundRect(5, 135, 8, 60, 15) + ctx.fillStyle = 'purple' + ctx.fill() } tests['lineTo()'] = function (ctx) { From 616859b50294d859d6d59929a766afe4e4f43ec9 Mon Sep 17 00:00:00 2001 From: Ian Chien Date: Sun, 7 Sep 2025 21:54:36 +0800 Subject: [PATCH 473/474] fix: reject loadImage when src is null or invalid (#2518) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Description: - To handle non-string/non-Buffer sources while fetching image. - Add test cases for '', null, and undefined inputs to loadImage() Test Result: ``` npm run test ... ✔ rejects when loadImage is called with null ✔ rejects when loadImage is called with undefined ✔ rejects when loadImage is called with empty string 291 passing (302ms) 6 pending ``` --- CHANGELOG.md | 1 + lib/image.js | 4 ++++ test/image.test.js | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b16096f69..aafa96fe8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Added ### Fixed * `roundRect()` shape incorrect when radii were large relative to rectangle size (#2400) +* Reject loadImage when src is null or invalid (#2304) 3.2.0 ================== diff --git a/lib/image.js b/lib/image.js index 9ffa3c794..72243439c 100644 --- a/lib/image.js +++ b/lib/image.js @@ -63,6 +63,10 @@ Object.defineProperty(Image.prototype, 'src', { } } else if (Buffer.isBuffer(val)) { setSource(this, val) + } else { + const err = new Error("Invalid image source") + if (typeof this.onerror === 'function') this.onerror(err) + else throw err } }, diff --git a/test/image.test.js b/test/image.test.js index a5d6f415c..edae78beb 100644 --- a/test/image.test.js +++ b/test/image.test.js @@ -516,6 +516,24 @@ describe('Image', function () { img.src = path.join(bmpDir, 'bomb.bmp') }) + it('rejects when loadImage is called with null', async function () { + await assert.rejects( + loadImage(null), + ) + }) + + it('rejects when loadImage is called with undefined', async function () { + await assert.rejects( + loadImage(undefined), + ) + }) + + it('rejects when loadImage is called with empty string', async function () { + await assert.rejects( + loadImage(''), + ) + }) + function testImgd (img, data) { const ctx = createCanvas(img.width, img.height).getContext('2d') ctx.drawImage(img, 0, 0) From 7f34c9bec84c9637b3dec216ae7f4a83a8022fdf Mon Sep 17 00:00:00 2001 From: martinwcf Date: Fri, 10 Oct 2025 15:45:11 +0200 Subject: [PATCH 474/474] Fix error message HTTP response status code in image src setter (#2532) --- CHANGELOG.md | 1 + lib/image.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aafa96fe8..59fad2f10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed +* Fix error message HTTP response status code in image src setter * `roundRect()` shape incorrect when radii were large relative to rectangle size (#2400) * Reject loadImage when src is null or invalid (#2304) diff --git a/lib/image.js b/lib/image.js index 72243439c..a6c81ba83 100644 --- a/lib/image.js +++ b/lib/image.js @@ -50,7 +50,7 @@ Object.defineProperty(Image.prototype, 'src', { }) .then(res => { if (!res.ok) { - throw new Error(`Server responded with ${res.statusCode}`) + throw new Error(`Server responded with ${res.status}`) } return res.arrayBuffer() })