// Copyright (c) 2010 LearnBoost <tj@learnboost.com>

#include "CanvasRenderingContext2d.h"

#include <algorithm>
#include "backend/ImageBackend.h"
#include <cairo-pdf.h>
#include "Canvas.h"
#include "CanvasGradient.h"
#include "CanvasPattern.h"
#include <cmath>
#include <cstdlib>
#include "Image.h"
#include "ImageData.h"
#include <limits>
#include <map>
#include "Point.h"
#include <string>
#include "Util.h"
#include <vector>

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<Napi::Number>().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<ObjectTemplate> 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<Context> 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<Napi::String>();
    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<double> 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;y<height;y++) {
              for (x=0;x<width;x++) {
                  int tot=pix[0];
                  if (x>0) 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<height-radius;y++) {
              for (x=radius;x<width-radius;x++) {
                  int l = x < radius ? 0 : x - radius;
                  int t = y < radius ? 0 : y - radius;
                  int r = x + radius >= 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<Napi::Object>();
  if (!Napi::New(env, Canvas::constructor)->HasInstance(obj))
    Napi::TypeError::New(env, "Canvas expected").ThrowAsJavaScriptException();
    return env.Null();
  Canvas *canvas = obj.Unwrap<Canvas>();

  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::Object>();

      Napi::Value pixelFormat = (ctxAttributes).Get(Napi::String::New(env, "pixelFormat"));
      if (pixelFormat.IsString()) {
        std::string utf8PixelFormat = pixelFormat.As<Napi::String>();
        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<Napi::Boolean>().Value().FromMaybe(false)) {
        format = CAIRO_FORMAT_RGB24;
      }
    }
    static_cast<ImageBackend*>(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<Napi::Function>());
  _parseFont.Reset(info[1].To<Napi::Function>());
}

/*
* Get format (string).
*/

Napi::Value Context2d::GetFormat(const Napi::CallbackInfo& info) {
  CHECK_RECEIVER(Context2d.GetFormat);
  Context2d *context = info.This().Unwrap<Context2d>();
  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<Context2d>();
  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<Napi::Number>().Int32Value().FromMaybe(0);
  int height = info[1].As<Napi::Number>().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<Napi::Object>();
  if (!Napi::New(env, ImageData::constructor)->HasInstance(obj))
    Napi::TypeError::New(env, "ImageData expected").ThrowAsJavaScriptException();
    return env.Null();

  Context2d *context = info.This().Unwrap<Context2d>();
  ImageData *imageData = obj.Unwrap<ImageData>();

  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<Napi::Number>().Int32Value().FromMaybe(0)
    , dy = info[2].As<Napi::Number>().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<Napi::Number>().Int32Value().FromMaybe(0);
      sy = info[4].As<Napi::Number>().Int32Value().FromMaybe(0);
      sw = info[5].As<Napi::Number>().Int32Value().FromMaybe(0);
      sh = info[6].As<Napi::Number>().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<Context2d>();
  Canvas *canvas = context->canvas();

  int sx = info[0].As<Napi::Number>().Int32Value().FromMaybe(0);
  int sy = info[1].As<Napi::Number>().Int32Value().FromMaybe(0);
  int sw = info[2].As<Napi::Number>().Int32Value().FromMaybe(0);
  int sh = info[3].As<Napi::Number>().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<ArrayBuffer> buffer = ArrayBuffer::New(Isolate::GetCurrent(), size);
  Local<TypedArray> 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<uint8_t> 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<Int32> swHandle = Napi::New(env, sw);
  Local<Int32> 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<Context2d>();
  Canvas *canvas = context->canvas();
  int32_t width, height;

  if (info[0].IsObject()) {
    Napi::Object obj = info[0].To<Napi::Object>();
    width = (obj).Get(Napi::String::New(env, "width".As<Napi::Number>().Int32Value())).FromMaybe(0);
    height = (obj).Get(Napi::String::New(env, "height".As<Napi::Number>().Int32Value())).FromMaybe(0);
  } else {
    width = info[0].As<Napi::Number>().Int32Value().FromMaybe(0);
    height = info[1].As<Napi::Number>().Int32Value().FromMaybe(0);
  }

  int stride = canvas->stride();
  double Bpp = static_cast<double>(stride) / canvas->getWidth();
  int nBytes = static_cast<int>(Bpp * width * height + .5);

  Local<ArrayBuffer> 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<Napi::Object>();

  // Image
  if (Napi::New(env, Image::constructor)->HasInstance(obj)) {
    Image *img = obj.Unwrap<Image>();
    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<Canvas>();
    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<Context2d>();
  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<Context2d>();
  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<Napi::Number>().DoubleValue().FromMaybe(0);
  if (n >= 0 && n <= 1) {
    Context2d *context = info.This().Unwrap<Context2d>();
    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<Context2d>();
  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<Context2d>();
  std::string quality = value.As<Napi::String>(.To<Napi::String>());
  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<Context2d>();
  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<Context2d>();
  context->state->imageSmoothingEnabled = value.As<Napi::Boolean>().Value().FromMaybe(false);
}

/*
 * Get pattern quality.
 */

Napi::Value Context2d::GetImageSmoothingEnabled(const Napi::CallbackInfo& info) {
  CHECK_RECEIVER(Context2d.GetImageSmoothingEnabled);
  Context2d *context = info.This().Unwrap<Context2d>();
  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<Context2d>();
  cairo_t *ctx = context->context();
  std::string opStr = value.As<Napi::String>(.To<Napi::String>()); // Unlike CSS colors, this *is* case-sensitive
  const std::map<std::string, cairo_operator_t> 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<Context2d>();
  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<Context2d>();
  context->state->shadowOffsetX = value.As<Napi::Number>().DoubleValue().FromMaybe(0);
}

/*
 * Get shadow offset y.
 */

Napi::Value Context2d::GetShadowOffsetY(const Napi::CallbackInfo& info) {
  CHECK_RECEIVER(Context2d.GetShadowOffsetY);
  Context2d *context = info.This().Unwrap<Context2d>();
  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<Context2d>();
  context->state->shadowOffsetY = value.As<Napi::Number>().DoubleValue().FromMaybe(0);
}

/*
 * Get shadow blur.
 */

Napi::Value Context2d::GetShadowBlur(const Napi::CallbackInfo& info) {
  CHECK_RECEIVER(Context2d.GetShadowBlur);
  Context2d *context = info.This().Unwrap<Context2d>();
  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<Napi::Number>().DoubleValue().FromMaybe(0);
  if (n >= 0) {
    Context2d *context = info.This().Unwrap<Context2d>();
    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<Context2d>();
  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<Napi::String>(.To<Napi::String>());
  Context2d *context = info.This().Unwrap<Context2d>();
  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<Context2d>();
  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<Napi::String>(.To<Napi::String>());
  Context2d *context = info.This().Unwrap<Context2d>();
  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<Context2d>();
  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<Napi::String>(.To<Napi::String>());
  Context2d *context = info.This().Unwrap<Context2d>();
  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<Float64Array> arr = Float64Array::New(ArrayBuffer::New(iso, 48), 0, 6);
  Napi::TypedArrayContents<double> 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<Napi::Number>().DoubleValue())).FromMaybe(0),
    (mat).Get(Napi::String::New(env, "b".As<Napi::Number>().DoubleValue())).FromMaybe(0),
    (mat).Get(Napi::String::New(env, "c".As<Napi::Number>().DoubleValue())).FromMaybe(0),
    (mat).Get(Napi::String::New(env, "d".As<Napi::Number>().DoubleValue())).FromMaybe(0),
    (mat).Get(Napi::String::New(env, "e".As<Napi::Number>().DoubleValue())).FromMaybe(0),
    (mat).Get(Napi::String::New(env, "f".As<Napi::Number>().DoubleValue())).FromMaybe(0)
  );
}


/*
 * Get current transform.
 */

Napi::Value Context2d::GetCurrentTransform(const Napi::CallbackInfo& info) {
  CHECK_RECEIVER(Context2d.GetCurrentTransform);
  Context2d *context = info.This().Unwrap<Context2d>();
  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<Context2d>();
  Local<Context> ctx = Napi::GetCurrentContext();
  Napi::Object mat = value.To<Napi::Object>();

#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<Context2d>();
  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<Context2d>();

  if (value.IsString()) {
    MaybeNapi::String mstr = value.To<Napi::String>();
    if (mstr.IsEmpty()) return;
    Napi::String str = mstr;
    context->_fillStyle.Reset();
    context->_setFillColor(str);
  } else if (value.IsObject()) {
    Napi::Object obj = value.To<Napi::Object>();
    if (Napi::New(env, Gradient::constructor)->HasInstance(obj)) {
      context->_fillStyle.Reset(value);
      Gradient *grad = obj.Unwrap<Gradient>();
      context->state->fillGradient = grad->pattern();
    } else if (Napi::New(env, Pattern::constructor)->HasInstance(obj)) {
      context->_fillStyle.Reset(value);
      Pattern *pattern = obj.Unwrap<Pattern>();
      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<Context2d>();
  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<Context2d>();

  if (value.IsString()) {
    MaybeNapi::String mstr = value.To<Napi::String>();
    if (mstr.IsEmpty()) return;
    Napi::String str = mstr;
    context->_strokeStyle.Reset();
    context->_setStrokeColor(str);
  } else if (value.IsObject()) {
    Napi::Object obj = value.To<Napi::Object>();
    if (Napi::New(env, Gradient::constructor)->HasInstance(obj)) {
      context->_strokeStyle.Reset(value);
      Gradient *grad = obj.Unwrap<Gradient>();
      context->state->strokeGradient = grad->pattern();
    } else if (Napi::New(env, Pattern::constructor)->HasInstance(obj)) {
      context->_strokeStyle.Reset(value);
      Pattern *pattern = obj.Unwrap<Pattern>();
      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<Context2d>();
  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<Napi::Number>().DoubleValue().FromMaybe(0);
  if (n > 0) {
    Context2d *context = info.This().Unwrap<Context2d>();
    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<Context2d>();
  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<Napi::Number>().DoubleValue().FromMaybe(0);
  if (n > 0 && n != std::numeric_limits<double>::infinity()) {
    Context2d *context = info.This().Unwrap<Context2d>();
    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<Context2d>();
  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<Context2d>();
  cairo_t *ctx = context->context();
  std::string type = value.As<Napi::String>(.To<Napi::String>());
  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<Context2d>();
  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<Context2d>();
  cairo_t *ctx = context->context();
  std::string type = value.As<Napi::String>(.To<Napi::String>());
  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<Context2d>();
    cairo_t *ctx = context->context();
    double x = info[0].As<Napi::Number>().DoubleValue().FromMaybe(0)
         , y = info[1].As<Napi::Number>().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<Napi::String>(.To<Napi::String>());
  uint32_t rgba = rgba_from_string(*str, &ok);
  if (ok) {
    Context2d *context = info.This().Unwrap<Context2d>();
    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<Context2d>();
  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<Napi::String>();
  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<Napi::String>();
  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<Napi::Boolean>().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<Context2d>();
  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<Context2d>();
  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<Context2d>();
  context->save();
}

/*
 * Restore state.
 */

Napi::Value Context2d::Restore(const Napi::CallbackInfo& info) {
  Context2d *context = info.This().Unwrap<Context2d>();
  context->restore();
}

/*
 * Creates a new subpath.
 */

Napi::Value Context2d::BeginPath(const Napi::CallbackInfo& info) {
  Context2d *context = info.This().Unwrap<Context2d>();
  cairo_new_path(context->context());
}

/*
 * Marks the subpath as closed.
 */

Napi::Value Context2d::ClosePath(const Napi::CallbackInfo& info) {
  Context2d *context = info.This().Unwrap<Context2d>();
  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<Context2d>();
  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<Context2d>();
  cairo_transform(context->context(), &matrix);
}

/*
 * Get the CTM
 */

Napi::Value Context2d::GetTransform(const Napi::CallbackInfo& info) {
  Context2d *context = info.This().Unwrap<Context2d>();
  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<Context2d>();
  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<Context2d>();
  if (info.Length() == 1) {
    Napi::Object mat = info[0].To<Napi::Object>();

    #if NODE_MAJOR_VERSION >= 8
      Local<Context> 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<Context2d>();
  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<Context2d>();
  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<Context2d>();
  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<Context2d>();
  context->setFillRule(info[0]);
  context->fill(true);
}

/*
 * Stroke the path.
 */

Napi::Value Context2d::Stroke(const Napi::CallbackInfo& info) {
  Context2d *context = info.This().Unwrap<Context2d>();
  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<Napi::String>(.To<Napi::String>());
  double x = args[0];
  double y = args[1];
  double scaled_by = 1;

  Context2d *context = info.This().Unwrap<Context2d>();
  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<Context2d>();
  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<Context2d>();
  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<Context2d>();

  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<Context> ctx = Napi::GetCurrentContext();

  Napi::String str = value.To<Napi::String>();
  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<Napi::Object>();

  std::string weight = (font).Get(Napi::New(env, "weight".As<Napi::String>()));
  std::string style = (font).Get(Napi::New(env, "style".As<Napi::String>()));
  double size = (font).Get(Napi::String::New(env, "size".As<Napi::Number>().DoubleValue())).FromMaybe(0);
  std::string unit = (font).Get(Napi::New(env, "unit".As<Napi::String>()));
  std::string family = (font).Get(Napi::New(env, "family".As<Napi::String>()));

  Context2d *context = info.This().Unwrap<Context2d>();

  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<Napi::String>().Utf8Value().c_str();
}

/*
 * Get text baseline.
 */

Napi::Value Context2d::GetTextBaseline(const Napi::CallbackInfo& info) {
  CHECK_RECEIVER(Context2d.GetTextBaseline);
  Context2d *context = info.This().Unwrap<Context2d>();
  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<Napi::String>(.To<Napi::String>());
  const std::map<std::string, text_baseline_t> 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<Context2d>();
  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<Context2d>();
  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<Napi::String>(.To<Napi::String>());
  const std::map<std::string, text_align_t> 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<Context2d>();
  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<Context2d>();
  cairo_t *ctx = context->context();

  std::string str = info[0].As<Napi::String>(.To<Napi::String>());
  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<Napi::Array>();
  uint32_t dashes = dash->Length() & 1 ? dash->Length() * 2 : dash->Length();
  uint32_t zero_dashes = 0;
  std::vector<double> a(dashes);
  for (uint32_t i=0; i<dashes; i++) {
    Napi::Value d = (dash).Get(i % dash->Length());
    if (!d.IsNumber()) return;
    a[i] = d.As<Napi::Number>().DoubleValue().FromMaybe(0);
    if (a[i] == 0) zero_dashes++;
    if (a[i] < 0 || !std::isfinite(a[i])) return;
  }

  Context2d *context = info.This().Unwrap<Context2d>();
  cairo_t *ctx = context->context();
  double offset;
  cairo_get_dash(ctx, NULL, &offset);
  if (zero_dashes == dashes) {
    std::vector<double> 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<Context2d>();
  cairo_t *ctx = context->context();
  int dashes = cairo_get_dash_count(ctx);
  std::vector<double> a(dashes);
  cairo_get_dash(ctx, a.data(), NULL);

  Napi::Array dash = Napi::Array::New(env, dashes);
  for (int i=0; i<dashes; i++) {
    (dash).Set(Napi::Number::New(env, i), Napi::Number::New(env, a[i])).Check();
  }

  return dash;
}

/*
 * Set line dash offset
 * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash
 */
void Context2d::SetLineDashOffset(const Napi::CallbackInfo& info, const Napi::Value& value) {
  CHECK_RECEIVER(Context2d.SetLineDashOffset);
  double offset = value.As<Napi::Number>().DoubleValue().FromMaybe(0);
  if (!std::isfinite(offset)) return;

  Context2d *context = info.This().Unwrap<Context2d>();
  cairo_t *ctx = context->context();

  int dashes = cairo_get_dash_count(ctx);
  std::vector<double> 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<Context2d>();
  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<Context2d>();
  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<Context2d>();
  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<Context2d>();
  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<Context2d>();
  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<double>& p, const Napi::Value& v) {
  if (v.IsObject()) { // 5.1 DOMPointInit
    auto rx = (v.As<Napi::Object>()).Get(Napi::String::New(env, "x"));
    auto ry = (v.As<Napi::Object>()).Get(Napi::String::New(env, "y"));
    if (rx.IsNumber() && ry.IsNumber()) {
      auto rxv = rx.As<Napi::Number>().DoubleValue();
      auto ryv = ry.As<Napi::Number>().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<Napi::Number>().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<Context2d>();
  cairo_t *ctx = context->context();

  // 4. Let normalizedRadii be an empty list
  Point<double> 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<Napi::Array>();
    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 <<radii>>
    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<double> 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<Napi::Boolean>().Value().FromMaybe(false);

  Context2d *context = info.This().Unwrap<Context2d>();
  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<Context2d>();
  cairo_t *ctx = context->context();

  // Current path point
  double x, y;
  cairo_get_current_point(ctx, &x, &y);
  Point<float> p0(x, y);

  // Point (x0,y0)
  Point<float> p1(args[0], args[1]);

  // Point (x1,y1)
  Point<float> 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<float> p1p0((p0.x - p1.x),(p0.y - p1.y));
  Point<float> 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<float> 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<float> t_p1p0((p1.x + factor_p1p0 * p1p0.x), (p1.y + factor_p1p0 * p1p0.y));

  Point<float> 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<float>(-orth_p1p0.x, -orth_p1p0.y);

  Point<float> p((t_p1p0.x + factor_ra * orth_p1p0.x), (t_p1p0.y + factor_ra * orth_p1p0.y));

  orth_p1p0 = Point<float>(-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<float> t_p1p2((p1.x + factor_p1p2 * p1p2.x), (p1.y + factor_p1p2 * p1p2.y));
  Point<float> 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<Napi::Boolean>().Value().FromMaybe(false);

  Context2d *context = info.This().Unwrap<Context2d>();
  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
