diff --git a/Readme.md b/Readme.md index e9e2fff7c..9f9cf1de1 100644 --- a/Readme.md +++ b/Readme.md @@ -19,16 +19,16 @@ node-canvas $ npm install canvas ``` -Unless previously installed you'll _need_ __Cairo__. For system-specific installation view the [Wiki](https://github.com/Automattic/node-canvas/wiki/_pages). +Unless previously installed you'll _need_ __Cairo__ and __Pango__. For system-specific installation view the [Wiki](https://github.com/Automattic/node-canvas/wiki/_pages). You can quickly install the dependencies by using the command for your OS: OS | Command ----- | ----- -OS X | `brew install pkg-config cairo libpng jpeg giflib` +OS X | `brew install pkg-config cairo pango libpng jpeg giflib` Ubuntu | `sudo apt-get install libcairo2-dev libjpeg8-dev libpango1.0-dev libgif-dev build-essential g++` Fedora | `sudo yum install cairo cairo-devel cairomm-devel libjpeg-turbo-devel pango pango-devel pangomm pangomm-devel giflib-devel` -Solaris | `pkgin install cairo pkg-config xproto renderproto kbproto xextproto` +Solaris | `pkgin install cairo pango pkg-config xproto renderproto kbproto xextproto` Windows | [Instructions on our wiki](https://github.com/Automattic/node-canvas/wiki/Installation---Windows) **El Capitan users:** If you have recently updated to El Capitan and are experiencing trouble when compiling, run the following command: `xcode-select --install`. Read more about the problem [on Stack Overflow](http://stackoverflow.com/a/32929012/148072). @@ -182,6 +182,26 @@ canvas.toDataURL('image/jpeg', {opts...}, function(err, jpeg){ }); // see Canvas canvas.toDataURL('image/jpeg', quality, function(err, jpeg){ }); // spec-following; quality from 0 to 1 ``` +### Canvas.registerFont for bundled fonts + +It can be useful to use a custom font file if you are distributing code that uses node-canvas and a specific font. Or perhaps you are using it to do automated tests and you want the renderings to be the same across operating systems regardless of what fonts are installed. + +To do that, you should use `Canvas.registerFont`. + +**You need to call it before the Canvas is created** + +```javascript +Canvas.registerFont('comicsans.ttf', {family: 'Comic Sans'}); + +var canvas = new Canvas(500, 500), + ctx = canvas.getContext('2d'); + +ctx.font = '12px "Comic Sans"'; +ctx.fillText(250, 10, 'Everyone hates this font :('); +``` + +The second argument is an object with properties that resemble the CSS properties that are specified in `@font-face` rules. You must specify at least `family`. `weight`, and `style` are optional (and default to "normal"). + ### CanvasRenderingContext2D#patternQuality Given one of the values below will alter pattern (gradients, images, etc) render quality, defaults to _good_. diff --git a/binding.gyp b/binding.gyp index 66f0115ed..4f56e1a4c 100755 --- a/binding.gyp +++ b/binding.gyp @@ -4,16 +4,12 @@ 'variables': { 'GTK_Root%': 'C:/GTK', # Set the location of GTK all-in-one bundle 'with_jpeg%': 'false', - 'with_gif%': 'false', - 'with_pango%': 'false', - 'with_freetype%': 'false' + 'with_gif%': 'false' } }, { # 'OS!="win"' 'variables': { 'with_jpeg%': ' Font Information and copy the Family Name +Canvas.registerFont(fontFile('Pfennig.ttf'), {family: 'pfennigFont'}) +Canvas.registerFont(fontFile('PfennigBold.ttf'), {family: 'pfennigFont', weight: 'bold'}) +Canvas.registerFont(fontFile('PfennigItalic.ttf'), {family: 'pfennigFont', style: 'italic'}) +Canvas.registerFont(fontFile('PfennigBoldItalic.ttf'), {family: 'pfennigFont', weight: 'bold', style: 'italic'}) var canvas = new Canvas(320, 320) var ctx = canvas.getContext('2d') -// Tell the ctx to use the font. -ctx.addFont(pfennigFont) - ctx.font = 'normal normal 50px Helvetica' ctx.fillText('Quo Vaids?', 0, 70) diff --git a/lib/canvas.js b/lib/canvas.js index d008f5aa0..8272c9b86 100644 --- a/lib/canvas.js +++ b/lib/canvas.js @@ -18,7 +18,6 @@ var canvas = require('./bindings') , PNGStream = require('./pngstream') , PDFStream = require('./pdfstream') , JPEGStream = require('./jpegstream') - , FontFace = canvas.FontFace , fs = require('fs') , packageJson = require("../package.json") , FORMATS = ['image/png', 'image/jpeg']; @@ -76,28 +75,13 @@ exports.JPEGStream = JPEGStream; exports.Image = Image; exports.ImageData = canvas.ImageData; -if (FontFace) { - var Font = function Font(name, path, idx) { - this.name = name; - this._faces = {}; - - this.addFace(path, 'normal', 'normal', idx); - }; - - Font.prototype.addFace = function(path, weight, style, idx) { - style = style || 'normal'; - weight = weight || 'normal'; - - var face = new FontFace(path, idx || 0); - this._faces[weight + '-' + style] = face; - }; - - Font.prototype.getFace = function(weightStyle) { - return this._faces[weightStyle] || this._faces['normal-normal']; - }; +/** + * Resolve paths for registerFont + */ - exports.Font = Font; -} +Canvas.registerFont = function(src, fontFace){ + return Canvas._registerFont(fs.realpathSync(src), fontFace); +}; /** * Context2d implementation. diff --git a/lib/context2d.js b/lib/context2d.js index 80d5e89e7..3ecb37922 100644 --- a/lib/context2d.js +++ b/lib/context2d.js @@ -77,7 +77,9 @@ var parseFont = exports.parseFont = function(str){ font.style = captures[2] || 'normal'; font.size = parseFloat(captures[3]); font.unit = captures[4]; - font.family = captures[5].replace(/["']/g, '').split(',')[0].trim(); + font.family = captures[5].replace(/["']/g, '').split(',').map(function (family) { + return family.trim(); + }).join(','); // TODO: dpi // TODO: remaining unit conversion @@ -235,19 +237,6 @@ Context2d.prototype.__defineGetter__('strokeStyle', function(){ return this.lastStrokeStyle || this.strokeColor; }); -/** - * Register `font` for usage. - * - * @param {Font} font - * @api public - */ - -Context2d.prototype.addFont = function(font) { - this._fonts = this._fonts || {}; - if (this._fonts[font.name]) return; - this._fonts[font.name] = font; -}; - /** * Set font. * @@ -261,22 +250,12 @@ Context2d.prototype.__defineSetter__('font', function(val){ var font; if (font = parseFont(val)) { this.lastFontString = val; - - var fonts = this._fonts; - if (fonts && fonts[font.family]) { - var fontObj = fonts[font.family]; - var type = font.weight + '-' + font.style; - - var fontFace = fontObj.getFace(type); - this._setFontFace(fontFace, font.size); - } else { - this._setFont( - font.weight - , font.style - , font.size - , font.unit - , font.family); - } + this._setFont( + font.weight + , font.style + , font.size + , font.unit + , font.family); } } }); diff --git a/src/Canvas.cc b/src/Canvas.cc index ce62053b3..c7415e246 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -4,22 +4,30 @@ // Copyright (c) 2010 LearnBoost // -#include "Canvas.h" -#include "PNG.h" -#include "CanvasRenderingContext2d.h" #include #include #include #include #include +#include #include #include + +#include "Canvas.h" +#include "PNG.h" +#include "CanvasRenderingContext2d.h" #include "closure.h" +#include "register_font.h" #ifdef HAVE_JPEG #include "JPEGStream.h" #endif +#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)." + Nan::Persistent Canvas::constructor; /* @@ -57,6 +65,9 @@ Canvas::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { 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::Set(target, Nan::New("Canvas").ToLocalChecked(), ctor->GetFunction()); } @@ -564,6 +575,77 @@ NAN_METHOD(Canvas::StreamJPEGSync) { #endif +char * +str_value(Local val, const char *fallback, bool can_be_number) { + if (val->IsString() || (can_be_number && val->IsNumber())) { + return g_strdup(*String::Utf8Value(val)); + } else if (fallback) { + return g_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); + } + + String::Utf8Value 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 = info[1]->ToObject(); + 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(js_user_desc->Get(family_prop), NULL, false); + char *weight = str_value(js_user_desc->Get(weight_prop), "normal", true); + char *style = str_value(js_user_desc->Get(style_prop), "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); + + std::vector::iterator it = _font_face_list.begin(); + FontFace *already_registered = NULL; + + for (; it != _font_face_list.end() && !already_registered; ++it) { + if (pango_font_description_equal(it->sys_desc, sys_desc)) { + already_registered = &(*it); + } + } + + if (already_registered) { + pango_font_description_free(already_registered->user_desc); + already_registered->user_desc = user_desc; + } else if (register_font((unsigned char *) *filePath)) { + FontFace face; + face.user_desc = user_desc; + face.sys_desc = sys_desc; + _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); + } + + g_free(family); + g_free(weight); + g_free(style); +} + /* * Initialize cairo surface. */ @@ -615,6 +697,113 @@ Canvas::~Canvas() { } } +std::vector +_init_font_face_list() { + std::vector x; + return x; +} + +std::vector Canvas::_font_face_list = _init_font_face_list(); + +/* + * 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) { + FontFace best; + PangoFontDescription *ret = NULL; + + // 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 + char **families = g_strsplit(pango_font_description_get_family(desc), ",", -1); + GString *resolved_families = g_string_new(""); + + for (int i = 0; families[i]; ++i) { + GString *renamed_families = g_string_new(""); + std::vector::iterator it = _font_face_list.begin(); + + for (; it != _font_face_list.end(); ++it) { + if (g_ascii_strcasecmp(families[i], pango_font_description_get_family(it->user_desc)) == 0) { + if (renamed_families->len) g_string_append(renamed_families, ","); + g_string_append(renamed_families, pango_font_description_get_family(it->sys_desc)); + + if (i == 0 && (best.user_desc == NULL || pango_font_description_better_match(desc, best.user_desc, it->user_desc))) { + best = *it; + } + } + } + + if (resolved_families->len) g_string_append(resolved_families, ","); + g_string_append(resolved_families, renamed_families->len ? renamed_families->str : families[i]); + g_string_free(renamed_families, true); + } + + ret = pango_font_description_copy(best.sys_desc ? best.sys_desc : desc); + pango_font_description_set_family_static(ret, resolved_families->str); + + g_strfreev(families); + g_string_free(resolved_families, false); + + return ret; +} + /* * Re-alloc the surface, destroying the previous. */ diff --git a/src/Canvas.h b/src/Canvas.h index d0ea25150..9411a1863 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -8,21 +8,18 @@ #ifndef __NODE_CANVAS_H__ #define __NODE_CANVAS_H__ -#include #include +#include #include #include - -#if HAVE_PANGO #include -#else +#include #include -#endif - #include -using namespace v8; + using namespace node; +using namespace v8; /* * Maxmimum states per context. @@ -43,6 +40,16 @@ typedef enum { CANVAS_TYPE_SVG } canvas_type_t; +/* + * FontFace describes a font file in terms of one PangoFontDescription that + * will resolve to it and one that the user describes it as (like @font-face) + */ +class FontFace { + public: + PangoFontDescription *sys_desc = NULL; + PangoFontDescription *user_desc = NULL; +}; + /* * Canvas. */ @@ -65,6 +72,7 @@ class Canvas: public Nan::ObjectWrap { static NAN_METHOD(StreamPNGSync); static NAN_METHOD(StreamPDFSync); static NAN_METHOD(StreamJPEGSync); + static NAN_METHOD(RegisterFont); static Local Error(cairo_status_t status); #if NODE_VERSION_AT_LEAST(0, 6, 0) static void ToBufferAsync(uv_work_t *req); @@ -79,6 +87,9 @@ class Canvas: public Nan::ObjectWrap { EIO_ToBuffer(eio_req *req); static int EIO_AfterToBuffer(eio_req *req); #endif + static PangoWeight GetWeightFromCSSString(const char *weight); + static PangoStyle GetStyleFromCSSString(const char *style); + static PangoFontDescription *ResolveFontDescription(const PangoFontDescription *desc); inline bool isPDF(){ return CANVAS_TYPE_PDF == type; } inline bool isSVG(){ return CANVAS_TYPE_SVG == type; } @@ -94,6 +105,7 @@ class Canvas: public Nan::ObjectWrap { ~Canvas(); cairo_surface_t *_surface; void *_closure; + static std::vector _font_face_list; }; #endif diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 66064ea9f..c570541ed 100755 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -6,11 +6,11 @@ // #include -#include #include #include #include #include + #include "Canvas.h" #include "Point.h" #include "Image.h" @@ -19,10 +19,6 @@ #include "CanvasGradient.h" #include "CanvasPattern.h" -#ifdef HAVE_FREETYPE -#include "FontFace.h" -#endif - // Windows doesn't support the C99 names for these #ifdef _MSC_VER #define isnan(x) _isnan(x) @@ -63,18 +59,6 @@ enum { , TEXT_BASELINE_HANGING }; -#if HAVE_PANGO - -/* - * State helper function - */ - -void state_assign_fontFamily(canvas_state_t *state, const char *str) { - free(state->fontFamily); - state->fontFamily = strndup(str, 100); -} - - /* * Simple helper macro for a rather verbose function call. */ @@ -84,8 +68,6 @@ void state_assign_fontFamily(canvas_state_t *state, const char *str) { pango_layout_get_font_description(LAYOUT), \ pango_context_get_language(pango_layout_get_context(LAYOUT))) -#endif - /* * Initialize Context2d. */ @@ -135,9 +117,6 @@ Context2d::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { Nan::SetPrototypeMethod(ctor, "setLineDash", SetLineDash); Nan::SetPrototypeMethod(ctor, "getLineDash", GetLineDash); Nan::SetPrototypeMethod(ctor, "_setFont", SetFont); -#ifdef HAVE_FREETYPE - Nan::SetPrototypeMethod(ctor, "_setFontFace", SetFontFace); -#endif Nan::SetPrototypeMethod(ctor, "_setFillColor", SetFillColor); Nan::SetPrototypeMethod(ctor, "_setStrokeColor", SetStrokeColor); Nan::SetPrototypeMethod(ctor, "_setFillPattern", SetFillPattern); @@ -171,9 +150,7 @@ Context2d::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { Context2d::Context2d(Canvas *canvas) { _canvas = canvas; _context = cairo_create(canvas->surface()); -#if HAVE_PANGO _layout = pango_cairo_create_layout(_context); -#endif cairo_set_line_width(_context, 1); state = states[stateno = 0] = (canvas_state_t *) malloc(sizeof(canvas_state_t)); state->shadowBlur = 0; @@ -190,14 +167,9 @@ Context2d::Context2d(Canvas *canvas) { state->shadow = transparent_black; state->patternQuality = CAIRO_FILTER_GOOD; state->textDrawingMode = TEXT_DRAW_PATHS; -#if HAVE_PANGO - state->fontWeight = PANGO_WEIGHT_NORMAL; - state->fontStyle = PANGO_STYLE_NORMAL; - state->fontSize = 10; - state->fontFamily = NULL; - state_assign_fontFamily(state, "sans serif"); - setFontFromState(); -#endif + state->fontDescription = pango_font_description_from_string("sans serif"); + pango_font_description_set_absolute_size(state->fontDescription, 10 * PANGO_SCALE); + pango_layout_set_font_description(_layout, state->fontDescription); } /* @@ -206,14 +178,10 @@ Context2d::Context2d(Canvas *canvas) { Context2d::~Context2d() { while(stateno >= 0) { -#if HAVE_PANGO - free(states[stateno]->fontFamily); -#endif + pango_font_description_free(states[stateno]->fontDescription); free(states[stateno--]); } -#if HAVE_PANGO g_object_unref(_layout); -#endif cairo_destroy(_context); } @@ -246,9 +214,7 @@ Context2d::saveState() { if (stateno == CANVAS_MAX_STATES) return; states[++stateno] = (canvas_state_t *) malloc(sizeof(canvas_state_t)); memcpy(states[stateno], state, sizeof(canvas_state_t)); -#if HAVE_PANGO - states[stateno]->fontFamily = strndup(state->fontFamily, 100); -#endif + states[stateno]->fontDescription = pango_font_description_copy(states[stateno-1]->fontDescription); state = states[stateno]; } @@ -259,16 +225,11 @@ Context2d::saveState() { void Context2d::restoreState() { if (0 == stateno) return; - // Olaf (2011-02-21): Free old state data -#if HAVE_PANGO - free(states[stateno]->fontFamily); -#endif + pango_font_description_free(states[stateno]->fontDescription); free(states[stateno]); states[stateno] = NULL; state = states[--stateno]; -#if HAVE_PANGO - setFontFromState(); -#endif + pango_layout_set_font_description(_layout, state->fontDescription); } /* @@ -1770,8 +1731,6 @@ NAN_METHOD(Context2d::StrokeText) { void Context2d::setTextPath(const char *str, double x, double y) { -#if HAVE_PANGO - PangoRectangle ink_rect, logical_rect; PangoFontMetrics *metrics = NULL; @@ -1814,59 +1773,6 @@ Context2d::setTextPath(const char *str, double x, double y) { } else if (state->textDrawingMode == TEXT_DRAW_GLYPHS) { pango_cairo_show_layout(_context, _layout); } - -#else - - cairo_text_extents_t te; - cairo_font_extents_t fe; - - // Alignment - switch (state->textAlignment) { - // center - case 0: - // Olaf (2011-02-19): te.x_bearing does not concern the alignment - cairo_text_extents(_context, str, &te); - x -= te.width / 2; - break; - // right - case 1: - // Olaf (2011-02-19): te.x_bearing does not concern the alignment - cairo_text_extents(_context, str, &te); - x -= te.width; - break; - } - - // Baseline approx - switch (state->textBaseline) { - case TEXT_BASELINE_TOP: - case TEXT_BASELINE_HANGING: - // Olaf (2011-02-26): fe.ascent approximates the distance between - // the top of the em square and the alphabetic baseline - cairo_font_extents(_context, &fe); - y += fe.ascent; - break; - case TEXT_BASELINE_MIDDLE: - // Olaf (2011-02-26): fe.ascent approximates the distance between - // the top of the em square and the alphabetic baseline - cairo_font_extents(_context, &fe); - y += (fe.ascent - fe.descent)/2; - break; - case TEXT_BASELINE_BOTTOM: - // Olaf (2011-02-26): we need to know the distance between the alphabetic - // baseline and the bottom of the em square - cairo_font_extents(_context, &fe); - y -= fe.descent; - break; - } - - cairo_move_to(_context, x, y); - if (state->textDrawingMode == TEXT_DRAW_PATHS) { - cairo_text_path(_context, str); - } else if (state->textDrawingMode == TEXT_DRAW_GLYPHS) { - cairo_show_text(_context, str); - } - -#endif } /* @@ -1901,35 +1807,6 @@ NAN_METHOD(Context2d::MoveTo) { , info[1]->NumberValue()); } -/* - * Set font face. - */ - -#ifdef HAVE_FREETYPE -NAN_METHOD(Context2d::SetFontFace) { - // Ignore invalid args - if (!info[0]->IsObject() - || !info[1]->IsNumber()) - return Nan::ThrowTypeError("Expected object and number"); - - Local obj = info[0]->ToObject(); - - if (!Nan::New(FontFace::constructor)->HasInstance(obj)) - return Nan::ThrowTypeError("FontFace expected"); - - FontFace *face = Nan::ObjectWrap::Unwrap(obj); - double size = info[1]->NumberValue(); - - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); - - cairo_set_font_size(ctx, size); - cairo_set_font_face(ctx, face->cairoFace()); - - return; -} -#endif - /* * Set font: * - weight @@ -1955,95 +1832,24 @@ NAN_METHOD(Context2d::SetFont) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); -#if HAVE_PANGO - - if (strlen(*family) > 0) state_assign_fontFamily(context->state, *family); - - if (size > 0) context->state->fontSize = size; - - 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; - } - } - context->state->fontStyle = s; - - PangoWeight w = PANGO_WEIGHT_NORMAL; - if (strlen(*weight) > 0) { - if (0 == strcmp("bold", *weight)) { - w = PANGO_WEIGHT_BOLD; - } 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; - } - } - context->state->fontWeight = w; - - context->setFontFromState(); - -#else - - cairo_t *ctx = context->context(); - - // Size - cairo_set_font_size(ctx, size); - - // Style - cairo_font_slant_t s = CAIRO_FONT_SLANT_NORMAL; - if (0 == strcmp("italic", *style)) { - s = CAIRO_FONT_SLANT_ITALIC; - } else if (0 == strcmp("oblique", *style)) { - s = CAIRO_FONT_SLANT_OBLIQUE; - } - - // Weight - cairo_font_weight_t w = CAIRO_FONT_WEIGHT_NORMAL; - if (0 == strcmp("bold", *weight)) { - w = CAIRO_FONT_WEIGHT_BOLD; - } - - cairo_select_font_face(ctx, *family, s, w); + PangoFontDescription *desc = pango_font_description_copy(context->state->fontDescription); + pango_font_description_free(context->state->fontDescription); -#endif -} + pango_font_description_set_style(desc, Canvas::GetStyleFromCSSString(*style)); + pango_font_description_set_weight(desc, Canvas::GetWeightFromCSSString(*weight)); -#if HAVE_PANGO + if (strlen(*family) > 0) pango_font_description_set_family(desc, *family); -/* - * Sets PangoLayout options from the current font state - */ + PangoFontDescription *sys_desc = Canvas::ResolveFontDescription(desc); + pango_font_description_free(desc); -void -Context2d::setFontFromState() { - PangoFontDescription *fd = pango_font_description_new(); + if (size > 0) pango_font_description_set_absolute_size(sys_desc, size * PANGO_SCALE); - pango_font_description_set_family(fd, state->fontFamily); - pango_font_description_set_absolute_size(fd, state->fontSize * PANGO_SCALE); - pango_font_description_set_style(fd, state->fontStyle); - pango_font_description_set_weight(fd, state->fontWeight); + context->state->fontDescription = sys_desc; - pango_layout_set_font_description(_layout, fd); - pango_font_description_free(fd); + pango_layout_set_font_description(context->_layout, sys_desc); } -#endif - /* * Return the given text extents. * TODO: Support for: @@ -2058,8 +1864,6 @@ NAN_METHOD(Context2d::MeasureText) { String::Utf8Value str(info[0]->ToString()); Local obj = Nan::New(); -#if HAVE_PANGO - PangoRectangle ink_rect, logical_rect; PangoFontMetrics *metrics; PangoLayout *layout = context->layout(); @@ -2117,61 +1921,6 @@ NAN_METHOD(Context2d::MeasureText) { pango_font_metrics_unref(metrics); -#else - - cairo_text_extents_t te; - cairo_font_extents_t fe; - - cairo_text_extents(ctx, *str, &te); - cairo_font_extents(ctx, &fe); - - double x_offset; - switch (context->state->textAlignment) { - case 0: // center - x_offset = te.width / 2; - break; - case 1: // right - x_offset = te.width; - break; - default: // left - x_offset = 0.0; - } - - double y_offset; - switch (context->state->textBaseline) { - case TEXT_BASELINE_TOP: - case TEXT_BASELINE_HANGING: - y_offset = fe.ascent; - break; - case TEXT_BASELINE_MIDDLE: - y_offset = (fe.ascent - fe.descent)/2; - break; - case TEXT_BASELINE_BOTTOM: - y_offset = -fe.descent; - break; - default: - y_offset = 0.0; - } - - obj->Set(Nan::New("width").ToLocalChecked(), - Nan::New(te.x_advance)); - obj->Set(Nan::New("actualBoundingBoxLeft").ToLocalChecked(), - Nan::New(x_offset - te.x_bearing)); - obj->Set(Nan::New("actualBoundingBoxRight").ToLocalChecked(), - Nan::New((te.x_bearing + te.width) - x_offset)); - obj->Set(Nan::New("actualBoundingBoxAscent").ToLocalChecked(), - Nan::New(-(te.y_bearing + y_offset))); - obj->Set(Nan::New("actualBoundingBoxDescent").ToLocalChecked(), - Nan::New(te.height + te.y_bearing + y_offset)); - obj->Set(Nan::New("emHeightAscent").ToLocalChecked(), - Nan::New(fe.ascent - y_offset)); - obj->Set(Nan::New("emHeightDescent").ToLocalChecked(), - Nan::New(fe.descent + y_offset)); - obj->Set(Nan::New("alphabeticBaseline").ToLocalChecked(), - Nan::New(y_offset)); - -#endif - info.GetReturnValue().Set(obj); } diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index 7d0b1394b..fccb5d184 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -8,17 +8,13 @@ #ifndef __NODE_CONTEXT2D_H__ #define __NODE_CONTEXT2D_H__ +#include +#include + #include "color.h" #include "Canvas.h" #include "CanvasGradient.h" -#ifdef HAVE_FREETYPE -#include -#include -#include FT_FREETYPE_H -#endif - -#include using namespace std; typedef enum { @@ -49,19 +45,10 @@ typedef struct { double shadowOffsetX; double shadowOffsetY; canvas_draw_mode_t textDrawingMode; - -#if HAVE_PANGO - PangoWeight fontWeight; - PangoStyle fontStyle; - double fontSize; - char *fontFamily; -#endif - + PangoFontDescription *fontDescription; } canvas_state_t; -#if HAVE_PANGO void state_assign_fontFamily(canvas_state_t *state, const char *str); -#endif class Context2d: public Nan::ObjectWrap { public: @@ -91,9 +78,6 @@ class Context2d: public Nan::ObjectWrap { static NAN_METHOD(FillText); static NAN_METHOD(StrokeText); static NAN_METHOD(SetFont); -#ifdef HAVE_FREETYPE - static NAN_METHOD(SetFontFace); -#endif static NAN_METHOD(SetFillColor); static NAN_METHOD(SetStrokeColor); static NAN_METHOD(SetFillPattern); @@ -166,20 +150,15 @@ class Context2d: public Nan::ObjectWrap { void stroke(bool preserve = false); void save(); void restore(); - -#if HAVE_PANGO void setFontFromState(); inline PangoLayout *layout(){ return _layout; } -#endif private: ~Context2d(); Canvas *_canvas; cairo_t *_context; cairo_path_t *_path; -#if HAVE_PANGO PangoLayout *_layout; -#endif }; #endif diff --git a/src/FontFace.cc b/src/FontFace.cc deleted file mode 100755 index b2409a4ad..000000000 --- a/src/FontFace.cc +++ /dev/null @@ -1,113 +0,0 @@ -// -// FontFace.cc -// -// Copyright (c) 2012 Julian Viereck -// - -#include "FontFace.h" - -#include - -Nan::Persistent FontFace::constructor; - -/* - * Destroy ft_face. - */ - -FontFace::~FontFace() { - // Decrement extra reference count added in ::New(...). - // Once there is no reference left to crFace, cairo will release the - // free type font face as well. - cairo_font_face_destroy(_crFace); -} - -/* - * Initialize FontFace. - */ - -void -FontFace::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { - Nan::HandleScope scope; - - // Constructor - Local ctor = Nan::New(FontFace::New); - constructor.Reset(ctor); - ctor->InstanceTemplate()->SetInternalFieldCount(1); - ctor->SetClassName(Nan::New("FontFace").ToLocalChecked()); - - // Prototype - Nan::Set(target, Nan::New("FontFace").ToLocalChecked(), ctor->GetFunction()); -} - -/* - * Initialize a new FontFace object. - */ - -FT_Library library; /* handle to library */ - -bool FontFace::_initLibrary = true; -static cairo_user_data_key_t key; - -/* - * Initialize a new FontFace. - */ - -NAN_METHOD(FontFace::New) { - if (!info.IsConstructCall()) { - return Nan::ThrowTypeError("Class constructors cannot be invoked without 'new'"); - } - - if (!info[0]->IsString() - || !info[1]->IsNumber()) { - return Nan::ThrowError("Wrong argument types passed to FontFace constructor"); - } - - String::Utf8Value filePath(info[0]); - int faceIdx = int(info[1]->NumberValue()); - - FT_Face ftFace; - FT_Error ftError; - cairo_font_face_t *crFace; - - if (_initLibrary) { - _initLibrary = false; - ftError = FT_Init_FreeType(&library); - if (ftError) { - return Nan::ThrowError("Could not load library"); - } - } - - // Create new freetype font face. - ftError = FT_New_Face(library, *filePath, faceIdx, &ftFace); - if (ftError) { - return Nan::ThrowError("Could not load font file"); - } - - #if HAVE_PANGO - // Load the font file in fontconfig - FcBool ok = FcConfigAppFontAddFile(FcConfigGetCurrent(), (FcChar8 *)(*filePath)); - if (!ok) { - return Nan::ThrowError("Could not load font in FontConfig"); - } - #endif - - // Create new cairo font face. - crFace = cairo_ft_font_face_create_for_ft_face(ftFace, 0); - - // If the cairo font face is released, release the FreeType font face as well. - int status = cairo_font_face_set_user_data (crFace, &key, - ftFace, (cairo_destroy_func_t) FT_Done_Face); - if (status) { - cairo_font_face_destroy (crFace); - FT_Done_Face (ftFace); - return Nan::ThrowError("Failed to setup cairo font face user data"); - } - - // Explicit reference count the cairo font face. Otherwise the font face might - // get released by cairo although the JS font face object is still alive. - cairo_font_face_reference(crFace); - - FontFace *face = new FontFace(crFace); - face->Wrap(info.This()); - info.GetReturnValue().Set(info.This()); -} diff --git a/src/FontFace.h b/src/FontFace.h deleted file mode 100644 index 229461549..000000000 --- a/src/FontFace.h +++ /dev/null @@ -1,32 +0,0 @@ -// -// FontFace.h -// -// Copyright (c) 2012 Julian Viereck -// - -#ifndef __NODE_TRUE_TYPE_FONT_FACE_H__ -#define __NODE_TRUE_TYPE_FONT_FACE_H__ - -#include "Canvas.h" - -#include -#include -#include FT_FREETYPE_H - -class FontFace: public Nan::ObjectWrap { - public: - static Nan::Persistent constructor; - static void Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target); - static NAN_METHOD(New); - FontFace(cairo_font_face_t *crFace) - :_crFace(crFace) {} - - inline cairo_font_face_t *cairoFace(){ return _crFace; } - private: - ~FontFace(); - cairo_font_face_t *_crFace; - static bool _initLibrary; -}; - -#endif - diff --git a/src/ImageData.h b/src/ImageData.h index db4e42a4c..074d1ff8a 100644 --- a/src/ImageData.h +++ b/src/ImageData.h @@ -10,7 +10,7 @@ #include "Canvas.h" #include -#include "v8.h" +#include class ImageData: public Nan::ObjectWrap { public: diff --git a/src/init.cc b/src/init.cc index c48e16c2b..dc2fa5963 100755 --- a/src/init.cc +++ b/src/init.cc @@ -6,17 +6,16 @@ // #include +#include +#include #include "Canvas.h" #include "Image.h" #include "ImageData.h" #include "CanvasGradient.h" #include "CanvasPattern.h" #include "CanvasRenderingContext2d.h" - -#ifdef HAVE_FREETYPE -#include "FontFace.h" +#include #include FT_FREETYPE_H -#endif // Compatibility with Visual Studio versions prior to VS2015 #if defined(_MSC_VER) && _MSC_VER < 1900 @@ -30,9 +29,6 @@ NAN_MODULE_INIT(init) { Context2d::Initialize(target); Gradient::Initialize(target); Pattern::Initialize(target); -#ifdef HAVE_FREETYPE - FontFace::Initialize(target); -#endif target->Set(Nan::New("cairoVersion").ToLocalChecked(), Nan::New(cairo_version_string()).ToLocalChecked()); #ifdef HAVE_JPEG @@ -72,11 +68,9 @@ NAN_MODULE_INIT(init) { #endif #endif -#ifdef HAVE_FREETYPE char freetype_version[10]; snprintf(freetype_version, 10, "%d.%d.%d", FREETYPE_MAJOR, FREETYPE_MINOR, FREETYPE_PATCH); target->Set(Nan::New("freetypeVersion").ToLocalChecked(), Nan::New(freetype_version).ToLocalChecked()); -#endif } NODE_MODULE(canvas,init); diff --git a/src/register_font.cc b/src/register_font.cc new file mode 100644 index 000000000..75fc87cc8 --- /dev/null +++ b/src/register_font.cc @@ -0,0 +1,251 @@ +#include +#include +#include + +#ifdef __APPLE__ +#include +#elif defined(_WIN32) +#include +#else +#include +#endif + +#include +#include FT_FREETYPE_H +#include FT_TRUETYPE_TABLES_H +#include FT_SFNT_NAMES_H +#include FT_TRUETYPE_IDS_H +#ifndef FT_SFNT_OS2 +#define FT_SFNT_OS2 ft_sfnt_os2 +#endif + +// OSX seems to read the strings in MacRoman encoding and ignore Unicode entries. +// You can verify this by opening a TTF with both Unicode and Macroman on OSX. +// It uses the MacRoman name, while Fontconfig and Windows use Unicode +#ifdef __APPLE__ +#define PREFERRED_PLATFORM_ID TT_PLATFORM_MACINTOSH +#define PREFERRED_ENCODING_ID TT_MAC_ID_ROMAN +#else +#define PREFERRED_PLATFORM_ID TT_PLATFORM_MICROSOFT +#define PREFERRED_ENCODING_ID TT_MS_ID_UNICODE_CS +#endif + +#define IS_PREFERRED_ENC(X) \ + X.platform_id == PREFERRED_PLATFORM_ID && X.encoding_id == PREFERRED_ENCODING_ID + +#define GET_NAME_RANK(X) \ + (IS_PREFERRED_ENC(X) ? 1 : 0) + (X.name_id == TT_NAME_ID_PREFERRED_FAMILY ? 1 : 0) + +/* + * Return a UTF-8 encoded string given a TrueType name buf+len + * and its platform and encoding + */ + +char * +to_utf8(FT_Byte* buf, FT_UInt len, FT_UShort pid, FT_UShort eid) { + size_t ret_len = len * 4; // max chars in a utf8 string + char *ret = (char*)malloc(ret_len + 1); // utf8 string + null + + if (!ret) return NULL; + + // In my testing of hundreds of fonts from the Google Font repo, the two types + // of fonts are TT_PLATFORM_MICROSOFT with TT_MS_ID_UNICODE_CS encoding, or + // TT_PLATFORM_MACINTOSH with TT_MAC_ID_ROMAN encoding. Usually both, never neither + + char const *fromcode; + + if (pid == TT_PLATFORM_MACINTOSH && eid == TT_MAC_ID_ROMAN) { + fromcode = "MAC"; + } else if (pid == TT_PLATFORM_MICROSOFT && eid == TT_MS_ID_UNICODE_CS) { + fromcode = "UTF-16BE"; + } else { + free(ret); + return NULL; + } + + GIConv cd = g_iconv_open("UTF-8", fromcode); + + if (cd == (GIConv)-1) { + free(ret); + return NULL; + } + + size_t inbytesleft = len; + size_t outbytesleft = ret_len; + + size_t n_converted = g_iconv(cd, (char**)&buf, &inbytesleft, &ret, &outbytesleft); + + ret -= ret_len - outbytesleft; // rewind the pointers to their + buf -= len - inbytesleft; // original starting positions + + if (n_converted == (size_t)-1) { + free(ret); + return NULL; + } else { + ret[ret_len - outbytesleft] = '\0'; + return ret; + } +} + +/* + * Find a family name in the face's name table, preferring the one the + * system, fall back to the other + */ + +typedef struct _NameDef { + const char *buf; + int rank; // the higher the more desirable +} NameDef; + +gint +_name_def_compare(gconstpointer a, gconstpointer b) { + return ((NameDef*)a)->rank > ((NameDef*)b)->rank ? -1 : 1; +} + +// Some versions of GTK+ do not have this, particualrly the one we +// currently link to in node-canvas's wiki +void +_free_g_list_item(gpointer data, gpointer user_data) { + NameDef *d = (NameDef *)data; + free((void *)(d->buf)); +} + +void +_g_list_free_full(GList *list) { + g_list_foreach(list, _free_g_list_item, NULL); + g_list_free(list); +} + +char * +get_family_name(FT_Face face) { + FT_SfntName name; + GList *list = NULL; + char *utf8name = NULL; + + for (unsigned i = 0; i < FT_Get_Sfnt_Name_Count(face); ++i) { + FT_Get_Sfnt_Name(face, i, &name); + + if (name.name_id == TT_NAME_ID_FONT_FAMILY || name.name_id == TT_NAME_ID_PREFERRED_FAMILY) { + char *buf = to_utf8(name.string, name.string_len, name.platform_id, name.encoding_id); + + if (buf) { + NameDef *d = (NameDef*)malloc(sizeof(NameDef)); + d->buf = (const char*)buf; + d->rank = GET_NAME_RANK(name); + + list = g_list_insert_sorted(list, (gpointer)d, _name_def_compare); + } + } + } + + GList *best_def = g_list_first(list); + if (best_def) utf8name = (char*) strdup(((NameDef*)best_def->data)->buf); + if (list) _g_list_free_full(list); + + return utf8name; +} + +PangoWeight +get_pango_weight(FT_UShort weight) { + switch (weight) { + case 100: return PANGO_WEIGHT_THIN; + case 200: return PANGO_WEIGHT_ULTRALIGHT; + case 300: return PANGO_WEIGHT_LIGHT; + #if PANGO_VERSION >= PANGO_VERSION_ENCODE(1, 36, 7) + case 350: return PANGO_WEIGHT_SEMILIGHT; + #endif + case 380: return PANGO_WEIGHT_BOOK; + case 400: return PANGO_WEIGHT_NORMAL; + case 500: return PANGO_WEIGHT_MEDIUM; + case 600: return PANGO_WEIGHT_SEMIBOLD; + case 700: return PANGO_WEIGHT_BOLD; + case 800: return PANGO_WEIGHT_ULTRABOLD; + case 900: return PANGO_WEIGHT_HEAVY; + case 1000: return PANGO_WEIGHT_ULTRAHEAVY; + default: return PANGO_WEIGHT_NORMAL; + } +} + +PangoStretch +get_pango_stretch(FT_UShort width) { + switch (width) { + case 1: return PANGO_STRETCH_ULTRA_CONDENSED; + case 2: return PANGO_STRETCH_EXTRA_CONDENSED; + case 3: return PANGO_STRETCH_CONDENSED; + case 4: return PANGO_STRETCH_SEMI_CONDENSED; + case 5: return PANGO_STRETCH_NORMAL; + case 6: return PANGO_STRETCH_SEMI_EXPANDED; + case 7: return PANGO_STRETCH_EXPANDED; + case 8: return PANGO_STRETCH_EXTRA_EXPANDED; + case 9: return PANGO_STRETCH_ULTRA_EXPANDED; + default: return PANGO_STRETCH_NORMAL; + } +} + +PangoStyle +get_pango_style(FT_Long flags) { + if (flags & FT_STYLE_FLAG_ITALIC) { + return PANGO_STYLE_ITALIC; + } else { + return PANGO_STYLE_NORMAL; + } +} + +/* + * Return a PangoFontDescription that will resolve to the font file + */ + +PangoFontDescription * +get_pango_font_description(unsigned char* filepath) { + FT_Library library; + FT_Face face; + PangoFontDescription *desc = pango_font_description_new(); + + if (!FT_Init_FreeType(&library) && !FT_New_Face(library, (const char*)filepath, 0, &face)) { + TT_OS2 *table = (TT_OS2*)FT_Get_Sfnt_Table(face, FT_SFNT_OS2); + if (table) { + char *family = get_family_name(face); + + if (family) pango_font_description_set_family_static(desc, family); + pango_font_description_set_weight(desc, get_pango_weight(table->usWeightClass)); + pango_font_description_set_stretch(desc, get_pango_stretch(table->usWidthClass)); + pango_font_description_set_style(desc, get_pango_style(face->style_flags)); + + FT_Done_Face(face); + + return desc; + } + } + + pango_font_description_free(desc); + + return NULL; +} + +/* + * Register font with the OS + */ + +bool +register_font(unsigned char *filepath) { + bool success; + + #ifdef __APPLE__ + CFURLRef filepathUrl = CFURLCreateFromFileSystemRepresentation(NULL, filepath, strlen((char*)filepath), false); + success = CTFontManagerRegisterFontsForURL(filepathUrl, kCTFontManagerScopeProcess, NULL); + #elif defined(_WIN32) + success = AddFontResourceEx((LPCSTR)filepath, FR_PRIVATE, 0) != 0; + #else + success = FcConfigAppFontAddFile(FcConfigGetCurrent(), (FcChar8 *)(filepath)); + #endif + + if (!success) return false; + + // Tell Pango to throw away the current FontMap and create a new one. This + // has the effect of registering the new font in Pango by re-looking up all + // font families. + pango_cairo_font_map_set_default(NULL); + + return true; +} + diff --git a/src/register_font.h b/src/register_font.h new file mode 100644 index 000000000..33d006bab --- /dev/null +++ b/src/register_font.h @@ -0,0 +1,5 @@ +#include + +PangoFontDescription *get_pango_font_description(unsigned char *filepath); +bool register_font(unsigned char *filepath); + diff --git a/test/canvas.test.js b/test/canvas.test.js index 1dbd2040c..b6c678df8 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -44,15 +44,15 @@ describe('Canvas', function () { , '20px monospace' , { size: 20, unit: 'px', family: 'monospace' } , '50px Arial, sans-serif' - , { size: 50, unit: 'px', family: 'Arial' } + , { size: 50, unit: 'px', family: 'Arial,sans-serif' } , 'bold italic 50px Arial, sans-serif' - , { style: 'italic', weight: 'bold', size: 50, unit: 'px', family: 'Arial' } + , { style: 'italic', weight: 'bold', size: 50, unit: 'px', family: 'Arial,sans-serif' } , '50px Helvetica , Arial, sans-serif' - , { size: 50, unit: 'px', family: 'Helvetica' } + , { size: 50, unit: 'px', family: 'Helvetica,Arial,sans-serif' } , '50px "Helvetica Neue", sans-serif' - , { size: 50, unit: 'px', family: 'Helvetica Neue' } + , { size: 50, unit: 'px', family: 'Helvetica Neue,sans-serif' } , '50px "Helvetica Neue", "foo bar baz" , sans-serif' - , { size: 50, unit: 'px', family: 'Helvetica Neue' } + , { size: 50, unit: 'px', family: 'Helvetica Neue,foo bar baz,sans-serif' } , "50px 'Helvetica Neue'" , { size: 50, unit: 'px', family: 'Helvetica Neue' } , 'italic 20px Arial'