@shqld/canvas 3.2.2-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/Readme.md +654 -0
  2. package/binding.gyp +229 -0
  3. package/browser.js +31 -0
  4. package/index.d.ts +507 -0
  5. package/index.js +94 -0
  6. package/lib/DOMMatrix.js +678 -0
  7. package/lib/bindings.js +113 -0
  8. package/lib/canvas.js +113 -0
  9. package/lib/context2d.js +11 -0
  10. package/lib/image.js +97 -0
  11. package/lib/jpegstream.js +41 -0
  12. package/lib/pattern.js +15 -0
  13. package/lib/pdfstream.js +35 -0
  14. package/lib/pngstream.js +42 -0
  15. package/package.json +77 -0
  16. package/scripts/install.js +19 -0
  17. package/src/Backends.h +9 -0
  18. package/src/Canvas.cc +1026 -0
  19. package/src/Canvas.h +128 -0
  20. package/src/CanvasError.h +37 -0
  21. package/src/CanvasGradient.cc +113 -0
  22. package/src/CanvasGradient.h +20 -0
  23. package/src/CanvasPattern.cc +129 -0
  24. package/src/CanvasPattern.h +33 -0
  25. package/src/CanvasRenderingContext2d.cc +3527 -0
  26. package/src/CanvasRenderingContext2d.h +238 -0
  27. package/src/CharData.h +233 -0
  28. package/src/FontParser.cc +605 -0
  29. package/src/FontParser.h +115 -0
  30. package/src/Image.cc +1719 -0
  31. package/src/Image.h +146 -0
  32. package/src/ImageData.cc +138 -0
  33. package/src/ImageData.h +26 -0
  34. package/src/InstanceData.h +12 -0
  35. package/src/JPEGStream.h +157 -0
  36. package/src/PNG.h +292 -0
  37. package/src/Point.h +11 -0
  38. package/src/Util.h +9 -0
  39. package/src/bmp/BMPParser.cc +459 -0
  40. package/src/bmp/BMPParser.h +60 -0
  41. package/src/bmp/LICENSE.md +24 -0
  42. package/src/closure.cc +52 -0
  43. package/src/closure.h +98 -0
  44. package/src/color.cc +796 -0
  45. package/src/color.h +30 -0
  46. package/src/dll_visibility.h +20 -0
  47. package/src/init.cc +114 -0
  48. package/src/register_font.cc +352 -0
  49. package/src/register_font.h +7 -0
  50. package/util/has_lib.js +119 -0
  51. package/util/win_jpeg_lookup.js +21 -0
@@ -0,0 +1,3527 @@
1
+ // Copyright (c) 2010 LearnBoost <tj@learnboost.com>
2
+
3
+ #include "CanvasRenderingContext2d.h"
4
+
5
+ #include <algorithm>
6
+ #include <cairo-pdf.h>
7
+ #include "Canvas.h"
8
+ #include "CanvasGradient.h"
9
+ #include "CanvasPattern.h"
10
+ #include "InstanceData.h"
11
+ #include "FontParser.h"
12
+ #include <cmath>
13
+ #include <cstdlib>
14
+ #include "Image.h"
15
+ #include "ImageData.h"
16
+ #include <limits>
17
+ #include <map>
18
+ #include "Point.h"
19
+ #include <string>
20
+ #include "Util.h"
21
+ #include <vector>
22
+
23
+ /*
24
+ * Rectangle arg assertions.
25
+ */
26
+
27
+ #define RECT_ARGS \
28
+ double args[4]; \
29
+ if(!checkArgs(info, args, 4)) \
30
+ return; \
31
+ double x = args[0]; \
32
+ double y = args[1]; \
33
+ double width = args[2]; \
34
+ double height = args[3];
35
+
36
+ constexpr double twoPi = M_PI * 2.;
37
+
38
+ /*
39
+ * Simple helper macro for a rather verbose function call.
40
+ */
41
+
42
+ #define PANGO_LAYOUT_GET_METRICS(LAYOUT) pango_context_get_metrics( \
43
+ pango_layout_get_context(LAYOUT), \
44
+ pango_layout_get_font_description(LAYOUT), \
45
+ pango_language_from_string(state->lang.c_str()))
46
+
47
+ inline static bool checkArgs(const Napi::CallbackInfo&info, double *args, int argsNum, int offset = 0){
48
+ Napi::Env env = info.Env();
49
+ int argsEnd = std::min(9, offset + argsNum);
50
+ bool areArgsValid = true;
51
+
52
+ napi_value argv[9];
53
+ size_t argc = 9;
54
+ napi_get_cb_info(env, static_cast<napi_callback_info>(info), &argc, argv, nullptr, nullptr);
55
+
56
+ for (int i = offset; i < argsEnd; i++) {
57
+ napi_valuetype type;
58
+ double val = 0;
59
+
60
+ napi_typeof(env, argv[i], &type);
61
+ if (type == napi_number) {
62
+ // fast path
63
+ napi_get_value_double(env, argv[i], &val);
64
+ } else {
65
+ napi_value num;
66
+ if (napi_coerce_to_number(env, argv[i], &num) == napi_ok) {
67
+ napi_get_value_double(env, num, &val);
68
+ }
69
+ }
70
+
71
+ if (areArgsValid) {
72
+ if (!std::isfinite(val)) {
73
+ // We should continue the loop instead of returning immediately
74
+ // See https://html.spec.whatwg.org/multipage/canvas.html
75
+
76
+ areArgsValid = false;
77
+ continue;
78
+ }
79
+
80
+ args[i - offset] = val;
81
+ }
82
+ }
83
+
84
+ return areArgsValid;
85
+ }
86
+
87
+ /*
88
+ * Initialize Context2d.
89
+ */
90
+
91
+ void
92
+ Context2d::Initialize(Napi::Env& env, Napi::Object& exports) {
93
+ Napi::HandleScope scope(env);
94
+ InstanceData* data = env.GetInstanceData<InstanceData>();
95
+
96
+ Napi::Function ctor = DefineClass(env, "CanvasRenderingContext2D", {
97
+ InstanceMethod<&Context2d::DrawImage>("drawImage", napi_default_method),
98
+ InstanceMethod<&Context2d::PutImageData>("putImageData", napi_default_method),
99
+ InstanceMethod<&Context2d::GetImageData>("getImageData", napi_default_method),
100
+ InstanceMethod<&Context2d::CreateImageData>("createImageData", napi_default_method),
101
+ InstanceMethod<&Context2d::AddPage>("addPage", napi_default_method),
102
+ InstanceMethod<&Context2d::Save>("save", napi_default_method),
103
+ InstanceMethod<&Context2d::Restore>("restore", napi_default_method),
104
+ InstanceMethod<&Context2d::Rotate>("rotate", napi_default_method),
105
+ InstanceMethod<&Context2d::Translate>("translate", napi_default_method),
106
+ InstanceMethod<&Context2d::Transform>("transform", napi_default_method),
107
+ InstanceMethod<&Context2d::GetTransform>("getTransform", napi_default_method),
108
+ InstanceMethod<&Context2d::ResetTransform>("resetTransform", napi_default_method),
109
+ InstanceMethod<&Context2d::SetTransform>("setTransform", napi_default_method),
110
+ InstanceMethod<&Context2d::IsPointInPath>("isPointInPath", napi_default_method),
111
+ InstanceMethod<&Context2d::Scale>("scale", napi_default_method),
112
+ InstanceMethod<&Context2d::Clip>("clip", napi_default_method),
113
+ InstanceMethod<&Context2d::Fill>("fill", napi_default_method),
114
+ InstanceMethod<&Context2d::Stroke>("stroke", napi_default_method),
115
+ InstanceMethod<&Context2d::FillText>("fillText", napi_default_method),
116
+ InstanceMethod<&Context2d::StrokeText>("strokeText", napi_default_method),
117
+ InstanceMethod<&Context2d::FillRect>("fillRect", napi_default_method),
118
+ InstanceMethod<&Context2d::StrokeRect>("strokeRect", napi_default_method),
119
+ InstanceMethod<&Context2d::ClearRect>("clearRect", napi_default_method),
120
+ InstanceMethod<&Context2d::Rect>("rect", napi_default_method),
121
+ InstanceMethod<&Context2d::RoundRect>("roundRect", napi_default_method),
122
+ InstanceMethod<&Context2d::MeasureText>("measureText", napi_default_method),
123
+ InstanceMethod<&Context2d::MoveTo>("moveTo", napi_default_method),
124
+ InstanceMethod<&Context2d::LineTo>("lineTo", napi_default_method),
125
+ InstanceMethod<&Context2d::BezierCurveTo>("bezierCurveTo", napi_default_method),
126
+ InstanceMethod<&Context2d::QuadraticCurveTo>("quadraticCurveTo", napi_default_method),
127
+ InstanceMethod<&Context2d::BeginPath>("beginPath", napi_default_method),
128
+ InstanceMethod<&Context2d::ClosePath>("closePath", napi_default_method),
129
+ InstanceMethod<&Context2d::Arc>("arc", napi_default_method),
130
+ InstanceMethod<&Context2d::ArcTo>("arcTo", napi_default_method),
131
+ InstanceMethod<&Context2d::Ellipse>("ellipse", napi_default_method),
132
+ InstanceMethod<&Context2d::SetLineDash>("setLineDash", napi_default_method),
133
+ InstanceMethod<&Context2d::GetLineDash>("getLineDash", napi_default_method),
134
+ InstanceMethod<&Context2d::CreatePattern>("createPattern", napi_default_method),
135
+ InstanceMethod<&Context2d::CreateLinearGradient>("createLinearGradient", napi_default_method),
136
+ InstanceMethod<&Context2d::CreateRadialGradient>("createRadialGradient", napi_default_method),
137
+ #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0)
138
+ InstanceMethod<&Context2d::BeginTag>("beginTag", napi_default_method),
139
+ InstanceMethod<&Context2d::EndTag>("endTag", napi_default_method),
140
+ #endif
141
+ InstanceAccessor<&Context2d::GetFormat>("pixelFormat", napi_default_jsproperty),
142
+ InstanceAccessor<&Context2d::GetPatternQuality, &Context2d::SetPatternQuality>("patternQuality", napi_default_jsproperty),
143
+ InstanceAccessor<&Context2d::GetImageSmoothingEnabled, &Context2d::SetImageSmoothingEnabled>("imageSmoothingEnabled", napi_default_jsproperty),
144
+ InstanceAccessor<&Context2d::GetGlobalCompositeOperation, &Context2d::SetGlobalCompositeOperation>("globalCompositeOperation", napi_default_jsproperty),
145
+ InstanceAccessor<&Context2d::GetGlobalAlpha, &Context2d::SetGlobalAlpha>("globalAlpha", napi_default_jsproperty),
146
+ InstanceAccessor<&Context2d::GetShadowColor, &Context2d::SetShadowColor>("shadowColor", napi_default_jsproperty),
147
+ InstanceAccessor<&Context2d::GetMiterLimit, &Context2d::SetMiterLimit>("miterLimit", napi_default_jsproperty),
148
+ InstanceAccessor<&Context2d::GetLineWidth, &Context2d::SetLineWidth>("lineWidth", napi_default_jsproperty),
149
+ InstanceAccessor<&Context2d::GetLineCap, &Context2d::SetLineCap>("lineCap", napi_default_jsproperty),
150
+ InstanceAccessor<&Context2d::GetLineJoin, &Context2d::SetLineJoin>("lineJoin", napi_default_jsproperty),
151
+ InstanceAccessor<&Context2d::GetLineDashOffset, &Context2d::SetLineDashOffset>("lineDashOffset", napi_default_jsproperty),
152
+ InstanceAccessor<&Context2d::GetShadowOffsetX, &Context2d::SetShadowOffsetX>("shadowOffsetX", napi_default_jsproperty),
153
+ InstanceAccessor<&Context2d::GetShadowOffsetY, &Context2d::SetShadowOffsetY>("shadowOffsetY", napi_default_jsproperty),
154
+ InstanceAccessor<&Context2d::GetShadowBlur, &Context2d::SetShadowBlur>("shadowBlur", napi_default_jsproperty),
155
+ InstanceAccessor<&Context2d::GetAntiAlias, &Context2d::SetAntiAlias>("antialias", napi_default_jsproperty),
156
+ InstanceAccessor<&Context2d::GetTextDrawingMode, &Context2d::SetTextDrawingMode>("textDrawingMode", napi_default_jsproperty),
157
+ InstanceAccessor<&Context2d::GetQuality, &Context2d::SetQuality>("quality", napi_default_jsproperty),
158
+ InstanceAccessor<&Context2d::GetCurrentTransform, &Context2d::SetCurrentTransform>("currentTransform", napi_default_jsproperty),
159
+ InstanceAccessor<&Context2d::GetFillStyle, &Context2d::SetFillStyle>("fillStyle", napi_default_jsproperty),
160
+ InstanceAccessor<&Context2d::GetStrokeStyle, &Context2d::SetStrokeStyle>("strokeStyle", napi_default_jsproperty),
161
+ InstanceAccessor<&Context2d::GetFont, &Context2d::SetFont>("font", napi_default_jsproperty),
162
+ InstanceAccessor<&Context2d::GetTextBaseline, &Context2d::SetTextBaseline>("textBaseline", napi_default_jsproperty),
163
+ InstanceAccessor<&Context2d::GetTextAlign, &Context2d::SetTextAlign>("textAlign", napi_default_jsproperty),
164
+ InstanceAccessor<&Context2d::GetDirection, &Context2d::SetDirection>("direction", napi_default_jsproperty),
165
+ InstanceAccessor<&Context2d::GetLanguage, &Context2d::SetLanguage>("lang", napi_default_jsproperty)
166
+ });
167
+
168
+ exports.Set("CanvasRenderingContext2d", ctor);
169
+ data->Context2dCtor = Napi::Persistent(ctor);
170
+ }
171
+
172
+ /*
173
+ * Create a cairo context.
174
+ */
175
+
176
+ Context2d::Context2d(const Napi::CallbackInfo& info) : Napi::ObjectWrap<Context2d>(info), env(info.Env()) {
177
+ InstanceData* data = env.GetInstanceData<InstanceData>();
178
+
179
+ if (!info[0].IsObject()) {
180
+ Napi::TypeError::New(env, "Canvas expected").ThrowAsJavaScriptException();
181
+ return;
182
+ }
183
+
184
+ Napi::Object obj = info[0].As<Napi::Object>();
185
+ if (!obj.InstanceOf(data->CanvasCtor.Value()).UnwrapOr(false)) {
186
+ if (!env.IsExceptionPending()) {
187
+ Napi::TypeError::New(env, "Canvas expected").ThrowAsJavaScriptException();
188
+ }
189
+ return;
190
+ }
191
+
192
+ _canvas = Canvas::Unwrap(obj);
193
+
194
+ if (_canvas->isImage()) {
195
+ cairo_format_t format = CAIRO_FORMAT_ARGB32;
196
+
197
+ if (info[1].IsObject()) {
198
+ Napi::Object ctxAttributes = info[1].As<Napi::Object>();
199
+ Napi::Value pixelFormat;
200
+
201
+ if (ctxAttributes.Get("pixelFormat").UnwrapTo(&pixelFormat) && pixelFormat.IsString()) {
202
+ std::string utf8PixelFormat = pixelFormat.As<Napi::String>();
203
+ if (utf8PixelFormat == "RGBA32") format = CAIRO_FORMAT_ARGB32;
204
+ else if (utf8PixelFormat == "RGB24") format = CAIRO_FORMAT_RGB24;
205
+ else if (utf8PixelFormat == "A8") format = CAIRO_FORMAT_A8;
206
+ else if (utf8PixelFormat == "RGB16_565") format = CAIRO_FORMAT_RGB16_565;
207
+ else if (utf8PixelFormat == "A1") format = CAIRO_FORMAT_A1;
208
+ #ifdef CAIRO_FORMAT_RGB30
209
+ else if (utf8PixelFormat == "RGB30") format = CAIRO_FORMAT_RGB30;
210
+ #endif
211
+ }
212
+
213
+ // alpha: false forces use of RGB24
214
+ Napi::Value alpha;
215
+
216
+ if (ctxAttributes.Get("alpha").UnwrapTo(&alpha) && alpha.IsBoolean() && !alpha.As<Napi::Boolean>().Value()) {
217
+ format = CAIRO_FORMAT_RGB24;
218
+ }
219
+ }
220
+
221
+ _canvas->setFormat(format);
222
+ }
223
+
224
+ _context = _canvas->createCairoContext();
225
+ _layout = pango_cairo_create_layout(_context);
226
+
227
+ // As of January 2023, Pango rounds glyph positions which renders text wider
228
+ // or narrower than the browser. See #2184 for more information
229
+ #if PANGO_VERSION_CHECK(1, 44, 0)
230
+ pango_context_set_round_glyph_positions(pango_layout_get_context(_layout), FALSE);
231
+ #endif
232
+
233
+ pango_layout_set_auto_dir(_layout, FALSE);
234
+
235
+ states.emplace();
236
+ state = &states.top();
237
+ pango_layout_set_font_description(_layout, state->fontDescription);
238
+ }
239
+
240
+ /*
241
+ * Destroy cairo context.
242
+ */
243
+
244
+ Context2d::~Context2d() {
245
+ if (_layout) g_object_unref(_layout);
246
+ if (_context) cairo_destroy(_context);
247
+ _resetPersistentHandles();
248
+ }
249
+
250
+ /*
251
+ * Reset canvas state.
252
+ */
253
+
254
+ void Context2d::resetState() {
255
+ states.pop();
256
+ states.emplace();
257
+ pango_layout_set_font_description(_layout, state->fontDescription);
258
+ _resetPersistentHandles();
259
+ }
260
+
261
+ void Context2d::_resetPersistentHandles() {
262
+ _fillStyle.Reset();
263
+ _strokeStyle.Reset();
264
+ }
265
+
266
+ /*
267
+ * Save cairo / canvas state.
268
+ */
269
+
270
+ void
271
+ Context2d::save() {
272
+ cairo_save(_context);
273
+ states.emplace(states.top());
274
+ state = &states.top();
275
+ }
276
+
277
+ /*
278
+ * Restore cairo / canvas state.
279
+ */
280
+
281
+ void
282
+ Context2d::restore() {
283
+ if (states.size() > 1) {
284
+ cairo_restore(_context);
285
+ states.pop();
286
+ state = &states.top();
287
+ pango_layout_set_font_description(_layout, state->fontDescription);
288
+ }
289
+ }
290
+
291
+ /*
292
+ * Save flat path.
293
+ */
294
+
295
+ void
296
+ Context2d::savePath() {
297
+ _path = cairo_copy_path_flat(_context);
298
+ cairo_new_path(_context);
299
+ }
300
+
301
+ /*
302
+ * Restore flat path.
303
+ */
304
+
305
+ void
306
+ Context2d::restorePath() {
307
+ cairo_new_path(_context);
308
+ cairo_append_path(_context, _path);
309
+ cairo_path_destroy(_path);
310
+ }
311
+
312
+ /*
313
+ * Create temporary surface for gradient or pattern transparency
314
+ */
315
+ cairo_pattern_t*
316
+ create_transparent_gradient(cairo_pattern_t *source, float alpha) {
317
+ double x0;
318
+ double y0;
319
+ double x1;
320
+ double y1;
321
+ double r0;
322
+ double r1;
323
+ int count;
324
+ int i;
325
+ double offset;
326
+ double r;
327
+ double g;
328
+ double b;
329
+ double a;
330
+ cairo_pattern_t *newGradient;
331
+ cairo_pattern_type_t type = cairo_pattern_get_type(source);
332
+ cairo_pattern_get_color_stop_count(source, &count);
333
+ if (type == CAIRO_PATTERN_TYPE_LINEAR) {
334
+ cairo_pattern_get_linear_points (source, &x0, &y0, &x1, &y1);
335
+ newGradient = cairo_pattern_create_linear(x0, y0, x1, y1);
336
+ } else if (type == CAIRO_PATTERN_TYPE_RADIAL) {
337
+ cairo_pattern_get_radial_circles(source, &x0, &y0, &r0, &x1, &y1, &r1);
338
+ newGradient = cairo_pattern_create_radial(x0, y0, r0, x1, y1, r1);
339
+ } else {
340
+ return NULL;
341
+ }
342
+ for ( i = 0; i < count; i++ ) {
343
+ cairo_pattern_get_color_stop_rgba(source, i, &offset, &r, &g, &b, &a);
344
+ cairo_pattern_add_color_stop_rgba(newGradient, offset, r, g, b, a * alpha);
345
+ }
346
+ return newGradient;
347
+ }
348
+
349
+ cairo_pattern_t*
350
+ create_transparent_pattern(cairo_pattern_t *source, float alpha) {
351
+ cairo_surface_t *surface;
352
+ cairo_pattern_get_surface(source, &surface);
353
+ int width = cairo_image_surface_get_width(surface);
354
+ int height = cairo_image_surface_get_height(surface);
355
+ cairo_surface_t *mask_surface = cairo_image_surface_create(
356
+ CAIRO_FORMAT_ARGB32,
357
+ width,
358
+ height);
359
+ cairo_t *mask_context = cairo_create(mask_surface);
360
+ if (cairo_status(mask_context) != CAIRO_STATUS_SUCCESS) {
361
+ return NULL;
362
+ }
363
+ cairo_set_source(mask_context, source);
364
+ cairo_paint_with_alpha(mask_context, alpha);
365
+ cairo_destroy(mask_context);
366
+ cairo_pattern_t* newPattern = cairo_pattern_create_for_surface(mask_surface);
367
+ cairo_surface_destroy(mask_surface);
368
+ return newPattern;
369
+ }
370
+
371
+ /*
372
+ * Fill and apply shadow.
373
+ */
374
+
375
+ void
376
+ Context2d::setFillRule(Napi::Value value) {
377
+ cairo_fill_rule_t rule = CAIRO_FILL_RULE_WINDING;
378
+ if (value.IsString()) {
379
+ std::string str = value.As<Napi::String>().Utf8Value();
380
+ if (str == "evenodd") {
381
+ rule = CAIRO_FILL_RULE_EVEN_ODD;
382
+ }
383
+ }
384
+ cairo_set_fill_rule(_context, rule);
385
+ }
386
+
387
+ void
388
+ Context2d::fill(bool preserve) {
389
+ cairo_pattern_t *new_pattern;
390
+ bool needsRestore = false;
391
+ if (state->fillPattern) {
392
+ if (state->globalAlpha < 1) {
393
+ new_pattern = create_transparent_pattern(state->fillPattern, state->globalAlpha);
394
+ if (new_pattern == NULL) {
395
+ Napi::Error::New(env, "Failed to initialize context").ThrowAsJavaScriptException();
396
+ // failed to allocate
397
+ return;
398
+ }
399
+ cairo_set_source(_context, new_pattern);
400
+ cairo_pattern_destroy(new_pattern);
401
+ } else {
402
+ cairo_pattern_set_filter(state->fillPattern, state->patternQuality);
403
+ cairo_set_source(_context, state->fillPattern);
404
+ }
405
+ repeat_type_t repeat = Pattern::get_repeat_type_for_cairo_pattern(state->fillPattern);
406
+ if (repeat == NO_REPEAT) {
407
+ cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_NONE);
408
+ } else if (repeat == REPEAT) {
409
+ cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_REPEAT);
410
+ } else {
411
+ cairo_save(_context);
412
+ cairo_path_t *savedPath = cairo_copy_path(_context);
413
+ cairo_surface_t *patternSurface = nullptr;
414
+ cairo_pattern_get_surface(cairo_get_source(_context), &patternSurface);
415
+
416
+ double width, height;
417
+ if (repeat == REPEAT_X) {
418
+ double x1, x2;
419
+ cairo_path_extents(_context, &x1, nullptr, &x2, nullptr);
420
+ width = x2 - x1;
421
+ height = cairo_image_surface_get_height(patternSurface);
422
+ } else {
423
+ double y1, y2;
424
+ cairo_path_extents(_context, nullptr, &y1, nullptr, &y2);
425
+ width = cairo_image_surface_get_width(patternSurface);
426
+ height = y2 - y1;
427
+ }
428
+
429
+ cairo_new_path(_context);
430
+ cairo_rectangle(_context, 0, 0, width, height);
431
+ cairo_clip(_context);
432
+ cairo_append_path(_context, savedPath);
433
+ cairo_path_destroy(savedPath);
434
+ cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_REPEAT);
435
+ needsRestore = true;
436
+ }
437
+ } else if (state->fillGradient) {
438
+ if (state->globalAlpha < 1) {
439
+ new_pattern = create_transparent_gradient(state->fillGradient, state->globalAlpha);
440
+ if (new_pattern == NULL) {
441
+ Napi::Error::New(env, "Unexpected gradient type").ThrowAsJavaScriptException();
442
+ // failed to recognize gradient
443
+ return;
444
+ }
445
+ cairo_pattern_set_filter(new_pattern, state->patternQuality);
446
+ cairo_set_source(_context, new_pattern);
447
+ cairo_pattern_destroy(new_pattern);
448
+ } else {
449
+ cairo_pattern_set_filter(state->fillGradient, state->patternQuality);
450
+ cairo_set_source(_context, state->fillGradient);
451
+ }
452
+ } else {
453
+ setSourceRGBA(state->fill);
454
+ }
455
+ if (preserve) {
456
+ hasShadow()
457
+ ? shadow(cairo_fill_preserve)
458
+ : cairo_fill_preserve(_context);
459
+ } else {
460
+ hasShadow()
461
+ ? shadow(cairo_fill)
462
+ : cairo_fill(_context);
463
+ }
464
+ if (needsRestore) {
465
+ cairo_restore(_context);
466
+ }
467
+ }
468
+
469
+ /*
470
+ * Stroke and apply shadow.
471
+ */
472
+
473
+ void
474
+ Context2d::stroke(bool preserve) {
475
+ cairo_pattern_t *new_pattern;
476
+ if (state->strokePattern) {
477
+ if (state->globalAlpha < 1) {
478
+ new_pattern = create_transparent_pattern(state->strokePattern, state->globalAlpha);
479
+ if (new_pattern == NULL) {
480
+ Napi::Error::New(env, "Failed to initialize context").ThrowAsJavaScriptException();
481
+ // failed to allocate
482
+ return;
483
+ }
484
+ cairo_set_source(_context, new_pattern);
485
+ cairo_pattern_destroy(new_pattern);
486
+ } else {
487
+ cairo_pattern_set_filter(state->strokePattern, state->patternQuality);
488
+ cairo_set_source(_context, state->strokePattern);
489
+ }
490
+ repeat_type_t repeat = Pattern::get_repeat_type_for_cairo_pattern(state->strokePattern);
491
+ if (NO_REPEAT == repeat) {
492
+ cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_NONE);
493
+ } else {
494
+ cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_REPEAT);
495
+ }
496
+ } else if (state->strokeGradient) {
497
+ if (state->globalAlpha < 1) {
498
+ new_pattern = create_transparent_gradient(state->strokeGradient, state->globalAlpha);
499
+ if (new_pattern == NULL) {
500
+ Napi::Error::New(env, "Unexpected gradient type").ThrowAsJavaScriptException();
501
+ // failed to recognize gradient
502
+ return;
503
+ }
504
+ cairo_pattern_set_filter(new_pattern, state->patternQuality);
505
+ cairo_set_source(_context, new_pattern);
506
+ cairo_pattern_destroy(new_pattern);
507
+ } else {
508
+ cairo_pattern_set_filter(state->strokeGradient, state->patternQuality);
509
+ cairo_set_source(_context, state->strokeGradient);
510
+ }
511
+ } else {
512
+ setSourceRGBA(state->stroke);
513
+ }
514
+
515
+ if (preserve) {
516
+ hasShadow()
517
+ ? shadow(cairo_stroke_preserve)
518
+ : cairo_stroke_preserve(_context);
519
+ } else {
520
+ hasShadow()
521
+ ? shadow(cairo_stroke)
522
+ : cairo_stroke(_context);
523
+ }
524
+ }
525
+
526
+ /*
527
+ * Apply shadow with the given draw fn.
528
+ */
529
+
530
+ void
531
+ Context2d::shadow(void (fn)(cairo_t *cr)) {
532
+ cairo_path_t *path = cairo_copy_path_flat(_context);
533
+ cairo_save(_context);
534
+
535
+ // shadowOffset is unaffected by current transform
536
+ cairo_matrix_t path_matrix;
537
+ cairo_get_matrix(_context, &path_matrix);
538
+ cairo_identity_matrix(_context);
539
+
540
+ // Apply shadow
541
+ cairo_push_group(_context);
542
+
543
+ // No need to invoke blur if shadowBlur is 0
544
+ if (state->shadowBlur) {
545
+ // find out extent of path
546
+ double x1, y1, x2, y2;
547
+ if (fn == cairo_fill || fn == cairo_fill_preserve) {
548
+ cairo_fill_extents(_context, &x1, &y1, &x2, &y2);
549
+ } else {
550
+ cairo_stroke_extents(_context, &x1, &y1, &x2, &y2);
551
+ }
552
+
553
+ // create new image surface that size + padding for blurring
554
+ double dx = x2-x1, dy = y2-y1;
555
+ cairo_user_to_device_distance(_context, &dx, &dy);
556
+ int pad = state->shadowBlur * 2;
557
+ cairo_surface_t *shadow_surface = cairo_image_surface_create(
558
+ CAIRO_FORMAT_ARGB32,
559
+ dx + 2 * pad,
560
+ dy + 2 * pad);
561
+ cairo_t *shadow_context = cairo_create(shadow_surface);
562
+
563
+ // transform path to the right place
564
+ cairo_translate(shadow_context, pad-x1, pad-y1);
565
+ cairo_transform(shadow_context, &path_matrix);
566
+
567
+ // set lineCap lineJoin lineDash
568
+ cairo_set_line_cap(shadow_context, cairo_get_line_cap(_context));
569
+ cairo_set_line_join(shadow_context, cairo_get_line_join(_context));
570
+
571
+ double offset;
572
+ int dashes = cairo_get_dash_count(_context);
573
+ std::vector<double> a(dashes);
574
+ cairo_get_dash(_context, a.data(), &offset);
575
+ cairo_set_dash(shadow_context, a.data(), dashes, offset);
576
+
577
+ // draw the path and blur
578
+ cairo_set_line_width(shadow_context, cairo_get_line_width(_context));
579
+ cairo_new_path(shadow_context);
580
+ cairo_append_path(shadow_context, path);
581
+ setSourceRGBA(shadow_context, state->shadow);
582
+ fn(shadow_context);
583
+ blur(shadow_surface, state->shadowBlur);
584
+
585
+ // paint to original context
586
+ cairo_set_source_surface(_context, shadow_surface,
587
+ x1 - pad + state->shadowOffsetX + 1,
588
+ y1 - pad + state->shadowOffsetY + 1);
589
+ cairo_paint(_context);
590
+ cairo_destroy(shadow_context);
591
+ cairo_surface_destroy(shadow_surface);
592
+ } else {
593
+ // Offset first, then apply path's transform
594
+ cairo_translate(
595
+ _context
596
+ , state->shadowOffsetX
597
+ , state->shadowOffsetY);
598
+ cairo_transform(_context, &path_matrix);
599
+
600
+ // Apply shadow
601
+ cairo_new_path(_context);
602
+ cairo_append_path(_context, path);
603
+ setSourceRGBA(state->shadow);
604
+
605
+ fn(_context);
606
+ }
607
+
608
+ // Paint the shadow
609
+ cairo_pop_group_to_source(_context);
610
+ cairo_paint(_context);
611
+
612
+ // Restore state
613
+ cairo_restore(_context);
614
+ cairo_new_path(_context);
615
+ cairo_append_path(_context, path);
616
+ fn(_context);
617
+
618
+ cairo_path_destroy(path);
619
+ }
620
+
621
+ /*
622
+ * Set source RGBA for the current context
623
+ */
624
+
625
+ void
626
+ Context2d::setSourceRGBA(rgba_t color) {
627
+ setSourceRGBA(_context, color);
628
+ }
629
+
630
+ /*
631
+ * Set source RGBA
632
+ */
633
+
634
+ void
635
+ Context2d::setSourceRGBA(cairo_t *ctx, rgba_t color) {
636
+ cairo_set_source_rgba(
637
+ ctx
638
+ , color.r
639
+ , color.g
640
+ , color.b
641
+ , color.a * state->globalAlpha);
642
+ }
643
+
644
+ /*
645
+ * Check if the context has a drawable shadow.
646
+ */
647
+
648
+ bool
649
+ Context2d::hasShadow() {
650
+ return state->shadow.a
651
+ && (state->shadowBlur || state->shadowOffsetX || state->shadowOffsetY);
652
+ }
653
+
654
+ /*
655
+ * Blur the given surface with the given radius.
656
+ */
657
+
658
+ void
659
+ Context2d::blur(cairo_surface_t *surface, int radius) {
660
+ // Steve Hanov, 2009
661
+ // Released into the public domain.
662
+ radius = radius * 0.57735f + 0.5f;
663
+ // get width, height
664
+ int width = cairo_image_surface_get_width( surface );
665
+ int height = cairo_image_surface_get_height( surface );
666
+ const unsigned int size = width * height * sizeof(unsigned);
667
+ unsigned* precalc = (unsigned*)malloc(size);
668
+ cairo_surface_flush( surface );
669
+ unsigned char* src = cairo_image_surface_get_data( surface );
670
+ double mul=1.f/((radius*2)*(radius*2));
671
+ int channel;
672
+
673
+ // The number of times to perform the averaging. According to wikipedia,
674
+ // three iterations is good enough to pass for a gaussian.
675
+ const int MAX_ITERATIONS = 3;
676
+ int iteration;
677
+
678
+ for ( iteration = 0; iteration < MAX_ITERATIONS; iteration++ ) {
679
+ for( channel = 0; channel < 4; channel++ ) {
680
+ int x,y;
681
+
682
+ // precomputation step.
683
+ unsigned char* pix = src;
684
+ unsigned* pre = precalc;
685
+
686
+ bool modified = false;
687
+
688
+ pix += channel;
689
+ for (y=0;y<height;y++) {
690
+ for (x=0;x<width;x++) {
691
+ int tot=pix[0];
692
+ if (x>0) tot+=pre[-1];
693
+ if (y>0) tot+=pre[-width];
694
+ if (x>0 && y>0) tot-=pre[-width-1];
695
+ *pre++=tot;
696
+ if (!modified) modified = true;
697
+ pix += 4;
698
+ }
699
+ }
700
+
701
+ if (!modified) {
702
+ memset(precalc, 0, size);
703
+ }
704
+
705
+ // blur step.
706
+ pix = src + (int)radius * width * 4 + (int)radius * 4 + channel;
707
+ for (y=radius;y<height-radius;y++) {
708
+ for (x=radius;x<width-radius;x++) {
709
+ int l = x < radius ? 0 : x - radius;
710
+ int t = y < radius ? 0 : y - radius;
711
+ int r = x + radius >= width ? width - 1 : x + radius;
712
+ int b = y + radius >= height ? height - 1 : y + radius;
713
+ int tot = precalc[r+b*width] + precalc[l+t*width] -
714
+ precalc[l+b*width] - precalc[r+t*width];
715
+ *pix=(unsigned char)(tot*mul);
716
+ pix += 4;
717
+ }
718
+ pix += (int)radius * 2 * 4;
719
+ }
720
+ }
721
+ }
722
+
723
+ cairo_surface_mark_dirty(surface);
724
+ free(precalc);
725
+ }
726
+
727
+ /*
728
+ * Get format (string).
729
+ */
730
+
731
+ Napi::Value
732
+ Context2d::GetFormat(const Napi::CallbackInfo& info) {
733
+ std::string pixelFormatString;
734
+ switch (canvas()->getFormat()) {
735
+ case CAIRO_FORMAT_ARGB32: pixelFormatString = "RGBA32"; break;
736
+ case CAIRO_FORMAT_RGB24: pixelFormatString = "RGB24"; break;
737
+ case CAIRO_FORMAT_A8: pixelFormatString = "A8"; break;
738
+ case CAIRO_FORMAT_A1: pixelFormatString = "A1"; break;
739
+ case CAIRO_FORMAT_RGB16_565: pixelFormatString = "RGB16_565"; break;
740
+ #ifdef CAIRO_FORMAT_RGB30
741
+ case CAIRO_FORMAT_RGB30: pixelFormatString = "RGB30"; break;
742
+ #endif
743
+ default: return env.Null();
744
+ }
745
+ return Napi::String::New(env, pixelFormatString);
746
+ }
747
+
748
+ /*
749
+ * Create a new page.
750
+ */
751
+
752
+ void
753
+ Context2d::AddPage(const Napi::CallbackInfo& info) {
754
+ if (!canvas()->isPDF()) {
755
+ Napi::Error::New(env, "only PDF canvases support .addPage()").ThrowAsJavaScriptException();
756
+ return;
757
+ }
758
+ cairo_show_page(context());
759
+ Napi::Number zero = Napi::Number::New(env, 0);
760
+ int width = info[0].ToNumber().UnwrapOr(zero).Int32Value();
761
+ int height = info[1].ToNumber().UnwrapOr(zero).Int32Value();
762
+ if (width < 1) width = canvas()->getWidth();
763
+ if (height < 1) height = canvas()->getHeight();
764
+ cairo_pdf_surface_set_size(canvas()->ensureSurface(), width, height);
765
+ }
766
+
767
+ /*
768
+ * Get text direction.
769
+ */
770
+ Napi::Value
771
+ Context2d::GetDirection(const Napi::CallbackInfo& info) {
772
+ return Napi::String::New(env, state->direction);
773
+ }
774
+
775
+ /*
776
+ * Set text direction.
777
+ */
778
+ void
779
+ Context2d::SetDirection(const Napi::CallbackInfo& info, const Napi::Value& value) {
780
+ if (!value.IsString()) return;
781
+
782
+ std::string dir = value.As<Napi::String>();
783
+ if (dir != "ltr" && dir != "rtl") return;
784
+
785
+ state->direction = dir;
786
+ }
787
+
788
+ /*
789
+ * Get language.
790
+ */
791
+ Napi::Value
792
+ Context2d::GetLanguage(const Napi::CallbackInfo& info) {
793
+ return Napi::String::New(env, state->lang);
794
+ }
795
+
796
+ /*
797
+ * Set language.
798
+ */
799
+ void
800
+ Context2d::SetLanguage(const Napi::CallbackInfo& info, const Napi::Value& value) {
801
+ if (!value.IsString()) return;
802
+
803
+ std::string lang = value.As<Napi::String>();
804
+ state->lang = lang;
805
+ }
806
+
807
+ /*
808
+ * Put image data.
809
+ *
810
+ * - imageData, dx, dy
811
+ * - imageData, dx, dy, sx, sy, sw, sh
812
+ *
813
+ */
814
+
815
+ void
816
+ Context2d::PutImageData(const Napi::CallbackInfo& info) {
817
+ if (!info[0].IsObject()) {
818
+ Napi::TypeError::New(env, "ImageData expected").ThrowAsJavaScriptException();
819
+ return;
820
+ }
821
+ Napi::Object obj = info[0].As<Napi::Object>();
822
+ InstanceData* data = env.GetInstanceData<InstanceData>();
823
+ if (!obj.InstanceOf(data->ImageDataCtor.Value()).UnwrapOr(false)) {
824
+ if (!env.IsExceptionPending()) {
825
+ Napi::TypeError::New(env, "ImageData expected").ThrowAsJavaScriptException();
826
+ }
827
+ return;
828
+ }
829
+
830
+ ImageData *imageData = ImageData::Unwrap(obj);
831
+ Napi::Number zero = Napi::Number::New(env, 0);
832
+
833
+ uint8_t *src = imageData->data();
834
+ uint8_t *dst = canvas()->data();
835
+
836
+ if (dst == nullptr) {
837
+ Napi::Error::New(env, "Not an image canvas").ThrowAsJavaScriptException();
838
+ return;
839
+ }
840
+
841
+ int dstStride = canvas()->stride();
842
+ int Bpp = dstStride / canvas()->getWidth();
843
+ int srcStride = Bpp * imageData->width();
844
+
845
+ int64_t sx = 0
846
+ , sy = 0
847
+ , sw = 0
848
+ , sh = 0
849
+ , dx = info[1].ToNumber().UnwrapOr(zero).Int32Value()
850
+ , dy = info[2].ToNumber().UnwrapOr(zero).Int32Value()
851
+ , rows
852
+ , cols;
853
+
854
+ switch (info.Length()) {
855
+ // imageData, dx, dy
856
+ case 3:
857
+ sw = imageData->width();
858
+ sh = imageData->height();
859
+ break;
860
+ // imageData, dx, dy, sx, sy, sw, sh
861
+ case 7:
862
+ sx = info[3].ToNumber().UnwrapOr(zero).Int32Value();
863
+ sy = info[4].ToNumber().UnwrapOr(zero).Int32Value();
864
+ sw = info[5].ToNumber().UnwrapOr(zero).Int32Value();
865
+ sh = info[6].ToNumber().UnwrapOr(zero).Int32Value();
866
+ // fix up negative height, width
867
+ if (sw < 0) sx += sw, sw = -sw;
868
+ if (sh < 0) sy += sh, sh = -sh;
869
+ // clamp the left edge
870
+ if (sx < 0) sw += sx, sx = 0;
871
+ if (sy < 0) sh += sy, sy = 0;
872
+ // clamp the right edge
873
+ if (sx + sw > imageData->width()) sw = imageData->width() - sx;
874
+ if (sy + sh > imageData->height()) sh = imageData->height() - sy;
875
+ // start destination at source offset
876
+ dx += sx;
877
+ dy += sy;
878
+ break;
879
+ default:
880
+ Napi::Error::New(env, "invalid arguments").ThrowAsJavaScriptException();
881
+ return;
882
+ }
883
+
884
+ // chop off outlying source data
885
+ if (dx < 0) sw += dx, sx -= dx, dx = 0;
886
+ if (dy < 0) sh += dy, sy -= dy, dy = 0;
887
+ // clamp width at canvas size
888
+ // Need to wrap std::min calls using parens to prevent macro expansion on
889
+ // windows. See http://stackoverflow.com/questions/5004858/stdmin-gives-error
890
+ cols = (std::min)(sw, (int)canvas()->getWidth() - dx);
891
+ rows = (std::min)(sh, (int)canvas()->getHeight() - dy);
892
+
893
+ if (cols <= 0 || rows <= 0) return;
894
+
895
+ switch (canvas()->getFormat()) {
896
+ case CAIRO_FORMAT_ARGB32: {
897
+ src += sy * srcStride + sx * 4;
898
+ dst += dstStride * dy + 4 * dx;
899
+ for (int y = 0; y < rows; ++y) {
900
+ uint8_t *dstRow = dst;
901
+ uint8_t *srcRow = src;
902
+ for (int x = 0; x < cols; ++x) {
903
+ // rgba
904
+ uint8_t r = *srcRow++;
905
+ uint8_t g = *srcRow++;
906
+ uint8_t b = *srcRow++;
907
+ uint8_t a = *srcRow++;
908
+
909
+ // argb
910
+ // performance optimization: fully transparent/opaque pixels can be
911
+ // processed more efficiently.
912
+ if (a == 0) {
913
+ *dstRow++ = 0;
914
+ *dstRow++ = 0;
915
+ *dstRow++ = 0;
916
+ *dstRow++ = 0;
917
+ } else if (a == 255) {
918
+ *dstRow++ = b;
919
+ *dstRow++ = g;
920
+ *dstRow++ = r;
921
+ *dstRow++ = a;
922
+ } else {
923
+ float alpha = (float)a / 255;
924
+ *dstRow++ = b * alpha;
925
+ *dstRow++ = g * alpha;
926
+ *dstRow++ = r * alpha;
927
+ *dstRow++ = a;
928
+ }
929
+ }
930
+ dst += dstStride;
931
+ src += srcStride;
932
+ }
933
+ break;
934
+ }
935
+ case CAIRO_FORMAT_RGB24: {
936
+ src += sy * srcStride + sx * 4;
937
+ dst += dstStride * dy + 4 * dx;
938
+ for (int y = 0; y < rows; ++y) {
939
+ uint8_t *dstRow = dst;
940
+ uint8_t *srcRow = src;
941
+ for (int x = 0; x < cols; ++x) {
942
+ // rgba
943
+ uint8_t r = *srcRow++;
944
+ uint8_t g = *srcRow++;
945
+ uint8_t b = *srcRow++;
946
+ srcRow++;
947
+
948
+ // argb
949
+ *dstRow++ = b;
950
+ *dstRow++ = g;
951
+ *dstRow++ = r;
952
+ *dstRow++ = 255;
953
+ }
954
+ dst += dstStride;
955
+ src += srcStride;
956
+ }
957
+ break;
958
+ }
959
+ case CAIRO_FORMAT_A8: {
960
+ src += sy * srcStride + sx;
961
+ dst += dstStride * dy + dx;
962
+ if (srcStride == dstStride && cols == dstStride) {
963
+ // fast path: strides are the same and doing a full-width put
964
+ memcpy(dst, src, cols * rows);
965
+ } else {
966
+ for (int y = 0; y < rows; ++y) {
967
+ memcpy(dst, src, cols);
968
+ dst += dstStride;
969
+ src += srcStride;
970
+ }
971
+ }
972
+ break;
973
+ }
974
+ case CAIRO_FORMAT_A1: {
975
+ // TODO Should this be totally packed, or maintain a stride divisible by 4?
976
+ Napi::Error::New(env, "putImageData for CANVAS_FORMAT_A1 is not yet implemented").ThrowAsJavaScriptException();
977
+
978
+ break;
979
+ }
980
+ case CAIRO_FORMAT_RGB16_565: {
981
+ src += sy * srcStride + sx * 2;
982
+ dst += dstStride * dy + 2 * dx;
983
+ for (int y = 0; y < rows; ++y) {
984
+ memcpy(dst, src, cols * 2);
985
+ dst += dstStride;
986
+ src += srcStride;
987
+ }
988
+ break;
989
+ }
990
+ #ifdef CAIRO_FORMAT_RGB30
991
+ case CAIRO_FORMAT_RGB30: {
992
+ // TODO
993
+ Napi::Error::New(env, "putImageData for CANVAS_FORMAT_RGB30 is not yet implemented").ThrowAsJavaScriptException();
994
+
995
+ break;
996
+ }
997
+ #endif
998
+ default: {
999
+ Napi::Error::New(env, "Invalid pixel format").ThrowAsJavaScriptException();
1000
+ return;
1001
+ }
1002
+ }
1003
+
1004
+ cairo_surface_mark_dirty_rectangle(
1005
+ canvas()->ensureSurface()
1006
+ , dx
1007
+ , dy
1008
+ , cols
1009
+ , rows);
1010
+ }
1011
+
1012
+ /*
1013
+ * Get image data.
1014
+ *
1015
+ * - sx, sy, sw, sh
1016
+ *
1017
+ */
1018
+
1019
+ Napi::Value
1020
+ Context2d::GetImageData(const Napi::CallbackInfo& info) {
1021
+ Napi::Number zero = Napi::Number::New(env, 0);
1022
+ Canvas *canvas = this->canvas();
1023
+
1024
+ // 64 bit integers for when (1) both sx, sw or sy, sh are negative (2) sx and
1025
+ // sy are decreased (3) signs are flipped
1026
+ int64_t sx = info[0].ToNumber().UnwrapOr(zero).Int32Value();
1027
+ int64_t sy = info[1].ToNumber().UnwrapOr(zero).Int32Value();
1028
+ int64_t sw = info[2].ToNumber().UnwrapOr(zero).Int32Value();
1029
+ int64_t sh = info[3].ToNumber().UnwrapOr(zero).Int32Value();
1030
+
1031
+ if (!sw) {
1032
+ Napi::Error::New(env, "IndexSizeError: The source width is 0.").ThrowAsJavaScriptException();
1033
+ return env.Undefined();
1034
+ }
1035
+ if (!sh) {
1036
+ Napi::Error::New(env, "IndexSizeError: The source height is 0.").ThrowAsJavaScriptException();
1037
+ return env.Undefined();
1038
+ }
1039
+
1040
+ int width = canvas->getWidth();
1041
+ int height = canvas->getHeight();
1042
+
1043
+ if (!width) {
1044
+ Napi::TypeError::New(env, "Canvas width is 0").ThrowAsJavaScriptException();
1045
+ return env.Undefined();
1046
+ }
1047
+ if (!height) {
1048
+ Napi::TypeError::New(env, "Canvas height is 0").ThrowAsJavaScriptException();
1049
+ return env.Undefined();
1050
+ }
1051
+
1052
+ // WebKit and Firefox have this behavior:
1053
+ // Flip the coordinates so the origin is top/left-most:
1054
+ if (sw < 0) {
1055
+ sx += sw;
1056
+ sw = -sw;
1057
+ }
1058
+ if (sh < 0) {
1059
+ sy += sh;
1060
+ sh = -sh;
1061
+ }
1062
+
1063
+ // Width and height to actually copy
1064
+ int64_t cw = sw;
1065
+ int64_t ch = sh;
1066
+ // Offsets in the destination image
1067
+ int64_t ox = 0;
1068
+ int64_t oy = 0;
1069
+
1070
+ // Clamp the copy width and height if the copy would go outside the image
1071
+ if (sx + sw > width) cw = width - sx;
1072
+ if (sy + sh > height) ch = height - sy;
1073
+
1074
+ // Clamp the copy origin if the copy would go outside the image
1075
+ if (sx < 0) {
1076
+ ox = -sx;
1077
+ cw += sx;
1078
+ sx = 0;
1079
+ }
1080
+ if (sy < 0) {
1081
+ oy = -sy;
1082
+ ch += sy;
1083
+ sy = 0;
1084
+ }
1085
+
1086
+ int srcStride = canvas->stride();
1087
+ int bpp = srcStride / width;
1088
+ // Note: barely fits: INT32_MAX * INT32_MAX * 4. Bpp can't be bigger than 4!
1089
+ uint64_t size = (uint64_t)sw * sh * bpp;
1090
+ int64_t dstStride = sw * bpp;
1091
+
1092
+ if (size > INT32_MAX) {
1093
+ // INT32_MAX is what Firefox limits the buffer to
1094
+ std::string msg = "buffer exceeds " + std::to_string(INT32_MAX) + " bytes";
1095
+ Napi::Error::New(env, msg).ThrowAsJavaScriptException();
1096
+ return env.Undefined();
1097
+ }
1098
+
1099
+ uint8_t *src = canvas->data();
1100
+
1101
+ if (src == nullptr) {
1102
+ Napi::Error::New(env, "Not an image canvas").ThrowAsJavaScriptException();
1103
+ return env.Null();
1104
+ }
1105
+
1106
+ Napi::ArrayBuffer buffer = Napi::ArrayBuffer::New(env, size);
1107
+ Napi::TypedArray dataArray;
1108
+
1109
+ if (canvas->getFormat() == CAIRO_FORMAT_RGB16_565) {
1110
+ dataArray = Napi::Uint16Array::New(env, size >> 1, buffer, 0);
1111
+ } else {
1112
+ dataArray = Napi::Uint8Array::New(env, size, buffer, 0, napi_uint8_clamped_array);
1113
+ }
1114
+
1115
+ uint8_t *dst = (uint8_t *)buffer.Data();
1116
+
1117
+ if (!(cw > 0 && ch > 0)) goto return_empty;
1118
+
1119
+ switch (canvas->getFormat()) {
1120
+ case CAIRO_FORMAT_ARGB32: {
1121
+ dst += oy * dstStride + ox * 4;
1122
+ // Rearrange alpha (argb -> rgba), undo alpha pre-multiplication,
1123
+ // and store in big-endian format
1124
+ for (int y = 0; y < ch; ++y) {
1125
+ uint32_t *row = (uint32_t *)(src + srcStride * (y + sy));
1126
+ for (int x = 0; x < cw; ++x) {
1127
+ int bx = x * 4;
1128
+ uint32_t *pixel = row + x + sx;
1129
+ uint8_t a = *pixel >> 24;
1130
+ uint8_t r = *pixel >> 16;
1131
+ uint8_t g = *pixel >> 8;
1132
+ uint8_t b = *pixel;
1133
+ dst[bx + 3] = a;
1134
+
1135
+ // Performance optimization: fully transparent/opaque pixels can be
1136
+ // processed more efficiently.
1137
+ if (a == 0 || a == 255) {
1138
+ dst[bx + 0] = r;
1139
+ dst[bx + 1] = g;
1140
+ dst[bx + 2] = b;
1141
+ } else {
1142
+ // Undo alpha pre-multiplication
1143
+ float alphaR = (float)255 / a;
1144
+ dst[bx + 0] = (int)((float)r * alphaR);
1145
+ dst[bx + 1] = (int)((float)g * alphaR);
1146
+ dst[bx + 2] = (int)((float)b * alphaR);
1147
+ }
1148
+
1149
+ }
1150
+ dst += dstStride;
1151
+ }
1152
+ break;
1153
+ }
1154
+ case CAIRO_FORMAT_RGB24: {
1155
+ dst += oy * dstStride + ox * 4;
1156
+ // Rearrange alpha (argb -> rgba) and store in big-endian format
1157
+ for (int y = 0; y < ch; ++y) {
1158
+ uint32_t *row = (uint32_t *)(src + srcStride * (y + sy));
1159
+ for (int x = 0; x < cw; ++x) {
1160
+ int bx = x * 4;
1161
+ uint32_t *pixel = row + x + sx;
1162
+ uint8_t r = *pixel >> 16;
1163
+ uint8_t g = *pixel >> 8;
1164
+ uint8_t b = *pixel;
1165
+
1166
+ dst[bx + 0] = r;
1167
+ dst[bx + 1] = g;
1168
+ dst[bx + 2] = b;
1169
+ dst[bx + 3] = 255;
1170
+ }
1171
+ dst += dstStride;
1172
+ }
1173
+ break;
1174
+ }
1175
+ case CAIRO_FORMAT_A8: {
1176
+ dst += oy * dstStride + ox;
1177
+ for (int y = 0; y < ch; ++y) {
1178
+ uint8_t *row = (uint8_t *)(src + srcStride * (y + sy));
1179
+ memcpy(dst, row + sx, cw);
1180
+ dst += dstStride;
1181
+ }
1182
+ break;
1183
+ }
1184
+ case CAIRO_FORMAT_A1: {
1185
+ // TODO Should this be totally packed, or maintain a stride divisible by 4?
1186
+ Napi::Error::New(env, "getImageData for CANVAS_FORMAT_A1 is not yet implemented").ThrowAsJavaScriptException();
1187
+
1188
+ break;
1189
+ }
1190
+ case CAIRO_FORMAT_RGB16_565: {
1191
+ dst += oy * dstStride + ox * 2;
1192
+ for (int y = 0; y < ch; ++y) {
1193
+ uint16_t *row = (uint16_t *)(src + srcStride * (y + sy));
1194
+ memcpy(dst, row + sx, cw * 2);
1195
+ dst += dstStride;
1196
+ }
1197
+ break;
1198
+ }
1199
+ #ifdef CAIRO_FORMAT_RGB30
1200
+ case CAIRO_FORMAT_RGB30: {
1201
+ // TODO
1202
+ Napi::Error::New(env, "getImageData for CANVAS_FORMAT_RGB30 is not yet implemented").ThrowAsJavaScriptException();
1203
+
1204
+ break;
1205
+ }
1206
+ #endif
1207
+ default: {
1208
+ // Unlikely
1209
+ Napi::Error::New(env, "Invalid pixel format").ThrowAsJavaScriptException();
1210
+ return env.Null();
1211
+ }
1212
+ }
1213
+
1214
+ return_empty:
1215
+ Napi::Number swHandle = Napi::Number::New(env, sw);
1216
+ Napi::Number shHandle = Napi::Number::New(env, sh);
1217
+ Napi::Function ctor = env.GetInstanceData<InstanceData>()->ImageDataCtor.Value();
1218
+ Napi::Maybe<Napi::Object> ret = ctor.New({ dataArray, swHandle, shHandle });
1219
+
1220
+ return ret.IsJust() ? ret.Unwrap() : env.Undefined();
1221
+ }
1222
+
1223
+ /**
1224
+ * Create `ImageData` with the given dimensions or
1225
+ * `ImageData` instance for dimensions.
1226
+ */
1227
+
1228
+ Napi::Value
1229
+ Context2d::CreateImageData(const Napi::CallbackInfo& info){
1230
+ Canvas *canvas = this->canvas();
1231
+ Napi::Number zero = Napi::Number::New(env, 0);
1232
+ uint32_t width, height;
1233
+
1234
+ if (info[0].IsObject()) {
1235
+ Napi::Object obj = info[0].As<Napi::Object>();
1236
+ width = obj.Get("width").UnwrapOr(zero).ToNumber().UnwrapOr(zero).Uint32Value();
1237
+ height = obj.Get("height").UnwrapOr(zero).ToNumber().UnwrapOr(zero).Uint32Value();
1238
+ } else {
1239
+ width = info[0].ToNumber().UnwrapOr(zero).Uint32Value();
1240
+ height = info[1].ToNumber().UnwrapOr(zero).Uint32Value();
1241
+ }
1242
+
1243
+ int stride = canvas->stride();
1244
+ double Bpp = static_cast<double>(stride) / canvas->getWidth();
1245
+ int64_t nBytes = Bpp * width * height + .5;
1246
+
1247
+ if (nBytes > INT32_MAX) {
1248
+ // INT32_MAX is what Firefox limits the buffer to
1249
+ std::string msg = "buffer exceeds " + std::to_string(INT32_MAX) + " bytes";
1250
+ Napi::Error::New(env, msg).ThrowAsJavaScriptException();
1251
+ return env.Undefined();
1252
+ }
1253
+
1254
+ Napi::ArrayBuffer ab = Napi::ArrayBuffer::New(env, nBytes);
1255
+ Napi::Value arr;
1256
+
1257
+ if (canvas->getFormat() == CAIRO_FORMAT_RGB16_565)
1258
+ arr = Napi::Uint16Array::New(env, nBytes / 2, ab, 0);
1259
+ else
1260
+ arr = Napi::Uint8Array::New(env, nBytes, ab, 0, napi_uint8_clamped_array);
1261
+
1262
+ Napi::Function ctor = env.GetInstanceData<InstanceData>()->ImageDataCtor.Value();
1263
+ Napi::Maybe<Napi::Object> ret = ctor.New({ arr, Napi::Number::New(env, width), Napi::Number::New(env, height) });
1264
+
1265
+ return ret.IsJust() ? ret.Unwrap() : env.Undefined();
1266
+ }
1267
+
1268
+ /*
1269
+ * Take a transform matrix and return its components
1270
+ * 0: angle, 1: scaleX, 2: scaleY, 3: skewX, 4: translateX, 5: translateY
1271
+ */
1272
+ void decompose_matrix(cairo_matrix_t matrix, double *destination) {
1273
+ double denom = pow(matrix.xx, 2) + pow(matrix.yx, 2);
1274
+ destination[0] = atan2(matrix.yx, matrix.xx);
1275
+ destination[1] = sqrt(denom);
1276
+ destination[2] = (matrix.xx * matrix.yy - matrix.xy * matrix.yx) / destination[1];
1277
+ destination[3] = atan2(matrix.xx * matrix.xy + matrix.yx * matrix.yy, denom);
1278
+ destination[4] = matrix.x0;
1279
+ destination[5] = matrix.y0;
1280
+ }
1281
+
1282
+ /*
1283
+ * Draw image src image to the destination (context).
1284
+ *
1285
+ * - dx, dy
1286
+ * - dx, dy, dw, dh
1287
+ * - sx, sy, sw, sh, dx, dy, dw, dh
1288
+ *
1289
+ */
1290
+
1291
+ void
1292
+ Context2d::DrawImage(const Napi::CallbackInfo& info) {
1293
+ int infoLen = info.Length();
1294
+
1295
+ if (infoLen != 3 && infoLen != 5 && infoLen != 9) {
1296
+ Napi::TypeError::New(env, "Invalid arguments").ThrowAsJavaScriptException();
1297
+ return;
1298
+ }
1299
+
1300
+ if (!info[0].IsObject()) {
1301
+ Napi::TypeError::New(env, "The first argument must be an object").ThrowAsJavaScriptException();
1302
+ return;
1303
+ }
1304
+
1305
+ double args[8];
1306
+ if(!checkArgs(info, args, infoLen - 1, 1))
1307
+ return;
1308
+
1309
+ double sx = 0
1310
+ , sy = 0
1311
+ , sw = 0
1312
+ , sh = 0
1313
+ , dx = 0
1314
+ , dy = 0
1315
+ , dw = 0
1316
+ , dh = 0
1317
+ , source_w = 0
1318
+ , source_h = 0;
1319
+
1320
+ cairo_surface_t *surface;
1321
+
1322
+ Napi::Object obj = info[0].As<Napi::Object>();
1323
+
1324
+ // Image
1325
+ if (obj.InstanceOf(env.GetInstanceData<InstanceData>()->ImageCtor.Value()).UnwrapOr(false)) {
1326
+ Image *img = Image::Unwrap(obj);
1327
+ if (!img->isComplete()) {
1328
+ Napi::Error::New(env, "Image given has not completed loading").ThrowAsJavaScriptException();
1329
+ return;
1330
+ }
1331
+ source_w = sw = img->width;
1332
+ source_h = sh = img->height;
1333
+ surface = img->surface();
1334
+
1335
+ // Canvas
1336
+ } else if (obj.InstanceOf(env.GetInstanceData<InstanceData>()->CanvasCtor.Value()).UnwrapOr(false)) {
1337
+ Canvas *canvas = Canvas::Unwrap(obj);
1338
+ source_w = sw = canvas->getWidth();
1339
+ source_h = sh = canvas->getHeight();
1340
+ surface = canvas->ensureSurface();
1341
+
1342
+ // Invalid
1343
+ } else {
1344
+ if (!env.IsExceptionPending()) {
1345
+ Napi::TypeError::New(env, "Image or Canvas expected").ThrowAsJavaScriptException();
1346
+ }
1347
+ return;
1348
+ }
1349
+
1350
+ cairo_t *ctx = context();
1351
+
1352
+ // Arguments
1353
+ switch (infoLen) {
1354
+ // img, sx, sy, sw, sh, dx, dy, dw, dh
1355
+ case 9:
1356
+ sx = args[0];
1357
+ sy = args[1];
1358
+ sw = args[2];
1359
+ sh = args[3];
1360
+ dx = args[4];
1361
+ dy = args[5];
1362
+ dw = args[6];
1363
+ dh = args[7];
1364
+ break;
1365
+ // img, dx, dy, dw, dh
1366
+ case 5:
1367
+ dx = args[0];
1368
+ dy = args[1];
1369
+ dw = args[2];
1370
+ dh = args[3];
1371
+ break;
1372
+ // img, dx, dy
1373
+ case 3:
1374
+ dx = args[0];
1375
+ dy = args[1];
1376
+ dw = sw;
1377
+ dh = sh;
1378
+ break;
1379
+ }
1380
+
1381
+ if (!(sw && sh && dw && dh))
1382
+ return;
1383
+
1384
+ // Start draw
1385
+ cairo_save(ctx);
1386
+
1387
+ cairo_matrix_t matrix;
1388
+ double transforms[6];
1389
+ cairo_get_matrix(ctx, &matrix);
1390
+ decompose_matrix(matrix, transforms);
1391
+ // extract the scale value from the current transform so that we know how many pixels we
1392
+ // need for our extra canvas in the drawImage operation.
1393
+ double current_scale_x = std::abs(transforms[1]);
1394
+ double current_scale_y = std::abs(transforms[2]);
1395
+ double extra_dx = 0;
1396
+ double extra_dy = 0;
1397
+ double fx = dw / sw * current_scale_x; // transforms[1] is scale on X
1398
+ double fy = dh / sh * current_scale_y; // transforms[2] is scale on X
1399
+ bool needScale = dw != sw || dh != sh;
1400
+ bool needCut = sw != source_w || sh != source_h || sx < 0 || sy < 0;
1401
+ bool sameCanvas = surface == canvas()->ensureSurface();
1402
+ bool needsExtraSurface = sameCanvas || needCut || needScale;
1403
+ cairo_surface_t *surfTemp = NULL;
1404
+ cairo_t *ctxTemp = NULL;
1405
+
1406
+ if (needsExtraSurface) {
1407
+ // we want to create the extra surface as small as possible.
1408
+ // fx and fy are the total scaling we need to apply to sw, sh.
1409
+ // from sw and sh we want to remove the part that is outside the source_w and soruce_h
1410
+ double real_w = sw;
1411
+ double real_h = sh;
1412
+ double translate_x = 0;
1413
+ double translate_y = 0;
1414
+ // if sx or sy are negative, a part of the area represented by sw and sh is empty
1415
+ // because there are empty pixels, so we cut it out.
1416
+ // On the other hand if sx or sy are positive, but sw and sh extend outside the real
1417
+ // source pixels, we cut the area in that case too.
1418
+ if (sx < 0) {
1419
+ extra_dx = -sx * fx;
1420
+ real_w = sw + sx;
1421
+ } else if (sx + sw > source_w) {
1422
+ real_w = sw - (sx + sw - source_w);
1423
+ }
1424
+ if (sy < 0) {
1425
+ extra_dy = -sy * fy;
1426
+ real_h = sh + sy;
1427
+ } else if (sy + sh > source_h) {
1428
+ real_h = sh - (sy + sh - source_h);
1429
+ }
1430
+ // if after cutting we are still bigger than source pixels, we restrict again
1431
+ if (real_w > source_w) {
1432
+ real_w = source_w;
1433
+ }
1434
+ if (real_h > source_h) {
1435
+ real_h = source_h;
1436
+ }
1437
+ // TODO: find a way to limit the surfTemp to real_w and real_h if fx and fy are bigger than 1.
1438
+ // there are no more pixel than the one available in the source, no need to create a bigger surface.
1439
+ surfTemp = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, round(real_w * fx), round(real_h * fy));
1440
+ ctxTemp = cairo_create(surfTemp);
1441
+ cairo_scale(ctxTemp, fx, fy);
1442
+ if (sx > 0) {
1443
+ translate_x = sx;
1444
+ }
1445
+ if (sy > 0) {
1446
+ translate_y = sy;
1447
+ }
1448
+ cairo_set_source_surface(ctxTemp, surface, -translate_x, -translate_y);
1449
+ cairo_pattern_set_filter(cairo_get_source(ctxTemp), state->imageSmoothingEnabled ? state->patternQuality : CAIRO_FILTER_NEAREST);
1450
+ cairo_pattern_set_extend(cairo_get_source(ctxTemp), CAIRO_EXTEND_REFLECT);
1451
+ cairo_paint_with_alpha(ctxTemp, 1);
1452
+ surface = surfTemp;
1453
+ }
1454
+ // apply shadow if there is one
1455
+ if (hasShadow()) {
1456
+ if(state->shadowBlur) {
1457
+ // we need to create a new surface in order to blur
1458
+ int pad = state->shadowBlur * 2;
1459
+ cairo_surface_t *shadow_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, dw + 2 * pad, dh + 2 * pad);
1460
+ cairo_t *shadow_context = cairo_create(shadow_surface);
1461
+
1462
+ // mask and blur
1463
+ setSourceRGBA(shadow_context, state->shadow);
1464
+ cairo_mask_surface(shadow_context, surface, pad, pad);
1465
+ blur(shadow_surface, state->shadowBlur);
1466
+
1467
+ // paint
1468
+ // @note: ShadowBlur looks different in each browser. This implementation matches chrome as close as possible.
1469
+ // The 1.4 offset comes from visual tests with Chrome. I have read the spec and part of the shadowBlur
1470
+ // implementation, and its not immediately clear why an offset is necessary, but without it, the result
1471
+ // in chrome is different.
1472
+ cairo_set_source_surface(ctx, shadow_surface,
1473
+ dx + state->shadowOffsetX - pad + 1.4,
1474
+ dy + state->shadowOffsetY - pad + 1.4);
1475
+ cairo_paint(ctx);
1476
+ // cleanup
1477
+ cairo_destroy(shadow_context);
1478
+ cairo_surface_destroy(shadow_surface);
1479
+ } else {
1480
+ setSourceRGBA(state->shadow);
1481
+ cairo_mask_surface(ctx, surface,
1482
+ dx + (state->shadowOffsetX),
1483
+ dy + (state->shadowOffsetY));
1484
+ }
1485
+ }
1486
+
1487
+ double scaled_dx = dx;
1488
+ double scaled_dy = dy;
1489
+
1490
+ if (needsExtraSurface && (current_scale_x != 1 || current_scale_y != 1)) {
1491
+ // in this case our surface contains already current_scale_x, we need to scale back
1492
+ cairo_scale(ctx, 1 / current_scale_x, 1 / current_scale_y);
1493
+ scaled_dx *= current_scale_x;
1494
+ scaled_dy *= current_scale_y;
1495
+ }
1496
+ // Paint
1497
+ cairo_set_source_surface(ctx, surface, scaled_dx + extra_dx, scaled_dy + extra_dy);
1498
+ cairo_pattern_set_filter(cairo_get_source(ctx), state->imageSmoothingEnabled ? state->patternQuality : CAIRO_FILTER_NEAREST);
1499
+ cairo_pattern_set_extend(cairo_get_source(ctx), CAIRO_EXTEND_NONE);
1500
+ cairo_paint_with_alpha(ctx, state->globalAlpha);
1501
+
1502
+ cairo_restore(ctx);
1503
+
1504
+ if (needsExtraSurface) {
1505
+ cairo_destroy(ctxTemp);
1506
+ cairo_surface_destroy(surfTemp);
1507
+ }
1508
+ }
1509
+
1510
+ /*
1511
+ * Get global alpha.
1512
+ */
1513
+
1514
+ Napi::Value
1515
+ Context2d::GetGlobalAlpha(const Napi::CallbackInfo& info) {
1516
+ return Napi::Number::New(env, state->globalAlpha);
1517
+ }
1518
+
1519
+ /*
1520
+ * Set global alpha.
1521
+ */
1522
+
1523
+ void
1524
+ Context2d::SetGlobalAlpha(const Napi::CallbackInfo& info, const Napi::Value& value) {
1525
+ Napi::Maybe<Napi::Number> numberValue = value.ToNumber();
1526
+ if (numberValue.IsJust()) {
1527
+ double n = numberValue.Unwrap().DoubleValue();
1528
+ if (n >= 0 && n <= 1) state->globalAlpha = n;
1529
+ }
1530
+ }
1531
+
1532
+ /*
1533
+ * Get global composite operation.
1534
+ */
1535
+
1536
+ Napi::Value
1537
+ Context2d::GetGlobalCompositeOperation(const Napi::CallbackInfo& info) {
1538
+ cairo_t *ctx = context();
1539
+
1540
+ const char *op{};
1541
+ switch (cairo_get_operator(ctx)) {
1542
+ // composite modes:
1543
+ case CAIRO_OPERATOR_CLEAR: op = "clear"; break;
1544
+ case CAIRO_OPERATOR_SOURCE: op = "copy"; break;
1545
+ case CAIRO_OPERATOR_DEST: op = "destination"; break;
1546
+ case CAIRO_OPERATOR_OVER: op = "source-over"; break;
1547
+ case CAIRO_OPERATOR_DEST_OVER: op = "destination-over"; break;
1548
+ case CAIRO_OPERATOR_IN: op = "source-in"; break;
1549
+ case CAIRO_OPERATOR_DEST_IN: op = "destination-in"; break;
1550
+ case CAIRO_OPERATOR_OUT: op = "source-out"; break;
1551
+ case CAIRO_OPERATOR_DEST_OUT: op = "destination-out"; break;
1552
+ case CAIRO_OPERATOR_ATOP: op = "source-atop"; break;
1553
+ case CAIRO_OPERATOR_DEST_ATOP: op = "destination-atop"; break;
1554
+ case CAIRO_OPERATOR_XOR: op = "xor"; break;
1555
+ case CAIRO_OPERATOR_ADD: op = "lighter"; break;
1556
+ // blend modes:
1557
+ // Note: "source-over" and "normal" are synonyms. Chrome and FF both report
1558
+ // "source-over" after setting gCO to "normal".
1559
+ // case CAIRO_OPERATOR_OVER: op = "normal";
1560
+ case CAIRO_OPERATOR_MULTIPLY: op = "multiply"; break;
1561
+ case CAIRO_OPERATOR_SCREEN: op = "screen"; break;
1562
+ case CAIRO_OPERATOR_OVERLAY: op = "overlay"; break;
1563
+ case CAIRO_OPERATOR_DARKEN: op = "darken"; break;
1564
+ case CAIRO_OPERATOR_LIGHTEN: op = "lighten"; break;
1565
+ case CAIRO_OPERATOR_COLOR_DODGE: op = "color-dodge"; break;
1566
+ case CAIRO_OPERATOR_COLOR_BURN: op = "color-burn"; break;
1567
+ case CAIRO_OPERATOR_HARD_LIGHT: op = "hard-light"; break;
1568
+ case CAIRO_OPERATOR_SOFT_LIGHT: op = "soft-light"; break;
1569
+ case CAIRO_OPERATOR_DIFFERENCE: op = "difference"; break;
1570
+ case CAIRO_OPERATOR_EXCLUSION: op = "exclusion"; break;
1571
+ case CAIRO_OPERATOR_HSL_HUE: op = "hue"; break;
1572
+ case CAIRO_OPERATOR_HSL_SATURATION: op = "saturation"; break;
1573
+ case CAIRO_OPERATOR_HSL_COLOR: op = "color"; break;
1574
+ case CAIRO_OPERATOR_HSL_LUMINOSITY: op = "luminosity"; break;
1575
+ // non-standard:
1576
+ case CAIRO_OPERATOR_SATURATE: op = "saturate"; break;
1577
+ default: op = "source-over";
1578
+ }
1579
+
1580
+ return Napi::String::New(env, op);
1581
+ }
1582
+
1583
+ /*
1584
+ * Set pattern quality.
1585
+ */
1586
+
1587
+ void
1588
+ Context2d::SetPatternQuality(const Napi::CallbackInfo& info, const Napi::Value& value) {
1589
+ if (value.IsString()) {
1590
+ std::string quality = value.As<Napi::String>().Utf8Value();
1591
+ if (quality == "fast") {
1592
+ state->patternQuality = CAIRO_FILTER_FAST;
1593
+ } else if (quality == "good") {
1594
+ state->patternQuality = CAIRO_FILTER_GOOD;
1595
+ } else if (quality == "best") {
1596
+ state->patternQuality = CAIRO_FILTER_BEST;
1597
+ } else if (quality == "nearest") {
1598
+ state->patternQuality = CAIRO_FILTER_NEAREST;
1599
+ } else if (quality == "bilinear") {
1600
+ state->patternQuality = CAIRO_FILTER_BILINEAR;
1601
+ }
1602
+ }
1603
+ }
1604
+
1605
+ /*
1606
+ * Get pattern quality.
1607
+ */
1608
+
1609
+ Napi::Value
1610
+ Context2d::GetPatternQuality(const Napi::CallbackInfo& info) {
1611
+ const char *quality;
1612
+ switch (state->patternQuality) {
1613
+ case CAIRO_FILTER_FAST: quality = "fast"; break;
1614
+ case CAIRO_FILTER_BEST: quality = "best"; break;
1615
+ case CAIRO_FILTER_NEAREST: quality = "nearest"; break;
1616
+ case CAIRO_FILTER_BILINEAR: quality = "bilinear"; break;
1617
+ default: quality = "good";
1618
+ }
1619
+ return Napi::String::New(env, quality);
1620
+ }
1621
+
1622
+ /*
1623
+ * Set ImageSmoothingEnabled value.
1624
+ */
1625
+
1626
+ void
1627
+ Context2d::SetImageSmoothingEnabled(const Napi::CallbackInfo& info, const Napi::Value& value) {
1628
+ Napi::Boolean boolValue;
1629
+ if (value.ToBoolean().UnwrapTo(&boolValue)) state->imageSmoothingEnabled = boolValue.Value();
1630
+ }
1631
+
1632
+ /*
1633
+ * Get pattern quality.
1634
+ */
1635
+
1636
+ Napi::Value
1637
+ Context2d::GetImageSmoothingEnabled(const Napi::CallbackInfo& info) {
1638
+ return Napi::Boolean::New(env, state->imageSmoothingEnabled);
1639
+ }
1640
+
1641
+ /*
1642
+ * Set global composite operation.
1643
+ */
1644
+
1645
+ void
1646
+ Context2d::SetGlobalCompositeOperation(const Napi::CallbackInfo& info, const Napi::Value& value) {
1647
+ cairo_t *ctx = this->context();
1648
+ Napi::String opStr;
1649
+ if (value.ToString().UnwrapTo(&opStr)) { // Unlike CSS colors, this *is* case-sensitive
1650
+ const std::map<std::string, cairo_operator_t> blendmodes = {
1651
+ // composite modes:
1652
+ {"clear", CAIRO_OPERATOR_CLEAR},
1653
+ {"copy", CAIRO_OPERATOR_SOURCE},
1654
+ {"destination", CAIRO_OPERATOR_DEST}, // this seems to have been omitted from the spec
1655
+ {"source-over", CAIRO_OPERATOR_OVER},
1656
+ {"destination-over", CAIRO_OPERATOR_DEST_OVER},
1657
+ {"source-in", CAIRO_OPERATOR_IN},
1658
+ {"destination-in", CAIRO_OPERATOR_DEST_IN},
1659
+ {"source-out", CAIRO_OPERATOR_OUT},
1660
+ {"destination-out", CAIRO_OPERATOR_DEST_OUT},
1661
+ {"source-atop", CAIRO_OPERATOR_ATOP},
1662
+ {"destination-atop", CAIRO_OPERATOR_DEST_ATOP},
1663
+ {"xor", CAIRO_OPERATOR_XOR},
1664
+ {"lighter", CAIRO_OPERATOR_ADD},
1665
+ // blend modes:
1666
+ {"normal", CAIRO_OPERATOR_OVER},
1667
+ {"multiply", CAIRO_OPERATOR_MULTIPLY},
1668
+ {"screen", CAIRO_OPERATOR_SCREEN},
1669
+ {"overlay", CAIRO_OPERATOR_OVERLAY},
1670
+ {"darken", CAIRO_OPERATOR_DARKEN},
1671
+ {"lighten", CAIRO_OPERATOR_LIGHTEN},
1672
+ {"color-dodge", CAIRO_OPERATOR_COLOR_DODGE},
1673
+ {"color-burn", CAIRO_OPERATOR_COLOR_BURN},
1674
+ {"hard-light", CAIRO_OPERATOR_HARD_LIGHT},
1675
+ {"soft-light", CAIRO_OPERATOR_SOFT_LIGHT},
1676
+ {"difference", CAIRO_OPERATOR_DIFFERENCE},
1677
+ {"exclusion", CAIRO_OPERATOR_EXCLUSION},
1678
+ {"hue", CAIRO_OPERATOR_HSL_HUE},
1679
+ {"saturation", CAIRO_OPERATOR_HSL_SATURATION},
1680
+ {"color", CAIRO_OPERATOR_HSL_COLOR},
1681
+ {"luminosity", CAIRO_OPERATOR_HSL_LUMINOSITY},
1682
+ // non-standard:
1683
+ {"saturate", CAIRO_OPERATOR_SATURATE}
1684
+ };
1685
+ auto op = blendmodes.find(opStr.Utf8Value());
1686
+ if (op != blendmodes.end()) cairo_set_operator(ctx, op->second);
1687
+ }
1688
+ }
1689
+
1690
+ /*
1691
+ * Get shadow offset x.
1692
+ */
1693
+
1694
+ Napi::Value
1695
+ Context2d::GetShadowOffsetX(const Napi::CallbackInfo& info) {
1696
+ return Napi::Number::New(env, state->shadowOffsetX);
1697
+ }
1698
+
1699
+ /*
1700
+ * Set shadow offset x.
1701
+ */
1702
+
1703
+ void
1704
+ Context2d::SetShadowOffsetX(const Napi::CallbackInfo& info, const Napi::Value& value) {
1705
+ Napi::Number numberValue;
1706
+ if (value.ToNumber().UnwrapTo(&numberValue)) state->shadowOffsetX = numberValue.DoubleValue();
1707
+ }
1708
+
1709
+ /*
1710
+ * Get shadow offset y.
1711
+ */
1712
+
1713
+ Napi::Value
1714
+ Context2d::GetShadowOffsetY(const Napi::CallbackInfo& info) {
1715
+ return Napi::Number::New(env, state->shadowOffsetY);
1716
+ }
1717
+
1718
+ /*
1719
+ * Set shadow offset y.
1720
+ */
1721
+
1722
+ void
1723
+ Context2d::SetShadowOffsetY(const Napi::CallbackInfo& info, const Napi::Value& value) {
1724
+ Napi::Number numberValue;
1725
+ if (value.ToNumber().UnwrapTo(&numberValue)) state->shadowOffsetY = numberValue.DoubleValue();
1726
+ }
1727
+
1728
+ /*
1729
+ * Get shadow blur.
1730
+ */
1731
+
1732
+ Napi::Value
1733
+ Context2d::GetShadowBlur(const Napi::CallbackInfo& info) {
1734
+ return Napi::Number::New(env, state->shadowBlur);
1735
+ }
1736
+
1737
+ /*
1738
+ * Set shadow blur.
1739
+ */
1740
+
1741
+ void
1742
+ Context2d::SetShadowBlur(const Napi::CallbackInfo& info, const Napi::Value& value) {
1743
+ Napi::Number n;
1744
+ if (value.ToNumber().UnwrapTo(&n)) {
1745
+ double v = n.DoubleValue();
1746
+ if (v >= 0 && v <= std::numeric_limits<decltype(state->shadowBlur)>::max()) {
1747
+ state->shadowBlur = v;
1748
+ }
1749
+ }
1750
+ }
1751
+
1752
+ /*
1753
+ * Get current antialiasing setting.
1754
+ */
1755
+
1756
+ Napi::Value
1757
+ Context2d::GetAntiAlias(const Napi::CallbackInfo& info) {
1758
+ const char *aa;
1759
+ switch (cairo_get_antialias(context())) {
1760
+ case CAIRO_ANTIALIAS_NONE: aa = "none"; break;
1761
+ case CAIRO_ANTIALIAS_GRAY: aa = "gray"; break;
1762
+ case CAIRO_ANTIALIAS_SUBPIXEL: aa = "subpixel"; break;
1763
+ default: aa = "default";
1764
+ }
1765
+ return Napi::String::New(env, aa);
1766
+ }
1767
+
1768
+ /*
1769
+ * Set antialiasing.
1770
+ */
1771
+
1772
+ void
1773
+ Context2d::SetAntiAlias(const Napi::CallbackInfo& info, const Napi::Value& value) {
1774
+ Napi::String stringValue;
1775
+
1776
+ if (value.ToString().UnwrapTo(&stringValue)) {
1777
+ std::string str = stringValue.Utf8Value();
1778
+ cairo_t *ctx = context();
1779
+ cairo_antialias_t a;
1780
+ if (str == "none") {
1781
+ a = CAIRO_ANTIALIAS_NONE;
1782
+ } else if (str == "default") {
1783
+ a = CAIRO_ANTIALIAS_DEFAULT;
1784
+ } else if (str == "gray") {
1785
+ a = CAIRO_ANTIALIAS_GRAY;
1786
+ } else if (str == "subpixel") {
1787
+ a = CAIRO_ANTIALIAS_SUBPIXEL;
1788
+ } else {
1789
+ a = cairo_get_antialias(ctx);
1790
+ }
1791
+ cairo_set_antialias(ctx, a);
1792
+ }
1793
+ }
1794
+
1795
+ /*
1796
+ * Get text drawing mode.
1797
+ */
1798
+
1799
+ Napi::Value
1800
+ Context2d::GetTextDrawingMode(const Napi::CallbackInfo& info) {
1801
+ const char *mode;
1802
+ if (state->textDrawingMode == TEXT_DRAW_PATHS) {
1803
+ mode = "path";
1804
+ } else if (state->textDrawingMode == TEXT_DRAW_GLYPHS) {
1805
+ mode = "glyph";
1806
+ } else {
1807
+ mode = "unknown";
1808
+ }
1809
+ return Napi::String::New(env, mode);
1810
+ }
1811
+
1812
+ /*
1813
+ * Set text drawing mode.
1814
+ */
1815
+
1816
+ void
1817
+ Context2d::SetTextDrawingMode(const Napi::CallbackInfo& info, const Napi::Value& value) {
1818
+ Napi::String stringValue;
1819
+ if (value.ToString().UnwrapTo(&stringValue)) {
1820
+ std::string str = stringValue.Utf8Value();
1821
+ if (str == "path") {
1822
+ state->textDrawingMode = TEXT_DRAW_PATHS;
1823
+ } else if (str == "glyph") {
1824
+ state->textDrawingMode = TEXT_DRAW_GLYPHS;
1825
+ }
1826
+ }
1827
+ }
1828
+
1829
+ /*
1830
+ * Get filter.
1831
+ */
1832
+
1833
+ Napi::Value
1834
+ Context2d::GetQuality(const Napi::CallbackInfo& info) {
1835
+ const char *filter;
1836
+ switch (cairo_pattern_get_filter(cairo_get_source(context()))) {
1837
+ case CAIRO_FILTER_FAST: filter = "fast"; break;
1838
+ case CAIRO_FILTER_BEST: filter = "best"; break;
1839
+ case CAIRO_FILTER_NEAREST: filter = "nearest"; break;
1840
+ case CAIRO_FILTER_BILINEAR: filter = "bilinear"; break;
1841
+ default: filter = "good";
1842
+ }
1843
+ return Napi::String::New(env, filter);
1844
+ }
1845
+
1846
+ /*
1847
+ * Set filter.
1848
+ */
1849
+
1850
+ void
1851
+ Context2d::SetQuality(const Napi::CallbackInfo& info, const Napi::Value& value) {
1852
+ Napi::String stringValue;
1853
+ if (value.ToString().UnwrapTo(&stringValue)) {
1854
+ std::string str = stringValue.Utf8Value();
1855
+ cairo_filter_t filter;
1856
+ if (str == "fast") {
1857
+ filter = CAIRO_FILTER_FAST;
1858
+ } else if (str == "best") {
1859
+ filter = CAIRO_FILTER_BEST;
1860
+ } else if (str == "nearest") {
1861
+ filter = CAIRO_FILTER_NEAREST;
1862
+ } else if (str == "bilinear") {
1863
+ filter = CAIRO_FILTER_BILINEAR;
1864
+ } else {
1865
+ filter = CAIRO_FILTER_GOOD;
1866
+ }
1867
+ cairo_pattern_set_filter(cairo_get_source(context()), filter);
1868
+ }
1869
+ }
1870
+
1871
+ /*
1872
+ * Helper for get current transform matrix
1873
+ */
1874
+
1875
+ Napi::Value
1876
+ Context2d::get_current_transform() {
1877
+ Napi::Float64Array arr = Napi::Float64Array::New(env, 6);
1878
+ double *dest = arr.Data();
1879
+ cairo_matrix_t matrix;
1880
+ cairo_get_matrix(context(), &matrix);
1881
+ dest[0] = matrix.xx;
1882
+ dest[1] = matrix.yx;
1883
+ dest[2] = matrix.xy;
1884
+ dest[3] = matrix.yy;
1885
+ dest[4] = matrix.x0;
1886
+ dest[5] = matrix.y0;
1887
+ Napi::Maybe<Napi::Object> ret = env.GetInstanceData<InstanceData>()->DOMMatrixCtor.Value().New({ arr });
1888
+ return ret.IsJust() ? ret.Unwrap() : env.Undefined();
1889
+ }
1890
+
1891
+ /*
1892
+ * Helper for get/set transform.
1893
+ */
1894
+
1895
+ void parse_matrix_from_object(cairo_matrix_t &matrix, Napi::Object mat) {
1896
+ Napi::Value zero = Napi::Number::New(mat.Env(), 0);
1897
+ cairo_matrix_init(&matrix,
1898
+ mat.Get("a").UnwrapOr(zero).As<Napi::Number>().DoubleValue(),
1899
+ mat.Get("b").UnwrapOr(zero).As<Napi::Number>().DoubleValue(),
1900
+ mat.Get("c").UnwrapOr(zero).As<Napi::Number>().DoubleValue(),
1901
+ mat.Get("d").UnwrapOr(zero).As<Napi::Number>().DoubleValue(),
1902
+ mat.Get("e").UnwrapOr(zero).As<Napi::Number>().DoubleValue(),
1903
+ mat.Get("f").UnwrapOr(zero).As<Napi::Number>().DoubleValue()
1904
+ );
1905
+ }
1906
+
1907
+
1908
+ /*
1909
+ * Get current transform.
1910
+ */
1911
+
1912
+ Napi::Value
1913
+ Context2d::GetCurrentTransform(const Napi::CallbackInfo& info) {
1914
+ return get_current_transform();
1915
+ }
1916
+
1917
+ /*
1918
+ * Set current transform.
1919
+ */
1920
+
1921
+ void
1922
+ Context2d::SetCurrentTransform(const Napi::CallbackInfo& info, const Napi::Value& value) {
1923
+ Napi::Object mat;
1924
+
1925
+ if (value.ToObject().UnwrapTo(&mat)) {
1926
+ if (!mat.InstanceOf(env.GetInstanceData<InstanceData>()->DOMMatrixCtor.Value()).UnwrapOr(false)) {
1927
+ if (!env.IsExceptionPending()) {
1928
+ Napi::TypeError::New(env, "Expected DOMMatrix").ThrowAsJavaScriptException();
1929
+ }
1930
+ return;
1931
+ }
1932
+
1933
+ cairo_matrix_t matrix;
1934
+ parse_matrix_from_object(matrix, mat);
1935
+
1936
+ cairo_transform(context(), &matrix);
1937
+ }
1938
+ }
1939
+
1940
+ /*
1941
+ * Get current fill style.
1942
+ */
1943
+
1944
+ Napi::Value
1945
+ Context2d::GetFillStyle(const Napi::CallbackInfo& info) {
1946
+ Napi::Value style;
1947
+
1948
+ if (_fillStyle.IsEmpty())
1949
+ style = _getFillColor();
1950
+ else
1951
+ style = _fillStyle.Value();
1952
+
1953
+ return style;
1954
+ }
1955
+
1956
+ /*
1957
+ * Set current fill style.
1958
+ */
1959
+
1960
+ void
1961
+ Context2d::SetFillStyle(const Napi::CallbackInfo& info, const Napi::Value& value) {
1962
+ if (value.IsString()) {
1963
+ _fillStyle.Reset();
1964
+ _setFillColor(value.As<Napi::String>());
1965
+ } else if (value.IsObject()) {
1966
+ InstanceData *data = env.GetInstanceData<InstanceData>();
1967
+ Napi::Object obj = value.As<Napi::Object>();
1968
+ if (obj.InstanceOf(data->CanvasGradientCtor.Value()).UnwrapOr(false)) {
1969
+ _fillStyle.Reset(obj);
1970
+ Gradient *grad = Gradient::Unwrap(obj);
1971
+ state->fillGradient = grad->pattern();
1972
+ } else if (obj.InstanceOf(data->CanvasPatternCtor.Value()).UnwrapOr(false)) {
1973
+ _fillStyle.Reset(obj);
1974
+ Pattern *pattern = Pattern::Unwrap(obj);
1975
+ state->fillPattern = pattern->pattern();
1976
+ }
1977
+ }
1978
+ }
1979
+
1980
+ /*
1981
+ * Get current stroke style.
1982
+ */
1983
+
1984
+ Napi::Value
1985
+ Context2d::GetStrokeStyle(const Napi::CallbackInfo& info) {
1986
+ Napi::Value style;
1987
+
1988
+ if (_strokeStyle.IsEmpty())
1989
+ style = _getStrokeColor();
1990
+ else
1991
+ style = _strokeStyle.Value();
1992
+
1993
+ return style;
1994
+ }
1995
+
1996
+ /*
1997
+ * Set current stroke style.
1998
+ */
1999
+
2000
+ void
2001
+ Context2d::SetStrokeStyle(const Napi::CallbackInfo& info, const Napi::Value& value) {
2002
+ if (value.IsString()) {
2003
+ _strokeStyle.Reset();
2004
+ _setStrokeColor(value.As<Napi::String>());
2005
+ } else if (value.IsObject()) {
2006
+ InstanceData *data = env.GetInstanceData<InstanceData>();
2007
+ Napi::Object obj = value.As<Napi::Object>();
2008
+ if (obj.InstanceOf(data->CanvasGradientCtor.Value()).UnwrapOr(false)) {
2009
+ _strokeStyle.Reset(obj);
2010
+ Gradient *grad = Gradient::Unwrap(obj);
2011
+ state->strokeGradient = grad->pattern();
2012
+ } else if (obj.InstanceOf(data->CanvasPatternCtor.Value()).UnwrapOr(false)) {
2013
+ _strokeStyle.Reset(value);
2014
+ Pattern *pattern = Pattern::Unwrap(obj);
2015
+ state->strokePattern = pattern->pattern();
2016
+ }
2017
+ }
2018
+ }
2019
+
2020
+ /*
2021
+ * Get miter limit.
2022
+ */
2023
+
2024
+ Napi::Value
2025
+ Context2d::GetMiterLimit(const Napi::CallbackInfo& info) {
2026
+ return Napi::Number::New(env, cairo_get_miter_limit(context()));
2027
+ }
2028
+
2029
+ /*
2030
+ * Set miter limit.
2031
+ */
2032
+
2033
+ void
2034
+ Context2d::SetMiterLimit(const Napi::CallbackInfo& info, const Napi::Value& value) {
2035
+ Napi::Maybe<Napi::Number> numberValue = value.ToNumber();
2036
+ if (numberValue.IsJust()) {
2037
+ double n = numberValue.Unwrap().DoubleValue();
2038
+ if (n > 0) cairo_set_miter_limit(context(), n);
2039
+ }
2040
+ }
2041
+
2042
+ /*
2043
+ * Get line width.
2044
+ */
2045
+
2046
+ Napi::Value
2047
+ Context2d::GetLineWidth(const Napi::CallbackInfo& info) {
2048
+ return Napi::Number::New(env, cairo_get_line_width(context()));
2049
+ }
2050
+
2051
+ /*
2052
+ * Set line width.
2053
+ */
2054
+
2055
+ void
2056
+ Context2d::SetLineWidth(const Napi::CallbackInfo& info, const Napi::Value& value) {
2057
+ Napi::Maybe<Napi::Number> numberValue = value.ToNumber();
2058
+ if (numberValue.IsJust()) {
2059
+ double n = numberValue.Unwrap().DoubleValue();
2060
+ if (n > 0 && n != std::numeric_limits<double>::infinity()) {
2061
+ cairo_set_line_width(context(), n);
2062
+ }
2063
+ }
2064
+ }
2065
+
2066
+ /*
2067
+ * Get line join.
2068
+ */
2069
+
2070
+ Napi::Value
2071
+ Context2d::GetLineJoin(const Napi::CallbackInfo& info) {
2072
+ const char *join;
2073
+ switch (cairo_get_line_join(context())) {
2074
+ case CAIRO_LINE_JOIN_BEVEL: join = "bevel"; break;
2075
+ case CAIRO_LINE_JOIN_ROUND: join = "round"; break;
2076
+ default: join = "miter";
2077
+ }
2078
+ return Napi::String::New(env, join);
2079
+ }
2080
+
2081
+ /*
2082
+ * Set line join.
2083
+ */
2084
+
2085
+ void
2086
+ Context2d::SetLineJoin(const Napi::CallbackInfo& info, const Napi::Value& value) {
2087
+ Napi::Maybe<Napi::String> stringValue = value.ToString();
2088
+ cairo_t *ctx = context();
2089
+
2090
+ if (stringValue.IsJust()) {
2091
+ std::string type = stringValue.Unwrap().Utf8Value();
2092
+ if (type == "round") {
2093
+ cairo_set_line_join(ctx, CAIRO_LINE_JOIN_ROUND);
2094
+ } else if (type == "bevel") {
2095
+ cairo_set_line_join(ctx, CAIRO_LINE_JOIN_BEVEL);
2096
+ } else {
2097
+ cairo_set_line_join(ctx, CAIRO_LINE_JOIN_MITER);
2098
+ }
2099
+ }
2100
+ }
2101
+
2102
+ /*
2103
+ * Get line cap.
2104
+ */
2105
+
2106
+ Napi::Value
2107
+ Context2d::GetLineCap(const Napi::CallbackInfo& info) {
2108
+ const char *cap;
2109
+ switch (cairo_get_line_cap(context())) {
2110
+ case CAIRO_LINE_CAP_ROUND: cap = "round"; break;
2111
+ case CAIRO_LINE_CAP_SQUARE: cap = "square"; break;
2112
+ default: cap = "butt";
2113
+ }
2114
+ return Napi::String::New(env, cap);
2115
+ }
2116
+
2117
+ /*
2118
+ * Set line cap.
2119
+ */
2120
+
2121
+ void
2122
+ Context2d::SetLineCap(const Napi::CallbackInfo& info, const Napi::Value& value) {
2123
+ Napi::Maybe<Napi::String> stringValue = value.ToString();
2124
+ cairo_t *ctx = context();
2125
+
2126
+ if (stringValue.IsJust()) {
2127
+ std::string type = stringValue.Unwrap().Utf8Value();
2128
+ if (type == "round") {
2129
+ cairo_set_line_cap(ctx, CAIRO_LINE_CAP_ROUND);
2130
+ } else if (type == "square") {
2131
+ cairo_set_line_cap(ctx, CAIRO_LINE_CAP_SQUARE);
2132
+ } else {
2133
+ cairo_set_line_cap(ctx, CAIRO_LINE_CAP_BUTT);
2134
+ }
2135
+ }
2136
+ }
2137
+
2138
+ /*
2139
+ * Check if the given point is within the current path.
2140
+ */
2141
+
2142
+ Napi::Value
2143
+ Context2d::IsPointInPath(const Napi::CallbackInfo& info) {
2144
+ if (info[0].IsNumber() && info[1].IsNumber()) {
2145
+ cairo_t *ctx = context();
2146
+ double x = info[0].As<Napi::Number>(), y = info[1].As<Napi::Number>();
2147
+ setFillRule(info[2]);
2148
+ return Napi::Boolean::New(env, cairo_in_fill(ctx, x, y) || cairo_in_stroke(ctx, x, y));
2149
+ }
2150
+ return Napi::Boolean::New(env, false);
2151
+ }
2152
+
2153
+ /*
2154
+ * Set shadow color.
2155
+ */
2156
+
2157
+ void
2158
+ Context2d::SetShadowColor(const Napi::CallbackInfo& info, const Napi::Value& value) {
2159
+ Napi::Maybe<Napi::String> stringValue = value.ToString();
2160
+ short ok;
2161
+
2162
+ if (stringValue.IsJust()) {
2163
+ std::string str = stringValue.Unwrap().Utf8Value();
2164
+ uint32_t rgba = rgba_from_string(str.c_str(), &ok);
2165
+ if (ok) state->shadow = rgba_create(rgba);
2166
+ }
2167
+ }
2168
+
2169
+ /*
2170
+ * Get shadow color.
2171
+ */
2172
+
2173
+ Napi::Value
2174
+ Context2d::GetShadowColor(const Napi::CallbackInfo& info) {
2175
+ char buf[64];
2176
+ rgba_to_string(state->shadow, buf, sizeof(buf));
2177
+ return Napi::String::New(env, buf);
2178
+ }
2179
+
2180
+ /*
2181
+ * Set fill color, used internally for fillStyle=
2182
+ */
2183
+
2184
+ void
2185
+ Context2d::_setFillColor(Napi::Value arg) {
2186
+ Napi::Maybe<Napi::String> stringValue = arg.ToString();
2187
+ short ok;
2188
+
2189
+ if (stringValue.IsJust()) {
2190
+ Napi::String str = stringValue.Unwrap();
2191
+ char buf[128] = {0};
2192
+ napi_status status = napi_get_value_string_utf8(env, str, buf, sizeof(buf) - 1, nullptr);
2193
+ if (status != napi_ok) return;
2194
+ uint32_t rgba = rgba_from_string(buf, &ok);
2195
+ if (!ok) return;
2196
+ state->fillPattern = state->fillGradient = NULL;
2197
+ state->fill = rgba_create(rgba);
2198
+ }
2199
+ }
2200
+
2201
+ /*
2202
+ * Get fill color.
2203
+ */
2204
+
2205
+ Napi::Value
2206
+ Context2d::_getFillColor() {
2207
+ char buf[64];
2208
+ rgba_to_string(state->fill, buf, sizeof(buf));
2209
+ return Napi::String::New(env, buf);
2210
+ }
2211
+
2212
+ /*
2213
+ * Set stroke color, used internally for strokeStyle=
2214
+ */
2215
+
2216
+ void
2217
+ Context2d::_setStrokeColor(Napi::Value arg) {
2218
+ short ok;
2219
+ std::string str = arg.As<Napi::String>();
2220
+ uint32_t rgba = rgba_from_string(str.c_str(), &ok);
2221
+ if (!ok) return;
2222
+ state->strokePattern = state->strokeGradient = NULL;
2223
+ state->stroke = rgba_create(rgba);
2224
+ }
2225
+
2226
+ /*
2227
+ * Get stroke color.
2228
+ */
2229
+
2230
+ Napi::Value
2231
+ Context2d::_getStrokeColor() {
2232
+ char buf[64];
2233
+ rgba_to_string(state->stroke, buf, sizeof(buf));
2234
+ return Napi::String::New(env, buf);
2235
+ }
2236
+
2237
+ Napi::Value
2238
+ Context2d::CreatePattern(const Napi::CallbackInfo& info) {
2239
+ Napi::Function ctor = env.GetInstanceData<InstanceData>()->CanvasPatternCtor.Value();
2240
+ Napi::Maybe<Napi::Object> ret = ctor.New({ info[0], info[1] });
2241
+ return ret.IsJust() ? ret.Unwrap() : env.Undefined();
2242
+ }
2243
+
2244
+ Napi::Value
2245
+ Context2d::CreateLinearGradient(const Napi::CallbackInfo& info) {
2246
+ Napi::Function ctor = env.GetInstanceData<InstanceData>()->CanvasGradientCtor.Value();
2247
+ Napi::Maybe<Napi::Object> ret = ctor.New({ info[0], info[1], info[2], info[3] });
2248
+ return ret.IsJust() ? ret.Unwrap() : env.Undefined();
2249
+
2250
+ }
2251
+
2252
+ Napi::Value
2253
+ Context2d::CreateRadialGradient(const Napi::CallbackInfo& info) {
2254
+ Napi::Function ctor = env.GetInstanceData<InstanceData>()->CanvasGradientCtor.Value();
2255
+ Napi::Maybe<Napi::Object> ret = ctor.New({ info[0], info[1], info[2], info[3], info[4], info[5] });
2256
+ return ret.IsJust() ? ret.Unwrap() : env.Undefined();
2257
+ }
2258
+
2259
+ /*
2260
+ * Bezier curve.
2261
+ */
2262
+
2263
+ void
2264
+ Context2d::BezierCurveTo(const Napi::CallbackInfo& info) {
2265
+ double args[6];
2266
+ if(!checkArgs(info, args, 6))
2267
+ return;
2268
+
2269
+ cairo_curve_to(context()
2270
+ , args[0]
2271
+ , args[1]
2272
+ , args[2]
2273
+ , args[3]
2274
+ , args[4]
2275
+ , args[5]);
2276
+ }
2277
+
2278
+ /*
2279
+ * Quadratic curve approximation from libsvg-cairo.
2280
+ */
2281
+
2282
+ void
2283
+ Context2d::QuadraticCurveTo(const Napi::CallbackInfo& info) {
2284
+ double args[4];
2285
+ if(!checkArgs(info, args, 4))
2286
+ return;
2287
+
2288
+ cairo_t *ctx = context();
2289
+
2290
+ double x, y
2291
+ , x1 = args[0]
2292
+ , y1 = args[1]
2293
+ , x2 = args[2]
2294
+ , y2 = args[3];
2295
+
2296
+ cairo_get_current_point(ctx, &x, &y);
2297
+
2298
+ if (0 == x && 0 == y) {
2299
+ x = x1;
2300
+ y = y1;
2301
+ }
2302
+
2303
+ cairo_curve_to(ctx
2304
+ , x + 2.0 / 3.0 * (x1 - x), y + 2.0 / 3.0 * (y1 - y)
2305
+ , x2 + 2.0 / 3.0 * (x1 - x2), y2 + 2.0 / 3.0 * (y1 - y2)
2306
+ , x2
2307
+ , y2);
2308
+ }
2309
+
2310
+ /*
2311
+ * Save state.
2312
+ */
2313
+
2314
+ void
2315
+ Context2d::Save(const Napi::CallbackInfo& info) {
2316
+ save();
2317
+ }
2318
+
2319
+ /*
2320
+ * Restore state.
2321
+ */
2322
+
2323
+ void
2324
+ Context2d::Restore(const Napi::CallbackInfo& info) {
2325
+ restore();
2326
+ }
2327
+
2328
+ /*
2329
+ * Creates a new subpath.
2330
+ */
2331
+
2332
+ void
2333
+ Context2d::BeginPath(const Napi::CallbackInfo& info) {
2334
+ cairo_new_path(context());
2335
+ }
2336
+
2337
+ /*
2338
+ * Marks the subpath as closed.
2339
+ */
2340
+
2341
+ void
2342
+ Context2d::ClosePath(const Napi::CallbackInfo& info) {
2343
+ cairo_close_path(context());
2344
+ }
2345
+
2346
+ /*
2347
+ * Rotate transformation.
2348
+ */
2349
+
2350
+ void
2351
+ Context2d::Rotate(const Napi::CallbackInfo& info) {
2352
+ double args[1];
2353
+ if(!checkArgs(info, args, 1))
2354
+ return;
2355
+
2356
+ cairo_rotate(context(), args[0]);
2357
+ }
2358
+
2359
+ /*
2360
+ * Modify the CTM.
2361
+ */
2362
+
2363
+ void
2364
+ Context2d::Transform(const Napi::CallbackInfo& info) {
2365
+ double args[6];
2366
+ if(!checkArgs(info, args, 6))
2367
+ return;
2368
+
2369
+ cairo_matrix_t matrix;
2370
+ cairo_matrix_init(&matrix
2371
+ , args[0]
2372
+ , args[1]
2373
+ , args[2]
2374
+ , args[3]
2375
+ , args[4]
2376
+ , args[5]);
2377
+
2378
+ cairo_transform(context(), &matrix);
2379
+ }
2380
+
2381
+ /*
2382
+ * Get the CTM
2383
+ */
2384
+
2385
+ Napi::Value
2386
+ Context2d::GetTransform(const Napi::CallbackInfo& info) {
2387
+ return get_current_transform();
2388
+ }
2389
+
2390
+ /*
2391
+ * Reset the CTM, used internally by setTransform().
2392
+ */
2393
+
2394
+ void
2395
+ Context2d::ResetTransform(const Napi::CallbackInfo& info) {
2396
+ cairo_identity_matrix(context());
2397
+ }
2398
+
2399
+ /*
2400
+ * Reset transform matrix to identity, then apply the given args.
2401
+ */
2402
+
2403
+ void
2404
+ Context2d::SetTransform(const Napi::CallbackInfo& info) {
2405
+ Napi::Object mat;
2406
+
2407
+ if (info.Length() == 1 && info[0].ToObject().UnwrapTo(&mat)) {
2408
+ if (!mat.InstanceOf(env.GetInstanceData<InstanceData>()->DOMMatrixCtor.Value()).UnwrapOr(false)) {
2409
+ if (!env.IsExceptionPending()) {
2410
+ Napi::TypeError::New(env, "Expected DOMMatrix").ThrowAsJavaScriptException();
2411
+ }
2412
+ return;
2413
+ }
2414
+
2415
+ cairo_matrix_t matrix;
2416
+ parse_matrix_from_object(matrix, mat);
2417
+
2418
+ cairo_set_matrix(context(), &matrix);
2419
+ } else {
2420
+ cairo_identity_matrix(context());
2421
+ Context2d::Transform(info);
2422
+ }
2423
+ }
2424
+
2425
+ /*
2426
+ * Translate transformation.
2427
+ */
2428
+
2429
+ void
2430
+ Context2d::Translate(const Napi::CallbackInfo& info) {
2431
+ double args[2];
2432
+ if(!checkArgs(info, args, 2))
2433
+ return;
2434
+
2435
+ cairo_translate(context(), args[0], args[1]);
2436
+ }
2437
+
2438
+ /*
2439
+ * Scale transformation.
2440
+ */
2441
+
2442
+ void
2443
+ Context2d::Scale(const Napi::CallbackInfo& info) {
2444
+ double args[2];
2445
+ if(!checkArgs(info, args, 2))
2446
+ return;
2447
+
2448
+ cairo_scale(context(), args[0], args[1]);
2449
+ }
2450
+
2451
+ /*
2452
+ * Use path as clipping region.
2453
+ */
2454
+
2455
+ void
2456
+ Context2d::Clip(const Napi::CallbackInfo& info) {
2457
+ setFillRule(info[0]);
2458
+ cairo_t *ctx = context();
2459
+ cairo_clip_preserve(ctx);
2460
+ }
2461
+
2462
+ /*
2463
+ * Fill the path.
2464
+ */
2465
+
2466
+ void
2467
+ Context2d::Fill(const Napi::CallbackInfo& info) {
2468
+ setFillRule(info[0]);
2469
+ fill(true);
2470
+ }
2471
+
2472
+ /*
2473
+ * Stroke the path.
2474
+ */
2475
+
2476
+ void
2477
+ Context2d::Stroke(const Napi::CallbackInfo& info) {
2478
+ stroke(true);
2479
+ }
2480
+
2481
+ /*
2482
+ * Helper for fillText/strokeText
2483
+ */
2484
+
2485
+ double
2486
+ get_text_scale(PangoLayout *layout, double maxWidth) {
2487
+
2488
+ PangoRectangle logical_rect;
2489
+ pango_layout_get_pixel_extents(layout, NULL, &logical_rect);
2490
+
2491
+ if (logical_rect.width > maxWidth) {
2492
+ return maxWidth / logical_rect.width;
2493
+ } else {
2494
+ return 1.0;
2495
+ }
2496
+ }
2497
+
2498
+ /*
2499
+ * Make sure the layout's font list is up-to-date
2500
+ */
2501
+ void
2502
+ Context2d::checkFonts() {
2503
+ // If fonts have been registered, the PangoContext is using an outdated FontMap
2504
+ if (canvas()->fontSerial != fontSerial) {
2505
+ pango_context_set_font_map(
2506
+ pango_layout_get_context(_layout),
2507
+ pango_cairo_font_map_get_default()
2508
+ );
2509
+
2510
+ fontSerial = canvas()->fontSerial;
2511
+ }
2512
+ }
2513
+
2514
+ void
2515
+ Context2d::paintText(const Napi::CallbackInfo& info, bool stroke) {
2516
+ int argsNum = info.Length() >= 4 ? 3 : 2;
2517
+
2518
+ if (argsNum == 3 && info[3].IsUndefined())
2519
+ argsNum = 2;
2520
+
2521
+ double args[3];
2522
+ if(!checkArgs(info, args, argsNum, 1))
2523
+ return;
2524
+
2525
+ Napi::String strValue;
2526
+
2527
+ if (!info[0].ToString().UnwrapTo(&strValue)) return;
2528
+
2529
+ std::string str = strValue.Utf8Value();
2530
+ double x = args[0];
2531
+ double y = args[1];
2532
+ double scaled_by = 1;
2533
+
2534
+ PangoLayout *layout = this->layout();
2535
+
2536
+ checkFonts();
2537
+ pango_layout_set_text(layout, str.c_str(), -1);
2538
+ if (state->lang != "") {
2539
+ pango_context_set_language(pango_layout_get_context(_layout), pango_language_from_string(state->lang.c_str()));
2540
+ }
2541
+ pango_cairo_update_layout(context(), layout);
2542
+
2543
+ PangoDirection pango_dir = state->direction == "ltr" ? PANGO_DIRECTION_LTR : PANGO_DIRECTION_RTL;
2544
+ pango_context_set_base_dir(pango_layout_get_context(_layout), pango_dir);
2545
+
2546
+ if (argsNum == 3) {
2547
+ if (args[2] <= 0) return;
2548
+ scaled_by = get_text_scale(layout, args[2]);
2549
+ cairo_save(context());
2550
+ cairo_scale(context(), scaled_by, 1);
2551
+ }
2552
+
2553
+ savePath();
2554
+ if (state->textDrawingMode == TEXT_DRAW_GLYPHS) {
2555
+ if (stroke == true) { this->stroke(); } else { this->fill(); }
2556
+ setTextPath(x / scaled_by, y);
2557
+ } else if (state->textDrawingMode == TEXT_DRAW_PATHS) {
2558
+ setTextPath(x / scaled_by, y);
2559
+ if (stroke == true) { this->stroke(); } else { this->fill(); }
2560
+ }
2561
+ restorePath();
2562
+ if (argsNum == 3) {
2563
+ cairo_restore(context());
2564
+ }
2565
+ }
2566
+
2567
+ /*
2568
+ * Fill text at (x, y).
2569
+ */
2570
+
2571
+ void
2572
+ Context2d::FillText(const Napi::CallbackInfo& info) {
2573
+ paintText(info, false);
2574
+ }
2575
+
2576
+ /*
2577
+ * Stroke text at (x ,y).
2578
+ */
2579
+
2580
+ void
2581
+ Context2d::StrokeText(const Napi::CallbackInfo& info) {
2582
+ paintText(info, true);
2583
+ }
2584
+
2585
+ /*
2586
+ * Gets the baseline adjustment in device pixels
2587
+ */
2588
+ inline double getBaselineAdjustment(PangoLayout* layout, short baseline) {
2589
+ PangoRectangle logical_rect;
2590
+ pango_layout_line_get_extents(pango_layout_get_line(layout, 0), NULL, &logical_rect);
2591
+
2592
+ double scale = 1.0 / PANGO_SCALE;
2593
+ double ascent = scale * pango_layout_get_baseline(layout);
2594
+ double descent = scale * logical_rect.height - ascent;
2595
+
2596
+ switch (baseline) {
2597
+ case TEXT_BASELINE_ALPHABETIC:
2598
+ return ascent;
2599
+ case TEXT_BASELINE_MIDDLE:
2600
+ return (ascent + descent) / 2.0;
2601
+ case TEXT_BASELINE_BOTTOM:
2602
+ return ascent + descent;
2603
+ default:
2604
+ return 0;
2605
+ }
2606
+ }
2607
+
2608
+ text_align_t
2609
+ Context2d::resolveTextAlignment() {
2610
+ text_align_t alignment = state->textAlignment;
2611
+
2612
+ // Convert start/end to left/right based on direction
2613
+ if (alignment == TEXT_ALIGNMENT_START) {
2614
+ return (state->direction == "rtl") ? TEXT_ALIGNMENT_RIGHT : TEXT_ALIGNMENT_LEFT;
2615
+ } else if (alignment == TEXT_ALIGNMENT_END) {
2616
+ return (state->direction == "rtl") ? TEXT_ALIGNMENT_LEFT : TEXT_ALIGNMENT_RIGHT;
2617
+ }
2618
+
2619
+ return alignment;
2620
+ }
2621
+
2622
+ /*
2623
+ * Set text path for the string in the layout at (x, y).
2624
+ * This function is called by paintText and won't behave correctly
2625
+ * if is not called from there.
2626
+ * it needs pango_layout_set_text and pango_cairo_update_layout to be called before
2627
+ */
2628
+
2629
+ void
2630
+ Context2d::setTextPath(double x, double y) {
2631
+ PangoRectangle logical_rect;
2632
+ text_align_t alignment = resolveTextAlignment();
2633
+
2634
+ switch (alignment) {
2635
+ case TEXT_ALIGNMENT_CENTER:
2636
+ pango_layout_get_pixel_extents(_layout, NULL, &logical_rect);
2637
+ x -= logical_rect.width / 2;
2638
+ break;
2639
+ case TEXT_ALIGNMENT_RIGHT:
2640
+ pango_layout_get_pixel_extents(_layout, NULL, &logical_rect);
2641
+ x -= logical_rect.width;
2642
+ break;
2643
+ default: // TEXT_ALIGNMENT_LEFT
2644
+ break;
2645
+ }
2646
+
2647
+ y -= getBaselineAdjustment(_layout, state->textBaseline);
2648
+
2649
+ cairo_move_to(_context, x, y);
2650
+ if (state->textDrawingMode == TEXT_DRAW_PATHS) {
2651
+ pango_cairo_layout_path(_context, _layout);
2652
+ } else if (state->textDrawingMode == TEXT_DRAW_GLYPHS) {
2653
+ pango_cairo_show_layout(_context, _layout);
2654
+ }
2655
+ }
2656
+
2657
+ /*
2658
+ * Adds a point to the current subpath.
2659
+ */
2660
+
2661
+ void
2662
+ Context2d::LineTo(const Napi::CallbackInfo& info) {
2663
+ double args[2];
2664
+ if(!checkArgs(info, args, 2))
2665
+ return;
2666
+
2667
+ cairo_line_to(context(), args[0], args[1]);
2668
+ }
2669
+
2670
+ /*
2671
+ * Creates a new subpath at the given point.
2672
+ */
2673
+
2674
+ void
2675
+ Context2d::MoveTo(const Napi::CallbackInfo& info) {
2676
+ double args[2];
2677
+ if(!checkArgs(info, args, 2))
2678
+ return;
2679
+
2680
+ cairo_move_to(context(), args[0], args[1]);
2681
+ }
2682
+
2683
+ /*
2684
+ * Get font.
2685
+ */
2686
+
2687
+ Napi::Value
2688
+ Context2d::GetFont(const Napi::CallbackInfo& info) {
2689
+ return Napi::String::New(env, state->font);
2690
+ }
2691
+
2692
+ /*
2693
+ * Set font:
2694
+ * - weight
2695
+ * - style
2696
+ * - size
2697
+ * - unit
2698
+ * - family
2699
+ */
2700
+
2701
+ void
2702
+ Context2d::SetFont(const Napi::CallbackInfo& info, const Napi::Value& value) {
2703
+ if (!value.IsString()) return;
2704
+
2705
+ std::string str = value.As<Napi::String>().Utf8Value();
2706
+ if (!str.length()) return;
2707
+
2708
+ bool success;
2709
+ auto props = FontParser::parse(str, &success);
2710
+ if (!success) return;
2711
+
2712
+ PangoFontDescription *desc = pango_font_description_copy(state->fontDescription);
2713
+ pango_font_description_free(state->fontDescription);
2714
+
2715
+ PangoStyle style = props.fontStyle == FontStyle::Italic ? PANGO_STYLE_ITALIC
2716
+ : props.fontStyle == FontStyle::Oblique ? PANGO_STYLE_OBLIQUE
2717
+ : PANGO_STYLE_NORMAL;
2718
+ pango_font_description_set_style(desc, style);
2719
+
2720
+ pango_font_description_set_weight(desc, static_cast<PangoWeight>(props.fontWeight));
2721
+
2722
+ std::string family = props.fontFamily.empty() ? "" : props.fontFamily[0];
2723
+ for (size_t i = 1; i < props.fontFamily.size(); i++) {
2724
+ family += "," + props.fontFamily[i];
2725
+ }
2726
+ if (family.length() > 0) {
2727
+ // See #1643 - Pango understands "sans" whereas CSS uses "sans-serif"
2728
+ std::string s1(family);
2729
+ std::string s2("sans-serif");
2730
+ if (streq_casein(s1, s2)) {
2731
+ pango_font_description_set_family(desc, "sans");
2732
+ } else {
2733
+ pango_font_description_set_family(desc, family.c_str());
2734
+ }
2735
+ }
2736
+
2737
+ PangoFontDescription *sys_desc = Canvas::ResolveFontDescription(desc);
2738
+ pango_font_description_free(desc);
2739
+
2740
+ if (props.fontSize > 0) pango_font_description_set_absolute_size(sys_desc, props.fontSize * PANGO_SCALE);
2741
+
2742
+ state->fontDescription = sys_desc;
2743
+ pango_layout_set_font_description(_layout, sys_desc);
2744
+
2745
+ state->font = str;
2746
+ }
2747
+
2748
+ /*
2749
+ * Get text baseline.
2750
+ */
2751
+
2752
+ Napi::Value
2753
+ Context2d::GetTextBaseline(const Napi::CallbackInfo& info) {
2754
+ const char* baseline;
2755
+ switch (state->textBaseline) {
2756
+ default:
2757
+ case TEXT_BASELINE_ALPHABETIC: baseline = "alphabetic"; break;
2758
+ case TEXT_BASELINE_TOP: baseline = "top"; break;
2759
+ case TEXT_BASELINE_BOTTOM: baseline = "bottom"; break;
2760
+ case TEXT_BASELINE_MIDDLE: baseline = "middle"; break;
2761
+ case TEXT_BASELINE_IDEOGRAPHIC: baseline = "ideographic"; break;
2762
+ case TEXT_BASELINE_HANGING: baseline = "hanging"; break;
2763
+ }
2764
+ return Napi::String::New(env, baseline);
2765
+ }
2766
+
2767
+ /*
2768
+ * Set text baseline.
2769
+ */
2770
+
2771
+ void
2772
+ Context2d::SetTextBaseline(const Napi::CallbackInfo& info, const Napi::Value& value) {
2773
+ if (!value.IsString()) return;
2774
+
2775
+ std::string opStr = value.As<Napi::String>();
2776
+ const std::map<std::string, text_baseline_t> modes = {
2777
+ {"alphabetic", TEXT_BASELINE_ALPHABETIC},
2778
+ {"top", TEXT_BASELINE_TOP},
2779
+ {"bottom", TEXT_BASELINE_BOTTOM},
2780
+ {"middle", TEXT_BASELINE_MIDDLE},
2781
+ {"ideographic", TEXT_BASELINE_IDEOGRAPHIC},
2782
+ {"hanging", TEXT_BASELINE_HANGING}
2783
+ };
2784
+ auto op = modes.find(opStr);
2785
+ if (op == modes.end()) return;
2786
+
2787
+ state->textBaseline = op->second;
2788
+ }
2789
+
2790
+ /*
2791
+ * Get text align.
2792
+ */
2793
+
2794
+ Napi::Value
2795
+ Context2d::GetTextAlign(const Napi::CallbackInfo& info) {
2796
+ const char* align;
2797
+ switch (state->textAlignment) {
2798
+ case TEXT_ALIGNMENT_LEFT: align = "left"; break;
2799
+ case TEXT_ALIGNMENT_START: align = "start"; break;
2800
+ case TEXT_ALIGNMENT_CENTER: align = "center"; break;
2801
+ case TEXT_ALIGNMENT_RIGHT: align = "right"; break;
2802
+ case TEXT_ALIGNMENT_END: align = "end"; break;
2803
+ default: align = "start";
2804
+ }
2805
+ return Napi::String::New(env, align);
2806
+ }
2807
+
2808
+ /*
2809
+ * Set text align.
2810
+ */
2811
+
2812
+ void
2813
+ Context2d::SetTextAlign(const Napi::CallbackInfo& info, const Napi::Value& value) {
2814
+ if (!value.IsString()) return;
2815
+
2816
+ std::string opStr = value.As<Napi::String>();
2817
+ const std::map<std::string, text_align_t> modes = {
2818
+ {"center", TEXT_ALIGNMENT_CENTER},
2819
+ {"left", TEXT_ALIGNMENT_LEFT},
2820
+ {"start", TEXT_ALIGNMENT_START},
2821
+ {"right", TEXT_ALIGNMENT_RIGHT},
2822
+ {"end", TEXT_ALIGNMENT_END}
2823
+ };
2824
+ auto op = modes.find(opStr);
2825
+ if (op == modes.end()) return;
2826
+
2827
+ state->textAlignment = op->second;
2828
+ }
2829
+
2830
+ /*
2831
+ * Return the given text extents.
2832
+ * TODO: Support for:
2833
+ * hangingBaseline, ideographicBaseline,
2834
+ * fontBoundingBoxAscent, fontBoundingBoxDescent
2835
+ */
2836
+
2837
+ Napi::Value
2838
+ Context2d::MeasureText(const Napi::CallbackInfo& info) {
2839
+ cairo_t *ctx = this->context();
2840
+
2841
+ Napi::String str;
2842
+ if (!info[0].ToString().UnwrapTo(&str)) return env.Undefined();
2843
+
2844
+ Napi::Object obj = Napi::Object::New(env);
2845
+
2846
+ PangoRectangle _ink_rect, _logical_rect;
2847
+ float_rectangle ink_rect, logical_rect;
2848
+ PangoFontMetrics *metrics;
2849
+ PangoLayout *layout = this->layout();
2850
+
2851
+ checkFonts();
2852
+ pango_layout_set_text(layout, str.Utf8Value().c_str(), -1);
2853
+ if (state->lang != "") {
2854
+ pango_context_set_language(pango_layout_get_context(_layout), pango_language_from_string(state->lang.c_str()));
2855
+ }
2856
+ pango_cairo_update_layout(ctx, layout);
2857
+
2858
+ // Normally you could use pango_layout_get_pixel_extents and be done, or use
2859
+ // pango_extents_to_pixels, but both of those round the pixels, so we have to
2860
+ // divide by PANGO_SCALE manually
2861
+ pango_layout_get_extents(layout, &_ink_rect, &_logical_rect);
2862
+
2863
+ float inverse_pango_scale = 1. / PANGO_SCALE;
2864
+
2865
+ logical_rect.x = _logical_rect.x * inverse_pango_scale;
2866
+ logical_rect.y = _logical_rect.y * inverse_pango_scale;
2867
+ logical_rect.width = _logical_rect.width * inverse_pango_scale;
2868
+ logical_rect.height = _logical_rect.height * inverse_pango_scale;
2869
+
2870
+ ink_rect.x = _ink_rect.x * inverse_pango_scale;
2871
+ ink_rect.y = _ink_rect.y * inverse_pango_scale;
2872
+ ink_rect.width = _ink_rect.width * inverse_pango_scale;
2873
+ ink_rect.height = _ink_rect.height * inverse_pango_scale;
2874
+
2875
+ metrics = PANGO_LAYOUT_GET_METRICS(layout);
2876
+
2877
+ text_align_t alignment = resolveTextAlignment();
2878
+
2879
+ double x_offset;
2880
+ switch (alignment) {
2881
+ case TEXT_ALIGNMENT_CENTER:
2882
+ x_offset = logical_rect.width / 2.;
2883
+ break;
2884
+ case TEXT_ALIGNMENT_RIGHT:
2885
+ x_offset = logical_rect.width;
2886
+ break;
2887
+ case TEXT_ALIGNMENT_LEFT:
2888
+ default:
2889
+ x_offset = 0.0;
2890
+ }
2891
+
2892
+ double y_offset = getBaselineAdjustment(layout, state->textBaseline);
2893
+
2894
+ obj.Set("width", Napi::Number::New(env, logical_rect.width));
2895
+ obj.Set("actualBoundingBoxLeft", Napi::Number::New(env, PANGO_LBEARING(ink_rect) + x_offset));
2896
+ obj.Set("actualBoundingBoxRight", Napi::Number::New(env, PANGO_RBEARING(ink_rect) - x_offset));
2897
+ obj.Set("actualBoundingBoxAscent", Napi::Number::New(env, y_offset + PANGO_ASCENT(ink_rect)));
2898
+ obj.Set("actualBoundingBoxDescent", Napi::Number::New(env, PANGO_DESCENT(ink_rect) - y_offset));
2899
+ obj.Set("emHeightAscent", Napi::Number::New(env, -(PANGO_ASCENT(logical_rect) - y_offset)));
2900
+ obj.Set("emHeightDescent", Napi::Number::New(env, PANGO_DESCENT(logical_rect) - y_offset));
2901
+ obj.Set("alphabeticBaseline", Napi::Number::New(env, -(pango_font_metrics_get_ascent(metrics) * inverse_pango_scale - y_offset)));
2902
+
2903
+ pango_font_metrics_unref(metrics);
2904
+
2905
+ return obj;
2906
+ }
2907
+
2908
+ /*
2909
+ * Set line dash
2910
+ * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash
2911
+ */
2912
+
2913
+ void
2914
+ Context2d::SetLineDash(const Napi::CallbackInfo& info) {
2915
+ if (!info[0].IsArray()) return;
2916
+ Napi::Array dash = info[0].As<Napi::Array>();
2917
+ uint32_t dashes = dash.Length() & 1 ? dash.Length() * 2 : dash.Length();
2918
+ uint32_t zero_dashes = 0;
2919
+ std::vector<double> a(dashes);
2920
+ for (uint32_t i=0; i<dashes; i++) {
2921
+ Napi::Number d;
2922
+ if (!dash.Get(i % dash.Length()).UnwrapTo(&d) || !d.IsNumber()) return;
2923
+ a[i] = d.As<Napi::Number>().DoubleValue();
2924
+ if (a[i] == 0) zero_dashes++;
2925
+ if (a[i] < 0 || !std::isfinite(a[i])) return;
2926
+ }
2927
+
2928
+ cairo_t *ctx = this->context();
2929
+ double offset;
2930
+ cairo_get_dash(ctx, NULL, &offset);
2931
+ if (zero_dashes == dashes) {
2932
+ std::vector<double> b(0);
2933
+ cairo_set_dash(ctx, b.data(), 0, offset);
2934
+ } else {
2935
+ cairo_set_dash(ctx, a.data(), dashes, offset);
2936
+ }
2937
+ }
2938
+
2939
+ /*
2940
+ * Get line dash
2941
+ * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash
2942
+ */
2943
+ Napi::Value
2944
+ Context2d::GetLineDash(const Napi::CallbackInfo& info) {
2945
+ cairo_t *ctx = this->context();
2946
+ int dashes = cairo_get_dash_count(ctx);
2947
+ std::vector<double> a(dashes);
2948
+ cairo_get_dash(ctx, a.data(), NULL);
2949
+
2950
+ Napi::Array dash = Napi::Array::New(env, dashes);
2951
+ for (int i=0; i<dashes; i++) {
2952
+ dash.Set(Napi::Number::New(env, i), Napi::Number::New(env, a[i]));
2953
+ }
2954
+
2955
+ return dash;
2956
+ }
2957
+
2958
+ /*
2959
+ * Set line dash offset
2960
+ * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash
2961
+ */
2962
+ void
2963
+ Context2d::SetLineDashOffset(const Napi::CallbackInfo& info, const Napi::Value& value) {
2964
+ Napi::Number numberValue;
2965
+ if (!value.ToNumber().UnwrapTo(&numberValue)) return;
2966
+ double offset = numberValue.DoubleValue();
2967
+ if (!std::isfinite(offset)) return;
2968
+
2969
+ cairo_t *ctx = this->context();
2970
+
2971
+ int dashes = cairo_get_dash_count(ctx);
2972
+ std::vector<double> a(dashes);
2973
+ cairo_get_dash(ctx, a.data(), NULL);
2974
+ cairo_set_dash(ctx, a.data(), dashes, offset);
2975
+ }
2976
+
2977
+ /*
2978
+ * Get line dash offset
2979
+ * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash
2980
+ */
2981
+ Napi::Value
2982
+ Context2d::GetLineDashOffset(const Napi::CallbackInfo& info) {
2983
+ cairo_t *ctx = this->context();
2984
+ double offset;
2985
+ cairo_get_dash(ctx, NULL, &offset);
2986
+
2987
+ return Napi::Number::New(env, offset);
2988
+ }
2989
+
2990
+ /*
2991
+ * Fill the rectangle defined by x, y, width and height.
2992
+ */
2993
+
2994
+ void
2995
+ Context2d::FillRect(const Napi::CallbackInfo& info) {
2996
+ RECT_ARGS;
2997
+ if (0 == width || 0 == height) return;
2998
+ cairo_t *ctx = context();
2999
+ savePath();
3000
+ cairo_rectangle(ctx, x, y, width, height);
3001
+ fill();
3002
+ restorePath();
3003
+ }
3004
+
3005
+ /*
3006
+ * Stroke the rectangle defined by x, y, width and height.
3007
+ */
3008
+
3009
+ void
3010
+ Context2d::StrokeRect(const Napi::CallbackInfo& info) {
3011
+ RECT_ARGS;
3012
+ if (0 == width && 0 == height) return;
3013
+ cairo_t *ctx = context();
3014
+ savePath();
3015
+ cairo_rectangle(ctx, x, y, width, height);
3016
+ stroke();
3017
+ restorePath();
3018
+ }
3019
+
3020
+ /*
3021
+ * Clears all pixels defined by x, y, width and height.
3022
+ */
3023
+
3024
+ void
3025
+ Context2d::ClearRect(const Napi::CallbackInfo& info) {
3026
+ RECT_ARGS;
3027
+ if (0 == width || 0 == height) return;
3028
+ cairo_t *ctx = context();
3029
+ cairo_save(ctx);
3030
+ savePath();
3031
+ cairo_rectangle(ctx, x, y, width, height);
3032
+ cairo_set_operator(ctx, CAIRO_OPERATOR_CLEAR);
3033
+ cairo_fill(ctx);
3034
+ restorePath();
3035
+ cairo_restore(ctx);
3036
+ }
3037
+
3038
+ /*
3039
+ * Adds a rectangle subpath.
3040
+ */
3041
+
3042
+ void
3043
+ Context2d::Rect(const Napi::CallbackInfo& info) {
3044
+ RECT_ARGS;
3045
+ cairo_t *ctx = context();
3046
+ if (width == 0) {
3047
+ cairo_move_to(ctx, x, y);
3048
+ cairo_line_to(ctx, x, y + height);
3049
+ } else if (height == 0) {
3050
+ cairo_move_to(ctx, x, y);
3051
+ cairo_line_to(ctx, x + width, y);
3052
+ } else {
3053
+ cairo_rectangle(ctx, x, y, width, height);
3054
+ }
3055
+ }
3056
+
3057
+ // Draws an arc with two potentially different radii.
3058
+ inline static
3059
+ void elli_arc(cairo_t* ctx, double xc, double yc, double rx, double ry, double a1, double a2, bool clockwise=true) {
3060
+ if (rx == 0. || ry == 0.) {
3061
+ cairo_line_to(ctx, xc + rx, yc + ry);
3062
+ } else {
3063
+ cairo_save(ctx);
3064
+ cairo_translate(ctx, xc, yc);
3065
+ cairo_scale(ctx, rx, ry);
3066
+ if (clockwise)
3067
+ cairo_arc(ctx, 0., 0., 1., a1, a2);
3068
+ else
3069
+ cairo_arc_negative(ctx, 0., 0., 1., a2, a1);
3070
+ cairo_restore(ctx);
3071
+ }
3072
+ }
3073
+
3074
+ inline static
3075
+ bool getRadius(Point<double>& p, const Napi::Value& v) {
3076
+ Napi::Env env = v.Env();
3077
+ if (v.IsObject()) { // 5.1 DOMPointInit
3078
+ Napi::Value rx;
3079
+ Napi::Value ry;
3080
+ auto rxMaybe = v.As<Napi::Object>().Get("x");
3081
+ auto ryMaybe = v.As<Napi::Object>().Get("y");
3082
+ if (rxMaybe.UnwrapTo(&rx) && rx.IsNumber() && ryMaybe.UnwrapTo(&ry) && ry.IsNumber()) {
3083
+ auto rxv = rx.As<Napi::Number>().DoubleValue();
3084
+ auto ryv = ry.As<Napi::Number>().DoubleValue();
3085
+ if (!std::isfinite(rxv) || !std::isfinite(ryv))
3086
+ return true;
3087
+ if (rxv < 0 || ryv < 0) {
3088
+ Napi::RangeError::New(env, "radii must be positive.").ThrowAsJavaScriptException();
3089
+
3090
+ return true;
3091
+ }
3092
+ p.x = rxv;
3093
+ p.y = ryv;
3094
+ return false;
3095
+ }
3096
+ } else if (v.IsNumber()) { // 5.2 unrestricted double
3097
+ auto rv = v.As<Napi::Number>().DoubleValue();
3098
+ if (!std::isfinite(rv))
3099
+ return true;
3100
+ if (rv < 0) {
3101
+ Napi::RangeError::New(env, "radii must be positive.").ThrowAsJavaScriptException();
3102
+
3103
+ return true;
3104
+ }
3105
+ p.x = p.y = rv;
3106
+ return false;
3107
+ }
3108
+ return true;
3109
+ }
3110
+
3111
+ /**
3112
+ * https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-roundrect
3113
+ * x, y, w, h, [radius|[radii]]
3114
+ */
3115
+ void
3116
+ Context2d::RoundRect(const Napi::CallbackInfo& info) {
3117
+ RECT_ARGS;
3118
+ cairo_t *ctx = this->context();
3119
+
3120
+ // 4. Let normalizedRadii be an empty list
3121
+ Point<double> normalizedRadii[4];
3122
+ size_t nRadii = 4;
3123
+
3124
+ if (info[4].IsUndefined()) {
3125
+ for (size_t i = 0; i < 4; i++)
3126
+ normalizedRadii[i].x = normalizedRadii[i].y = 0.;
3127
+
3128
+ } else if (info[4].IsArray()) {
3129
+ auto radiiList = info[4].As<Napi::Array>();
3130
+ nRadii = radiiList.Length();
3131
+ if (!(nRadii >= 1 && nRadii <= 4)) {
3132
+ Napi::RangeError::New(env, "radii must be a list of one, two, three or four radii.").ThrowAsJavaScriptException();
3133
+ return;
3134
+ }
3135
+ // 5. For each radius of radii
3136
+ for (size_t i = 0; i < nRadii; i++) {
3137
+ Napi::Value r;
3138
+ if (!radiiList.Get(i).UnwrapTo(&r) || getRadius(normalizedRadii[i], r))
3139
+ return;
3140
+ }
3141
+
3142
+ } else {
3143
+ // 2. If radii is a double, then set radii to <<radii>>
3144
+ if (getRadius(normalizedRadii[0], info[4]))
3145
+ return;
3146
+ for (size_t i = 1; i < 4; i++) {
3147
+ normalizedRadii[i].x = normalizedRadii[0].x;
3148
+ normalizedRadii[i].y = normalizedRadii[0].y;
3149
+ }
3150
+ }
3151
+
3152
+ Point<double> upperLeft, upperRight, lowerRight, lowerLeft;
3153
+ if (nRadii == 4) {
3154
+ upperLeft = normalizedRadii[0];
3155
+ upperRight = normalizedRadii[1];
3156
+ lowerRight = normalizedRadii[2];
3157
+ lowerLeft = normalizedRadii[3];
3158
+ } else if (nRadii == 3) {
3159
+ upperLeft = normalizedRadii[0];
3160
+ upperRight = normalizedRadii[1];
3161
+ lowerLeft = normalizedRadii[1];
3162
+ lowerRight = normalizedRadii[2];
3163
+ } else if (nRadii == 2) {
3164
+ upperLeft = normalizedRadii[0];
3165
+ lowerRight = normalizedRadii[0];
3166
+ upperRight = normalizedRadii[1];
3167
+ lowerLeft = normalizedRadii[1];
3168
+ } else {
3169
+ upperLeft = normalizedRadii[0];
3170
+ upperRight = normalizedRadii[0];
3171
+ lowerRight = normalizedRadii[0];
3172
+ lowerLeft = normalizedRadii[0];
3173
+ }
3174
+
3175
+ bool clockwise = true;
3176
+ if (width < 0) {
3177
+ clockwise = false;
3178
+ x += width;
3179
+ width = -width;
3180
+ std::swap(upperLeft, upperRight);
3181
+ std::swap(lowerLeft, lowerRight);
3182
+ }
3183
+
3184
+ if (height < 0) {
3185
+ clockwise = !clockwise;
3186
+ y += height;
3187
+ height = -height;
3188
+ std::swap(upperLeft, lowerLeft);
3189
+ std::swap(upperRight, lowerRight);
3190
+ }
3191
+
3192
+ // 11. Corner curves must not overlap. Scale radii to prevent this.
3193
+ {
3194
+ auto top = upperLeft.x + upperRight.x;
3195
+ auto right = upperRight.y + lowerRight.y;
3196
+ auto bottom = lowerRight.x + lowerLeft.x;
3197
+ auto left = upperLeft.y + lowerLeft.y;
3198
+ auto scale = std::min({ width / top, height / right, width / bottom, height / left });
3199
+ if (scale < 1.) {
3200
+ upperLeft.x *= scale;
3201
+ upperLeft.y *= scale;
3202
+ upperRight.x *= scale;
3203
+ upperRight.y *= scale;
3204
+ lowerLeft.x *= scale;
3205
+ lowerLeft.y *= scale;
3206
+ lowerRight.x *= scale;
3207
+ lowerRight.y *= scale;
3208
+ }
3209
+ }
3210
+
3211
+ // 12. Draw
3212
+ cairo_move_to(ctx, x + upperLeft.x, y);
3213
+ if (clockwise) {
3214
+ cairo_line_to(ctx, x + width - upperRight.x, y);
3215
+ elli_arc(ctx, x + width - upperRight.x, y + upperRight.y, upperRight.x, upperRight.y, 3. * M_PI / 2., 0.);
3216
+ cairo_line_to(ctx, x + width, y + height - lowerRight.y);
3217
+ elli_arc(ctx, x + width - lowerRight.x, y + height - lowerRight.y, lowerRight.x, lowerRight.y, 0, M_PI / 2.);
3218
+ cairo_line_to(ctx, x + lowerLeft.x, y + height);
3219
+ elli_arc(ctx, x + lowerLeft.x, y + height - lowerLeft.y, lowerLeft.x, lowerLeft.y, M_PI / 2., M_PI);
3220
+ cairo_line_to(ctx, x, y + upperLeft.y);
3221
+ elli_arc(ctx, x + upperLeft.x, y + upperLeft.y, upperLeft.x, upperLeft.y, M_PI, 3. * M_PI / 2.);
3222
+ } else {
3223
+ elli_arc(ctx, x + upperLeft.x, y + upperLeft.y, upperLeft.x, upperLeft.y, M_PI, 3. * M_PI / 2., false);
3224
+ cairo_line_to(ctx, x, y + upperLeft.y);
3225
+ elli_arc(ctx, x + lowerLeft.x, y + height - lowerLeft.y, lowerLeft.x, lowerLeft.y, M_PI / 2., M_PI, false);
3226
+ cairo_line_to(ctx, x + lowerLeft.x, y + height);
3227
+ elli_arc(ctx, x + width - lowerRight.x, y + height - lowerRight.y, lowerRight.x, lowerRight.y, 0, M_PI / 2., false);
3228
+ cairo_line_to(ctx, x + width, y + height - lowerRight.y);
3229
+ elli_arc(ctx, x + width - upperRight.x, y + upperRight.y, upperRight.x, upperRight.y, 3. * M_PI / 2., 0., false);
3230
+ cairo_line_to(ctx, x + width - upperRight.x, y);
3231
+ }
3232
+ cairo_close_path(ctx);
3233
+ }
3234
+
3235
+ // Adapted from https://chromium.googlesource.com/chromium/blink/+/refs/heads/main/Source/modules/canvas2d/CanvasPathMethods.cpp
3236
+ static void canonicalizeAngle(double& startAngle, double& endAngle) {
3237
+ // Make 0 <= startAngle < 2*PI
3238
+ double newStartAngle = std::fmod(startAngle, twoPi);
3239
+ if (newStartAngle < 0) {
3240
+ newStartAngle += twoPi;
3241
+ // Check for possible catastrophic cancellation in cases where
3242
+ // newStartAngle was a tiny negative number (c.f. crbug.com/503422)
3243
+ if (newStartAngle >= twoPi)
3244
+ newStartAngle -= twoPi;
3245
+ }
3246
+ double delta = newStartAngle - startAngle;
3247
+ startAngle = newStartAngle;
3248
+ endAngle = endAngle + delta;
3249
+ }
3250
+
3251
+ // Adapted from https://chromium.googlesource.com/chromium/blink/+/refs/heads/main/Source/modules/canvas2d/CanvasPathMethods.cpp
3252
+ static double adjustEndAngle(double startAngle, double endAngle, bool counterclockwise) {
3253
+ double newEndAngle = endAngle;
3254
+ /* http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-arc
3255
+ * If the counterclockwise argument is false and endAngle-startAngle is equal to or greater than 2pi, or,
3256
+ * if the counterclockwise argument is true and startAngle-endAngle is equal to or greater than 2pi,
3257
+ * then the arc is the whole circumference of this ellipse, and the point at startAngle along this circle's circumference,
3258
+ * measured in radians clockwise from the ellipse's semi-major axis, acts as both the start point and the end point.
3259
+ */
3260
+ if (!counterclockwise && endAngle - startAngle >= twoPi)
3261
+ newEndAngle = startAngle + twoPi;
3262
+ else if (counterclockwise && startAngle - endAngle >= twoPi)
3263
+ newEndAngle = startAngle - twoPi;
3264
+ /*
3265
+ * Otherwise, the arc is the path along the circumference of this ellipse from the start point to the end point,
3266
+ * going anti-clockwise if the counterclockwise argument is true, and clockwise otherwise.
3267
+ * Since the points are on the ellipse, as opposed to being simply angles from zero,
3268
+ * the arc can never cover an angle greater than 2pi radians.
3269
+ */
3270
+ /* NOTE: When startAngle = 0, endAngle = 2Pi and counterclockwise = true, the spec does not indicate clearly.
3271
+ * We draw the entire circle, because some web sites use arc(x, y, r, 0, 2*Math.PI, true) to draw circle.
3272
+ * We preserve backward-compatibility.
3273
+ */
3274
+ else if (!counterclockwise && startAngle > endAngle)
3275
+ newEndAngle = startAngle + (twoPi - std::fmod(startAngle - endAngle, twoPi));
3276
+ else if (counterclockwise && startAngle < endAngle)
3277
+ newEndAngle = startAngle - (twoPi - std::fmod(endAngle - startAngle, twoPi));
3278
+ return newEndAngle;
3279
+ }
3280
+
3281
+ /*
3282
+ * Adds an arc at x, y with the given radii and start/end angles.
3283
+ */
3284
+
3285
+ void
3286
+ Context2d::Arc(const Napi::CallbackInfo& info) {
3287
+ double args[5];
3288
+ if(!checkArgs(info, args, 5))
3289
+ return;
3290
+
3291
+ auto x = args[0];
3292
+ auto y = args[1];
3293
+ auto radius = args[2];
3294
+ auto startAngle = args[3];
3295
+ auto endAngle = args[4];
3296
+
3297
+ if (radius < 0) {
3298
+ Napi::RangeError::New(env, "The radius provided is negative.").ThrowAsJavaScriptException();
3299
+ return;
3300
+ }
3301
+
3302
+ Napi::Boolean counterclockwiseValue;
3303
+ if (!info[5].ToBoolean().UnwrapTo(&counterclockwiseValue)) return;
3304
+ bool counterclockwise = counterclockwiseValue.Value();
3305
+
3306
+ cairo_t *ctx = context();
3307
+
3308
+ canonicalizeAngle(startAngle, endAngle);
3309
+ endAngle = adjustEndAngle(startAngle, endAngle, counterclockwise);
3310
+
3311
+ if (counterclockwise) {
3312
+ cairo_arc_negative(ctx, x, y, radius, startAngle, endAngle);
3313
+ } else {
3314
+ cairo_arc(ctx, x, y, radius, startAngle, endAngle);
3315
+ }
3316
+ }
3317
+
3318
+ /*
3319
+ * Adds an arcTo point (x0,y0) to (x1,y1) with the given radius.
3320
+ *
3321
+ * Implementation influenced by WebKit.
3322
+ */
3323
+
3324
+ void
3325
+ Context2d::ArcTo(const Napi::CallbackInfo& info) {
3326
+ double args[5];
3327
+ if(!checkArgs(info, args, 5))
3328
+ return;
3329
+
3330
+ cairo_t *ctx = context();
3331
+
3332
+ // Current path point
3333
+ double x, y;
3334
+ cairo_get_current_point(ctx, &x, &y);
3335
+ Point<float> p0(x, y);
3336
+
3337
+ // Point (x0,y0)
3338
+ Point<float> p1(args[0], args[1]);
3339
+
3340
+ // Point (x1,y1)
3341
+ Point<float> p2(args[2], args[3]);
3342
+
3343
+ float radius = args[4];
3344
+
3345
+ if ((p1.x == p0.x && p1.y == p0.y)
3346
+ || (p1.x == p2.x && p1.y == p2.y)
3347
+ || radius == 0.f) {
3348
+ cairo_line_to(ctx, p1.x, p1.y);
3349
+ return;
3350
+ }
3351
+
3352
+ Point<float> p1p0((p0.x - p1.x),(p0.y - p1.y));
3353
+ Point<float> p1p2((p2.x - p1.x),(p2.y - p1.y));
3354
+ float p1p0_length = sqrtf(p1p0.x * p1p0.x + p1p0.y * p1p0.y);
3355
+ float p1p2_length = sqrtf(p1p2.x * p1p2.x + p1p2.y * p1p2.y);
3356
+
3357
+ double cos_phi = (p1p0.x * p1p2.x + p1p0.y * p1p2.y) / (p1p0_length * p1p2_length);
3358
+ // all points on a line logic
3359
+ if (-1 == cos_phi) {
3360
+ cairo_line_to(ctx, p1.x, p1.y);
3361
+ return;
3362
+ }
3363
+
3364
+ if (1 == cos_phi) {
3365
+ // add infinite far away point
3366
+ unsigned int max_length = 65535;
3367
+ double factor_max = max_length / p1p0_length;
3368
+ Point<float> ep((p0.x + factor_max * p1p0.x), (p0.y + factor_max * p1p0.y));
3369
+ cairo_line_to(ctx, ep.x, ep.y);
3370
+ return;
3371
+ }
3372
+
3373
+ float tangent = radius / tan(acos(cos_phi) / 2);
3374
+ float factor_p1p0 = tangent / p1p0_length;
3375
+ Point<float> t_p1p0((p1.x + factor_p1p0 * p1p0.x), (p1.y + factor_p1p0 * p1p0.y));
3376
+
3377
+ Point<float> orth_p1p0(p1p0.y, -p1p0.x);
3378
+ float orth_p1p0_length = sqrt(orth_p1p0.x * orth_p1p0.x + orth_p1p0.y * orth_p1p0.y);
3379
+ float factor_ra = radius / orth_p1p0_length;
3380
+
3381
+ double cos_alpha = (orth_p1p0.x * p1p2.x + orth_p1p0.y * p1p2.y) / (orth_p1p0_length * p1p2_length);
3382
+ if (cos_alpha < 0.f)
3383
+ orth_p1p0 = Point<float>(-orth_p1p0.x, -orth_p1p0.y);
3384
+
3385
+ Point<float> p((t_p1p0.x + factor_ra * orth_p1p0.x), (t_p1p0.y + factor_ra * orth_p1p0.y));
3386
+
3387
+ orth_p1p0 = Point<float>(-orth_p1p0.x, -orth_p1p0.y);
3388
+ float sa = acos(orth_p1p0.x / orth_p1p0_length);
3389
+ if (orth_p1p0.y < 0.f)
3390
+ sa = 2 * M_PI - sa;
3391
+
3392
+ bool anticlockwise = false;
3393
+
3394
+ float factor_p1p2 = tangent / p1p2_length;
3395
+ Point<float> t_p1p2((p1.x + factor_p1p2 * p1p2.x), (p1.y + factor_p1p2 * p1p2.y));
3396
+ Point<float> orth_p1p2((t_p1p2.x - p.x),(t_p1p2.y - p.y));
3397
+ float orth_p1p2_length = sqrtf(orth_p1p2.x * orth_p1p2.x + orth_p1p2.y * orth_p1p2.y);
3398
+ float ea = acos(orth_p1p2.x / orth_p1p2_length);
3399
+
3400
+ if (orth_p1p2.y < 0) ea = 2 * M_PI - ea;
3401
+ if ((sa > ea) && ((sa - ea) < M_PI)) anticlockwise = true;
3402
+ if ((sa < ea) && ((ea - sa) > M_PI)) anticlockwise = true;
3403
+
3404
+ cairo_line_to(ctx, t_p1p0.x, t_p1p0.y);
3405
+
3406
+ if (anticlockwise && M_PI * 2 != radius) {
3407
+ cairo_arc_negative(ctx
3408
+ , p.x
3409
+ , p.y
3410
+ , radius
3411
+ , sa
3412
+ , ea);
3413
+ } else {
3414
+ cairo_arc(ctx
3415
+ , p.x
3416
+ , p.y
3417
+ , radius
3418
+ , sa
3419
+ , ea);
3420
+ }
3421
+ }
3422
+
3423
+ /*
3424
+ * Adds an ellipse to the path which is centered at (x, y) position with the
3425
+ * radii radiusX and radiusY starting at startAngle and ending at endAngle
3426
+ * going in the given direction by anticlockwise (defaulting to clockwise).
3427
+ */
3428
+
3429
+ void
3430
+ Context2d::Ellipse(const Napi::CallbackInfo& info) {
3431
+ double args[7];
3432
+ if(!checkArgs(info, args, 7))
3433
+ return;
3434
+
3435
+ double radiusX = args[2];
3436
+ double radiusY = args[3];
3437
+
3438
+ if (radiusX == 0 || radiusY == 0) return;
3439
+
3440
+ double x = args[0];
3441
+ double y = args[1];
3442
+ double rotation = args[4];
3443
+ double startAngle = args[5];
3444
+ double endAngle = args[6];
3445
+ Napi::Boolean anticlockwiseValue;
3446
+
3447
+ if (!info[7].ToBoolean().UnwrapTo(&anticlockwiseValue)) return;
3448
+ bool anticlockwise = anticlockwiseValue.Value();
3449
+
3450
+ cairo_t *ctx = context();
3451
+
3452
+ // See https://www.cairographics.org/cookbook/ellipses/
3453
+ double xRatio = radiusX / radiusY;
3454
+
3455
+ cairo_matrix_t save_matrix;
3456
+ cairo_get_matrix(ctx, &save_matrix);
3457
+ cairo_translate(ctx, x, y);
3458
+ cairo_rotate(ctx, rotation);
3459
+ cairo_scale(ctx, xRatio, 1.0);
3460
+ cairo_translate(ctx, -x, -y);
3461
+ if (anticlockwise && M_PI * 2 != args[4]) {
3462
+ cairo_arc_negative(ctx,
3463
+ x,
3464
+ y,
3465
+ radiusY,
3466
+ startAngle,
3467
+ endAngle);
3468
+ } else {
3469
+ cairo_arc(ctx,
3470
+ x,
3471
+ y,
3472
+ radiusY,
3473
+ startAngle,
3474
+ endAngle);
3475
+ }
3476
+ cairo_set_matrix(ctx, &save_matrix);
3477
+ }
3478
+
3479
+ #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0)
3480
+
3481
+ void
3482
+ Context2d::BeginTag(const Napi::CallbackInfo& info) {
3483
+ std::string tagName = "";
3484
+ std::string attributes = "";
3485
+
3486
+ if (info.Length() == 0) {
3487
+ Napi::TypeError::New(env, "Tag name is required").ThrowAsJavaScriptException();
3488
+ return;
3489
+ } else {
3490
+ if (!info[0].IsString()) {
3491
+ Napi::TypeError::New(env, "Tag name must be a string.").ThrowAsJavaScriptException();
3492
+ return;
3493
+ } else {
3494
+ tagName = info[0].As<Napi::String>().Utf8Value();
3495
+ }
3496
+
3497
+ if (info.Length() > 1) {
3498
+ if (!info[1].IsString()) {
3499
+ Napi::TypeError::New(env, "Attributes must be a string matching Cairo's attribute format").ThrowAsJavaScriptException();
3500
+ return;
3501
+ } else {
3502
+ attributes = info[1].As<Napi::String>().Utf8Value();
3503
+ }
3504
+ }
3505
+ }
3506
+
3507
+ cairo_tag_begin(_context, tagName.c_str(), attributes.c_str());
3508
+ }
3509
+
3510
+ void
3511
+ Context2d::EndTag(const Napi::CallbackInfo& info) {
3512
+ if (info.Length() == 0) {
3513
+ Napi::TypeError::New(env, "Tag name is required").ThrowAsJavaScriptException();
3514
+ return;
3515
+ }
3516
+
3517
+ if (!info[0].IsString()) {
3518
+ Napi::TypeError::New(env, "Tag name must be a string.").ThrowAsJavaScriptException();
3519
+ return;
3520
+ }
3521
+
3522
+ std::string tagName = info[0].As<Napi::String>().Utf8Value();
3523
+
3524
+ cairo_tag_end(_context, tagName.c_str());
3525
+ }
3526
+
3527
+ #endif