// 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 Napi; Napi::FunctionReference 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())) { \ Napi::TypeError::New(env, "Method " #prop " called on incompatible receiver").ThrowAsJavaScriptException(); \ 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 Napi::CallbackInfo&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].As().DoubleValue().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; } Napi::FunctionReference Context2d::_DOMMatrix; Napi::FunctionReference Context2d::_parseFont; /* * Initialize Context2d. */ void Context2d::Initialize(Napi::Env& env, Napi::Object& target) { Napi::HandleScope scope(env); // Constructor Napi::FunctionReference ctor = Napi::Function::New(env, Context2d::New); constructor.Reset(ctor); ctor->SetClassName(Napi::String::New(env, "CanvasRenderingContext2D")); // Prototype Local proto = ctor->PrototypeTemplate(); InstanceMethod("drawImage", &DrawImage), InstanceMethod("putImageData", &PutImageData), InstanceMethod("getImageData", &GetImageData), InstanceMethod("createImageData", &CreateImageData), InstanceMethod("addPage", &AddPage), InstanceMethod("save", &Save), InstanceMethod("restore", &Restore), InstanceMethod("rotate", &Rotate), InstanceMethod("translate", &Translate), InstanceMethod("transform", &Transform), InstanceMethod("getTransform", &GetTransform), InstanceMethod("resetTransform", &ResetTransform), InstanceMethod("setTransform", &SetTransform), InstanceMethod("isPointInPath", &IsPointInPath), InstanceMethod("scale", &Scale), InstanceMethod("clip", &Clip), InstanceMethod("fill", &Fill), InstanceMethod("stroke", &Stroke), InstanceMethod("fillText", &FillText), InstanceMethod("strokeText", &StrokeText), InstanceMethod("fillRect", &FillRect), InstanceMethod("strokeRect", &StrokeRect), InstanceMethod("clearRect", &ClearRect), InstanceMethod("rect", &Rect), InstanceMethod("roundRect", &RoundRect), InstanceMethod("measureText", &MeasureText), InstanceMethod("moveTo", &MoveTo), InstanceMethod("lineTo", &LineTo), InstanceMethod("bezierCurveTo", &BezierCurveTo), InstanceMethod("quadraticCurveTo", &QuadraticCurveTo), InstanceMethod("beginPath", &BeginPath), InstanceMethod("closePath", &ClosePath), InstanceMethod("arc", &Arc), InstanceMethod("arcTo", &ArcTo), InstanceMethod("ellipse", &Ellipse), InstanceMethod("setLineDash", &SetLineDash), InstanceMethod("getLineDash", &GetLineDash), InstanceMethod("createPattern", &CreatePattern), InstanceMethod("createLinearGradient", &CreateLinearGradient), InstanceMethod("createRadialGradient", &CreateRadialGradient), Napi::SetAccessor(proto, Napi::String::New(env, "pixelFormat"), GetFormat); Napi::SetAccessor(proto, Napi::String::New(env, "patternQuality"), GetPatternQuality, SetPatternQuality); Napi::SetAccessor(proto, Napi::String::New(env, "imageSmoothingEnabled"), GetImageSmoothingEnabled, SetImageSmoothingEnabled); Napi::SetAccessor(proto, Napi::String::New(env, "globalCompositeOperation"), GetGlobalCompositeOperation, SetGlobalCompositeOperation); Napi::SetAccessor(proto, Napi::String::New(env, "globalAlpha"), GetGlobalAlpha, SetGlobalAlpha); Napi::SetAccessor(proto, Napi::String::New(env, "shadowColor"), GetShadowColor, SetShadowColor); Napi::SetAccessor(proto, Napi::String::New(env, "miterLimit"), GetMiterLimit, SetMiterLimit); Napi::SetAccessor(proto, Napi::String::New(env, "lineWidth"), GetLineWidth, SetLineWidth); Napi::SetAccessor(proto, Napi::String::New(env, "lineCap"), GetLineCap, SetLineCap); Napi::SetAccessor(proto, Napi::String::New(env, "lineJoin"), GetLineJoin, SetLineJoin); Napi::SetAccessor(proto, Napi::String::New(env, "lineDashOffset"), GetLineDashOffset, SetLineDashOffset); Napi::SetAccessor(proto, Napi::String::New(env, "shadowOffsetX"), GetShadowOffsetX, SetShadowOffsetX); Napi::SetAccessor(proto, Napi::String::New(env, "shadowOffsetY"), GetShadowOffsetY, SetShadowOffsetY); Napi::SetAccessor(proto, Napi::String::New(env, "shadowBlur"), GetShadowBlur, SetShadowBlur); Napi::SetAccessor(proto, Napi::String::New(env, "antialias"), GetAntiAlias, SetAntiAlias); Napi::SetAccessor(proto, Napi::String::New(env, "textDrawingMode"), GetTextDrawingMode, SetTextDrawingMode); Napi::SetAccessor(proto, Napi::String::New(env, "quality"), GetQuality, SetQuality); Napi::SetAccessor(proto, Napi::String::New(env, "currentTransform"), GetCurrentTransform, SetCurrentTransform); Napi::SetAccessor(proto, Napi::String::New(env, "fillStyle"), GetFillStyle, SetFillStyle); Napi::SetAccessor(proto, Napi::String::New(env, "strokeStyle"), GetStrokeStyle, SetStrokeStyle); Napi::SetAccessor(proto, Napi::String::New(env, "font"), GetFont, SetFont); Napi::SetAccessor(proto, Napi::String::New(env, "textBaseline"), GetTextBaseline, SetTextBaseline); Napi::SetAccessor(proto, Napi::String::New(env, "textAlign"), GetTextAlign, SetTextAlign); Local ctx = Napi::GetCurrentContext(); (target).Set(Napi::String::New(env, "CanvasRenderingContext2d"), ctor->GetFunction(ctx)); (target).Set(Napi::String::New(env, "CanvasRenderingContext2dInit"), Napi::Function::New(env, 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 { Napi::Error::New(env, "Unexpected gradient type").ThrowAsJavaScriptException(); 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) { Napi::Error::New(env, "Failed to initialize context").ThrowAsJavaScriptException(); 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(Napi::Value value) { Napi::Env env = value.Env(); cairo_fill_rule_t rule = CAIRO_FILL_RULE_WINDING; if (value.IsString()) { std::string str = value.As(); 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; Napi::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; Napi::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; Napi::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; Napi::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. */ Napi::Value Context2d::New(const Napi::CallbackInfo& info) { if (!info.IsConstructCall()) { Napi::TypeError::New(env, "Class constructors cannot be invoked without 'new'").ThrowAsJavaScriptException(); return env.Null(); } if (!info[0].IsObject()) Napi::TypeError::New(env, "Canvas expected").ThrowAsJavaScriptException(); return env.Null(); Napi::Object obj = info[0].To(); if (!Napi::New(env, Canvas::constructor)->HasInstance(obj)) Napi::TypeError::New(env, "Canvas expected").ThrowAsJavaScriptException(); return env.Null(); Canvas *canvas = obj.Unwrap(); bool isImageBackend = canvas->backend()->getName() == "image"; if (isImageBackend) { cairo_format_t format = ImageBackend::DEFAULT_FORMAT; if (info[1].IsObject()) { Napi::Object ctxAttributes = info[1].To(); Napi::Value pixelFormat = (ctxAttributes).Get(Napi::String::New(env, "pixelFormat")); if (pixelFormat.IsString()) { std::string utf8PixelFormat = pixelFormat.As(); 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 Napi::Value alpha = (ctxAttributes).Get(Napi::String::New(env, "alpha")); if (alpha->IsBoolean() && !alpha.As().Value().FromMaybe(false)) { format = CAIRO_FORMAT_RGB24; } } static_cast(canvas->backend())->setFormat(format); } Context2d *context = new Context2d(canvas); context->Wrap(info.This()); return info.This(); } /* * Save some external modules as private references. */ Napi::Value Context2d::SaveExternalModules(const Napi::CallbackInfo& info) { _DOMMatrix.Reset(info[0].To()); _parseFont.Reset(info[1].To()); } /* * Get format (string). */ Napi::Value Context2d::GetFormat(const Napi::CallbackInfo& info) { CHECK_RECEIVER(Context2d.GetFormat); Context2d *context = info.This().Unwrap(); 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(); } return Napi::String::New(env, pixelFormatString); } /* * Create a new page. */ Napi::Value Context2d::AddPage(const Napi::CallbackInfo& info) { Context2d *context = info.This().Unwrap(); if (context->canvas()->backend()->getName() != "pdf") { Napi::Error::New(env, "only PDF canvases support .addPage()").ThrowAsJavaScriptException(); return env.Null(); } cairo_show_page(context->context()); int width = info[0].As().Int32Value().FromMaybe(0); int height = info[1].As().Int32Value().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 * */ Napi::Value Context2d::PutImageData(const Napi::CallbackInfo& info) { if (!info[0].IsObject()) Napi::TypeError::New(env, "ImageData expected").ThrowAsJavaScriptException(); return env.Null(); Napi::Object obj = info[0].To(); if (!Napi::New(env, ImageData::constructor)->HasInstance(obj)) Napi::TypeError::New(env, "ImageData expected").ThrowAsJavaScriptException(); return env.Null(); Context2d *context = info.This().Unwrap(); ImageData *imageData = obj.Unwrap(); 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 = info[1].As().Int32Value().FromMaybe(0) , dy = info[2].As().Int32Value().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 = info[3].As().Int32Value().FromMaybe(0); sy = info[4].As().Int32Value().FromMaybe(0); sw = info[5].As().Int32Value().FromMaybe(0); sh = info[6].As().Int32Value().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: Napi::Error::New(env, "invalid arguments").ThrowAsJavaScriptException(); return env.Null(); } // 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? Napi::Error::New(env, "putImageData for CANVAS_FORMAT_A1 is not yet implemented").ThrowAsJavaScriptException(); 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 Napi::Error::New(env, "putImageData for CANVAS_FORMAT_RGB30 is not yet implemented").ThrowAsJavaScriptException(); break; } #endif default: { Napi::Error::New(env, "Invalid pixel format or not an image canvas").ThrowAsJavaScriptException(); return env.Null(); } } cairo_surface_mark_dirty_rectangle( context->canvas()->surface() , dx , dy , cols , rows); } /* * Get image data. * * - sx, sy, sw, sh * */ Napi::Value Context2d::GetImageData(const Napi::CallbackInfo& info) { Context2d *context = info.This().Unwrap(); Canvas *canvas = context->canvas(); int sx = info[0].As().Int32Value().FromMaybe(0); int sy = info[1].As().Int32Value().FromMaybe(0); int sw = info[2].As().Int32Value().FromMaybe(0); int sh = info[3].As().Int32Value().FromMaybe(0); if (!sw) Napi::Error::New(env, "IndexSizeError: The source width is 0.").ThrowAsJavaScriptException(); return env.Null(); if (!sh) Napi::Error::New(env, "IndexSizeError: The source height is 0.").ThrowAsJavaScriptException(); return env.Null(); int width = canvas->getWidth(); int height = canvas->getHeight(); if (!width) Napi::TypeError::New(env, "Canvas width is 0").ThrowAsJavaScriptException(); return env.Null(); if (!height) Napi::TypeError::New(env, "Canvas height is 0").ThrowAsJavaScriptException(); return env.Null(); // 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); } Napi::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? Napi::Error::New(env, "getImageData for CANVAS_FORMAT_A1 is not yet implemented").ThrowAsJavaScriptException(); 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 Napi::Error::New(env, "getImageData for CANVAS_FORMAT_RGB30 is not yet implemented").ThrowAsJavaScriptException(); break; } #endif default: { // Unlikely Napi::Error::New(env, "Invalid pixel format or not an image canvas").ThrowAsJavaScriptException(); return env.Null(); } } const int argc = 3; Local swHandle = Napi::New(env, sw); Local shHandle = Napi::New(env, sh); Napi::Value argv[argc] = { dataArray, swHandle, shHandle }; Napi::Function ctor = Napi::GetFunction(Napi::New(env, ImageData::constructor)); Napi::Object instance = Napi::NewInstance(ctor, argc, argv); return instance; } /** * Create `ImageData` with the given dimensions or * `ImageData` instance for dimensions. */ Napi::Value Context2d::CreateImageData(const Napi::CallbackInfo& info){ Isolate *iso = Isolate::GetCurrent(); Context2d *context = info.This().Unwrap(); Canvas *canvas = context->canvas(); int32_t width, height; if (info[0].IsObject()) { Napi::Object obj = info[0].To(); width = (obj).Get(Napi::String::New(env, "width".As().Int32Value())).FromMaybe(0); height = (obj).Get(Napi::String::New(env, "height".As().Int32Value())).FromMaybe(0); } else { width = info[0].As().Int32Value().FromMaybe(0); height = info[1].As().Int32Value().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); Napi::Object 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; Napi::Value argv[argc] = { arr, Napi::New(env, width), Napi::New(env, height) }; Napi::Function ctor = Napi::GetFunction(Napi::New(env, ImageData::constructor)); Napi::Object instance = Napi::NewInstance(ctor, argc, argv); return 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 * */ Napi::Value Context2d::DrawImage(const Napi::CallbackInfo& info) { int infoLen = info.Length(); if (infoLen != 3 && infoLen != 5 && infoLen != 9) Napi::TypeError::New(env, "Invalid arguments").ThrowAsJavaScriptException(); return env.Null(); if (!info[0].IsObject()) Napi::TypeError::New(env, "The first argument must be an object").ThrowAsJavaScriptException(); return env.Null(); 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; Napi::Object obj = info[0].To(); // Image if (Napi::New(env, Image::constructor)->HasInstance(obj)) { Image *img = obj.Unwrap(); if (!img->isComplete()) { Napi::Error::New(env, "Image given has not completed loading").ThrowAsJavaScriptException(); return env.Null(); } source_w = sw = img->width; source_h = sh = img->height; surface = img->surface(); // Canvas } else if (Napi::New(env, Canvas::constructor)->HasInstance(obj)) { Canvas *canvas = obj.Unwrap(); source_w = sw = canvas->getWidth(); source_h = sh = canvas->getHeight(); surface = canvas->surface(); // Invalid } else { Napi::TypeError::New(env, "Image or Canvas expected").ThrowAsJavaScriptException(); return env.Null(); } Context2d *context = info.This().Unwrap(); 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. */ Napi::Value Context2d::GetGlobalAlpha(const Napi::CallbackInfo& info) { CHECK_RECEIVER(Context2d.GetGlobalAlpha); Context2d *context = info.This().Unwrap(); return Napi::Number::New(env, context->state->globalAlpha); } /* * Set global alpha. */ void Context2d::SetGlobalAlpha(const Napi::CallbackInfo& info, const Napi::Value& value) { CHECK_RECEIVER(Context2d.SetGlobalAlpha); double n = value.As().DoubleValue().FromMaybe(0); if (n >= 0 && n <= 1) { Context2d *context = info.This().Unwrap(); context->state->globalAlpha = n; } } /* * Get global composite operation. */ Napi::Value Context2d::GetGlobalCompositeOperation(const Napi::CallbackInfo& info) { CHECK_RECEIVER(Context2d.GetGlobalCompositeOperation); Context2d *context = info.This().Unwrap(); 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; } return Napi::New(env, op); } /* * Set pattern quality. */ void Context2d::SetPatternQuality(const Napi::CallbackInfo& info, const Napi::Value& value) { CHECK_RECEIVER(Context2d.SetPatternQuality); Context2d *context = info.This().Unwrap(); std::string quality = value.As(.To()); 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. */ Napi::Value Context2d::GetPatternQuality(const Napi::CallbackInfo& info) { CHECK_RECEIVER(Context2d.GetPatternQuality); Context2d *context = info.This().Unwrap(); 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"; } return Napi::New(env, quality); } /* * Set ImageSmoothingEnabled value. */ void Context2d::SetImageSmoothingEnabled(const Napi::CallbackInfo& info, const Napi::Value& value) { CHECK_RECEIVER(Context2d.SetImageSmoothingEnabled); Context2d *context = info.This().Unwrap(); context->state->imageSmoothingEnabled = value.As().Value().FromMaybe(false); } /* * Get pattern quality. */ Napi::Value Context2d::GetImageSmoothingEnabled(const Napi::CallbackInfo& info) { CHECK_RECEIVER(Context2d.GetImageSmoothingEnabled); Context2d *context = info.This().Unwrap(); return Napi::Boolean::New(env, context->state->imageSmoothingEnabled); } /* * Set global composite operation. */ void Context2d::SetGlobalCompositeOperation(const Napi::CallbackInfo& info, const Napi::Value& value) { CHECK_RECEIVER(Context2d.SetGlobalCompositeOperation); Context2d *context = info.This().Unwrap(); cairo_t *ctx = context->context(); std::string opStr = value.As(.To()); // 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. */ Napi::Value Context2d::GetShadowOffsetX(const Napi::CallbackInfo& info) { CHECK_RECEIVER(Context2d.GetShadowOffsetX); Context2d *context = info.This().Unwrap(); return Napi::Number::New(env, context->state->shadowOffsetX); } /* * Set shadow offset x. */ void Context2d::SetShadowOffsetX(const Napi::CallbackInfo& info, const Napi::Value& value) { CHECK_RECEIVER(Context2d.SetShadowOffsetX); Context2d *context = info.This().Unwrap(); context->state->shadowOffsetX = value.As().DoubleValue().FromMaybe(0); } /* * Get shadow offset y. */ Napi::Value Context2d::GetShadowOffsetY(const Napi::CallbackInfo& info) { CHECK_RECEIVER(Context2d.GetShadowOffsetY); Context2d *context = info.This().Unwrap(); return Napi::Number::New(env, context->state->shadowOffsetY); } /* * Set shadow offset y. */ void Context2d::SetShadowOffsetY(const Napi::CallbackInfo& info, const Napi::Value& value) { CHECK_RECEIVER(Context2d.SetShadowOffsetY); Context2d *context = info.This().Unwrap(); context->state->shadowOffsetY = value.As().DoubleValue().FromMaybe(0); } /* * Get shadow blur. */ Napi::Value Context2d::GetShadowBlur(const Napi::CallbackInfo& info) { CHECK_RECEIVER(Context2d.GetShadowBlur); Context2d *context = info.This().Unwrap(); return Napi::Number::New(env, context->state->shadowBlur); } /* * Set shadow blur. */ void Context2d::SetShadowBlur(const Napi::CallbackInfo& info, const Napi::Value& value) { CHECK_RECEIVER(Context2d.SetShadowBlur); int n = value.As().DoubleValue().FromMaybe(0); if (n >= 0) { Context2d *context = info.This().Unwrap(); context->state->shadowBlur = n; } } /* * Get current antialiasing setting. */ Napi::Value Context2d::GetAntiAlias(const Napi::CallbackInfo& info) { CHECK_RECEIVER(Context2d.GetAntiAlias); Context2d *context = info.This().Unwrap(); 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"; } return Napi::New(env, aa); } /* * Set antialiasing. */ void Context2d::SetAntiAlias(const Napi::CallbackInfo& info, const Napi::Value& value) { CHECK_RECEIVER(Context2d.SetAntiAlias); std::string str = value.As(.To()); Context2d *context = info.This().Unwrap(); 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. */ Napi::Value Context2d::GetTextDrawingMode(const Napi::CallbackInfo& info) { CHECK_RECEIVER(Context2d.GetTextDrawingMode); Context2d *context = info.This().Unwrap(); 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"; } return Napi::New(env, mode); } /* * Set text drawing mode. */ void Context2d::SetTextDrawingMode(const Napi::CallbackInfo& info, const Napi::Value& value) { CHECK_RECEIVER(Context2d.SetTextDrawingMode); std::string str = value.As(.To()); Context2d *context = info.This().Unwrap(); 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. */ Napi::Value Context2d::GetQuality(const Napi::CallbackInfo& info) { CHECK_RECEIVER(Context2d.GetQuality); Context2d *context = info.This().Unwrap(); 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"; } return Napi::New(env, filter); } /* * Set filter. */ void Context2d::SetQuality(const Napi::CallbackInfo& info, const Napi::Value& value) { CHECK_RECEIVER(Context2d.SetQuality); std::string str = value.As(.To()); Context2d *context = info.This().Unwrap(); 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 */ Napi::Object get_current_transform(Context2d *context) { Isolate *iso = Isolate::GetCurrent(); Local arr = Float64Array::New(ArrayBuffer::New(iso, 48), 0, 6); Napi::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; Napi::Value argv[argc] = { arr }; return Napi::NewInstance(context->_DOMMatrix.Get(iso), argc, argv); } /* * Helper for get/set transform. */ void parse_matrix_from_object(cairo_matrix_t &matrix, Napi::Object mat) { cairo_matrix_init(&matrix, (mat).Get(Napi::String::New(env, "a".As().DoubleValue())).FromMaybe(0), (mat).Get(Napi::String::New(env, "b".As().DoubleValue())).FromMaybe(0), (mat).Get(Napi::String::New(env, "c".As().DoubleValue())).FromMaybe(0), (mat).Get(Napi::String::New(env, "d".As().DoubleValue())).FromMaybe(0), (mat).Get(Napi::String::New(env, "e".As().DoubleValue())).FromMaybe(0), (mat).Get(Napi::String::New(env, "f".As().DoubleValue())).FromMaybe(0) ); } /* * Get current transform. */ Napi::Value Context2d::GetCurrentTransform(const Napi::CallbackInfo& info) { CHECK_RECEIVER(Context2d.GetCurrentTransform); Context2d *context = info.This().Unwrap(); Napi::Object instance = get_current_transform(context); return instance; } /* * Set current transform. */ void Context2d::SetCurrentTransform(const Napi::CallbackInfo& info, const Napi::Value& value) { CHECK_RECEIVER(Context2d.SetCurrentTransform); Context2d *context = info.This().Unwrap(); Local ctx = Napi::GetCurrentContext(); Napi::Object mat = value.To(); #if NODE_MAJOR_VERSION >= 8 if (!mat->InstanceOf(ctx, _DOMMatrix.Get(Isolate::GetCurrent())).ToChecked()) { Napi::TypeError::New(env, "Expected DOMMatrix").ThrowAsJavaScriptException(); return env.Null(); } #endif cairo_matrix_t matrix; parse_matrix_from_object(matrix, mat); cairo_transform(context->context(), &matrix); } /* * Get current fill style. */ Napi::Value Context2d::GetFillStyle(const Napi::CallbackInfo& info) { CHECK_RECEIVER(Context2d.GetFillStyle); Context2d *context = info.This().Unwrap(); Isolate *iso = Isolate::GetCurrent(); Napi::Value style; if (context->_fillStyle.IsEmpty()) style = context->_getFillColor(); else style = context->_fillStyle.Get(iso); return style; } /* * Set current fill style. */ void Context2d::SetFillStyle(const Napi::CallbackInfo& info, const Napi::Value& value) { CHECK_RECEIVER(Context2d.SetFillStyle); Context2d *context = info.This().Unwrap(); if (value.IsString()) { MaybeNapi::String mstr = value.To(); if (mstr.IsEmpty()) return; Napi::String str = mstr; context->_fillStyle.Reset(); context->_setFillColor(str); } else if (value.IsObject()) { Napi::Object obj = value.To(); if (Napi::New(env, Gradient::constructor)->HasInstance(obj)) { context->_fillStyle.Reset(value); Gradient *grad = obj.Unwrap(); context->state->fillGradient = grad->pattern(); } else if (Napi::New(env, Pattern::constructor)->HasInstance(obj)) { context->_fillStyle.Reset(value); Pattern *pattern = obj.Unwrap(); context->state->fillPattern = pattern->pattern(); } } } /* * Get current stroke style. */ Napi::Value Context2d::GetStrokeStyle(const Napi::CallbackInfo& info) { CHECK_RECEIVER(Context2d.GetStrokeStyle); Context2d *context = info.This().Unwrap(); Napi::Value style; if (context->_strokeStyle.IsEmpty()) style = context->_getStrokeColor(); else style = context->_strokeStyle.Get(Isolate::GetCurrent()); return style; } /* * Set current stroke style. */ void Context2d::SetStrokeStyle(const Napi::CallbackInfo& info, const Napi::Value& value) { CHECK_RECEIVER(Context2d.SetStrokeStyle); Context2d *context = info.This().Unwrap(); if (value.IsString()) { MaybeNapi::String mstr = value.To(); if (mstr.IsEmpty()) return; Napi::String str = mstr; context->_strokeStyle.Reset(); context->_setStrokeColor(str); } else if (value.IsObject()) { Napi::Object obj = value.To(); if (Napi::New(env, Gradient::constructor)->HasInstance(obj)) { context->_strokeStyle.Reset(value); Gradient *grad = obj.Unwrap(); context->state->strokeGradient = grad->pattern(); } else if (Napi::New(env, Pattern::constructor)->HasInstance(obj)) { context->_strokeStyle.Reset(value); Pattern *pattern = obj.Unwrap(); context->state->strokePattern = pattern->pattern(); } } } /* * Get miter limit. */ Napi::Value Context2d::GetMiterLimit(const Napi::CallbackInfo& info) { CHECK_RECEIVER(Context2d.GetMiterLimit); Context2d *context = info.This().Unwrap(); return Napi::Number::New(env, cairo_get_miter_limit(context->context())); } /* * Set miter limit. */ void Context2d::SetMiterLimit(const Napi::CallbackInfo& info, const Napi::Value& value) { CHECK_RECEIVER(Context2d.SetMiterLimit); double n = value.As().DoubleValue().FromMaybe(0); if (n > 0) { Context2d *context = info.This().Unwrap(); cairo_set_miter_limit(context->context(), n); } } /* * Get line width. */ Napi::Value Context2d::GetLineWidth(const Napi::CallbackInfo& info) { CHECK_RECEIVER(Context2d.GetLineWidth); Context2d *context = info.This().Unwrap(); return Napi::Number::New(env, cairo_get_line_width(context->context())); } /* * Set line width. */ void Context2d::SetLineWidth(const Napi::CallbackInfo& info, const Napi::Value& value) { CHECK_RECEIVER(Context2d.SetLineWidth); double n = value.As().DoubleValue().FromMaybe(0); if (n > 0 && n != std::numeric_limits::infinity()) { Context2d *context = info.This().Unwrap(); cairo_set_line_width(context->context(), n); } } /* * Get line join. */ Napi::Value Context2d::GetLineJoin(const Napi::CallbackInfo& info) { CHECK_RECEIVER(Context2d.GetLineJoin); Context2d *context = info.This().Unwrap(); 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"; } return Napi::New(env, join); } /* * Set line join. */ void Context2d::SetLineJoin(const Napi::CallbackInfo& info, const Napi::Value& value) { CHECK_RECEIVER(Context2d.SetLineJoin); Context2d *context = info.This().Unwrap(); cairo_t *ctx = context->context(); std::string type = value.As(.To()); 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. */ Napi::Value Context2d::GetLineCap(const Napi::CallbackInfo& info) { CHECK_RECEIVER(Context2d.GetLineCap); Context2d *context = info.This().Unwrap(); 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"; } return Napi::New(env, cap); } /* * Set line cap. */ void Context2d::SetLineCap(const Napi::CallbackInfo& info, const Napi::Value& value) { CHECK_RECEIVER(Context2d.SetLineCap); Context2d *context = info.This().Unwrap(); cairo_t *ctx = context->context(); std::string type = value.As(.To()); 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. */ Napi::Value Context2d::IsPointInPath(const Napi::CallbackInfo& info) { if (info[0].IsNumber() && info[1].IsNumber()) { Context2d *context = info.This().Unwrap(); cairo_t *ctx = context->context(); double x = info[0].As().DoubleValue().FromMaybe(0) , y = info[1].As().DoubleValue().FromMaybe(0); context->setFillRule(info[2]); return Napi::Boolean::New(env, cairo_in_fill(ctx, x, y) || cairo_in_stroke(ctx, x, y)); return; } return env.False(); } /* * Set shadow color. */ void Context2d::SetShadowColor(const Napi::CallbackInfo& info, const Napi::Value& value) { CHECK_RECEIVER(Context2d.SetShadowColor); short ok; std::string str = value.As(.To()); uint32_t rgba = rgba_from_string(*str, &ok); if (ok) { Context2d *context = info.This().Unwrap(); context->state->shadow = rgba_create(rgba); } } /* * Get shadow color. */ Napi::Value Context2d::GetShadowColor(const Napi::CallbackInfo& info) { CHECK_RECEIVER(Context2d.GetShadowColor); char buf[64]; Context2d *context = info.This().Unwrap(); rgba_to_string(context->state->shadow, buf, sizeof(buf)); return Napi::String::New(env, buf); } /* * Set fill color, used internally for fillStyle= */ void Context2d::_setFillColor(Napi::Value arg) { Napi::Env env = arg.Env(); short ok; std::string str = arg.As(); uint32_t rgba = rgba_from_string(*str, &ok); if (!ok) return; state->fillPattern = state->fillGradient = NULL; state->fill = rgba_create(rgba); } /* * Get fill color. */ Napi::Value Context2d::_getFillColor() { char buf[64]; rgba_to_string(state->fill, buf, sizeof(buf)); return Napi::String::New(env, buf); } /* * Set stroke color, used internally for strokeStyle= */ void Context2d::_setStrokeColor(Napi::Value arg) { Napi::Env env = arg.Env(); short ok; std::string str = arg.As(); uint32_t rgba = rgba_from_string(*str, &ok); if (!ok) return; state->strokePattern = state->strokeGradient = NULL; state->stroke = rgba_create(rgba); } /* * Get stroke color. */ Napi::Value Context2d::_getStrokeColor() { char buf[64]; rgba_to_string(state->stroke, buf, sizeof(buf)); return Napi::String::New(env, buf); } Napi::Value Context2d::CreatePattern(const Napi::CallbackInfo& info) { Napi::Value image = info[0]; Napi::Value repetition = info[1]; if (!repetition.As().Value().FromMaybe(false)) repetition = Napi::String::New(env, "repeat"); const int argc = 2; Napi::Value argv[argc] = { image, repetition }; Napi::Function ctor = Napi::GetFunction(Napi::New(env, Pattern::constructor)); Napi::Object instance = Napi::NewInstance(ctor, argc, argv); return instance; } Napi::Value Context2d::CreateLinearGradient(const Napi::CallbackInfo& info) { const int argc = 4; Napi::Value argv[argc] = { info[0], info[1], info[2], info[3] }; Napi::Function ctor = Napi::GetFunction(Napi::New(env, Gradient::constructor)); Napi::Object instance = Napi::NewInstance(ctor, argc, argv); return instance; } Napi::Value Context2d::CreateRadialGradient(const Napi::CallbackInfo& info) { const int argc = 6; Napi::Value argv[argc] = { info[0], info[1], info[2], info[3], info[4], info[5] }; Napi::Function ctor = Napi::GetFunction(Napi::New(env, Gradient::constructor)); Napi::Object instance = Napi::NewInstance(ctor, argc, argv); return instance; } /* * Bezier curve. */ Napi::Value Context2d::BezierCurveTo(const Napi::CallbackInfo& info) { double args[6]; if(!checkArgs(info, args, 6)) return; Context2d *context = info.This().Unwrap(); cairo_curve_to(context->context() , args[0] , args[1] , args[2] , args[3] , args[4] , args[5]); } /* * Quadratic curve approximation from libsvg-cairo. */ Napi::Value Context2d::QuadraticCurveTo(const Napi::CallbackInfo& info) { double args[4]; if(!checkArgs(info, args, 4)) return; Context2d *context = info.This().Unwrap(); 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. */ Napi::Value Context2d::Save(const Napi::CallbackInfo& info) { Context2d *context = info.This().Unwrap(); context->save(); } /* * Restore state. */ Napi::Value Context2d::Restore(const Napi::CallbackInfo& info) { Context2d *context = info.This().Unwrap(); context->restore(); } /* * Creates a new subpath. */ Napi::Value Context2d::BeginPath(const Napi::CallbackInfo& info) { Context2d *context = info.This().Unwrap(); cairo_new_path(context->context()); } /* * Marks the subpath as closed. */ Napi::Value Context2d::ClosePath(const Napi::CallbackInfo& info) { Context2d *context = info.This().Unwrap(); cairo_close_path(context->context()); } /* * Rotate transformation. */ Napi::Value Context2d::Rotate(const Napi::CallbackInfo& info) { double args[1]; if(!checkArgs(info, args, 1)) return; Context2d *context = info.This().Unwrap(); cairo_rotate(context->context(), args[0]); } /* * Modify the CTM. */ Napi::Value Context2d::Transform(const Napi::CallbackInfo& info) { 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 = info.This().Unwrap(); cairo_transform(context->context(), &matrix); } /* * Get the CTM */ Napi::Value Context2d::GetTransform(const Napi::CallbackInfo& info) { Context2d *context = info.This().Unwrap(); Napi::Object instance = get_current_transform(context); return instance; } /* * Reset the CTM, used internally by setTransform(). */ Napi::Value Context2d::ResetTransform(const Napi::CallbackInfo& info) { Context2d *context = info.This().Unwrap(); cairo_identity_matrix(context->context()); } /* * Reset transform matrix to identity, then apply the given args. */ Napi::Value Context2d::SetTransform(const Napi::CallbackInfo& info) { Context2d *context = info.This().Unwrap(); if (info.Length() == 1) { Napi::Object mat = info[0].To(); #if NODE_MAJOR_VERSION >= 8 Local ctx = Napi::GetCurrentContext(); if (!mat->InstanceOf(ctx, _DOMMatrix.Get(Isolate::GetCurrent())).ToChecked()) { Napi::TypeError::New(env, "Expected DOMMatrix").ThrowAsJavaScriptException(); return env.Null(); } #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. */ Napi::Value Context2d::Translate(const Napi::CallbackInfo& info) { double args[2]; if(!checkArgs(info, args, 2)) return; Context2d *context = info.This().Unwrap(); cairo_translate(context->context(), args[0], args[1]); } /* * Scale transformation. */ Napi::Value Context2d::Scale(const Napi::CallbackInfo& info) { double args[2]; if(!checkArgs(info, args, 2)) return; Context2d *context = info.This().Unwrap(); cairo_scale(context->context(), args[0], args[1]); } /* * Use path as clipping region. */ Napi::Value Context2d::Clip(const Napi::CallbackInfo& info) { Context2d *context = info.This().Unwrap(); context->setFillRule(info[0]); cairo_t *ctx = context->context(); cairo_clip_preserve(ctx); } /* * Fill the path. */ Napi::Value Context2d::Fill(const Napi::CallbackInfo& info) { Context2d *context = info.This().Unwrap(); context->setFillRule(info[0]); context->fill(true); } /* * Stroke the path. */ Napi::Value Context2d::Stroke(const Napi::CallbackInfo& info) { Context2d *context = info.This().Unwrap(); 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 Napi::CallbackInfo&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; std::string str = info[0].As(.To()); double x = args[0]; double y = args[1]; double scaled_by = 1; Context2d *context = info.This().Unwrap(); 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). */ Napi::Value Context2d::FillText(const Napi::CallbackInfo& info) { paintText(info, false); } /* * Stroke text at (x ,y). */ Napi::Value Context2d::StrokeText(const Napi::CallbackInfo& info) { 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. */ Napi::Value Context2d::LineTo(const Napi::CallbackInfo& info) { double args[2]; if(!checkArgs(info, args, 2)) return; Context2d *context = info.This().Unwrap(); cairo_line_to(context->context(), args[0], args[1]); } /* * Creates a new subpath at the given point. */ Napi::Value Context2d::MoveTo(const Napi::CallbackInfo& info) { double args[2]; if(!checkArgs(info, args, 2)) return; Context2d *context = info.This().Unwrap(); cairo_move_to(context->context(), args[0], args[1]); } /* * Get font. */ Napi::Value Context2d::GetFont(const Napi::CallbackInfo& info) { CHECK_RECEIVER(Context2d.GetFont); Context2d *context = info.This().Unwrap(); return Napi::New(env, context->state->font); } /* * Set font: * - weight * - style * - size * - unit * - family */ void Context2d::SetFont(const Napi::CallbackInfo& info, const Napi::Value& value) { CHECK_RECEIVER(Context2d.SetFont); if (!value.IsString()) return; Isolate *iso = Isolate::GetCurrent(); Local ctx = Napi::GetCurrentContext(); Napi::String str = value.To(); if (!str->Length()) return; const int argc = 1; Napi::Value argv[argc] = { value }; Napi::Value mparsed = Napi::Call(_parseFont.Get(iso), ctx->Global(), argc, argv); // parseFont returns undefined for invalid CSS font strings if (mparsed->IsUndefined()) return; Napi::Object font = mparsed.To(); std::string weight = (font).Get(Napi::New(env, "weight".As())); std::string style = (font).Get(Napi::New(env, "style".As())); double size = (font).Get(Napi::String::New(env, "size".As().DoubleValue())).FromMaybe(0); std::string unit = (font).Get(Napi::New(env, "unit".As())); std::string family = (font).Get(Napi::New(env, "family".As())); Context2d *context = info.This().Unwrap(); 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 = value->As().Utf8Value().c_str(); } /* * Get text baseline. */ Napi::Value Context2d::GetTextBaseline(const Napi::CallbackInfo& info) { CHECK_RECEIVER(Context2d.GetTextBaseline); Context2d *context = info.This().Unwrap(); 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; } return Napi::New(env, baseline); } /* * Set text baseline. */ void Context2d::SetTextBaseline(const Napi::CallbackInfo& info, const Napi::Value& value) { CHECK_RECEIVER(Context2d.SetTextBaseline); if (!value.IsString()) return; std::string opStr = value.As(.To()); 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 = info.This().Unwrap(); context->state->textBaseline = op->second; } /* * Get text align. */ Napi::Value Context2d::GetTextAlign(const Napi::CallbackInfo& info) { CHECK_RECEIVER(Context2d.GetTextAlign); Context2d *context = info.This().Unwrap(); 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; } return Napi::New(env, align); } /* * Set text align. */ void Context2d::SetTextAlign(const Napi::CallbackInfo& info, const Napi::Value& value) { CHECK_RECEIVER(Context2d.SetTextAlign); if (!value.IsString()) return; std::string opStr = value.As(.To()); 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 = info.This().Unwrap(); context->state->textAlignment = op->second; } /* * Return the given text extents. * TODO: Support for: * hangingBaseline, ideographicBaseline, * fontBoundingBoxAscent, fontBoundingBoxDescent */ Napi::Value Context2d::MeasureText(const Napi::CallbackInfo& info) { Context2d *context = info.This().Unwrap(); cairo_t *ctx = context->context(); std::string str = info[0].As(.To()); Napi::Object obj = Napi::Object::New(env); 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); (obj).Set(Napi::String::New(env, "width"), Napi::Number::New(env, logical_rect.width)).Check(); (obj).Set(Napi::String::New(env, "actualBoundingBoxLeft"), Napi::Number::New(env, PANGO_LBEARING(ink_rect) + x_offset)).Check(); (obj).Set(Napi::String::New(env, "actualBoundingBoxRight"), Napi::Number::New(env, PANGO_RBEARING(ink_rect) - x_offset)).Check(); (obj).Set(Napi::String::New(env, "actualBoundingBoxAscent"), Napi::Number::New(env, y_offset + PANGO_ASCENT(ink_rect))).Check(); (obj).Set(Napi::String::New(env, "actualBoundingBoxDescent"), Napi::Number::New(env, PANGO_DESCENT(ink_rect) - y_offset)).Check(); (obj).Set(Napi::String::New(env, "emHeightAscent"), Napi::Number::New(env, -(PANGO_ASCENT(logical_rect) - y_offset))).Check(); (obj).Set(Napi::String::New(env, "emHeightDescent"), Napi::Number::New(env, PANGO_DESCENT(logical_rect) - y_offset)).Check(); (obj).Set(Napi::String::New(env, "alphabeticBaseline"), Napi::Number::New(env, -(pango_font_metrics_get_ascent(metrics) * inverse_pango_scale - y_offset))).Check(); pango_font_metrics_unref(metrics); return obj; } /* * Set line dash * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash */ Napi::Value 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; iLength()); if (!d.IsNumber()) return; a[i] = d.As().DoubleValue().FromMaybe(0); if (a[i] == 0) zero_dashes++; if (a[i] < 0 || !std::isfinite(a[i])) return; } Context2d *context = info.This().Unwrap(); 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 */ Napi::Value Context2d::GetLineDash(const Napi::CallbackInfo& info) { Context2d *context = info.This().Unwrap(); cairo_t *ctx = context->context(); int dashes = cairo_get_dash_count(ctx); std::vector a(dashes); cairo_get_dash(ctx, a.data(), NULL); Napi::Array dash = Napi::Array::New(env, dashes); for (int i=0; i().DoubleValue().FromMaybe(0); if (!std::isfinite(offset)) return; Context2d *context = info.This().Unwrap(); 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 */ Napi::Value Context2d::GetLineDashOffset(const Napi::CallbackInfo& info) { CHECK_RECEIVER(Context2d.GetLineDashOffset); Context2d *context = info.This().Unwrap(); cairo_t *ctx = context->context(); double offset; cairo_get_dash(ctx, NULL, &offset); return Napi::Number::New(env, offset); } /* * Fill the rectangle defined by x, y, width and height. */ Napi::Value Context2d::FillRect(const Napi::CallbackInfo& info) { RECT_ARGS; if (0 == width || 0 == height) return; Context2d *context = info.This().Unwrap(); 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. */ Napi::Value Context2d::StrokeRect(const Napi::CallbackInfo& info) { RECT_ARGS; if (0 == width && 0 == height) return; Context2d *context = info.This().Unwrap(); 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. */ Napi::Value Context2d::ClearRect(const Napi::CallbackInfo& info) { RECT_ARGS; if (0 == width || 0 == height) return; Context2d *context = info.This().Unwrap(); 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. */ Napi::Value Context2d::Rect(const Napi::CallbackInfo& info) { RECT_ARGS; Context2d *context = info.This().Unwrap(); 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 Napi::Value& v) { if (v.IsObject()) { // 5.1 DOMPointInit auto rx = (v.As()).Get(Napi::String::New(env, "x")); auto ry = (v.As()).Get(Napi::String::New(env, "y")); if (rx.IsNumber() && 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) { 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 = v.As().DoubleValue(); if (!std::isfinite(rv)) return true; if (rv < 0) { Napi::RangeError::New(env, "radii must be positive.").ThrowAsJavaScriptException(); 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]] */ Napi::Value Context2d::RoundRect(const Napi::CallbackInfo& info) { RECT_ARGS; Context2d *context = info.This().Unwrap(); 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)) { Napi::RangeError::New(env, "radii must be a list of one, two, three or four radii.").ThrowAsJavaScriptException(); return env.Null(); } // 5. For each radius of radii for (size_t i = 0; i < nRadii; i++) { auto r = (radiiList).Get(i); 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. */ Napi::Value Context2d::Arc(const Napi::CallbackInfo& info) { 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) { Napi::RangeError::New(env, "The radius provided is negative.").ThrowAsJavaScriptException(); return env.Null(); } bool counterclockwise = info[5].As().Value().FromMaybe(false); Context2d *context = info.This().Unwrap(); 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. */ Napi::Value Context2d::ArcTo(const Napi::CallbackInfo& info) { double args[5]; if(!checkArgs(info, args, 5)) return; Context2d *context = info.This().Unwrap(); 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). */ Napi::Value Context2d::Ellipse(const Napi::CallbackInfo& info) { 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 = info[7].As().Value().FromMaybe(false); Context2d *context = info.This().Unwrap(); 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