// Copyright (c) 2010 LearnBoost #include "CanvasRenderingContext2d.h" #include #include "backend/ImageBackend.h" #include #include "Canvas.h" #include "CanvasGradient.h" #include "CanvasPattern.h" #include #include #include "Image.h" #include "ImageData.h" #include #include #include "Point.h" #include #include "Util.h" #include using namespace v8; Nan::Persistent Context2d::constructor; /* * Rectangle arg assertions. */ #define RECT_ARGS \ 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]; #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.; /* * Simple helper macro for a rather verbose function call. */ #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))) 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 = Nan::To(info[i]).FromMaybe(0); if (areArgsValid) { if (!std::isfinite(val)) { // 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; } 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)); } /* * Create a cairo context. */ 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); } /* * Destroy cairo context. */ Context2d::~Context2d() { g_object_unref(_layout); cairo_destroy(_context); _resetPersistentHandles(); } /* * Reset canvas state. */ void Context2d::resetState() { states.pop(); states.emplace(); pango_layout_set_font_description(_layout, state->fontDescription); _resetPersistentHandles(); } void Context2d::_resetPersistentHandles() { _fillStyle.Reset(); _strokeStyle.Reset(); } /* * Save cairo / canvas state. */ void Context2d::save() { cairo_save(_context); states.emplace(states.top()); state = &states.top(); } /* * Restore cairo / canvas state. */ void Context2d::restore() { if (states.size() > 1) { cairo_restore(_context); states.pop(); state = &states.top(); pango_layout_set_font_description(_layout, state->fontDescription); } } /* * Save flat path. */ void Context2d::savePath() { _path = cairo_copy_path_flat(_context); cairo_new_path(_context); } /* * Restore flat path. */ void Context2d::restorePath() { cairo_new_path(_context); cairo_append_path(_context, _path); 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. */ void Context2d::setFillRule(v8::Local value) { cairo_fill_rule_t rule = CAIRO_FILL_RULE_WINDING; if (value->IsString()) { Nan::Utf8String str(value); if (std::strcmp(*str, "evenodd") == 0) { rule = CAIRO_FILL_RULE_EVEN_ODD; } } cairo_set_fill_rule(_context, rule); } 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); 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_pattern_set_filter(state->fillPattern, state->patternQuality); cairo_set_source(_context, state->fillPattern); } repeat_type_t repeat = Pattern::get_repeat_type_for_cairo_pattern(state->fillPattern); 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) { 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) : cairo_fill_preserve(_context); } else { hasShadow() ? shadow(cairo_fill) : cairo_fill(_context); } if (needsRestore) { cairo_restore(_context); } } /* * Stroke and apply shadow. */ void Context2d::stroke(bool preserve) { cairo_pattern_t *new_pattern; if (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_pattern_set_filter(state->strokePattern, state->patternQuality); 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); } else { cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_REPEAT); } } else if (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); } if (preserve) { hasShadow() ? shadow(cairo_stroke_preserve) : cairo_stroke_preserve(_context); } else { hasShadow() ? shadow(cairo_stroke) : cairo_stroke(_context); } } /* * Apply shadow with the given draw fn. */ void Context2d::shadow(void (fn)(cairo_t *cr)) { cairo_path_t *path = cairo_copy_path_flat(_context); cairo_save(_context); // shadowOffset is unaffected by current transform cairo_matrix_t path_matrix; cairo_get_matrix(_context, &path_matrix); cairo_identity_matrix(_context); // Apply shadow cairo_push_group(_context); // No need to invoke blur if shadowBlur is 0 if (state->shadowBlur) { // find out extent of path double x1, y1, x2, y2; if (fn == cairo_fill || fn == cairo_fill_preserve) { cairo_fill_extents(_context, &x1, &y1, &x2, &y2); } else { cairo_stroke_extents(_context, &x1, &y1, &x2, &y2); } // create new image surface that size + padding for blurring double dx = x2-x1, dy = y2-y1; cairo_user_to_device_distance(_context, &dx, &dy); int pad = state->shadowBlur * 2; cairo_surface_t *shadow_surface = cairo_image_surface_create( CAIRO_FORMAT_ARGB32, dx + 2 * pad, dy + 2 * pad); cairo_t *shadow_context = cairo_create(shadow_surface); // transform path to the right place cairo_translate(shadow_context, pad-x1, pad-y1); cairo_transform(shadow_context, &path_matrix); // set lineCap lineJoin lineDash cairo_set_line_cap(shadow_context, cairo_get_line_cap(_context)); cairo_set_line_join(shadow_context, cairo_get_line_join(_context)); double offset; int dashes = cairo_get_dash_count(_context); std::vector a(dashes); cairo_get_dash(_context, a.data(), &offset); cairo_set_dash(shadow_context, a.data(), dashes, offset); // draw the path and blur cairo_set_line_width(shadow_context, cairo_get_line_width(_context)); cairo_new_path(shadow_context); cairo_append_path(shadow_context, path); setSourceRGBA(shadow_context, state->shadow); fn(shadow_context); blur(shadow_surface, state->shadowBlur); // paint to original context cairo_set_source_surface(_context, shadow_surface, x1 - pad + state->shadowOffsetX + 1, y1 - pad + state->shadowOffsetY + 1); cairo_paint(_context); cairo_destroy(shadow_context); cairo_surface_destroy(shadow_surface); } else { // Offset first, then apply path's transform cairo_translate( _context , state->shadowOffsetX , state->shadowOffsetY); cairo_transform(_context, &path_matrix); // Apply shadow cairo_new_path(_context); cairo_append_path(_context, path); setSourceRGBA(state->shadow); fn(_context); } // Paint the shadow cairo_pop_group_to_source(_context); cairo_paint(_context); // Restore state cairo_restore(_context); cairo_new_path(_context); cairo_append_path(_context, path); fn(_context); cairo_path_destroy(path); } /* * Set source RGBA for the current context */ void Context2d::setSourceRGBA(rgba_t color) { setSourceRGBA(_context, color); } /* * Set source RGBA */ void Context2d::setSourceRGBA(cairo_t *ctx, rgba_t color) { cairo_set_source_rgba( ctx , color.r , color.g , color.b , color.a * state->globalAlpha); } /* * Check if the context has a drawable shadow. */ bool Context2d::hasShadow() { return state->shadow.a && (state->shadowBlur || state->shadowOffsetX || state->shadowOffsetY); } /* * Blur the given surface with the given radius. */ void Context2d::blur(cairo_surface_t *surface, int radius) { // Steve Hanov, 2009 // Released into the public domain. radius = radius * 0.57735f + 0.5f; // 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)); cairo_surface_flush( surface ); unsigned char* src = cairo_image_surface_get_data( surface ); double mul=1.f/((radius*2)*(radius*2)); int channel; // The number of times to perform the averaging. According to wikipedia, // three iterations is good enough to pass for a gaussian. const int MAX_ITERATIONS = 3; int iteration; for ( iteration = 0; iteration < MAX_ITERATIONS; iteration++ ) { for( channel = 0; channel < 4; channel++ ) { int x,y; // precomputation step. unsigned char* pix = src; unsigned* pre = precalc; pix += channel; for (y=0;y0) tot+=pre[-1]; if (y>0) tot+=pre[-width]; if (x>0 && y>0) tot-=pre[-width-1]; *pre++=tot; pix += 4; } } // blur step. pix = src + (int)radius * width * 4 + (int)radius * 4 + channel; for (y=radius;y= width ? width - 1 : x + radius; int b = y + radius >= height ? height - 1 : y + radius; int tot = precalc[r+b*width] + precalc[l+t*width] - precalc[l+b*width] - precalc[r+t*width]; *pix=(unsigned char)(tot*mul); pix += 4; } pix += (int)radius * 2 * 4; } } } cairo_surface_mark_dirty(surface); 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()); 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. */ 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()"); } 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; } /* * Put image data. * * - imageData, dx, dy * - imageData, dx, dy, sx, sy, sw, sh * */ 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"); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); ImageData *imageData = Nan::ObjectWrap::Unwrap(obj); uint8_t *src = imageData->data(); uint8_t *dst = context->canvas()->data(); int dstStride = context->canvas()->stride(); int Bpp = dstStride / context->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) , rows , cols; switch (info.Length()) { // imageData, dx, dy case 3: sw = imageData->width(); sh = imageData->height(); 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); // fix up negative height, width if (sw < 0) sx += sw, sw = -sw; if (sh < 0) sy += sh, sh = -sh; // clamp the left edge if (sx < 0) sw += sx, sx = 0; if (sy < 0) sh += sy, sy = 0; // clamp the right edge if (sx + sw > imageData->width()) sw = imageData->width() - sx; if (sy + sh > imageData->height()) sh = imageData->height() - sy; // start destination at source offset dx += sx; dy += sy; 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()) { 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++ = 255; } 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 or not an image canvas"); return; } } cairo_surface_mark_dirty_rectangle( context->canvas()->surface() , dx , dy , cols , rows); } /* * Get image data. * * - sx, sy, sw, sh * */ NAN_METHOD(Context2d::GetImageData) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Canvas *canvas = context->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); if (!sw) return Nan::ThrowError("IndexSizeError: The source width is 0."); 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) { sx += sw; sw = -sw; } if (sh < 0) { sy += sh; 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. if (sx < 0) { sw += sx; sx = 0; } if (sy < 0) { sh += sy; sy = 0; } int srcStride = canvas->stride(); int bpp = srcStride / width; int size = sw * sh * bpp; int dstStride = sw * bpp; uint8_t *src = canvas->data(); Local buffer = ArrayBuffer::New(Isolate::GetCurrent(), size); Local dataArray; if (canvas->backend()->getFormat() == CAIRO_FORMAT_RGB16_565) { dataArray = Uint16Array::New(buffer, 0, size >> 1); } else { dataArray = Uint8ClampedArray::New(buffer, 0, size); } Nan::TypedArrayContents typedArrayContents(dataArray); uint8_t* dst = *typedArrayContents; 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 r = *pixel >> 16; uint8_t g = *pixel >> 8; uint8_t b = *pixel; 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 or not an image canvas"); return; } } 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(); 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 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); } 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); } /* * Take a transform matrix and return its components * 0: angle, 1: scaleX, 2: scaleY, 3: skewX, 4: translateX, 5: translateY */ void decompose_matrix(cairo_matrix_t matrix, double *destination) { double denom = pow(matrix.xx, 2) + pow(matrix.yx, 2); destination[0] = atan2(matrix.yx, matrix.xx); destination[1] = sqrt(denom); destination[2] = (matrix.xx * matrix.yy - matrix.xy * matrix.yx) / destination[1]; destination[3] = atan2(matrix.xx * matrix.xy + matrix.yx * matrix.yy, denom); destination[4] = matrix.x0; destination[5] = matrix.y0; } /* * Draw image src image to the destination (context). * * - dx, dy * - dx, dy, dw, dh * - sx, sy, sw, sh, dx, dy, dw, dh * */ NAN_METHOD(Context2d::DrawImage) { 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; double sx = 0 , sy = 0 , sw = 0 , sh = 0 , dx = 0 , dy = 0 , dw = 0 , dh = 0 , source_w = 0 , source_h = 0; 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 (!img->isComplete()) { return Nan::ThrowError("Image given has not completed loading"); } 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); source_w = sw = canvas->getWidth(); source_h = sh = canvas->getHeight(); surface = canvas->surface(); // Invalid } else { return Nan::ThrowTypeError("Image or Canvas expected"); } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); // Arguments switch (infoLen) { // img, sx, sy, sw, sh, dx, dy, dw, dh case 9: 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 = args[0]; dy = args[1]; dw = args[2]; dh = args[3]; break; // img, dx, dy case 3: dx = args[0]; dy = args[1]; dw = sw; dh = sh; break; } if (!(sw && sh && dw && dh)) return; // Start draw cairo_save(ctx); cairo_matrix_t matrix; double transforms[6]; cairo_get_matrix(context->context(), &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. double current_scale_x = std::abs(transforms[1]); double current_scale_y = std::abs(transforms[2]); double extra_dx = 0; double extra_dy = 0; double fx = dw / sw * current_scale_x; // transforms[1] is scale on X 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 needsExtraSurface = sameCanvas || needCut || needScale; cairo_surface_t *surfTemp = NULL; cairo_t *ctxTemp = NULL; if (needsExtraSurface) { // we want to create the extra surface as small as possible. // fx and fy are the total scaling we need to apply to sw, sh. // from sw and sh we want to remove the part that is outside the source_w and soruce_h double real_w = sw; double real_h = sh; double translate_x = 0; double translate_y = 0; // if sx or sy are negative, a part of the area represented by sw and sh is empty // because there are empty pixels, so we cut it out. // On the other hand if sx or sy are positive, but sw and sh extend outside the real // source pixels, we cut the area in that case too. if (sx < 0) { extra_dx = -sx * fx; real_w = sw + sx; } else if (sx + sw > source_w) { real_w = sw - (sx + sw - source_w); } if (sy < 0) { extra_dy = -sy * fy; real_h = sh + sy; } else if (sy + sh > source_h) { real_h = sh - (sy + sh - source_h); } // if after cutting we are still bigger than source pixels, we restrict again if (real_w > source_w) { real_w = source_w; } if (real_h > source_h) { real_h = source_h; } // TODO: find a way to limit the surfTemp to real_w and real_h if fx and fy are bigger than 1. // there are no more pixel than the one available in the source, no need to create a bigger surface. surfTemp = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, round(real_w * fx), round(real_h * fy)); ctxTemp = cairo_create(surfTemp); cairo_scale(ctxTemp, fx, fy); if (sx > 0) { translate_x = sx; } if (sy > 0) { 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_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) { // we need to create a new surface in order to blur int pad = context->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); cairo_mask_surface(shadow_context, surface, pad, pad); context->blur(shadow_surface, context->state->shadowBlur); // paint // @note: ShadowBlur looks different in each browser. This implementation matches chrome as close as possible. // The 1.4 offset comes from visual tests with Chrome. I have read the spec and part of the shadowBlur // 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); 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 + (context->state->shadowOffsetX), dy + (context->state->shadowOffsetY)); } } double scaled_dx = dx; double scaled_dy = dy; if (needsExtraSurface && (current_scale_x != 1 || current_scale_y != 1)) { // in this case our surface contains already current_scale_x, we need to scale back cairo_scale(ctx, 1 / current_scale_x, 1 / current_scale_y); scaled_dx *= current_scale_x; scaled_dy *= current_scale_y; } // 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_extend(cairo_get_source(ctx), CAIRO_EXTEND_NONE); cairo_paint_with_alpha(ctx, context->state->globalAlpha); cairo_restore(ctx); if (needsExtraSurface) { cairo_destroy(ctxTemp); cairo_surface_destroy(surfTemp); } } /* * 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)); } /* * 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; } } /* * Get global composite operation. */ NAN_GETTER(Context2d::GetGlobalCompositeOperation) { CHECK_RECEIVER(Context2d.GetGlobalCompositeOperation); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); const char *op = "source-over"; switch (cairo_get_operator(ctx)) { // 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_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_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"; case CAIRO_OPERATOR_MULTIPLY: op = "multiply"; break; case CAIRO_OPERATOR_SCREEN: op = "screen"; break; case CAIRO_OPERATOR_OVERLAY: op = "overlay"; 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; 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; // non-standard: case CAIRO_OPERATOR_SATURATE: op = "saturate"; break; } info.GetReturnValue().Set(Nan::New(op).ToLocalChecked()); } /* * 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; } } /* * Get pattern quality. */ NAN_GETTER(Context2d::GetPatternQuality) { CHECK_RECEIVER(Context2d.GetPatternQuality); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *quality; switch (context->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()); } /* * 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); } /* * 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)); } /* * 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); } /* * 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)); } /* * 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); } /* * 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)); } /* * 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); } /* * 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)); } /* * 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; } } /* * Get current antialiasing setting. */ NAN_GETTER(Context2d::GetAntiAlias) { CHECK_RECEIVER(Context2d.GetAntiAlias); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *aa; switch (cairo_get_antialias(context->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()); } /* * 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); } cairo_set_antialias(ctx, a); } /* * Get text drawing mode. */ NAN_GETTER(Context2d::GetTextDrawingMode) { CHECK_RECEIVER(Context2d.GetTextDrawingMode); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *mode; if (context->state->textDrawingMode == TEXT_DRAW_PATHS) { mode = "path"; } else if (context->state->textDrawingMode == TEXT_DRAW_GLYPHS) { mode = "glyph"; } else { mode = "unknown"; } info.GetReturnValue().Set(Nan::New(mode).ToLocalChecked()); } /* * 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; } } /* * Get filter. */ NAN_GETTER(Context2d::GetQuality) { CHECK_RECEIVER(Context2d.GetQuality); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *filter; switch (cairo_pattern_get_filter(cairo_get_source(context->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()); } /* * 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; } 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); 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(); } /* * Helper for get/set transform. */ void parse_matrix_from_object(cairo_matrix_t &matrix, Local mat) { 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) ); } /* * 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); } /* * 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(); #if NODE_MAJOR_VERSION >= 8 if (!mat->InstanceOf(ctx, _DOMMatrix.Get(Isolate::GetCurrent())).ToChecked()) { return Nan::ThrowTypeError("Expected DOMMatrix"); } #endif cairo_matrix_t matrix; parse_matrix_from_object(matrix, mat); cairo_transform(context->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; 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) { 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(); } } } /* * Get current stroke style. */ NAN_GETTER(Context2d::GetStrokeStyle) { CHECK_RECEIVER(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) { 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(); } } } /* * 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()))); } /* * 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); } } /* * 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()))); } /* * 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); } } /* * Get line join. */ NAN_GETTER(Context2d::GetLineJoin) { CHECK_RECEIVER(Context2d.GetLineJoin); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *join; switch (cairo_get_line_join(context->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()); } /* * 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); } } /* * Get line cap. */ NAN_GETTER(Context2d::GetLineCap) { CHECK_RECEIVER(Context2d.GetLineCap); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *cap; switch (cairo_get_line_cap(context->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()); } /* * 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); } } /* * 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; } info.GetReturnValue().Set(Nan::False()); } /* * Set shadow color. */ NAN_SETTER(Context2d::SetShadowColor) { CHECK_RECEIVER(Context2d.SetShadowColor); 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); } } /* * Get shadow color. */ NAN_GETTER(Context2d::GetShadowColor) { CHECK_RECEIVER(Context2d.GetShadowColor); 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()); } /* * Set fill color, used internally for fillStyle= */ void Context2d::_setFillColor(Local arg) { 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); } /* * Get fill color. */ Local Context2d::_getFillColor() { char buf[64]; rgba_to_string(state->fill, buf, sizeof(buf)); return Nan::New(buf).ToLocalChecked(); } /* * Set stroke color, used internally for strokeStyle= */ void Context2d::_setStrokeColor(Local arg) { short ok; Nan::Utf8String str(arg); uint32_t rgba = rgba_from_string(*str, &ok); if (!ok) return; state->strokePattern = state->strokeGradient = NULL; state->stroke = rgba_create(rgba); } /* * Get stroke color. */ Local Context2d::_getStrokeColor() { char buf[64]; 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); } /* * Bezier curve. */ NAN_METHOD(Context2d::BezierCurveTo) { double args[6]; if(!checkArgs(info, args, 6)) return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_curve_to(context->context() , args[0] , args[1] , args[2] , args[3] , args[4] , args[5]); } /* * Quadratic curve approximation from libsvg-cairo. */ NAN_METHOD(Context2d::QuadraticCurveTo) { 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 = args[0] , y1 = args[1] , x2 = args[2] , y2 = args[3]; cairo_get_current_point(ctx, &x, &y); if (0 == x && 0 == y) { x = x1; y = y1; } cairo_curve_to(ctx , x + 2.0 / 3.0 * (x1 - x), y + 2.0 / 3.0 * (y1 - y) , x2 + 2.0 / 3.0 * (x1 - x2), y2 + 2.0 / 3.0 * (y1 - y2) , x2 , y2); } /* * Save state. */ NAN_METHOD(Context2d::Save) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); context->save(); } /* * Restore state. */ NAN_METHOD(Context2d::Restore) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); context->restore(); } /* * Creates a new subpath. */ NAN_METHOD(Context2d::BeginPath) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_new_path(context->context()); } /* * Marks the subpath as closed. */ NAN_METHOD(Context2d::ClosePath) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_close_path(context->context()); } /* * Rotate transformation. */ NAN_METHOD(Context2d::Rotate) { double args[1]; if(!checkArgs(info, args, 1)) return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_rotate(context->context(), args[0]); } /* * Modify the CTM. */ NAN_METHOD(Context2d::Transform) { double args[6]; if(!checkArgs(info, args, 6)) return; cairo_matrix_t matrix; cairo_matrix_init(&matrix , args[0] , args[1] , args[2] , args[3] , args[4] , args[5]); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_transform(context->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); } /* * Reset the CTM, used internally by setTransform(). */ NAN_METHOD(Context2d::ResetTransform) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_identity_matrix(context->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(); #if NODE_MAJOR_VERSION >= 8 Local ctx = Nan::GetCurrentContext(); if (!mat->InstanceOf(ctx, _DOMMatrix.Get(Isolate::GetCurrent())).ToChecked()) { return Nan::ThrowTypeError("Expected DOMMatrix"); } #endif cairo_matrix_t matrix; parse_matrix_from_object(matrix, mat); cairo_set_matrix(context->context(), &matrix); } else { cairo_identity_matrix(context->context()); Context2d::Transform(info); } } /* * Translate transformation. */ NAN_METHOD(Context2d::Translate) { double args[2]; if(!checkArgs(info, args, 2)) return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_translate(context->context(), args[0], args[1]); } /* * Scale transformation. */ NAN_METHOD(Context2d::Scale) { double args[2]; if(!checkArgs(info, args, 2)) return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_scale(context->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(); cairo_clip_preserve(ctx); } /* * Fill the path. */ NAN_METHOD(Context2d::Fill) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); context->setFillRule(info[0]); context->fill(true); } /* * Stroke the path. */ NAN_METHOD(Context2d::Stroke) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); context->stroke(true); } /* * Helper for fillText/strokeText */ double get_text_scale(PangoLayout *layout, double maxWidth) { PangoRectangle logical_rect; pango_layout_get_pixel_extents(layout, NULL, &logical_rect); if (logical_rect.width > maxWidth) { return maxWidth / logical_rect.width; } else { return 1.0; } } void paintText(const Nan::FunctionCallbackInfo &info, bool stroke) { int argsNum = info.Length() >= 4 ? 3 : 2; 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()); double x = args[0]; double y = args[1]; 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(layout, args[2]); cairo_save(context->context()); cairo_scale(context->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(); } } context->restorePath(); if (argsNum == 3) { cairo_restore(context->context()); } } /* * Fill text at (x, y). */ NAN_METHOD(Context2d::FillText) { paintText(info, false); } /* * Stroke text at (x ,y). */ NAN_METHOD(Context2d::StrokeText) { paintText(info, true); } /* * Gets the baseline adjustment in device pixels */ 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 ascent; case TEXT_BASELINE_MIDDLE: return (ascent + descent) / 2.0; case TEXT_BASELINE_BOTTOM: return ascent + descent; default: return 0; } } /* * 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(double x, double y) { PangoRectangle logical_rect; switch (state->textAlignment) { 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; } y -= getBaselineAdjustment(_layout, state->textBaseline); cairo_move_to(_context, x, y); if (state->textDrawingMode == TEXT_DRAW_PATHS) { pango_cairo_layout_path(_context, _layout); } else if (state->textDrawingMode == TEXT_DRAW_GLYPHS) { pango_cairo_show_layout(_context, _layout); } } /* * Adds a point to the current subpath. */ NAN_METHOD(Context2d::LineTo) { 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]); } /* * Creates a new subpath at the given point. */ NAN_METHOD(Context2d::MoveTo) { 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]); } /* * 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()); } /* * Set font: * - weight * - style * - size * - unit * - family */ NAN_SETTER(Context2d::SetFont) { CHECK_RECEIVER(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 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(); 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()); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); PangoFontDescription *desc = pango_font_description_copy(context->state->fontDescription); pango_font_description_free(context->state->fontDescription); pango_font_description_set_style(desc, Canvas::GetStyleFromCSSString(*style)); pango_font_description_set_weight(desc, Canvas::GetWeightFromCSSString(*weight)); if (strlen(*family) > 0) { // See #1643 - Pango understands "sans" whereas CSS uses "sans-serif" 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); } } 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); context->state->fontDescription = sys_desc; pango_layout_set_font_description(context->_layout, sys_desc); context->state->font = *Nan::Utf8String(value); } /* * Get text baseline. */ NAN_GETTER(Context2d::GetTextBaseline) { CHECK_RECEIVER(Context2d.GetTextBaseline); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); 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()); } /* * Set text baseline. */ NAN_SETTER(Context2d::SetTextBaseline) { CHECK_RECEIVER(Context2d.SetTextBaseline); if (!value->IsString()) return; Nan::Utf8String opStr(Nan::To(value).ToLocalChecked()); 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; } /* * Get text align. */ NAN_GETTER(Context2d::GetTextAlign) { CHECK_RECEIVER(Context2d.GetTextAlign); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); 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()); } /* * Set text align. */ NAN_SETTER(Context2d::SetTextAlign) { CHECK_RECEIVER(Context2d.SetTextAlign); if (!value->IsString()) return; Nan::Utf8String opStr(Nan::To(value).ToLocalChecked()); 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; } /* * Return the given text extents. * TODO: Support for: * hangingBaseline, ideographicBaseline, * fontBoundingBoxAscent, fontBoundingBoxDescent */ NAN_METHOD(Context2d::MeasureText) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); Nan::Utf8String str(Nan::To(info[0]).ToLocalChecked()); Local obj = Nan::New(); 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); // 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; switch (context->state->textAlignment) { 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; } 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(); pango_font_metrics_unref(metrics); info.GetReturnValue().Set(obj); } /* * 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]); 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); 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(); double offset; cairo_get_dash(ctx, NULL, &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); } } /* * 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(); int dashes = cairo_get_dash_count(ctx); std::vector a(dashes); cairo_get_dash(ctx, a.data(), NULL); Local dash = Nan::New(dashes); for (int i=0; i(i), Nan::New(a[i])).Check(); } info.GetReturnValue().Set(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); if (!std::isfinite(offset)) return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); int dashes = cairo_get_dash_count(ctx); std::vector a(dashes); cairo_get_dash(ctx, a.data(), NULL); cairo_set_dash(ctx, a.data(), dashes, offset); } /* * 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(); double offset; cairo_get_dash(ctx, NULL, &offset); info.GetReturnValue().Set(Nan::New(offset)); } /* * Fill the rectangle defined by x, y, width and height. */ NAN_METHOD(Context2d::FillRect) { RECT_ARGS; if (0 == width || 0 == height) return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); context->savePath(); cairo_rectangle(ctx, x, y, width, height); context->fill(); context->restorePath(); } /* * Stroke the rectangle defined by x, y, width and height. */ NAN_METHOD(Context2d::StrokeRect) { RECT_ARGS; if (0 == width && 0 == height) return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); context->savePath(); cairo_rectangle(ctx, x, y, width, height); context->stroke(); context->restorePath(); } /* * Clears all pixels defined by x, y, width and height. */ NAN_METHOD(Context2d::ClearRect) { RECT_ARGS; if (0 == width || 0 == height) return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); cairo_save(ctx); context->savePath(); cairo_rectangle(ctx, x, y, width, height); cairo_set_operator(ctx, CAIRO_OPERATOR_CLEAR); cairo_fill(ctx); context->restorePath(); cairo_restore(ctx); } /* * Adds a rectangle subpath. */ NAN_METHOD(Context2d::Rect) { RECT_ARGS; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); if (width == 0) { cairo_move_to(ctx, x, y); cairo_line_to(ctx, x, y + height); } else if (height == 0) { cairo_move_to(ctx, x, y); cairo_line_to(ctx, x + width, y); } else { cairo_rectangle(ctx, x, y, width, height); } } // 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 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. */ NAN_METHOD(Context2d::Arc) { double args[5]; if(!checkArgs(info, args, 5)) return; 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(); 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); } } /* * Adds an arcTo point (x0,y0) to (x1,y1) with the given radius. * * Implementation influenced by WebKit. */ NAN_METHOD(Context2d::ArcTo) { double args[5]; if(!checkArgs(info, args, 5)) return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); // Current path point double x, y; cairo_get_current_point(ctx, &x, &y); Point p0(x, y); // Point (x0,y0) Point p1(args[0], args[1]); // Point (x1,y1) Point p2(args[2], args[3]); float radius = args[4]; if ((p1.x == p0.x && p1.y == p0.y) || (p1.x == p2.x && p1.y == p2.y) || radius == 0.f) { cairo_line_to(ctx, p1.x, p1.y); return; } Point p1p0((p0.x - p1.x),(p0.y - p1.y)); Point p1p2((p2.x - p1.x),(p2.y - p1.y)); float p1p0_length = sqrtf(p1p0.x * p1p0.x + p1p0.y * p1p0.y); float p1p2_length = sqrtf(p1p2.x * p1p2.x + p1p2.y * p1p2.y); double cos_phi = (p1p0.x * p1p2.x + p1p0.y * p1p2.y) / (p1p0_length * p1p2_length); // all points on a line logic if (-1 == cos_phi) { cairo_line_to(ctx, p1.x, p1.y); return; } if (1 == cos_phi) { // add infinite far away point unsigned int max_length = 65535; double factor_max = max_length / p1p0_length; Point ep((p0.x + factor_max * p1p0.x), (p0.y + factor_max * p1p0.y)); cairo_line_to(ctx, ep.x, ep.y); return; } float tangent = radius / tan(acos(cos_phi) / 2); float factor_p1p0 = tangent / p1p0_length; Point t_p1p0((p1.x + factor_p1p0 * p1p0.x), (p1.y + factor_p1p0 * p1p0.y)); Point orth_p1p0(p1p0.y, -p1p0.x); float orth_p1p0_length = sqrt(orth_p1p0.x * orth_p1p0.x + orth_p1p0.y * orth_p1p0.y); float factor_ra = radius / orth_p1p0_length; double cos_alpha = (orth_p1p0.x * p1p2.x + orth_p1p0.y * p1p2.y) / (orth_p1p0_length * p1p2_length); if (cos_alpha < 0.f) orth_p1p0 = Point(-orth_p1p0.x, -orth_p1p0.y); Point p((t_p1p0.x + factor_ra * orth_p1p0.x), (t_p1p0.y + factor_ra * orth_p1p0.y)); orth_p1p0 = Point(-orth_p1p0.x, -orth_p1p0.y); float sa = acos(orth_p1p0.x / orth_p1p0_length); if (orth_p1p0.y < 0.f) sa = 2 * M_PI - sa; bool anticlockwise = false; float factor_p1p2 = tangent / p1p2_length; Point t_p1p2((p1.x + factor_p1p2 * p1p2.x), (p1.y + factor_p1p2 * p1p2.y)); Point orth_p1p2((t_p1p2.x - p.x),(t_p1p2.y - p.y)); float orth_p1p2_length = sqrtf(orth_p1p2.x * orth_p1p2.x + orth_p1p2.y * orth_p1p2.y); float ea = acos(orth_p1p2.x / orth_p1p2_length); if (orth_p1p2.y < 0) ea = 2 * M_PI - ea; if ((sa > ea) && ((sa - ea) < M_PI)) anticlockwise = true; if ((sa < ea) && ((ea - sa) > M_PI)) anticlockwise = true; cairo_line_to(ctx, t_p1p0.x, t_p1p0.y); if (anticlockwise && M_PI * 2 != radius) { cairo_arc_negative(ctx , p.x , p.y , radius , sa , ea); } else { cairo_arc(ctx , p.x , p.y , radius , sa , 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) { double args[7]; if(!checkArgs(info, args, 7)) return; double radiusX = args[2]; double radiusY = args[3]; if (radiusX == 0 || radiusY == 0) return; double x = args[0]; double y = args[1]; double rotation = args[4]; double startAngle = args[5]; double endAngle = args[6]; bool anticlockwise = Nan::To(info[7]).FromMaybe(false); 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 != args[4]) { cairo_arc_negative(ctx, x, y, radiusY, startAngle, endAngle); } else { cairo_arc(ctx, x, y, radiusY, startAngle, endAngle); } cairo_set_matrix(ctx, &save_matrix); } #undef CHECK_RECEIVER