@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.
- package/Readme.md +654 -0
- package/binding.gyp +229 -0
- package/browser.js +31 -0
- package/index.d.ts +507 -0
- package/index.js +94 -0
- package/lib/DOMMatrix.js +678 -0
- package/lib/bindings.js +113 -0
- package/lib/canvas.js +113 -0
- package/lib/context2d.js +11 -0
- package/lib/image.js +97 -0
- package/lib/jpegstream.js +41 -0
- package/lib/pattern.js +15 -0
- package/lib/pdfstream.js +35 -0
- package/lib/pngstream.js +42 -0
- package/package.json +77 -0
- package/scripts/install.js +19 -0
- package/src/Backends.h +9 -0
- package/src/Canvas.cc +1026 -0
- package/src/Canvas.h +128 -0
- package/src/CanvasError.h +37 -0
- package/src/CanvasGradient.cc +113 -0
- package/src/CanvasGradient.h +20 -0
- package/src/CanvasPattern.cc +129 -0
- package/src/CanvasPattern.h +33 -0
- package/src/CanvasRenderingContext2d.cc +3527 -0
- package/src/CanvasRenderingContext2d.h +238 -0
- package/src/CharData.h +233 -0
- package/src/FontParser.cc +605 -0
- package/src/FontParser.h +115 -0
- package/src/Image.cc +1719 -0
- package/src/Image.h +146 -0
- package/src/ImageData.cc +138 -0
- package/src/ImageData.h +26 -0
- package/src/InstanceData.h +12 -0
- package/src/JPEGStream.h +157 -0
- package/src/PNG.h +292 -0
- package/src/Point.h +11 -0
- package/src/Util.h +9 -0
- package/src/bmp/BMPParser.cc +459 -0
- package/src/bmp/BMPParser.h +60 -0
- package/src/bmp/LICENSE.md +24 -0
- package/src/closure.cc +52 -0
- package/src/closure.h +98 -0
- package/src/color.cc +796 -0
- package/src/color.h +30 -0
- package/src/dll_visibility.h +20 -0
- package/src/init.cc +114 -0
- package/src/register_font.cc +352 -0
- package/src/register_font.h +7 -0
- package/util/has_lib.js +119 -0
- 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
|