// Copyright (c) 2010 LearnBoost #include "Canvas.h" #include // std::min #include #include #include #include "CanvasRenderingContext2d.h" #include "closure.h" #include #include #include #include #include "PNG.h" #include "register_font.h" #include #include #include #include #include "Util.h" #include #include "node_buffer.h" #ifdef HAVE_JPEG #include "JPEGStream.h" #endif #include "backend/ImageBackend.h" #include "backend/PdfBackend.h" #include "backend/SvgBackend.h" #define GENERIC_FACE_ERROR \ "The second argument to registerFont is required, and should be an object " \ "with at least a family (string) and optionally weight (string/number) " \ "and style (string)." #define CHECK_RECEIVER(prop) \ if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { \ Nan::ThrowTypeError("Method " #prop " called on incompatible receiver"); \ return; \ } using namespace v8; using namespace std; Nan::Persistent Canvas::constructor; std::vector font_face_list; /* * Initialize Canvas. */ void Canvas::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { Nan::HandleScope scope; // Constructor Local ctor = Nan::New(Canvas::New); constructor.Reset(ctor); ctor->InstanceTemplate()->SetInternalFieldCount(1); ctor->SetClassName(Nan::New("Canvas").ToLocalChecked()); // Prototype Local proto = ctor->PrototypeTemplate(); Nan::SetPrototypeMethod(ctor, "toBuffer", ToBuffer); Nan::SetPrototypeMethod(ctor, "streamPNGSync", StreamPNGSync); Nan::SetPrototypeMethod(ctor, "streamPDFSync", StreamPDFSync); #ifdef HAVE_JPEG Nan::SetPrototypeMethod(ctor, "streamJPEGSync", StreamJPEGSync); #endif Nan::SetAccessor(proto, Nan::New("type").ToLocalChecked(), GetType); Nan::SetAccessor(proto, Nan::New("stride").ToLocalChecked(), GetStride); Nan::SetAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, SetWidth); Nan::SetAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, SetHeight); Nan::SetTemplate(proto, "PNG_NO_FILTERS", Nan::New(PNG_NO_FILTERS)); Nan::SetTemplate(proto, "PNG_FILTER_NONE", Nan::New(PNG_FILTER_NONE)); Nan::SetTemplate(proto, "PNG_FILTER_SUB", Nan::New(PNG_FILTER_SUB)); Nan::SetTemplate(proto, "PNG_FILTER_UP", Nan::New(PNG_FILTER_UP)); Nan::SetTemplate(proto, "PNG_FILTER_AVG", Nan::New(PNG_FILTER_AVG)); Nan::SetTemplate(proto, "PNG_FILTER_PAETH", Nan::New(PNG_FILTER_PAETH)); Nan::SetTemplate(proto, "PNG_ALL_FILTERS", Nan::New(PNG_ALL_FILTERS)); // Class methods Nan::SetMethod(ctor, "_registerFont", RegisterFont); Nan::SetMethod(ctor, "_deregisterAllFonts", DeregisterAllFonts); Local ctx = Nan::GetCurrentContext(); Nan::Set(target, Nan::New("Canvas").ToLocalChecked(), ctor->GetFunction(ctx).ToLocalChecked()); } /* * Initialize a Canvas with the given width and height. */ NAN_METHOD(Canvas::New) { if (!info.IsConstructCall()) { return Nan::ThrowTypeError("Class constructors cannot be invoked without 'new'"); } Backend* backend = NULL; if (info[0]->IsNumber()) { int width = Nan::To(info[0]).FromMaybe(0), height = 0; if (info[1]->IsNumber()) height = Nan::To(info[1]).FromMaybe(0); if (info[2]->IsString()) { if (0 == strcmp("pdf", *Nan::Utf8String(info[2]))) backend = new PdfBackend(width, height); else if (0 == strcmp("svg", *Nan::Utf8String(info[2]))) backend = new SvgBackend(width, height); else backend = new ImageBackend(width, height); } else backend = new ImageBackend(width, height); } else if (info[0]->IsObject()) { if (Nan::New(ImageBackend::constructor)->HasInstance(info[0]) || Nan::New(PdfBackend::constructor)->HasInstance(info[0]) || Nan::New(SvgBackend::constructor)->HasInstance(info[0])) { backend = Nan::ObjectWrap::Unwrap(Nan::To(info[0]).ToLocalChecked()); }else{ return Nan::ThrowTypeError("Invalid arguments"); } } else { backend = new ImageBackend(0, 0); } if (!backend->isSurfaceValid()) { delete backend; return Nan::ThrowError(backend->getError()); } Canvas* canvas = new Canvas(backend); canvas->Wrap(info.This()); backend->setCanvas(canvas); info.GetReturnValue().Set(info.This()); } /* * Get type string. */ NAN_GETTER(Canvas::GetType) { CHECK_RECEIVER(Canvas.GetType); Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(canvas->backend()->getName()).ToLocalChecked()); } /* * Get stride. */ NAN_GETTER(Canvas::GetStride) { CHECK_RECEIVER(Canvas.GetStride); Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(canvas->stride())); } /* * Get width. */ NAN_GETTER(Canvas::GetWidth) { CHECK_RECEIVER(Canvas.GetWidth); Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(canvas->getWidth())); } /* * Set width. */ NAN_SETTER(Canvas::SetWidth) { CHECK_RECEIVER(Canvas.SetWidth); if (value->IsNumber()) { Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); canvas->backend()->setWidth(Nan::To(value).FromMaybe(0)); canvas->resurface(info.This()); } } /* * Get height. */ NAN_GETTER(Canvas::GetHeight) { CHECK_RECEIVER(Canvas.GetHeight); Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(canvas->getHeight())); } /* * Set height. */ NAN_SETTER(Canvas::SetHeight) { CHECK_RECEIVER(Canvas.SetHeight); if (value->IsNumber()) { Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); canvas->backend()->setHeight(Nan::To(value).FromMaybe(0)); canvas->resurface(info.This()); } } /* * EIO toBuffer callback. */ void Canvas::ToPngBufferAsync(uv_work_t *req) { PngClosure* closure = static_cast(req->data); closure->status = canvas_write_to_png_stream( closure->canvas->surface(), PngClosure::writeVec, closure); } #ifdef HAVE_JPEG void Canvas::ToJpegBufferAsync(uv_work_t *req) { JpegClosure* closure = static_cast(req->data); write_to_jpeg_buffer(closure->canvas->surface(), closure); } #endif /* * EIO after toBuffer callback. */ void Canvas::ToBufferAsyncAfter(uv_work_t *req) { Nan::HandleScope scope; Nan::AsyncResource async("canvas:ToBufferAsyncAfter"); Closure* closure = static_cast(req->data); delete req; if (closure->status) { Local argv[1] = { Canvas::Error(closure->status) }; closure->cb.Call(1, argv, &async); } else { Local buf = Nan::CopyBuffer((char*)&closure->vec[0], closure->vec.size()).ToLocalChecked(); Local argv[2] = { Nan::Null(), buf }; closure->cb.Call(sizeof argv / sizeof *argv, argv, &async); } closure->canvas->Unref(); delete closure; } static void parsePNGArgs(Local arg, PngClosure& pngargs) { if (arg->IsObject()) { Local obj = Nan::To(arg).ToLocalChecked(); Local cLevel = Nan::Get(obj, Nan::New("compressionLevel").ToLocalChecked()).ToLocalChecked(); if (cLevel->IsUint32()) { uint32_t val = Nan::To(cLevel).FromMaybe(0); // See quote below from spec section 4.12.5.5. if (val <= 9) pngargs.compressionLevel = val; } Local rez = Nan::Get(obj, Nan::New("resolution").ToLocalChecked()).ToLocalChecked(); if (rez->IsUint32()) { uint32_t val = Nan::To(rez).FromMaybe(0); if (val > 0) pngargs.resolution = val; } Local filters = Nan::Get(obj, Nan::New("filters").ToLocalChecked()).ToLocalChecked(); if (filters->IsUint32()) pngargs.filters = Nan::To(filters).FromMaybe(0); Local palette = Nan::Get(obj, Nan::New("palette").ToLocalChecked()).ToLocalChecked(); if (palette->IsUint8ClampedArray()) { Local palette_ta = palette.As(); pngargs.nPaletteColors = palette_ta->Length(); if (pngargs.nPaletteColors % 4 != 0) { throw "Palette length must be a multiple of 4."; } pngargs.nPaletteColors /= 4; Nan::TypedArrayContents _paletteColors(palette_ta); pngargs.palette = *_paletteColors; // Optional background color index: Local backgroundIndexVal = Nan::Get(obj, Nan::New("backgroundIndex").ToLocalChecked()).ToLocalChecked(); if (backgroundIndexVal->IsUint32()) { pngargs.backgroundIndex = static_cast(Nan::To(backgroundIndexVal).FromMaybe(0)); } } } } #ifdef HAVE_JPEG static void parseJPEGArgs(Local arg, JpegClosure& jpegargs) { // "If Type(quality) is not Number, or if quality is outside that range, the // user agent must use its default quality value, as if the quality argument // had not been given." - 4.12.5.5 if (arg->IsObject()) { Local obj = Nan::To(arg).ToLocalChecked(); Local qual = Nan::Get(obj, Nan::New("quality").ToLocalChecked()).ToLocalChecked(); if (qual->IsNumber()) { double quality = Nan::To(qual).FromMaybe(0); if (quality >= 0.0 && quality <= 1.0) { jpegargs.quality = static_cast(100.0 * quality); } } Local chroma = Nan::Get(obj, Nan::New("chromaSubsampling").ToLocalChecked()).ToLocalChecked(); if (chroma->IsBoolean()) { bool subsample = Nan::To(chroma).FromMaybe(0); jpegargs.chromaSubsampling = subsample ? 2 : 1; } else if (chroma->IsNumber()) { jpegargs.chromaSubsampling = Nan::To(chroma).FromMaybe(0); } Local progressive = Nan::Get(obj, Nan::New("progressive").ToLocalChecked()).ToLocalChecked(); if (!progressive->IsUndefined()) { jpegargs.progressive = Nan::To(progressive).FromMaybe(0); } } } #endif #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0) static inline void setPdfMetaStr(cairo_surface_t* surf, Local opts, cairo_pdf_metadata_t t, const char* pName) { auto propName = Nan::New(pName).ToLocalChecked(); auto propValue = Nan::Get(opts, propName).ToLocalChecked(); if (propValue->IsString()) { // (copies char data) cairo_pdf_surface_set_metadata(surf, t, *Nan::Utf8String(propValue)); } } static inline void setPdfMetaDate(cairo_surface_t* surf, Local opts, cairo_pdf_metadata_t t, const char* pName) { auto propName = Nan::New(pName).ToLocalChecked(); auto propValue = Nan::Get(opts, propName).ToLocalChecked(); if (propValue->IsDate()) { auto date = static_cast(propValue.As()->ValueOf() / 1000); // ms -> s char buf[sizeof "2011-10-08T07:07:09Z"]; strftime(buf, sizeof buf, "%FT%TZ", gmtime(&date)); cairo_pdf_surface_set_metadata(surf, t, buf); } } static void setPdfMetadata(Canvas* canvas, Local opts) { cairo_surface_t* surf = canvas->surface(); setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_TITLE, "title"); setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_AUTHOR, "author"); setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_SUBJECT, "subject"); setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_KEYWORDS, "keywords"); setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_CREATOR, "creator"); setPdfMetaDate(surf, opts, CAIRO_PDF_METADATA_CREATE_DATE, "creationDate"); setPdfMetaDate(surf, opts, CAIRO_PDF_METADATA_MOD_DATE, "modDate"); } #endif // CAIRO 16+ /* * Converts/encodes data to a Buffer. Async when a callback function is passed. * PDF canvases: (any) => Buffer ("application/pdf", config) => Buffer * SVG canvases: (any) => Buffer * ARGB data: ("raw") => Buffer * PNG-encoded () => Buffer (undefined|"image/png", {compressionLevel?: number, filter?: number}) => Buffer ((err: null|Error, buffer) => any) ((err: null|Error, buffer) => any, undefined|"image/png", {compressionLevel?: number, filter?: number}) * JPEG-encoded ("image/jpeg") => Buffer ("image/jpeg", {quality?: number, progressive?: Boolean, chromaSubsampling?: Boolean|number}) => Buffer ((err: null|Error, buffer) => any, "image/jpeg") ((err: null|Error, buffer) => any, "image/jpeg", {quality?: number, progressive?: Boolean, chromaSubsampling?: Boolean|number}) */ NAN_METHOD(Canvas::ToBuffer) { cairo_status_t status; Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); // Vector canvases, sync only const std::string name = canvas->backend()->getName(); if (name == "pdf" || name == "svg") { // mime type may be present, but it's not checked PdfSvgClosure* closure; if (name == "pdf") { closure = static_cast(canvas->backend())->closure(); #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0) if (info[1]->IsObject()) { // toBuffer("application/pdf", config) setPdfMetadata(canvas, Nan::To(info[1]).ToLocalChecked()); } #endif // CAIRO 16+ } else { closure = static_cast(canvas->backend())->closure(); } cairo_surface_finish(canvas->surface()); Local buf = Nan::CopyBuffer((char*)&closure->vec[0], closure->vec.size()).ToLocalChecked(); info.GetReturnValue().Set(buf); return; } // Raw ARGB data -- just a memcpy() if (info[0]->StrictEquals(Nan::New("raw").ToLocalChecked())) { cairo_surface_t *surface = canvas->surface(); cairo_surface_flush(surface); if (canvas->nBytes() > node::Buffer::kMaxLength) { Nan::ThrowError("Data exceeds maximum buffer length."); return; } const unsigned char *data = cairo_image_surface_get_data(surface); Isolate* iso = Nan::GetCurrentContext()->GetIsolate(); Local buf = node::Buffer::Copy(iso, reinterpret_cast(data), canvas->nBytes()).ToLocalChecked(); info.GetReturnValue().Set(buf); return; } // Sync PNG, default if (info[0]->IsUndefined() || info[0]->StrictEquals(Nan::New("image/png").ToLocalChecked())) { try { PngClosure closure(canvas); parsePNGArgs(info[1], closure); if (closure.nPaletteColors == 0xFFFFFFFF) { Nan::ThrowError("Palette length must be a multiple of 4."); return; } Nan::TryCatch try_catch; status = canvas_write_to_png_stream(canvas->surface(), PngClosure::writeVec, &closure); if (try_catch.HasCaught()) { try_catch.ReThrow(); } else if (status) { throw status; } else { // TODO it's possible to avoid this copy Local buf = Nan::CopyBuffer((char *)&closure.vec[0], closure.vec.size()).ToLocalChecked(); info.GetReturnValue().Set(buf); } } catch (cairo_status_t ex) { Nan::ThrowError(Canvas::Error(ex)); } catch (const char* ex) { Nan::ThrowError(ex); } return; } // Async PNG if (info[0]->IsFunction() && (info[1]->IsUndefined() || info[1]->StrictEquals(Nan::New("image/png").ToLocalChecked()))) { PngClosure* closure; try { closure = new PngClosure(canvas); parsePNGArgs(info[2], *closure); } catch (cairo_status_t ex) { Nan::ThrowError(Canvas::Error(ex)); return; } catch (const char* ex) { Nan::ThrowError(ex); return; } canvas->Ref(); closure->cb.Reset(info[0].As()); uv_work_t* req = new uv_work_t; req->data = closure; // Make sure the surface exists since we won't have an isolate context in the async block: canvas->surface(); uv_queue_work(uv_default_loop(), req, ToPngBufferAsync, (uv_after_work_cb)ToBufferAsyncAfter); return; } #ifdef HAVE_JPEG // Sync JPEG Local jpegStr = Nan::New("image/jpeg").ToLocalChecked(); if (info[0]->StrictEquals(jpegStr)) { try { JpegClosure closure(canvas); parseJPEGArgs(info[1], closure); Nan::TryCatch try_catch; write_to_jpeg_buffer(canvas->surface(), &closure); if (try_catch.HasCaught()) { try_catch.ReThrow(); } else { // TODO it's possible to avoid this copy. Local buf = Nan::CopyBuffer((char *)&closure.vec[0], closure.vec.size()).ToLocalChecked(); info.GetReturnValue().Set(buf); } } catch (cairo_status_t ex) { Nan::ThrowError(Canvas::Error(ex)); } return; } // Async JPEG if (info[0]->IsFunction() && info[1]->StrictEquals(jpegStr)) { JpegClosure* closure = new JpegClosure(canvas); parseJPEGArgs(info[2], *closure); canvas->Ref(); closure->cb.Reset(info[0].As()); uv_work_t* req = new uv_work_t; req->data = closure; // Make sure the surface exists since we won't have an isolate context in the async block: canvas->surface(); uv_queue_work(uv_default_loop(), req, ToJpegBufferAsync, (uv_after_work_cb)ToBufferAsyncAfter); return; } #endif } /* * Canvas::StreamPNG callback. */ static cairo_status_t streamPNG(void *c, const uint8_t *data, unsigned len) { Nan::HandleScope scope; Nan::AsyncResource async("canvas:StreamPNG"); PngClosure* closure = (PngClosure*) c; Local buf = Nan::CopyBuffer((char *)data, len).ToLocalChecked(); Local argv[3] = { Nan::Null() , buf , Nan::New(len) }; closure->cb.Call(sizeof argv / sizeof *argv, argv, &async); return CAIRO_STATUS_SUCCESS; } /* * Stream PNG data synchronously. TODO async * StreamPngSync(this, options: {palette?: Uint8ClampedArray, backgroundIndex?: uint32, compressionLevel: uint32, filters: uint32}) */ NAN_METHOD(Canvas::StreamPNGSync) { if (!info[0]->IsFunction()) return Nan::ThrowTypeError("callback function required"); Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); PngClosure closure(canvas); parsePNGArgs(info[1], closure); closure.cb.Reset(Local::Cast(info[0])); Nan::TryCatch try_catch; cairo_status_t status = canvas_write_to_png_stream(canvas->surface(), streamPNG, &closure); if (try_catch.HasCaught()) { try_catch.ReThrow(); return; } else if (status) { Local argv[1] = { Canvas::Error(status) }; Nan::Call(closure.cb, Nan::GetCurrentContext()->Global(), sizeof argv / sizeof *argv, argv); } else { Local argv[3] = { Nan::Null() , Nan::Null() , Nan::New(0) }; Nan::Call(closure.cb, Nan::GetCurrentContext()->Global(), sizeof argv / sizeof *argv, argv); } return; } struct PdfStreamInfo { Local fn; uint32_t len; uint8_t* data; }; /* * Canvas::StreamPDF FreeCallback */ void stream_pdf_free(char *, void *) {} /* * Canvas::StreamPDF callback. */ static cairo_status_t streamPDF(void *c, const uint8_t *data, unsigned len) { Nan::HandleScope scope; Nan::AsyncResource async("canvas:StreamPDF"); PdfStreamInfo* streaminfo = static_cast(c); // TODO this is technically wrong, we're returning a pointer to the data in a // vector in a class with automatic storage duration. If the canvas goes out // of scope while we're in the handler, a use-after-free could happen. Local buf = Nan::NewBuffer(const_cast(reinterpret_cast(data)), len, stream_pdf_free, 0).ToLocalChecked(); Local argv[3] = { Nan::Null() , buf , Nan::New(len) }; async.runInAsyncScope(Nan::GetCurrentContext()->Global(), streaminfo->fn, sizeof argv / sizeof *argv, argv); return CAIRO_STATUS_SUCCESS; } cairo_status_t canvas_write_to_pdf_stream(cairo_surface_t *surface, cairo_write_func_t write_func, PdfStreamInfo* streaminfo) { size_t whole_chunks = streaminfo->len / PAGE_SIZE; size_t remainder = streaminfo->len - whole_chunks * PAGE_SIZE; for (size_t i = 0; i < whole_chunks; ++i) { write_func(streaminfo, &streaminfo->data[i * PAGE_SIZE], PAGE_SIZE); } if (remainder) { write_func(streaminfo, &streaminfo->data[whole_chunks * PAGE_SIZE], remainder); } return CAIRO_STATUS_SUCCESS; } /* * Stream PDF data synchronously. */ NAN_METHOD(Canvas::StreamPDFSync) { if (!info[0]->IsFunction()) return Nan::ThrowTypeError("callback function required"); Canvas *canvas = Nan::ObjectWrap::Unwrap(info.Holder()); if (canvas->backend()->getName() != "pdf") return Nan::ThrowTypeError("wrong canvas type"); #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0) if (info[1]->IsObject()) { setPdfMetadata(canvas, Nan::To(info[1]).ToLocalChecked()); } #endif cairo_surface_finish(canvas->surface()); PdfSvgClosure* closure = static_cast(canvas->backend())->closure(); Local fn = info[0].As(); PdfStreamInfo streaminfo; streaminfo.fn = fn; streaminfo.data = &closure->vec[0]; streaminfo.len = closure->vec.size(); Nan::TryCatch try_catch; cairo_status_t status = canvas_write_to_pdf_stream(canvas->surface(), streamPDF, &streaminfo); if (try_catch.HasCaught()) { try_catch.ReThrow(); } else if (status) { Local error = Canvas::Error(status); Nan::Call(fn, Nan::GetCurrentContext()->Global(), 1, &error); } else { Local argv[3] = { Nan::Null() , Nan::Null() , Nan::New(0) }; Nan::Call(fn, Nan::GetCurrentContext()->Global(), sizeof argv / sizeof *argv, argv); } } /* * Stream JPEG data synchronously. */ #ifdef HAVE_JPEG static uint32_t getSafeBufSize(Canvas* canvas) { // Don't allow the buffer size to exceed the size of the canvas (#674) // TODO not sure if this is really correct, but it fixed #674 return (std::min)(canvas->getWidth() * canvas->getHeight() * 4, static_cast(PAGE_SIZE)); } NAN_METHOD(Canvas::StreamJPEGSync) { if (!info[1]->IsFunction()) return Nan::ThrowTypeError("callback function required"); Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); JpegClosure closure(canvas); parseJPEGArgs(info[0], closure); closure.cb.Reset(Local::Cast(info[1])); Nan::TryCatch try_catch; uint32_t bufsize = getSafeBufSize(canvas); write_to_jpeg_stream(canvas->surface(), bufsize, &closure); if (try_catch.HasCaught()) { try_catch.ReThrow(); } return; } #endif char * str_value(Local val, const char *fallback, bool can_be_number) { if (val->IsString() || (can_be_number && val->IsNumber())) { return strdup(*Nan::Utf8String(val)); } else if (fallback) { return strdup(fallback); } else { return NULL; } } NAN_METHOD(Canvas::RegisterFont) { if (!info[0]->IsString()) { return Nan::ThrowError("Wrong argument type"); } else if (!info[1]->IsObject()) { return Nan::ThrowError(GENERIC_FACE_ERROR); } Nan::Utf8String filePath(info[0]); PangoFontDescription *sys_desc = get_pango_font_description((unsigned char *) *filePath); if (!sys_desc) return Nan::ThrowError("Could not parse font file"); PangoFontDescription *user_desc = pango_font_description_new(); // now check the attrs, there are many ways to be wrong Local js_user_desc = Nan::To(info[1]).ToLocalChecked(); Local family_prop = Nan::New("family").ToLocalChecked(); Local weight_prop = Nan::New("weight").ToLocalChecked(); Local style_prop = Nan::New("style").ToLocalChecked(); char *family = str_value(Nan::Get(js_user_desc, family_prop).ToLocalChecked(), NULL, false); char *weight = str_value(Nan::Get(js_user_desc, weight_prop).ToLocalChecked(), "normal", true); char *style = str_value(Nan::Get(js_user_desc, style_prop).ToLocalChecked(), "normal", false); if (family && weight && style) { pango_font_description_set_weight(user_desc, Canvas::GetWeightFromCSSString(weight)); pango_font_description_set_style(user_desc, Canvas::GetStyleFromCSSString(style)); pango_font_description_set_family(user_desc, family); auto found = std::find_if(font_face_list.begin(), font_face_list.end(), [&](FontFace& f) { return pango_font_description_equal(f.sys_desc, sys_desc); }); if (found != font_face_list.end()) { pango_font_description_free(found->user_desc); found->user_desc = user_desc; } else if (register_font((unsigned char *) *filePath)) { FontFace face; face.user_desc = user_desc; face.sys_desc = sys_desc; strncpy((char *)face.file_path, (char *) *filePath, 1023); font_face_list.push_back(face); } else { pango_font_description_free(user_desc); Nan::ThrowError("Could not load font to the system's font host"); } } else { pango_font_description_free(user_desc); Nan::ThrowError(GENERIC_FACE_ERROR); } free(family); free(weight); free(style); } NAN_METHOD(Canvas::DeregisterAllFonts) { // Unload all fonts from pango to free up memory bool success = true; std::for_each(font_face_list.begin(), font_face_list.end(), [&](FontFace& f) { if (!deregister_font( (unsigned char *)f.file_path )) success = false; pango_font_description_free(f.user_desc); pango_font_description_free(f.sys_desc); }); font_face_list.clear(); if (!success) Nan::ThrowError("Could not deregister one or more fonts"); } /* * Initialize cairo surface. */ Canvas::Canvas(Backend* backend) : ObjectWrap() { _backend = backend; } /* * Destroy cairo surface. */ Canvas::~Canvas() { if (_backend != NULL) { delete _backend; } } /* * Get a PangoStyle from a CSS string (like "italic") */ PangoStyle Canvas::GetStyleFromCSSString(const char *style) { PangoStyle s = PANGO_STYLE_NORMAL; if (strlen(style) > 0) { if (0 == strcmp("italic", style)) { s = PANGO_STYLE_ITALIC; } else if (0 == strcmp("oblique", style)) { s = PANGO_STYLE_OBLIQUE; } } return s; } /* * Get a PangoWeight from a CSS string ("bold", "100", etc) */ PangoWeight Canvas::GetWeightFromCSSString(const char *weight) { PangoWeight w = PANGO_WEIGHT_NORMAL; if (strlen(weight) > 0) { if (0 == strcmp("bold", weight)) { w = PANGO_WEIGHT_BOLD; } else if (0 == strcmp("100", weight)) { w = PANGO_WEIGHT_THIN; } else if (0 == strcmp("200", weight)) { w = PANGO_WEIGHT_ULTRALIGHT; } else if (0 == strcmp("300", weight)) { w = PANGO_WEIGHT_LIGHT; } else if (0 == strcmp("400", weight)) { w = PANGO_WEIGHT_NORMAL; } else if (0 == strcmp("500", weight)) { w = PANGO_WEIGHT_MEDIUM; } else if (0 == strcmp("600", weight)) { w = PANGO_WEIGHT_SEMIBOLD; } else if (0 == strcmp("700", weight)) { w = PANGO_WEIGHT_BOLD; } else if (0 == strcmp("800", weight)) { w = PANGO_WEIGHT_ULTRABOLD; } else if (0 == strcmp("900", weight)) { w = PANGO_WEIGHT_HEAVY; } } return w; } /* * Given a user description, return a description that will select the * font either from the system or @font-face */ PangoFontDescription * Canvas::ResolveFontDescription(const PangoFontDescription *desc) { // One of the user-specified families could map to multiple SFNT family names // if someone registered two different fonts under the same family name. // https://drafts.csswg.org/css-fonts-3/#font-style-matching FontFace best; istringstream families(pango_font_description_get_family(desc)); unordered_set seen_families; string resolved_families; bool first = true; for (string family; getline(families, family, ','); ) { string renamed_families; for (auto& ff : font_face_list) { string pangofamily = string(pango_font_description_get_family(ff.user_desc)); if (streq_casein(family, pangofamily)) { const char* sys_desc_family_name = pango_font_description_get_family(ff.sys_desc); bool unseen = seen_families.find(sys_desc_family_name) == seen_families.end(); bool better = best.user_desc == nullptr || pango_font_description_better_match(desc, best.user_desc, ff.user_desc); // Avoid sending duplicate SFNT font names due to a bug in Pango for macOS: // https://bugzilla.gnome.org/show_bug.cgi?id=762873 if (unseen) { seen_families.insert(sys_desc_family_name); if (better) { renamed_families = string(sys_desc_family_name) + (renamed_families.size() ? "," : "") + renamed_families; } else { renamed_families = renamed_families + (renamed_families.size() ? "," : "") + sys_desc_family_name; } } if (first && better) best = ff; } } if (resolved_families.size()) resolved_families += ','; resolved_families += renamed_families.size() ? renamed_families : family; first = false; } PangoFontDescription* ret = pango_font_description_copy(best.sys_desc ? best.sys_desc : desc); pango_font_description_set_family(ret, resolved_families.c_str()); return ret; } /* * Re-alloc the surface, destroying the previous. */ void Canvas::resurface(Local canvas) { Nan::HandleScope scope; Local context; backend()->recreateSurface(); // Reset context context = Nan::Get(canvas, Nan::New("context").ToLocalChecked()).ToLocalChecked(); if (!context->IsUndefined()) { Context2d *context2d = ObjectWrap::Unwrap(Nan::To(context).ToLocalChecked()); cairo_t *prev = context2d->context(); context2d->setContext(createCairoContext()); context2d->resetState(); cairo_destroy(prev); } } /** * Wrapper around cairo_create() * (do not call cairo_create directly, call this instead) */ cairo_t* Canvas::createCairoContext() { cairo_t* ret = cairo_create(surface()); cairo_set_line_width(ret, 1); // Cairo defaults to 2 return ret; } /* * Construct an Error from the given cairo status. */ Local Canvas::Error(cairo_status_t status) { return Exception::Error(Nan::New(cairo_status_to_string(status)).ToLocalChecked()); } #undef CHECK_RECEIVER