@shqld/canvas 3.2.2-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/Readme.md +654 -0
  2. package/binding.gyp +229 -0
  3. package/browser.js +31 -0
  4. package/index.d.ts +507 -0
  5. package/index.js +94 -0
  6. package/lib/DOMMatrix.js +678 -0
  7. package/lib/bindings.js +113 -0
  8. package/lib/canvas.js +113 -0
  9. package/lib/context2d.js +11 -0
  10. package/lib/image.js +97 -0
  11. package/lib/jpegstream.js +41 -0
  12. package/lib/pattern.js +15 -0
  13. package/lib/pdfstream.js +35 -0
  14. package/lib/pngstream.js +42 -0
  15. package/package.json +77 -0
  16. package/scripts/install.js +19 -0
  17. package/src/Backends.h +9 -0
  18. package/src/Canvas.cc +1026 -0
  19. package/src/Canvas.h +128 -0
  20. package/src/CanvasError.h +37 -0
  21. package/src/CanvasGradient.cc +113 -0
  22. package/src/CanvasGradient.h +20 -0
  23. package/src/CanvasPattern.cc +129 -0
  24. package/src/CanvasPattern.h +33 -0
  25. package/src/CanvasRenderingContext2d.cc +3527 -0
  26. package/src/CanvasRenderingContext2d.h +238 -0
  27. package/src/CharData.h +233 -0
  28. package/src/FontParser.cc +605 -0
  29. package/src/FontParser.h +115 -0
  30. package/src/Image.cc +1719 -0
  31. package/src/Image.h +146 -0
  32. package/src/ImageData.cc +138 -0
  33. package/src/ImageData.h +26 -0
  34. package/src/InstanceData.h +12 -0
  35. package/src/JPEGStream.h +157 -0
  36. package/src/PNG.h +292 -0
  37. package/src/Point.h +11 -0
  38. package/src/Util.h +9 -0
  39. package/src/bmp/BMPParser.cc +459 -0
  40. package/src/bmp/BMPParser.h +60 -0
  41. package/src/bmp/LICENSE.md +24 -0
  42. package/src/closure.cc +52 -0
  43. package/src/closure.h +98 -0
  44. package/src/color.cc +796 -0
  45. package/src/color.h +30 -0
  46. package/src/dll_visibility.h +20 -0
  47. package/src/init.cc +114 -0
  48. package/src/register_font.cc +352 -0
  49. package/src/register_font.h +7 -0
  50. package/util/has_lib.js +119 -0
  51. package/util/win_jpeg_lookup.js +21 -0
package/src/Canvas.cc ADDED
@@ -0,0 +1,1026 @@
1
+ // Copyright (c) 2010 LearnBoost <tj@learnboost.com>
2
+
3
+ #include "Canvas.h"
4
+ #include "InstanceData.h"
5
+ #include <algorithm> // std::min
6
+ #include <assert.h>
7
+ #include <cairo-pdf.h>
8
+ #include <cairo-svg.h>
9
+ #include "CanvasRenderingContext2d.h"
10
+ #include "closure.h"
11
+ #include <cstring>
12
+ #include <cctype>
13
+ #include <ctime>
14
+ #include <glib.h>
15
+ #include "PNG.h"
16
+ #include "register_font.h"
17
+ #include <sstream>
18
+ #include <stdlib.h>
19
+ #include <string>
20
+ #include <unordered_set>
21
+ #include "Util.h"
22
+ #include <vector>
23
+ #include "node_buffer.h"
24
+ #include "FontParser.h"
25
+
26
+ #ifdef HAVE_JPEG
27
+ #include "JPEGStream.h"
28
+ #endif
29
+
30
+ #define GENERIC_FACE_ERROR \
31
+ "The second argument to registerFont is required, and should be an object " \
32
+ "with at least a family (string) and optionally weight (string/number) " \
33
+ "and style (string)."
34
+
35
+ #define CAIRO_MAX_SIZE 32767
36
+
37
+ using namespace std;
38
+
39
+ std::vector<FontFace> Canvas::font_face_list;
40
+
41
+ // Increases each time a font is (de)registered
42
+ int Canvas::fontSerial = 1;
43
+
44
+ /*
45
+ * Initialize Canvas.
46
+ */
47
+
48
+ void
49
+ Canvas::Initialize(Napi::Env& env, Napi::Object& exports) {
50
+ Napi::HandleScope scope(env);
51
+ InstanceData* data = env.GetInstanceData<InstanceData>();
52
+
53
+ // Constructor
54
+ Napi::Function ctor = DefineClass(env, "Canvas", {
55
+ InstanceMethod<&Canvas::ToBuffer>("toBuffer", napi_default_method),
56
+ InstanceMethod<&Canvas::StreamPNGSync>("streamPNGSync", napi_default_method),
57
+ InstanceMethod<&Canvas::StreamPDFSync>("streamPDFSync", napi_default_method),
58
+ #ifdef HAVE_JPEG
59
+ InstanceMethod<&Canvas::StreamJPEGSync>("streamJPEGSync", napi_default_method),
60
+ #endif
61
+ InstanceAccessor<&Canvas::GetType>("type", napi_default_jsproperty),
62
+ InstanceAccessor<&Canvas::GetStride>("stride", napi_default_jsproperty),
63
+ InstanceAccessor<&Canvas::GetWidth, &Canvas::SetWidth>("width", napi_default_jsproperty),
64
+ InstanceAccessor<&Canvas::GetHeight, &Canvas::SetHeight>("height", napi_default_jsproperty),
65
+ StaticValue("PNG_NO_FILTERS", Napi::Number::New(env, PNG_NO_FILTERS), napi_default_jsproperty),
66
+ StaticValue("PNG_FILTER_NONE", Napi::Number::New(env, PNG_FILTER_NONE), napi_default_jsproperty),
67
+ StaticValue("PNG_FILTER_SUB", Napi::Number::New(env, PNG_FILTER_SUB), napi_default_jsproperty),
68
+ StaticValue("PNG_FILTER_UP", Napi::Number::New(env, PNG_FILTER_UP), napi_default_jsproperty),
69
+ StaticValue("PNG_FILTER_AVG", Napi::Number::New(env, PNG_FILTER_AVG), napi_default_jsproperty),
70
+ StaticValue("PNG_FILTER_PAETH", Napi::Number::New(env, PNG_FILTER_PAETH), napi_default_jsproperty),
71
+ StaticValue("PNG_ALL_FILTERS", Napi::Number::New(env, PNG_ALL_FILTERS), napi_default_jsproperty),
72
+ StaticMethod<&Canvas::RegisterFont>("_registerFont", napi_default_method),
73
+ StaticMethod<&Canvas::DeregisterAllFonts>("_deregisterAllFonts", napi_default_method),
74
+ StaticMethod<&Canvas::ParseFont>("parseFont", napi_default_method)
75
+ });
76
+
77
+ data->CanvasCtor = Napi::Persistent(ctor);
78
+ exports.Set("Canvas", ctor);
79
+ }
80
+
81
+ /*
82
+ * Initialize a Canvas with the given width and height.
83
+ */
84
+
85
+ Canvas::Canvas(const Napi::CallbackInfo& info) : Napi::ObjectWrap<Canvas>(info), env(info.Env()) {
86
+ InstanceData* data = env.GetInstanceData<InstanceData>();
87
+ ctor = Napi::Persistent(data->CanvasCtor.Value());
88
+
89
+ _surface = nullptr;
90
+ _closure = nullptr;
91
+ width = 0;
92
+ height = 0;
93
+ format = CAIRO_FORMAT_ARGB32;
94
+
95
+ if (info[0].IsNumber()) {
96
+ uint32_t width = info[0].As<Napi::Number>().Uint32Value();
97
+ uint32_t height = 0;
98
+
99
+ if (info[1].IsNumber()) height = info[1].As<Napi::Number>().Uint32Value();
100
+
101
+ if (width > CAIRO_MAX_SIZE) {
102
+ std::string msg = "Canvas width cannot exceed " + std::to_string(CAIRO_MAX_SIZE);
103
+ Napi::Error::New(env, msg).ThrowAsJavaScriptException();
104
+ return;
105
+ }
106
+
107
+ if (height > CAIRO_MAX_SIZE) {
108
+ std::string msg = "Canvas height cannot exceed " + std::to_string(CAIRO_MAX_SIZE);
109
+ Napi::Error::New(env, msg).ThrowAsJavaScriptException();
110
+ return;
111
+ }
112
+
113
+ this->width = width;
114
+ this->height = height;
115
+
116
+ if (info[2].IsString()) {
117
+ std::string str = info[2].As<Napi::String>();
118
+ if (str == "pdf") {
119
+ type = CANVAS_TYPE_PDF;
120
+ } else if (str == "svg") {
121
+ type = CANVAS_TYPE_SVG;
122
+ } else {
123
+ type = CANVAS_TYPE_IMAGE;
124
+ }
125
+ } else {
126
+ type = CANVAS_TYPE_IMAGE;
127
+ }
128
+ } else {
129
+ type = CANVAS_TYPE_IMAGE;
130
+ }
131
+
132
+ cairo_status_t status = cairo_surface_status(ensureSurface());
133
+
134
+ if (status != CAIRO_STATUS_SUCCESS) {
135
+ Napi::Error::New(env, cairo_status_to_string(status)).ThrowAsJavaScriptException();
136
+ return;
137
+ }
138
+ }
139
+
140
+ Canvas::~Canvas() {
141
+ destroySurface();
142
+ }
143
+
144
+ /*
145
+ * Get type string.
146
+ */
147
+
148
+ Napi::Value
149
+ Canvas::GetType(const Napi::CallbackInfo& info) {
150
+ switch (type) {
151
+ case CANVAS_TYPE_PDF:
152
+ return Napi::String::New(env, "pdf");
153
+ case CANVAS_TYPE_SVG:
154
+ return Napi::String::New(env, "svg");
155
+ default:
156
+ return Napi::String::New(env, "image");
157
+ }
158
+ }
159
+
160
+ /*
161
+ * Get stride.
162
+ */
163
+ Napi::Value
164
+ Canvas::GetStride(const Napi::CallbackInfo& info) {
165
+ return Napi::Number::New(env, cairo_image_surface_get_stride(ensureSurface()));
166
+ }
167
+
168
+ /*
169
+ * Get width.
170
+ */
171
+
172
+ Napi::Value
173
+ Canvas::GetWidth(const Napi::CallbackInfo& info) {
174
+ return Napi::Number::New(env, getWidth());
175
+ }
176
+
177
+ /*
178
+ * Set width.
179
+ */
180
+
181
+ void
182
+ Canvas::SetWidth(const Napi::CallbackInfo& info, const Napi::Value& value) {
183
+ if (value.IsNumber()) {
184
+ uint32_t width = value.As<Napi::Number>().Uint32Value();
185
+ if (width <= CAIRO_MAX_SIZE) {
186
+ resurface(info.This().As<Napi::Object>(), width, this->height);
187
+ }
188
+ }
189
+ }
190
+
191
+ /*
192
+ * Get height.
193
+ */
194
+
195
+ Napi::Value
196
+ Canvas::GetHeight(const Napi::CallbackInfo& info) {
197
+ return Napi::Number::New(env, getHeight());
198
+ }
199
+
200
+ /*
201
+ * Set height.
202
+ */
203
+
204
+ void
205
+ Canvas::SetHeight(const Napi::CallbackInfo& info, const Napi::Value& value) {
206
+ if (value.IsNumber()) {
207
+ uint32_t height = value.As<Napi::Number>().Uint32Value();
208
+ if (height <= CAIRO_MAX_SIZE) {
209
+ resurface(info.This().As<Napi::Object>(), this->width, height);
210
+ }
211
+ }
212
+ }
213
+
214
+ /*
215
+ * EIO toBuffer callback.
216
+ */
217
+
218
+ void
219
+ Canvas::ToPngBufferAsync(Closure* base) {
220
+ PngClosure* closure = static_cast<PngClosure*>(base);
221
+
222
+ closure->status = canvas_write_to_png_stream(
223
+ closure->canvas->ensureSurface(),
224
+ PngClosure::writeVec,
225
+ closure);
226
+ }
227
+
228
+ #ifdef HAVE_JPEG
229
+ void
230
+ Canvas::ToJpegBufferAsync(Closure* base) {
231
+ JpegClosure* closure = static_cast<JpegClosure*>(base);
232
+ write_to_jpeg_buffer(closure->canvas->ensureSurface(), closure);
233
+ }
234
+ #endif
235
+
236
+ static void
237
+ parsePNGArgs(Napi::Value arg, PngClosure& pngargs) {
238
+ if (arg.IsObject()) {
239
+ Napi::Object obj = arg.As<Napi::Object>();
240
+ Napi::Value cLevel;
241
+
242
+ if (obj.Get("compressionLevel").UnwrapTo(&cLevel) && cLevel.IsNumber()) {
243
+ uint32_t val = cLevel.As<Napi::Number>().Uint32Value();
244
+ // See quote below from spec section 4.12.5.5.
245
+ if (val <= 9) pngargs.compressionLevel = val;
246
+ }
247
+
248
+ Napi::Value rez;
249
+ if (obj.Get("resolution").UnwrapTo(&rez) && rez.IsNumber()) {
250
+ uint32_t val = rez.As<Napi::Number>().Uint32Value();
251
+ if (val > 0) pngargs.resolution = val;
252
+ }
253
+
254
+ Napi::Value filters;
255
+ if (obj.Get("filters").UnwrapTo(&filters) && filters.IsNumber()) {
256
+ pngargs.filters = filters.As<Napi::Number>().Uint32Value();
257
+ }
258
+
259
+ Napi::Value palette;
260
+ if (obj.Get("palette").UnwrapTo(&palette) && palette.IsTypedArray()) {
261
+ Napi::TypedArray palette_ta = palette.As<Napi::TypedArray>();
262
+ if (palette_ta.TypedArrayType() == napi_uint8_clamped_array) {
263
+ pngargs.nPaletteColors = palette_ta.ElementLength();
264
+ if (pngargs.nPaletteColors % 4 != 0) {
265
+ throw "Palette length must be a multiple of 4.";
266
+ }
267
+ pngargs.palette = palette_ta.As<Napi::Uint8Array>().Data();
268
+ pngargs.nPaletteColors /= 4;
269
+ // Optional background color index:
270
+ Napi::Value backgroundIndexVal;
271
+ if (obj.Get("backgroundIndex").UnwrapTo(&backgroundIndexVal) && backgroundIndexVal.IsNumber()) {
272
+ pngargs.backgroundIndex = backgroundIndexVal.As<Napi::Number>().Uint32Value();
273
+ }
274
+ }
275
+ }
276
+ }
277
+ }
278
+
279
+ #ifdef HAVE_JPEG
280
+ static void parseJPEGArgs(Napi::Value arg, JpegClosure& jpegargs) {
281
+ // "If Type(quality) is not Number, or if quality is outside that range, the
282
+ // user agent must use its default quality value, as if the quality argument
283
+ // had not been given." - 4.12.5.5
284
+ if (arg.IsObject()) {
285
+ Napi::Object obj = arg.As<Napi::Object>();
286
+
287
+ Napi::Value qual;
288
+ if (obj.Get("quality").UnwrapTo(&qual) && qual.IsNumber()) {
289
+ double quality = qual.As<Napi::Number>().DoubleValue();
290
+ if (quality >= 0.0 && quality <= 1.0) {
291
+ jpegargs.quality = static_cast<uint32_t>(100.0 * quality);
292
+ }
293
+ }
294
+
295
+ Napi::Value chroma;
296
+ if (obj.Get("chromaSubsampling").UnwrapTo(&chroma)) {
297
+ if (chroma.IsBoolean()) {
298
+ bool subsample = chroma.As<Napi::Boolean>().Value();
299
+ jpegargs.chromaSubsampling = subsample ? 2 : 1;
300
+ } else if (chroma.IsNumber()) {
301
+ jpegargs.chromaSubsampling = chroma.As<Napi::Number>().Uint32Value();
302
+ }
303
+ }
304
+
305
+ Napi::Value progressive;
306
+ if (obj.Get("progressive").UnwrapTo(&progressive) && progressive.IsBoolean()) {
307
+ jpegargs.progressive = progressive.As<Napi::Boolean>().Value();
308
+ }
309
+ }
310
+ }
311
+ #endif
312
+
313
+ #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0)
314
+
315
+ static inline void setPdfMetaStr(cairo_surface_t* surf, Napi::Object opts,
316
+ cairo_pdf_metadata_t t, const char* propName) {
317
+ Napi::Value propValue;
318
+ if (opts.Get(propName).UnwrapTo(&propValue) && propValue.IsString()) {
319
+ // (copies char data)
320
+ cairo_pdf_surface_set_metadata(surf, t, propValue.As<Napi::String>().Utf8Value().c_str());
321
+ }
322
+ }
323
+
324
+ static inline void setPdfMetaDate(cairo_surface_t* surf, Napi::Object opts,
325
+ cairo_pdf_metadata_t t, const char* propName) {
326
+ Napi::Value propValue;
327
+ if (opts.Get(propName).UnwrapTo(&propValue) && propValue.IsDate()) {
328
+ auto date = static_cast<time_t>(propValue.As<Napi::Date>().ValueOf() / 1000); // ms -> s
329
+ char buf[sizeof "2011-10-08T07:07:09Z"];
330
+ strftime(buf, sizeof buf, "%FT%TZ", gmtime(&date));
331
+ cairo_pdf_surface_set_metadata(surf, t, buf);
332
+ }
333
+ }
334
+
335
+ static void setPdfMetadata(Canvas* canvas, Napi::Object opts) {
336
+ cairo_surface_t* surf = canvas->ensureSurface();
337
+
338
+ setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_TITLE, "title");
339
+ setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_AUTHOR, "author");
340
+ setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_SUBJECT, "subject");
341
+ setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_KEYWORDS, "keywords");
342
+ setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_CREATOR, "creator");
343
+ setPdfMetaDate(surf, opts, CAIRO_PDF_METADATA_CREATE_DATE, "creationDate");
344
+ setPdfMetaDate(surf, opts, CAIRO_PDF_METADATA_MOD_DATE, "modDate");
345
+ }
346
+
347
+ #endif // CAIRO 16+
348
+
349
+ /*
350
+ * Converts/encodes data to a Buffer. Async when a callback function is passed.
351
+
352
+ * PDF canvases:
353
+ (any) => Buffer
354
+ ("application/pdf", config) => Buffer
355
+
356
+ * SVG canvases:
357
+ (any) => Buffer
358
+
359
+ * ARGB data:
360
+ ("raw") => Buffer
361
+
362
+ * PNG-encoded
363
+ () => Buffer
364
+ (undefined|"image/png", {compressionLevel?: number, filter?: number}) => Buffer
365
+ ((err: null|Error, buffer) => any)
366
+ ((err: null|Error, buffer) => any, undefined|"image/png", {compressionLevel?: number, filter?: number})
367
+
368
+ * JPEG-encoded
369
+ ("image/jpeg") => Buffer
370
+ ("image/jpeg", {quality?: number, progressive?: Boolean, chromaSubsampling?: Boolean|number}) => Buffer
371
+ ((err: null|Error, buffer) => any, "image/jpeg")
372
+ ((err: null|Error, buffer) => any, "image/jpeg", {quality?: number, progressive?: Boolean, chromaSubsampling?: Boolean|number})
373
+ */
374
+
375
+ Napi::Value
376
+ Canvas::ToBuffer(const Napi::CallbackInfo& info) {
377
+ cairo_status_t status;
378
+
379
+ // Vector canvases, sync only
380
+ if (isPDF() || isSVG()) {
381
+ // mime type may be present, but it's not checked
382
+ PdfSvgClosure* closure = static_cast<PdfSvgClosure*>(_closure);
383
+ if (isPDF()) {
384
+ #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0)
385
+ if (info[1].IsObject()) { // toBuffer("application/pdf", config)
386
+ setPdfMetadata(this, info[1].As<Napi::Object>());
387
+ }
388
+ #endif // CAIRO 16+
389
+ }
390
+
391
+ cairo_surface_t *surf = ensureSurface();
392
+ cairo_surface_finish(surf);
393
+
394
+ cairo_status_t status = cairo_surface_status(surf);
395
+ if (status != CAIRO_STATUS_SUCCESS) {
396
+ Napi::Error::New(env, cairo_status_to_string(status)).ThrowAsJavaScriptException();
397
+ return env.Undefined();
398
+ }
399
+
400
+ return Napi::Buffer<uint8_t>::Copy(env, &closure->vec[0], closure->vec.size());
401
+ }
402
+
403
+ // Raw ARGB data -- just a memcpy()
404
+ if (info[0].StrictEquals(Napi::String::New(env, "raw"))) {
405
+ cairo_surface_t *surface = ensureSurface();
406
+ cairo_surface_flush(surface);
407
+ if (nBytes() > node::Buffer::kMaxLength) {
408
+ Napi::Error::New(env, "Data exceeds maximum buffer length.").ThrowAsJavaScriptException();
409
+ return env.Undefined();
410
+ }
411
+ return Napi::Buffer<uint8_t>::Copy(env, cairo_image_surface_get_data(surface), nBytes());
412
+ }
413
+
414
+ // Sync PNG, default
415
+ if (info[0].IsUndefined() || info[0].StrictEquals(Napi::String::New(env, "image/png"))) {
416
+ try {
417
+ PngClosure closure(this);
418
+ parsePNGArgs(info[1], closure);
419
+ if (closure.nPaletteColors == 0xFFFFFFFF) {
420
+ Napi::Error::New(env, "Palette length must be a multiple of 4.").ThrowAsJavaScriptException();
421
+ return env.Undefined();
422
+ }
423
+
424
+ status = canvas_write_to_png_stream(ensureSurface(), PngClosure::writeVec, &closure);
425
+
426
+ if (!env.IsExceptionPending()) {
427
+ if (status) {
428
+ throw status; // TODO: throw in js?
429
+ } else {
430
+ // TODO it's possible to avoid this copy
431
+ return Napi::Buffer<uint8_t>::Copy(env, &closure.vec[0], closure.vec.size());
432
+ }
433
+ }
434
+ } catch (cairo_status_t ex) {
435
+ CairoError(ex).ThrowAsJavaScriptException();
436
+ } catch (const char* ex) {
437
+ Napi::Error::New(env, ex).ThrowAsJavaScriptException();
438
+ }
439
+
440
+ return env.Undefined();
441
+ }
442
+
443
+ // Async PNG
444
+ if (info[0].IsFunction() &&
445
+ (info[1].IsUndefined() || info[1].StrictEquals(Napi::String::New(env, "image/png")))) {
446
+
447
+ PngClosure* closure;
448
+ try {
449
+ closure = new PngClosure(this);
450
+ parsePNGArgs(info[2], *closure);
451
+ } catch (cairo_status_t ex) {
452
+ CairoError(ex).ThrowAsJavaScriptException();
453
+ return env.Undefined();
454
+ } catch (const char* ex) {
455
+ Napi::Error::New(env, ex).ThrowAsJavaScriptException();
456
+ return env.Undefined();
457
+ }
458
+
459
+ Ref();
460
+ closure->cb = Napi::Persistent(info[0].As<Napi::Function>());
461
+
462
+ // Make sure the surface exists since we won't have an isolate context in the async block:
463
+ ensureSurface();
464
+ EncodingWorker* worker = new EncodingWorker(env);
465
+ worker->Init(&ToPngBufferAsync, closure);
466
+ worker->Queue();
467
+
468
+ return env.Undefined();
469
+ }
470
+
471
+ #ifdef HAVE_JPEG
472
+ // Sync JPEG
473
+ Napi::Value jpegStr = Napi::String::New(env, "image/jpeg");
474
+ if (info[0].StrictEquals(jpegStr)) {
475
+ try {
476
+ JpegClosure closure(this);
477
+ parseJPEGArgs(info[1], closure);
478
+
479
+ write_to_jpeg_buffer(ensureSurface(), &closure);
480
+
481
+ if (!env.IsExceptionPending()) {
482
+ // TODO it's possible to avoid this copy.
483
+ return Napi::Buffer<uint8_t>::Copy(env, &closure.vec[0], closure.vec.size());
484
+ }
485
+ } catch (cairo_status_t ex) {
486
+ CairoError(ex).ThrowAsJavaScriptException();
487
+ return env.Undefined();
488
+ }
489
+ return env.Undefined();
490
+ }
491
+
492
+ // Async JPEG
493
+ if (info[0].IsFunction() && info[1].StrictEquals(jpegStr)) {
494
+ JpegClosure* closure = new JpegClosure(this);
495
+ parseJPEGArgs(info[2], *closure);
496
+
497
+ Ref();
498
+ closure->cb = Napi::Persistent(info[0].As<Napi::Function>());
499
+
500
+ // Make sure the surface exists since we won't have an isolate context in the async block:
501
+ ensureSurface();
502
+ EncodingWorker* worker = new EncodingWorker(env);
503
+ worker->Init(&ToJpegBufferAsync, closure);
504
+ worker->Queue();
505
+ return env.Undefined();
506
+ }
507
+ #endif
508
+
509
+ return env.Undefined();
510
+ }
511
+
512
+ /*
513
+ * Canvas::StreamPNG callback.
514
+ */
515
+
516
+ static cairo_status_t
517
+ streamPNG(void *c, const uint8_t *data, unsigned len) {
518
+ PngClosure* closure = (PngClosure*) c;
519
+ Napi::Env env = closure->canvas->env;
520
+ Napi::HandleScope scope(env);
521
+ Napi::AsyncContext async(env, "canvas:StreamPNG");
522
+ Napi::Value buf = Napi::Buffer<uint8_t>::Copy(env, data, len);
523
+ closure->cb.MakeCallback(env.Global(), { env.Null(), buf, Napi::Number::New(env, len) }, async);
524
+ return CAIRO_STATUS_SUCCESS;
525
+ }
526
+
527
+ /*
528
+ * Stream PNG data synchronously. TODO async
529
+ * StreamPngSync(this, options: {palette?: Uint8ClampedArray, backgroundIndex?: uint32, compressionLevel: uint32, filters: uint32})
530
+ */
531
+
532
+ void
533
+ Canvas::StreamPNGSync(const Napi::CallbackInfo& info) {
534
+ if (!info[0].IsFunction()) {
535
+ Napi::TypeError::New(env, "callback function required").ThrowAsJavaScriptException();
536
+ return;
537
+ }
538
+
539
+ PngClosure closure(this);
540
+ parsePNGArgs(info[1], closure);
541
+
542
+ closure.cb = Napi::Persistent(info[0].As<Napi::Function>());
543
+
544
+ cairo_status_t status = canvas_write_to_png_stream(ensureSurface(), streamPNG, &closure);
545
+
546
+ if (!env.IsExceptionPending()) {
547
+ if (status) {
548
+ closure.cb.Call(env.Global(), { CairoError(status).Value() });
549
+ } else {
550
+ closure.cb.Call(env.Global(), { env.Null(), env.Null(), Napi::Number::New(env, 0) });
551
+ }
552
+ }
553
+ }
554
+
555
+
556
+ struct PdfStreamInfo {
557
+ Napi::Function fn;
558
+ uint32_t len;
559
+ uint8_t* data;
560
+ };
561
+
562
+ /*
563
+ * Canvas::StreamPDF callback.
564
+ */
565
+
566
+ static cairo_status_t
567
+ streamPDF(void *c, const uint8_t *data, unsigned len) {
568
+ PdfStreamInfo* streaminfo = static_cast<PdfStreamInfo*>(c);
569
+ Napi::Env env = streaminfo->fn.Env();
570
+ Napi::HandleScope scope(env);
571
+ Napi::AsyncContext async(env, "canvas:StreamPDF");
572
+ // TODO this is technically wrong, we're returning a pointer to the data in a
573
+ // vector in a class with automatic storage duration. If the canvas goes out
574
+ // of scope while we're in the handler, a use-after-free could happen.
575
+ Napi::Value buf = Napi::Buffer<uint8_t>::New(env, (uint8_t *)(data), len);
576
+ streaminfo->fn.MakeCallback(env.Global(), { env.Null(), buf, Napi::Number::New(env, len) }, async);
577
+ return CAIRO_STATUS_SUCCESS;
578
+ }
579
+
580
+
581
+ cairo_status_t canvas_write_to_pdf_stream(cairo_surface_t *surface, cairo_write_func_t write_func, PdfStreamInfo* streaminfo) {
582
+ size_t whole_chunks = streaminfo->len / PAGE_SIZE;
583
+ size_t remainder = streaminfo->len - whole_chunks * PAGE_SIZE;
584
+
585
+ for (size_t i = 0; i < whole_chunks; ++i) {
586
+ write_func(streaminfo, &streaminfo->data[i * PAGE_SIZE], PAGE_SIZE);
587
+ }
588
+
589
+ if (remainder) {
590
+ write_func(streaminfo, &streaminfo->data[whole_chunks * PAGE_SIZE], remainder);
591
+ }
592
+
593
+ return CAIRO_STATUS_SUCCESS;
594
+ }
595
+
596
+ /*
597
+ * Stream PDF data synchronously.
598
+ */
599
+
600
+ void
601
+ Canvas::StreamPDFSync(const Napi::CallbackInfo& info) {
602
+ if (!info[0].IsFunction()) {
603
+ Napi::TypeError::New(env, "callback function required").ThrowAsJavaScriptException();
604
+ return;
605
+ }
606
+
607
+ if (!isPDF()) {
608
+ Napi::TypeError::New(env, "wrong canvas type").ThrowAsJavaScriptException();
609
+ return;
610
+ }
611
+
612
+ #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0)
613
+ if (info[1].IsObject()) {
614
+ setPdfMetadata(this, info[1].As<Napi::Object>());
615
+ }
616
+ #endif
617
+
618
+ cairo_surface_finish(ensureSurface());
619
+
620
+ PdfSvgClosure *closure = static_cast<PdfSvgClosure *>(_closure);
621
+ Napi::Function fn = info[0].As<Napi::Function>();
622
+ PdfStreamInfo streaminfo;
623
+ streaminfo.fn = fn;
624
+ streaminfo.data = &closure->vec[0];
625
+ streaminfo.len = closure->vec.size();
626
+
627
+ cairo_status_t status = canvas_write_to_pdf_stream(ensureSurface(), streamPDF, &streaminfo);
628
+
629
+ if (!env.IsExceptionPending()) {
630
+ if (status) {
631
+ fn.Call(env.Global(), { CairoError(status).Value() });
632
+ } else {
633
+ fn.Call(env.Global(), { env.Null(), env.Null(), Napi::Number::New(env, 0) });
634
+ }
635
+ }
636
+ }
637
+
638
+ /*
639
+ * Stream JPEG data synchronously.
640
+ */
641
+
642
+ #ifdef HAVE_JPEG
643
+ static uint32_t getSafeBufSize(Canvas* canvas) {
644
+ // Don't allow the buffer size to exceed the size of the canvas (#674)
645
+ // TODO not sure if this is really correct, but it fixed #674
646
+ return (std::min)((uint32_t)canvas->getWidth() * canvas->getHeight() * 4, (uint32_t)PAGE_SIZE);
647
+ }
648
+
649
+ void
650
+ Canvas::StreamJPEGSync(const Napi::CallbackInfo& info) {
651
+ if (!info[1].IsFunction()) {
652
+ Napi::TypeError::New(env, "callback function required").ThrowAsJavaScriptException();
653
+ return;
654
+ }
655
+
656
+ JpegClosure closure(this);
657
+ parseJPEGArgs(info[0], closure);
658
+ closure.cb = Napi::Persistent(info[1].As<Napi::Function>());
659
+
660
+ uint32_t bufsize = getSafeBufSize(this);
661
+ write_to_jpeg_stream(ensureSurface(), bufsize, &closure);
662
+ }
663
+ #endif
664
+
665
+ char *
666
+ str_value(Napi::Maybe<Napi::Value> maybe, const char *fallback, bool can_be_number) {
667
+ Napi::Value val;
668
+ if (maybe.UnwrapTo(&val)) {
669
+ if (val.IsString() || (can_be_number && val.IsNumber())) {
670
+ Napi::String strVal;
671
+ if (val.ToString().UnwrapTo(&strVal)) return strdup(strVal.Utf8Value().c_str());
672
+ } else if (fallback) {
673
+ return strdup(fallback);
674
+ }
675
+ }
676
+
677
+ return NULL;
678
+ }
679
+
680
+ void
681
+ Canvas::RegisterFont(const Napi::CallbackInfo& info) {
682
+ Napi::Env env = info.Env();
683
+ if (!info[0].IsString()) {
684
+ Napi::Error::New(env, "Wrong argument type").ThrowAsJavaScriptException();
685
+ return;
686
+ } else if (!info[1].IsObject()) {
687
+ Napi::Error::New(env, GENERIC_FACE_ERROR).ThrowAsJavaScriptException();
688
+ return;
689
+ }
690
+
691
+ std::string filePath = info[0].As<Napi::String>();
692
+ PangoFontDescription *sys_desc = get_pango_font_description((unsigned char *)(filePath.c_str()));
693
+
694
+ if (!sys_desc) {
695
+ Napi::Error::New(env, "Could not parse font file").ThrowAsJavaScriptException();
696
+ return;
697
+ }
698
+
699
+ PangoFontDescription *user_desc = pango_font_description_new();
700
+
701
+ // now check the attrs, there are many ways to be wrong
702
+ Napi::Object js_user_desc = info[1].As<Napi::Object>();
703
+
704
+ // TODO: use FontParser on these values just like the FontFace API works
705
+ char *family = str_value(js_user_desc.Get("family"), NULL, false);
706
+ char *weight = str_value(js_user_desc.Get("weight"), "normal", true);
707
+ char *style = str_value(js_user_desc.Get("style"), "normal", false);
708
+
709
+ if (family && weight && style) {
710
+ pango_font_description_set_weight(user_desc, Canvas::GetWeightFromCSSString(weight));
711
+ pango_font_description_set_style(user_desc, Canvas::GetStyleFromCSSString(style));
712
+ pango_font_description_set_family(user_desc, family);
713
+
714
+ auto found = std::find_if(font_face_list.begin(), font_face_list.end(), [&](FontFace& f) {
715
+ return pango_font_description_equal(f.sys_desc, sys_desc);
716
+ });
717
+
718
+ if (found != font_face_list.end()) {
719
+ pango_font_description_free(found->user_desc);
720
+ found->user_desc = user_desc;
721
+ } else if (register_font((unsigned char *) filePath.c_str())) {
722
+ FontFace face;
723
+ face.user_desc = user_desc;
724
+ face.sys_desc = sys_desc;
725
+ strncpy((char *)face.file_path, (char *) filePath.c_str(), 1023);
726
+ font_face_list.push_back(face);
727
+ } else {
728
+ pango_font_description_free(user_desc);
729
+ Napi::Error::New(env, "Could not load font to the system's font host").ThrowAsJavaScriptException();
730
+
731
+ }
732
+ } else {
733
+ pango_font_description_free(user_desc);
734
+ if (!env.IsExceptionPending()) {
735
+ Napi::Error::New(env, GENERIC_FACE_ERROR).ThrowAsJavaScriptException();
736
+ }
737
+ }
738
+
739
+ free(family);
740
+ free(weight);
741
+ free(style);
742
+ fontSerial++;
743
+ }
744
+
745
+ void
746
+ Canvas::DeregisterAllFonts(const Napi::CallbackInfo& info) {
747
+ Napi::Env env = info.Env();
748
+ // Unload all fonts from pango to free up memory
749
+ bool success = true;
750
+
751
+ std::for_each(font_face_list.begin(), font_face_list.end(), [&](FontFace& f) {
752
+ if (!deregister_font( (unsigned char *)f.file_path )) success = false;
753
+ pango_font_description_free(f.user_desc);
754
+ pango_font_description_free(f.sys_desc);
755
+ });
756
+
757
+ font_face_list.clear();
758
+ fontSerial++;
759
+ if (!success) Napi::Error::New(env, "Could not deregister one or more fonts").ThrowAsJavaScriptException();
760
+ }
761
+
762
+ /*
763
+ * Do not use! This is only exported for testing
764
+ */
765
+ Napi::Value
766
+ Canvas::ParseFont(const Napi::CallbackInfo& info) {
767
+ Napi::Env env = info.Env();
768
+
769
+ if (info.Length() != 1) return env.Undefined();
770
+
771
+ Napi::String str;
772
+ if (!info[0].ToString().UnwrapTo(&str)) return env.Undefined();
773
+
774
+ bool ok;
775
+ auto props = FontParser::parse(str, &ok);
776
+ if (!ok) return env.Undefined();
777
+
778
+ Napi::Object obj = Napi::Object::New(env);
779
+ obj.Set("size", Napi::Number::New(env, props.fontSize));
780
+ Napi::Array families = Napi::Array::New(env);
781
+ obj.Set("families", families);
782
+
783
+ unsigned int index = 0;
784
+
785
+ for (auto& family : props.fontFamily) {
786
+ families[index++] = Napi::String::New(env, family);
787
+ }
788
+
789
+ obj.Set("weight", Napi::Number::New(env, props.fontWeight));
790
+ obj.Set("variant", Napi::Number::New(env, static_cast<int>(props.fontVariant)));
791
+ obj.Set("style", Napi::Number::New(env, static_cast<int>(props.fontStyle)));
792
+
793
+ return obj;
794
+ }
795
+
796
+ /*
797
+ * Get a PangoStyle from a CSS string (like "italic")
798
+ */
799
+
800
+ PangoStyle
801
+ Canvas::GetStyleFromCSSString(const char *style) {
802
+ PangoStyle s = PANGO_STYLE_NORMAL;
803
+
804
+ if (strlen(style) > 0) {
805
+ if (0 == strcmp("italic", style)) {
806
+ s = PANGO_STYLE_ITALIC;
807
+ } else if (0 == strcmp("oblique", style)) {
808
+ s = PANGO_STYLE_OBLIQUE;
809
+ }
810
+ }
811
+
812
+ return s;
813
+ }
814
+
815
+ /*
816
+ * Get a PangoWeight from a CSS string ("bold", "100", etc)
817
+ */
818
+
819
+ PangoWeight
820
+ Canvas::GetWeightFromCSSString(const char *weight) {
821
+ PangoWeight w = PANGO_WEIGHT_NORMAL;
822
+
823
+ if (strlen(weight) > 0) {
824
+ if (0 == strcmp("bold", weight)) {
825
+ w = PANGO_WEIGHT_BOLD;
826
+ } else if (0 == strcmp("100", weight)) {
827
+ w = PANGO_WEIGHT_THIN;
828
+ } else if (0 == strcmp("200", weight)) {
829
+ w = PANGO_WEIGHT_ULTRALIGHT;
830
+ } else if (0 == strcmp("300", weight)) {
831
+ w = PANGO_WEIGHT_LIGHT;
832
+ } else if (0 == strcmp("400", weight)) {
833
+ w = PANGO_WEIGHT_NORMAL;
834
+ } else if (0 == strcmp("500", weight)) {
835
+ w = PANGO_WEIGHT_MEDIUM;
836
+ } else if (0 == strcmp("600", weight)) {
837
+ w = PANGO_WEIGHT_SEMIBOLD;
838
+ } else if (0 == strcmp("700", weight)) {
839
+ w = PANGO_WEIGHT_BOLD;
840
+ } else if (0 == strcmp("800", weight)) {
841
+ w = PANGO_WEIGHT_ULTRABOLD;
842
+ } else if (0 == strcmp("900", weight)) {
843
+ w = PANGO_WEIGHT_HEAVY;
844
+ }
845
+ }
846
+
847
+ return w;
848
+ }
849
+
850
+ /*
851
+ * Given a user description, return a description that will select the
852
+ * font either from the system or @font-face
853
+ */
854
+
855
+ PangoFontDescription *
856
+ Canvas::ResolveFontDescription(const PangoFontDescription *desc) {
857
+ // One of the user-specified families could map to multiple SFNT family names
858
+ // if someone registered two different fonts under the same family name.
859
+ // https://drafts.csswg.org/css-fonts-3/#font-style-matching
860
+ FontFace best;
861
+ istringstream families(pango_font_description_get_family(desc));
862
+ unordered_set<string> seen_families;
863
+ string resolved_families;
864
+ bool first = true;
865
+
866
+ for (string family; getline(families, family, ','); ) {
867
+ string renamed_families;
868
+ for (auto& ff : font_face_list) {
869
+ string pangofamily = string(pango_font_description_get_family(ff.user_desc));
870
+ if (streq_casein(family, pangofamily)) {
871
+ const char* sys_desc_family_name = pango_font_description_get_family(ff.sys_desc);
872
+ bool unseen = seen_families.find(sys_desc_family_name) == seen_families.end();
873
+ bool better = best.user_desc == nullptr || pango_font_description_better_match(desc, best.user_desc, ff.user_desc);
874
+
875
+ // Avoid sending duplicate SFNT font names due to a bug in Pango for macOS:
876
+ // https://bugzilla.gnome.org/show_bug.cgi?id=762873
877
+ if (unseen) {
878
+ seen_families.insert(sys_desc_family_name);
879
+
880
+ if (better) {
881
+ renamed_families = string(sys_desc_family_name) + (renamed_families.size() ? "," : "") + renamed_families;
882
+ } else {
883
+ renamed_families = renamed_families + (renamed_families.size() ? "," : "") + sys_desc_family_name;
884
+ }
885
+ }
886
+
887
+ if (first && better) best = ff;
888
+ }
889
+ }
890
+
891
+ if (resolved_families.size()) resolved_families += ',';
892
+ resolved_families += renamed_families.size() ? renamed_families : family;
893
+ first = false;
894
+ }
895
+
896
+ PangoFontDescription* ret = pango_font_description_copy(best.sys_desc ? best.sys_desc : desc);
897
+ pango_font_description_set_family(ret, resolved_families.c_str());
898
+
899
+ return ret;
900
+ }
901
+
902
+ // This returns an approximate value only, suitable for
903
+ // Napi::MemoryManagement:: AdjustExternalMemory.
904
+ // The formats that don't map to intrinsic types (RGB30, A1) round up.
905
+ uint8_t
906
+ Canvas::approxBytesPerPixel() {
907
+ switch (format) {
908
+ case CAIRO_FORMAT_ARGB32:
909
+ case CAIRO_FORMAT_RGB24:
910
+ return 4;
911
+ #ifdef CAIRO_FORMAT_RGB30
912
+ case CAIRO_FORMAT_RGB30:
913
+ return 3;
914
+ #endif
915
+ case CAIRO_FORMAT_RGB16_565:
916
+ return 2;
917
+ case CAIRO_FORMAT_A8:
918
+ case CAIRO_FORMAT_A1:
919
+ return 1;
920
+ default:
921
+ return 0;
922
+ }
923
+ }
924
+
925
+ void
926
+ Canvas::setFormat(cairo_format_t format) {
927
+ if (this->format != format) {
928
+ destroySurface();
929
+ this->format = format;
930
+ }
931
+ }
932
+
933
+ cairo_format_t
934
+ Canvas::getFormat() {
935
+ return isImage() ? format : CAIRO_FORMAT_INVALID;
936
+ }
937
+
938
+ /*
939
+ * Re-alloc the surface, destroying the previous.
940
+ */
941
+
942
+ void
943
+ Canvas::resurface(Napi::Object This, uint16_t width, uint16_t height) {
944
+ Napi::HandleScope scope(env);
945
+ Napi::Value context;
946
+
947
+ if (type == CANVAS_TYPE_PDF) {
948
+ ensureSurface();
949
+ cairo_pdf_surface_set_size(_surface, width, height);
950
+ this->width = width;
951
+ this->height = height;
952
+ } else {
953
+ destroySurface();
954
+ this->width = width;
955
+ this->height = height;
956
+ ensureSurface();
957
+ if (This.Get("context").UnwrapTo(&context) && context.IsObject()) {
958
+ // Reset context
959
+ Context2d *context2d = Context2d::Unwrap(context.As<Napi::Object>());
960
+ cairo_t *prev = context2d->context();
961
+ context2d->setContext(createCairoContext());
962
+ context2d->resetState();
963
+ cairo_destroy(prev);
964
+ }
965
+ }
966
+ }
967
+
968
+ cairo_surface_t *
969
+ Canvas::ensureSurface() {
970
+ if (_surface) {
971
+ return _surface;
972
+ }
973
+
974
+ assert(!_closure);
975
+
976
+ if (type == CANVAS_TYPE_PDF) {
977
+ _closure = new PdfSvgClosure(this);
978
+ _surface = cairo_pdf_surface_create_for_stream(PdfSvgClosure::writeVec, _closure, width, height);
979
+ } else if (type == CANVAS_TYPE_SVG) {
980
+ _closure = new PdfSvgClosure(this);
981
+ _surface = cairo_svg_surface_create_for_stream(PdfSvgClosure::writeVec, _closure, width, height);
982
+ } else {
983
+ _surface = cairo_image_surface_create(format, width, height);
984
+ Napi::MemoryManagement::AdjustExternalMemory(env, (int64_t)approxBytesPerPixel() * width * height);
985
+ }
986
+
987
+ assert(_surface);
988
+ return _surface;
989
+ }
990
+
991
+ void
992
+ Canvas::destroySurface() {
993
+ if (_surface) {
994
+ // flush any operations that may use the closure that is freed below
995
+ cairo_surface_finish(_surface);
996
+ if (type == CANVAS_TYPE_IMAGE) {
997
+ Napi::MemoryManagement::AdjustExternalMemory(env, -(int64_t)approxBytesPerPixel() * width * height);
998
+ }
999
+ cairo_surface_destroy(_surface);
1000
+ _surface = nullptr;
1001
+ }
1002
+ if (_closure) {
1003
+ delete _closure;
1004
+ _closure = nullptr;
1005
+ }
1006
+ }
1007
+
1008
+ /**
1009
+ * Wrapper around cairo_create()
1010
+ * (do not call cairo_create directly, call this instead)
1011
+ */
1012
+ cairo_t*
1013
+ Canvas::createCairoContext() {
1014
+ cairo_t* ret = cairo_create(ensureSurface());
1015
+ cairo_set_line_width(ret, 1); // Cairo defaults to 2
1016
+ return ret;
1017
+ }
1018
+
1019
+ /*
1020
+ * Construct an Error from the given cairo status.
1021
+ */
1022
+
1023
+ Napi::Error
1024
+ Canvas::CairoError(cairo_status_t status) {
1025
+ return Napi::Error::New(env, cairo_status_to_string(status));
1026
+ }