@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
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
|
+
}
|