From 7716ae4a13136d9dd3d8a282084a382f104d6549 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Thu, 11 Feb 2016 13:32:10 -0500 Subject: [PATCH 01/14] delete the non-Pango build. depend on Pango * this commit gets rid of the HAVE_PANGO pragma and HAVE_FREETYPE, preferring the Pango sections of HAVE_PANGO. using Cairo methods for fonts is discouraged (http://cairographics.org/FAQ/#using_pango), and freetype was only ever needed for cairo font rendering, so that code was running in the Pango build for no reason at all. * it also gets rid of context2d's `addFont`, which was never compatible with the Pango build. `FontFace` is completely removed since it was never used by the Pango build either, and instead of parsing the fonts with FreeType we will instead prefer to let the OS do that in succeeding commits * the Windows build has been updated to work with Pango, which it did not before TL;DR - Pango is now required and and as of this commit custom fonts are removed --- Readme.md | 6 +- binding.gyp | 76 +++++------- lib/canvas.js | 24 ---- lib/context2d.js | 35 +----- src/Canvas.h | 5 - src/CanvasRenderingContext2d.cc | 199 +------------------------------- src/CanvasRenderingContext2d.h | 23 +--- src/FontFace.cc | 113 ------------------ src/FontFace.h | 32 ----- src/ImageData.h | 2 +- src/init.cc | 14 --- 11 files changed, 40 insertions(+), 489 deletions(-) delete mode 100755 src/FontFace.cc delete mode 100644 src/FontFace.h diff --git a/Readme.md b/Readme.md index e9e2fff7c..a0f3e7af9 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). diff --git a/binding.gyp b/binding.gyp index 66f0115ed..bbb412377 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%': ' #include #include - -#if HAVE_PANGO #include -#else #include -#endif - #include using namespace v8; diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 66064ea9f..933a6760e 100755 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -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,8 +59,6 @@ enum { , TEXT_BASELINE_HANGING }; -#if HAVE_PANGO - /* * State helper function */ @@ -84,8 +78,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 +127,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 +160,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 +177,12 @@ 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 } /* @@ -206,14 +191,10 @@ Context2d::Context2d(Canvas *canvas) { Context2d::~Context2d() { while(stateno >= 0) { -#if HAVE_PANGO free(states[stateno]->fontFamily); -#endif free(states[stateno--]); } -#if HAVE_PANGO g_object_unref(_layout); -#endif cairo_destroy(_context); } @@ -246,9 +227,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]->fontFamily = _strndup(state->fontFamily, 100); state = states[stateno]; } @@ -260,15 +239,11 @@ void Context2d::restoreState() { if (0 == stateno) return; // Olaf (2011-02-21): Free old state data -#if HAVE_PANGO free(states[stateno]->fontFamily); -#endif free(states[stateno]); states[stateno] = NULL; state = states[--stateno]; -#if HAVE_PANGO setFontFromState(); -#endif } /* @@ -1770,8 +1745,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 +1787,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 +1821,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,8 +1846,6 @@ 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; @@ -1996,35 +1885,8 @@ NAN_METHOD(Context2d::SetFont) { 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); - -#endif } -#if HAVE_PANGO - /* * Sets PangoLayout options from the current font state */ @@ -2042,8 +1904,6 @@ Context2d::setFontFromState() { pango_font_description_free(fd); } -#endif - /* * Return the given text extents. * TODO: Support for: @@ -2058,8 +1918,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 +1975,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..1bd803d93 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,14 @@ typedef struct { double shadowOffsetX; double shadowOffsetY; canvas_draw_mode_t textDrawingMode; - -#if HAVE_PANGO PangoWeight fontWeight; PangoStyle fontStyle; double fontSize; char *fontFamily; -#endif } 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 +82,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 +154,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..e4d76771b 100755 --- a/src/init.cc +++ b/src/init.cc @@ -13,11 +13,6 @@ #include "CanvasPattern.h" #include "CanvasRenderingContext2d.h" -#ifdef HAVE_FREETYPE -#include "FontFace.h" -#include FT_FREETYPE_H -#endif - // Compatibility with Visual Studio versions prior to VS2015 #if defined(_MSC_VER) && _MSC_VER < 1900 #define snprintf _snprintf @@ -30,9 +25,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 @@ -71,12 +63,6 @@ NAN_MODULE_INIT(init) { target->Set(Nan::New("gifVersion").ToLocalChecked(), Nan::New(GIF_LIB_VERSION).ToLocalChecked()); #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); From 84a252cdbde76df659204c208af7ddc1fdc18290 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Thu, 11 Feb 2016 14:36:57 -0500 Subject: [PATCH 02/14] implement Canvas::registerFont(ttf) this is implemented differently depending on the operating system. for MacOS, CTFontManagerRegisterFontsForURL is the call to add a temporary font. for Linux we use FontConfig, and for Windows use AddFontResourceEx. all have been tested successfully updated README and examples/font.js --- Readme.md | 18 ++++++++++++++++++ binding.gyp | 1 + examples/font.js | 27 +++++++++++---------------- lib/canvas.js | 7 +++++++ src/Canvas.cc | 7 ++++--- src/Canvas.h | 4 ++-- src/CanvasRenderingContext2d.cc | 26 ++++++++++++++++++++++++-- src/init.cc | 15 +++++++++++++++ src/register_font.cc | 21 +++++++++++++++++++++ src/register_font.h | 2 ++ 10 files changed, 105 insertions(+), 23 deletions(-) create mode 100644 src/register_font.cc create mode 100644 src/register_font.h diff --git a/Readme.md b/Readme.md index a0f3e7af9..2095ea827 100644 --- a/Readme.md +++ b/Readme.md @@ -182,6 +182,24 @@ 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 they have installed. + +To do that, you should use `Canvas#registerFont`. + +**You need to call it before the Canvas is created** + +```javascript +Canvas.registerFont('comicsans.ttf'); + +var canvas = new Canvas(500, 500), + ctx = canvas.getContext('2d'); + +ctx.font = '12px "Comic Sans"'; +ctx.fillText(250, 10, 'Everyone hates this font :('); +``` + ### 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 bbb412377..520c8c12e 100755 --- a/binding.gyp +++ b/binding.gyp @@ -53,6 +53,7 @@ 'src/color.cc', 'src/Image.cc', 'src/ImageData.cc', + 'src/register_font.cc', 'src/init.cc' ], 'conditions': [ diff --git a/examples/font.js b/examples/font.js index f3d7a2d07..d60d24043 100644 --- a/examples/font.js +++ b/examples/font.js @@ -2,38 +2,33 @@ var fs = require('fs') var path = require('path') var Canvas = require('..') -var Font = Canvas.Font - -if (!Font) { - throw new Error('Need to compile with font support') -} - function fontFile (name) { return path.join(__dirname, '/pfennigFont/', name) } -var pfennigFont = new Font('pfennigFont', fontFile('Pfennig.ttf')) -pfennigFont.addFace(fontFile('PfennigBold.ttf'), 'bold') -pfennigFont.addFace(fontFile('PfennigItalic.ttf'), 'normal', 'italic') -pfennigFont.addFace(fontFile('PfennigBoldItalic.ttf'), 'bold', 'italic') +// Pass each font, including all of its individual variants if there are any, to +// `registerFont`. When you set `ctx.font`, refer to the styles and the family +// name as it is embedded in the TTF. If you aren't sure, open the font in +// FontForge and visit Element -> Font Information and copy the Family Name +Canvas.registerFont(fontFile('Pfennig.ttf')) +Canvas.registerFont(fontFile('PfennigBold.ttf')) +Canvas.registerFont(fontFile('PfennigItalic.ttf')) +Canvas.registerFont(fontFile('PfennigBoldItalic.ttf')) 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) -ctx.font = 'bold 50px pfennigFont' +ctx.font = 'bold 50px Pfennig' ctx.fillText('Quo Vaids?', 0, 140) -ctx.font = 'italic 50px pfennigFont' +ctx.font = 'italic 50px Pfennig' ctx.fillText('Quo Vaids?', 0, 210) -ctx.font = 'bold italic 50px pfennigFont' +ctx.font = 'bold italic 50px Pfennig' ctx.fillText('Quo Vaids?', 0, 280) canvas.createPNGStream().pipe(fs.createWriteStream(path.join(__dirname, 'font.png'))) diff --git a/lib/canvas.js b/lib/canvas.js index 1403fabac..f598ccceb 100644 --- a/lib/canvas.js +++ b/lib/canvas.js @@ -14,6 +14,7 @@ var canvas = require('./bindings') , Canvas = canvas.Canvas , Image = canvas.Image , cairoVersion = canvas.cairoVersion + , registerFont = canvas.registerFont , Context2d = require('./context2d') , PNGStream = require('./pngstream') , PDFStream = require('./pdfstream') @@ -34,6 +35,12 @@ var Canvas = exports = module.exports = Canvas; exports.version = packageJson.version; +/** + * Register custom font + */ + +exports.registerFont = registerFont; + /** * Cairo version. */ diff --git a/src/Canvas.cc b/src/Canvas.cc index ce62053b3..0592b3e85 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -4,9 +4,6 @@ // Copyright (c) 2010 LearnBoost // -#include "Canvas.h" -#include "PNG.h" -#include "CanvasRenderingContext2d.h" #include #include #include @@ -14,6 +11,10 @@ #include #include #include + +#include "Canvas.h" +#include "PNG.h" +#include "CanvasRenderingContext2d.h" #include "closure.h" #ifdef HAVE_JPEG diff --git a/src/Canvas.h b/src/Canvas.h index 3353b9482..2b300c5cc 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -8,16 +8,16 @@ #ifndef __NODE_CANVAS_H__ #define __NODE_CANVAS_H__ -#include #include +#include #include #include #include #include #include -using namespace v8; using namespace node; +using namespace v8; /* * Maxmimum states per context. diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 933a6760e..4beb67bfc 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" @@ -32,6 +32,28 @@ Nan::Persistent Context2d::constructor; +/* + * Custom strndup since Windows doesn't have it + */ +static char* +_strndup(const char *s, size_t n) { + size_t i; + const char *p = s; + char *ret = NULL; + + for (i = 0; i < n && *p; i++, p++) + ; + + ret = (char*)malloc(i + 1); + + if (ret) { + memcpy(ret, s, i); + ret[i] = '\0'; + } + + return ret; +} + /* * Rectangle arg assertions. */ @@ -65,7 +87,7 @@ enum { void state_assign_fontFamily(canvas_state_t *state, const char *str) { free(state->fontFamily); - state->fontFamily = strndup(str, 100); + state->fontFamily = _strndup(str, 100); } diff --git a/src/init.cc b/src/init.cc index e4d76771b..a8a683d0a 100755 --- a/src/init.cc +++ b/src/init.cc @@ -12,12 +12,25 @@ #include "CanvasGradient.h" #include "CanvasPattern.h" #include "CanvasRenderingContext2d.h" +#include "register_font.h" // Compatibility with Visual Studio versions prior to VS2015 #if defined(_MSC_VER) && _MSC_VER < 1900 #define snprintf _snprintf #endif +NAN_METHOD(register_font_js) { + if (!info[0]->IsString()) { + return Nan::ThrowError("Wrong argument type"); + } + + String::Utf8Value filePath(info[0]); + + if (!register_font((unsigned char*) *filePath)) { + Nan::ThrowError("Could not load font to the system's font host"); + } +} + NAN_MODULE_INIT(init) { Canvas::Initialize(target); Image::Initialize(target); @@ -26,6 +39,8 @@ NAN_MODULE_INIT(init) { Gradient::Initialize(target); Pattern::Initialize(target); + Nan::SetMethod(target, "registerFont", register_font_js); + target->Set(Nan::New("cairoVersion").ToLocalChecked(), Nan::New(cairo_version_string()).ToLocalChecked()); #ifdef HAVE_JPEG diff --git a/src/register_font.cc b/src/register_font.cc new file mode 100644 index 000000000..bdf48ef18 --- /dev/null +++ b/src/register_font.cc @@ -0,0 +1,21 @@ +#ifdef __APPLE__ +#include +#include +#elif defined(_WIN32) +#include +#else +#include +#endif + +bool +register_font(unsigned char *filepath) { + #ifdef __APPLE__ + CFURLRef filepathUrl = CFURLCreateFromFileSystemRepresentation(NULL, filepath, strlen((char*)filepath), false); + return CTFontManagerRegisterFontsForURL(filepathUrl, kCTFontManagerScopeProcess, NULL); + #elif defined(_WIN32) + return AddFontResourceEx((LPCSTR)filepath, FR_PRIVATE, 0) != 0; + #else + return FcConfigAppFontAddFile(FcConfigGetCurrent(), (FcChar8 *)(filepath)); + #endif +} + diff --git a/src/register_font.h b/src/register_font.h new file mode 100644 index 000000000..0ba5e596f --- /dev/null +++ b/src/register_font.h @@ -0,0 +1,2 @@ +bool register_font(unsigned char *filepath); + From 4dec67574602930901cf50a3d03f7ac229d87122 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Mon, 22 Feb 2016 12:14:55 -0500 Subject: [PATCH 03/14] fix fonts not working after a call to getContext issue was limited to osx and windows before this, if you create a canvas and get its context, the fonts would freeze - you cannot add any fonts after getContext is called once, even if you set up a new canvas. i fixed this by refreshing the Pango FontMap (the component that does font lookups) after you create a canvas+context, that canvas+context will not be able to use new fonts, but that is less of a problem --- src/register_font.cc | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/register_font.cc b/src/register_font.cc index bdf48ef18..831d009bb 100644 --- a/src/register_font.cc +++ b/src/register_font.cc @@ -1,3 +1,7 @@ +#include +#include +#include + #ifdef __APPLE__ #include #include @@ -9,13 +13,22 @@ bool register_font(unsigned char *filepath) { + bool success; + #ifdef __APPLE__ CFURLRef filepathUrl = CFURLCreateFromFileSystemRepresentation(NULL, filepath, strlen((char*)filepath), false); - return CTFontManagerRegisterFontsForURL(filepathUrl, kCTFontManagerScopeProcess, NULL); + success = CTFontManagerRegisterFontsForURL(filepathUrl, kCTFontManagerScopeProcess, NULL); #elif defined(_WIN32) - return AddFontResourceEx((LPCSTR)filepath, FR_PRIVATE, 0) != 0; + success = AddFontResourceEx((LPCSTR)filepath, FR_PRIVATE, 0) != 0; #else - return FcConfigAppFontAddFile(FcConfigGetCurrent(), (FcChar8 *)(filepath)); + success = FcConfigAppFontAddFile(FcConfigGetCurrent(), (FcChar8 *)(filepath)); #endif + + // 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. + if (success) pango_cairo_font_map_set_default(NULL); + + return success; } From 1a8c00c64c4d24aec01fc7e1be9b1b3db0d804e4 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Wed, 30 Mar 2016 20:03:35 -0400 Subject: [PATCH 04/14] clean up register_font to have clearer flow --- src/register_font.cc | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/register_font.cc b/src/register_font.cc index 831d009bb..03f741bd0 100644 --- a/src/register_font.cc +++ b/src/register_font.cc @@ -24,11 +24,13 @@ register_font(unsigned char *filepath) { 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. - if (success) pango_cairo_font_map_set_default(NULL); - - return success; + pango_cairo_font_map_set_default(NULL); + + return true; } From 40105d4e7bf4b6cd1a140d0ab863ad79063001bf Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sun, 24 Apr 2016 16:54:31 +0000 Subject: [PATCH 05/14] use PangoFontDescription to manage font on state this has the benefit of really simplifying the code, since we can use pango_font_description_copy and remove the need for a couple helpers. there is also no more need for setFontFromState since doing that becomes a one-liner the behavior of ctx.font should be unchanged, I tested the default font as well as save and restore, of course --- src/CanvasRenderingContext2d.cc | 81 +++++++-------------------------- src/CanvasRenderingContext2d.h | 6 +-- 2 files changed, 18 insertions(+), 69 deletions(-) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 4beb67bfc..337e7b19f 100755 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -32,28 +32,6 @@ Nan::Persistent Context2d::constructor; -/* - * Custom strndup since Windows doesn't have it - */ -static char* -_strndup(const char *s, size_t n) { - size_t i; - const char *p = s; - char *ret = NULL; - - for (i = 0; i < n && *p; i++, p++) - ; - - ret = (char*)malloc(i + 1); - - if (ret) { - memcpy(ret, s, i); - ret[i] = '\0'; - } - - return ret; -} - /* * Rectangle arg assertions. */ @@ -81,16 +59,6 @@ enum { , TEXT_BASELINE_HANGING }; -/* - * 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. */ @@ -199,12 +167,9 @@ Context2d::Context2d(Canvas *canvas) { state->shadow = transparent_black; state->patternQuality = CAIRO_FILTER_GOOD; state->textDrawingMode = TEXT_DRAW_PATHS; - state->fontWeight = PANGO_WEIGHT_NORMAL; - state->fontStyle = PANGO_STYLE_NORMAL; - state->fontSize = 10; - state->fontFamily = NULL; - state_assign_fontFamily(state, "sans serif"); - setFontFromState(); + 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); } /* @@ -213,7 +178,7 @@ Context2d::Context2d(Canvas *canvas) { Context2d::~Context2d() { while(stateno >= 0) { - free(states[stateno]->fontFamily); + pango_font_description_free(states[stateno]->fontDescription); free(states[stateno--]); } g_object_unref(_layout); @@ -249,7 +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)); - states[stateno]->fontFamily = _strndup(state->fontFamily, 100); + states[stateno]->fontDescription = pango_font_description_copy(states[stateno-1]->fontDescription); state = states[stateno]; } @@ -260,12 +225,11 @@ Context2d::saveState() { void Context2d::restoreState() { if (0 == stateno) return; - // Olaf (2011-02-21): Free old state data - free(states[stateno]->fontFamily); + pango_font_description_free(states[stateno]->fontDescription); free(states[stateno]); states[stateno] = NULL; state = states[--stateno]; - setFontFromState(); + pango_layout_set_font_description(_layout, state->fontDescription); } /* @@ -1868,9 +1832,13 @@ NAN_METHOD(Context2d::SetFont) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - if (strlen(*family) > 0) state_assign_fontFamily(context->state, *family); + PangoFontDescription *desc = pango_font_description_copy(context->state->fontDescription); + pango_font_description_free(context->state->fontDescription); + context->state->fontDescription = desc; - if (size > 0) context->state->fontSize = size; + if (strlen(*family) > 0) pango_font_description_set_family(desc, *family); + + if (size > 0) pango_font_description_set_absolute_size(desc, size * PANGO_SCALE); PangoStyle s = PANGO_STYLE_NORMAL; if (strlen(*style) > 0) { @@ -1880,7 +1848,8 @@ NAN_METHOD(Context2d::SetFont) { s = PANGO_STYLE_OBLIQUE; } } - context->state->fontStyle = s; + + pango_font_description_set_style(desc, s); PangoWeight w = PANGO_WEIGHT_NORMAL; if (strlen(*weight) > 0) { @@ -1904,26 +1873,10 @@ NAN_METHOD(Context2d::SetFont) { w = PANGO_WEIGHT_HEAVY; } } - context->state->fontWeight = w; - - context->setFontFromState(); -} - -/* - * Sets PangoLayout options from the current font state - */ - -void -Context2d::setFontFromState() { - PangoFontDescription *fd = pango_font_description_new(); - 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); + pango_font_description_set_weight(desc, w); - pango_layout_set_font_description(_layout, fd); - pango_font_description_free(fd); + pango_layout_set_font_description(context->_layout, desc); } /* diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index 1bd803d93..fccb5d184 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -45,11 +45,7 @@ typedef struct { double shadowOffsetX; double shadowOffsetY; canvas_draw_mode_t textDrawingMode; - PangoWeight fontWeight; - PangoStyle fontStyle; - double fontSize; - char *fontFamily; - + PangoFontDescription *fontDescription; } canvas_state_t; void state_assign_fontFamily(canvas_state_t *state, const char *str); From 9bdcc5d958221e0b49cf144797a250a29f6261d9 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sun, 24 Apr 2016 18:48:22 +0000 Subject: [PATCH 06/14] support @font-face style attrs in registerFont * registerFont is now implemented as a static member function of the Canvas class itself. there is a static list of fonts registered on the class, which are picked up by setting the font when necessary. * register_font will fill in its second arg with a PangoFontDescription that should resolve to the file specified * to do that, freetype is now required again --- binding.gyp | 8 +- lib/canvas.js | 13 ++- src/Canvas.cc | 129 ++++++++++++++++++++++ src/Canvas.h | 17 +++ src/CanvasRenderingContext2d.cc | 44 ++------ src/init.cc | 23 ++-- src/register_font.cc | 185 +++++++++++++++++++++++++++++++- src/register_font.h | 4 +- 8 files changed, 365 insertions(+), 58 deletions(-) diff --git a/binding.gyp b/binding.gyp index 520c8c12e..9b2e7d68e 100755 --- a/binding.gyp +++ b/binding.gyp @@ -4,11 +4,13 @@ 'variables': { 'GTK_Root%': 'C:/GTK', # Set the location of GTK all-in-one bundle 'with_jpeg%': 'false', + 'with_freetype%': 'false', 'with_gif%': 'false' } }, { # 'OS!="win"' 'variables': { 'with_jpeg%': ' #include #include +#include #include #include @@ -16,6 +17,7 @@ #include "PNG.h" #include "CanvasRenderingContext2d.h" #include "closure.h" +#include "register_font.h" #ifdef HAVE_JPEG #include "JPEGStream.h" @@ -58,6 +60,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()); } @@ -565,6 +570,41 @@ NAN_METHOD(Canvas::StreamJPEGSync) { #endif +NAN_METHOD(Canvas::RegisterFont) { + FontFace face; + + if (!info[0]->IsString()) { + return Nan::ThrowError("Wrong argument type"); + } + + String::Utf8Value filePath(info[0]); + + if (!register_font((unsigned char*) *filePath, &face.target_desc)) { + Nan::ThrowError("Could not load font to the system's font host"); + } else { + PangoFontDescription* d = pango_font_description_copy(face.target_desc); + + if (!info[1]->Equals(Nan::Null())) { + String::Utf8Value weight(info[1]->ToString()); + pango_font_description_set_weight(d, Canvas::GetWeightFromCSSString(*weight)); + } + + if (!info[2]->Equals(Nan::Null())) { + String::Utf8Value style(info[2]->ToString()); + pango_font_description_set_style(d, Canvas::GetStyleFromCSSString(*style)); + } + + if (!info[3]->Equals(Nan::Null())) { + String::Utf8Value family(info[3]->ToString()); + pango_font_description_set_family(d, *family); + } + + face.user_desc = d; + + _font_face_list.push_back(face); + } +} + /* * Initialize cairo surface. */ @@ -616,6 +656,95 @@ 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(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(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; +} + +/* + * Tries to find a matching font given to registerFont + */ + +PangoFontDescription * +Canvas::FindCustomFace(PangoFontDescription *desc) { + PangoFontDescription* best_match = NULL; + PangoFontDescription* best_match_target = NULL; + std::vector::iterator it = _font_face_list.begin(); + + while (it != _font_face_list.end()) { + FontFace f = *it; + + if (g_ascii_strcasecmp(pango_font_description_get_family(desc), + pango_font_description_get_family(f.user_desc)) == 0 + && pango_font_description_better_match(desc, best_match, f.user_desc)) { + + best_match = f.user_desc; + best_match_target = f.target_desc; + } + + ++it; + } + + return best_match_target; +} + /* * Re-alloc the surface, destroying the previous. */ diff --git a/src/Canvas.h b/src/Canvas.h index 2b300c5cc..0d7da5d5e 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -13,9 +13,11 @@ #include #include #include +#include #include #include + using namespace node; using namespace v8; @@ -38,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 *target_desc; + PangoFontDescription *user_desc; +}; + /* * Canvas. */ @@ -60,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); @@ -74,6 +87,9 @@ class Canvas: public Nan::ObjectWrap { EIO_ToBuffer(eio_req *req); static int EIO_AfterToBuffer(eio_req *req); #endif + static PangoWeight GetWeightFromCSSString(char *weight); + static PangoStyle GetStyleFromCSSString(char *style); + static PangoFontDescription* FindCustomFace(PangoFontDescription *desc); inline bool isPDF(){ return CANVAS_TYPE_PDF == type; } inline bool isSVG(){ return CANVAS_TYPE_SVG == type; } @@ -89,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 337e7b19f..5d404aef2 100755 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -1834,47 +1834,21 @@ NAN_METHOD(Context2d::SetFont) { PangoFontDescription *desc = pango_font_description_copy(context->state->fontDescription); pango_font_description_free(context->state->fontDescription); - context->state->fontDescription = desc; - if (strlen(*family) > 0) pango_font_description_set_family(desc, *family); + pango_font_description_set_style(desc, Canvas::GetStyleFromCSSString(*style)); + pango_font_description_set_weight(desc, Canvas::GetWeightFromCSSString(*weight)); - if (size > 0) pango_font_description_set_absolute_size(desc, size * PANGO_SCALE); + if (strlen(*family) > 0) pango_font_description_set_family(desc, *family); - 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; - } + PangoFontDescription *target_desc; + if ((target_desc = Canvas::FindCustomFace(desc))) { + pango_font_description_free(desc); + desc = pango_font_description_copy(target_desc); } - pango_font_description_set_style(desc, 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; - } - } + if (size > 0) pango_font_description_set_absolute_size(desc, size * PANGO_SCALE); - pango_font_description_set_weight(desc, w); + context->state->fontDescription = desc; pango_layout_set_font_description(context->_layout, desc); } diff --git a/src/init.cc b/src/init.cc index a8a683d0a..dc2fa5963 100755 --- a/src/init.cc +++ b/src/init.cc @@ -6,31 +6,22 @@ // #include +#include +#include #include "Canvas.h" #include "Image.h" #include "ImageData.h" #include "CanvasGradient.h" #include "CanvasPattern.h" #include "CanvasRenderingContext2d.h" -#include "register_font.h" +#include +#include FT_FREETYPE_H // Compatibility with Visual Studio versions prior to VS2015 #if defined(_MSC_VER) && _MSC_VER < 1900 #define snprintf _snprintf #endif -NAN_METHOD(register_font_js) { - if (!info[0]->IsString()) { - return Nan::ThrowError("Wrong argument type"); - } - - String::Utf8Value filePath(info[0]); - - if (!register_font((unsigned char*) *filePath)) { - Nan::ThrowError("Could not load font to the system's font host"); - } -} - NAN_MODULE_INIT(init) { Canvas::Initialize(target); Image::Initialize(target); @@ -39,8 +30,6 @@ NAN_MODULE_INIT(init) { Gradient::Initialize(target); Pattern::Initialize(target); - Nan::SetMethod(target, "registerFont", register_font_js); - target->Set(Nan::New("cairoVersion").ToLocalChecked(), Nan::New(cairo_version_string()).ToLocalChecked()); #ifdef HAVE_JPEG @@ -78,6 +67,10 @@ NAN_MODULE_INIT(init) { target->Set(Nan::New("gifVersion").ToLocalChecked(), Nan::New(GIF_LIB_VERSION).ToLocalChecked()); #endif #endif + + 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()); } NODE_MODULE(canvas,init); diff --git a/src/register_font.cc b/src/register_font.cc index 03f741bd0..18b9bba05 100644 --- a/src/register_font.cc +++ b/src/register_font.cc @@ -3,7 +3,6 @@ #include #ifdef __APPLE__ -#include #include #elif defined(_WIN32) #include @@ -11,8 +10,186 @@ #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 + +/* + * 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 + */ + +char * +get_family_name(FT_Face face) { + FT_SfntName name; + 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) { + char *utf8candidate = to_utf8(name.string, name.string_len, name.platform_id, name.encoding_id); + + if (utf8candidate) { + if (utf8name) free(utf8name); + + if (IS_PREFERRED_ENC(name)) { + return utf8candidate; + } else { + utf8name = utf8candidate; + } + } + } + } + + 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; + case 350: return PANGO_WEIGHT_SEMILIGHT; + 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; + } +} + +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); + 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) { +register_font(unsigned char *filepath, PangoFontDescription **desc) { bool success; #ifdef __APPLE__ @@ -23,9 +200,11 @@ register_font(unsigned char *filepath) { #else success = FcConfigAppFontAddFile(FcConfigGetCurrent(), (FcChar8 *)(filepath)); #endif - + if (!success) return false; + *desc = get_pango_font_description(filepath); + // 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. diff --git a/src/register_font.h b/src/register_font.h index 0ba5e596f..60ace17dd 100644 --- a/src/register_font.h +++ b/src/register_font.h @@ -1,2 +1,4 @@ -bool register_font(unsigned char *filepath); +#include + +bool register_font(unsigned char *filepath, PangoFontDescription** desc); From 2d889058b79750d92b498fac8224e926a007d85e Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sun, 24 Apr 2016 15:27:08 -0400 Subject: [PATCH 07/14] PANGO_WEIGHT_SEMILIGHT was added in pango 1.36.7 --- src/register_font.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/register_font.cc b/src/register_font.cc index 18b9bba05..4d20e9278 100644 --- a/src/register_font.cc +++ b/src/register_font.cc @@ -121,7 +121,9 @@ get_pango_weight(FT_UShort 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; From ec4a52b9c9f1782e97e601d682466e44619d1854 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sun, 24 Apr 2016 15:42:06 -0400 Subject: [PATCH 08/14] font example: demonstrate custom font naming --- examples/font.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/font.js b/examples/font.js index d60d24043..ce0244049 100644 --- a/examples/font.js +++ b/examples/font.js @@ -10,10 +10,10 @@ function fontFile (name) { // `registerFont`. When you set `ctx.font`, refer to the styles and the family // name as it is embedded in the TTF. If you aren't sure, open the font in // FontForge and visit Element -> Font Information and copy the Family Name -Canvas.registerFont(fontFile('Pfennig.ttf')) -Canvas.registerFont(fontFile('PfennigBold.ttf')) -Canvas.registerFont(fontFile('PfennigItalic.ttf')) -Canvas.registerFont(fontFile('PfennigBoldItalic.ttf')) +Canvas.registerFont(fontFile('Pfennig.ttf'), {family: 'pfennigFont'}) +Canvas.registerFont(fontFile('PfennigBold.ttf'), {family: 'pfennigFont'}) +Canvas.registerFont(fontFile('PfennigItalic.ttf'), {family: 'pfennigFont'}) +Canvas.registerFont(fontFile('PfennigBoldItalic.ttf'), {family: 'pfennigFont'}) var canvas = new Canvas(320, 320) var ctx = canvas.getContext('2d') @@ -22,13 +22,13 @@ ctx.font = 'normal normal 50px Helvetica' ctx.fillText('Quo Vaids?', 0, 70) -ctx.font = 'bold 50px Pfennig' +ctx.font = 'bold 50px pfennigFont' ctx.fillText('Quo Vaids?', 0, 140) -ctx.font = 'italic 50px Pfennig' +ctx.font = 'italic 50px pfennigFont' ctx.fillText('Quo Vaids?', 0, 210) -ctx.font = 'bold italic 50px Pfennig' +ctx.font = 'bold italic 50px pfennigFont' ctx.fillText('Quo Vaids?', 0, 280) canvas.createPNGStream().pipe(fs.createWriteStream(path.join(__dirname, 'font.png'))) From e3ac8bdfc1991a903ebd737f798c20d7466041e8 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sun, 1 May 2016 18:51:01 -0400 Subject: [PATCH 09/14] require 2nd arg to registerFont, match CSS spec better * Canvas.registerFont is now implemented entirely in C++ * second arg is required and must have at least `family`. the others default to 'normal' per w3 fonts specification * when there is a family match for registered fonts, it should be chosen even if weight/style are wrong. this is also in the spec * update example and documentation --- Readme.md | 10 +++--- examples/font.js | 6 ++-- lib/canvas.js | 16 ---------- src/Canvas.cc | 83 +++++++++++++++++++++++++++++++++++------------- src/Canvas.h | 4 +-- 5 files changed, 72 insertions(+), 47 deletions(-) diff --git a/Readme.md b/Readme.md index 2095ea827..9f9cf1de1 100644 --- a/Readme.md +++ b/Readme.md @@ -182,16 +182,16 @@ 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 +### 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 they have installed. +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`. +To do that, you should use `Canvas.registerFont`. **You need to call it before the Canvas is created** ```javascript -Canvas.registerFont('comicsans.ttf'); +Canvas.registerFont('comicsans.ttf', {family: 'Comic Sans'}); var canvas = new Canvas(500, 500), ctx = canvas.getContext('2d'); @@ -200,6 +200,8 @@ 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/examples/font.js b/examples/font.js index ce0244049..2cf485639 100644 --- a/examples/font.js +++ b/examples/font.js @@ -11,9 +11,9 @@ function fontFile (name) { // name as it is embedded in the TTF. If you aren't sure, open the font in // FontForge and visit Element -> Font Information and copy the Family Name Canvas.registerFont(fontFile('Pfennig.ttf'), {family: 'pfennigFont'}) -Canvas.registerFont(fontFile('PfennigBold.ttf'), {family: 'pfennigFont'}) -Canvas.registerFont(fontFile('PfennigItalic.ttf'), {family: 'pfennigFont'}) -Canvas.registerFont(fontFile('PfennigBoldItalic.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') diff --git a/lib/canvas.js b/lib/canvas.js index 6b6653c52..1403fabac 100644 --- a/lib/canvas.js +++ b/lib/canvas.js @@ -34,22 +34,6 @@ var Canvas = exports = module.exports = Canvas; exports.version = packageJson.version; -/** - * Register custom font - */ - -exports.registerFont = function(src, fontFace) { - var weight, style, family; - - if (typeof fontFace === 'object') { - weight = fontFace.weight; - style = fontFace.style; - family = fontFace.family; - } - - this._registerFont(src, weight, style, family); -}; - /** * Cairo version. */ diff --git a/src/Canvas.cc b/src/Canvas.cc index b661cf11b..17c2bd628 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -23,6 +23,11 @@ #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; /* @@ -61,7 +66,7 @@ Canvas::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { Nan::SetTemplate(proto, "PNG_ALL_FILTERS", Nan::New(PNG_ALL_FILTERS)); // Class methods - Nan::SetMethod(ctor, "_registerFont", RegisterFont); + Nan::SetMethod(ctor, "registerFont", RegisterFont); Nan::Set(target, Nan::New("Canvas").ToLocalChecked(), ctor->GetFunction()); } @@ -582,26 +587,59 @@ NAN_METHOD(Canvas::RegisterFont) { if (!register_font((unsigned char*) *filePath, &face.target_desc)) { Nan::ThrowError("Could not load font to the system's font host"); } else { - PangoFontDescription* d = pango_font_description_copy(face.target_desc); + PangoFontDescription* d = pango_font_description_new(); + + if (!info[1]->IsObject()) { + Nan::ThrowError(GENERIC_FACE_ERROR); + } else { // now check the attrs, there are many ways to be wrong + Local 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(); + + const char* family; + const char* weight = "normal"; + const char* style = "normal"; + + Local family_val = desc->Get(family_prop); + if (family_val->IsString()) { + family = strdup(*String::Utf8Value(family_val)); + } else { + Nan::ThrowError(GENERIC_FACE_ERROR); + return; + } - if (!info[1]->Equals(Nan::Null())) { - String::Utf8Value weight(info[1]->ToString()); - pango_font_description_set_weight(d, Canvas::GetWeightFromCSSString(*weight)); - } + if (desc->HasOwnProperty(weight_prop)) { + Local weight_val = desc->Get(weight_prop); + if (weight_val->IsString() || weight_val->IsNumber()) { + weight = strdup(*String::Utf8Value(weight_val)); + } else { + Nan::ThrowError(GENERIC_FACE_ERROR); + return; + } + } - if (!info[2]->Equals(Nan::Null())) { - String::Utf8Value style(info[2]->ToString()); - pango_font_description_set_style(d, Canvas::GetStyleFromCSSString(*style)); - } + if (desc->HasOwnProperty(style_prop)) { + Local style_val = desc->Get(style_prop); + if (style_val->IsString()) { + style = strdup(*String::Utf8Value(style_val)); + } else { + Nan::ThrowError(GENERIC_FACE_ERROR); + return; + } + } - if (!info[3]->Equals(Nan::Null())) { - String::Utf8Value family(info[3]->ToString()); - pango_font_description_set_family(d, *family); - } + pango_font_description_set_weight(d, Canvas::GetWeightFromCSSString(weight)); + pango_font_description_set_style(d, Canvas::GetStyleFromCSSString(style)); + pango_font_description_set_family(d, family); - face.user_desc = d; + free((char*)family); + if (desc->HasOwnProperty(weight_prop)) free((char*)weight); + if (desc->HasOwnProperty(style_prop)) free((char*)style); - _font_face_list.push_back(face); + face.user_desc = d; + _font_face_list.push_back(face); + } } } @@ -669,7 +707,7 @@ std::vector Canvas::_font_face_list = _init_font_face_list(); */ PangoStyle -Canvas::GetStyleFromCSSString(char *style) { +Canvas::GetStyleFromCSSString(const char *style) { PangoStyle s = PANGO_STYLE_NORMAL; if (strlen(style) > 0) { @@ -688,7 +726,7 @@ Canvas::GetStyleFromCSSString(char *style) { */ PangoWeight -Canvas::GetWeightFromCSSString(char *weight) { +Canvas::GetWeightFromCSSString(const char *weight) { PangoWeight w = PANGO_WEIGHT_NORMAL; if (strlen(weight) > 0) { @@ -732,11 +770,12 @@ Canvas::FindCustomFace(PangoFontDescription *desc) { FontFace f = *it; if (g_ascii_strcasecmp(pango_font_description_get_family(desc), - pango_font_description_get_family(f.user_desc)) == 0 - && pango_font_description_better_match(desc, best_match, f.user_desc)) { + pango_font_description_get_family(f.user_desc)) == 0) { - best_match = f.user_desc; - best_match_target = f.target_desc; + if (best_match == NULL || pango_font_description_better_match(desc, best_match, f.user_desc)) { + best_match = f.user_desc; + best_match_target = f.target_desc; + } } ++it; diff --git a/src/Canvas.h b/src/Canvas.h index 0d7da5d5e..b782e7bb6 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -87,8 +87,8 @@ class Canvas: public Nan::ObjectWrap { EIO_ToBuffer(eio_req *req); static int EIO_AfterToBuffer(eio_req *req); #endif - static PangoWeight GetWeightFromCSSString(char *weight); - static PangoStyle GetStyleFromCSSString(char *style); + static PangoWeight GetWeightFromCSSString(const char *weight); + static PangoStyle GetStyleFromCSSString(const char *style); static PangoFontDescription* FindCustomFace(PangoFontDescription *desc); inline bool isPDF(){ return CANVAS_TYPE_PDF == type; } From 57a0c17d8af64fd74fa2b95e357f09fa2b64cb0d Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sun, 1 May 2016 18:51:32 -0400 Subject: [PATCH 10/14] fix the Windows build for FT --- binding.gyp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/binding.gyp b/binding.gyp index 9b2e7d68e..4f56e1a4c 100755 --- a/binding.gyp +++ b/binding.gyp @@ -4,13 +4,11 @@ 'variables': { 'GTK_Root%': 'C:/GTK', # Set the location of GTK all-in-one bundle 'with_jpeg%': 'false', - 'with_freetype%': 'false', 'with_gif%': 'false' } }, { # 'OS!="win"' 'variables': { 'with_jpeg%': ' Date: Sat, 4 Jun 2016 14:10:59 +0000 Subject: [PATCH 11/14] more robust SFNT name, support preferred family name OSes will register the font with the preferred family name instead of the family name if the preferred entry is present --- src/register_font.cc | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/register_font.cc b/src/register_font.cc index 4d20e9278..b17774b8e 100644 --- a/src/register_font.cc +++ b/src/register_font.cc @@ -33,6 +33,9 @@ #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 @@ -89,29 +92,42 @@ to_utf8(FT_Byte* buf, FT_UInt len, FT_UShort pid, FT_UShort eid) { * 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; +} + 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) { - char *utf8candidate = to_utf8(name.string, name.string_len, name.platform_id, name.encoding_id); + 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 (utf8candidate) { - if (utf8name) free(utf8name); + if (buf) { + NameDef *d = (NameDef*)malloc(sizeof(NameDef)); + d->buf = (const char*)buf; + d->rank = GET_NAME_RANK(name); - if (IS_PREFERRED_ENC(name)) { - return utf8candidate; - } else { - utf8name = utf8candidate; - } + 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, free); + return utf8name; } From 12971f64a66b154cca38d4fec5350cb43e7fae4f Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Wed, 27 Jul 2016 12:21:06 -0400 Subject: [PATCH 12/14] support GTK+ versions without g_list_free_full particularly, the wiki links to a GTK build that does not have g_list_free_full for building on Windows --- src/register_font.cc | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/register_font.cc b/src/register_font.cc index b17774b8e..f98a66fce 100644 --- a/src/register_font.cc +++ b/src/register_font.cc @@ -102,6 +102,20 @@ _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; @@ -126,7 +140,7 @@ get_family_name(FT_Face face) { 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, free); + if (list) _g_list_free_full(list); return utf8name; } From 377ada5fd8a469ff04624711c1c6f04897b77ae3 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Tue, 2 Aug 2016 00:09:46 +0000 Subject: [PATCH 13/14] support family name lists, both system and custom * so you can now specify ctx.font = 'custom1, arial'; etc and the later fonts will be used for glyphs that aren't in the first ones * i'm now calling them sys_desc and user_desc to distinguish the description that matches the font on the system vs the description the user passes to Canvas.registerFont * cleaned up some style --- lib/context2d.js | 4 +- src/Canvas.cc | 67 +++++++++++++++++++++------------ src/Canvas.h | 8 ++-- src/CanvasRenderingContext2d.cc | 13 +++---- test/canvas.test.js | 10 ++--- 5 files changed, 59 insertions(+), 43 deletions(-) diff --git a/lib/context2d.js b/lib/context2d.js index df0688823..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 diff --git a/src/Canvas.cc b/src/Canvas.cc index 17c2bd628..6dd5ba3f9 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -584,10 +584,10 @@ NAN_METHOD(Canvas::RegisterFont) { String::Utf8Value filePath(info[0]); - if (!register_font((unsigned char*) *filePath, &face.target_desc)) { + if (!register_font((unsigned char *) *filePath, &face.sys_desc)) { Nan::ThrowError("Could not load font to the system's font host"); } else { - PangoFontDescription* d = pango_font_description_new(); + PangoFontDescription *d = pango_font_description_new(); if (!info[1]->IsObject()) { Nan::ThrowError(GENERIC_FACE_ERROR); @@ -597,9 +597,9 @@ NAN_METHOD(Canvas::RegisterFont) { Local weight_prop = Nan::New("weight").ToLocalChecked(); Local style_prop = Nan::New("style").ToLocalChecked(); - const char* family; - const char* weight = "normal"; - const char* style = "normal"; + const char *family; + const char *weight = "normal"; + const char *style = "normal"; Local family_val = desc->Get(family_prop); if (family_val->IsString()) { @@ -633,9 +633,9 @@ NAN_METHOD(Canvas::RegisterFont) { pango_font_description_set_style(d, Canvas::GetStyleFromCSSString(style)); pango_font_description_set_family(d, family); - free((char*)family); - if (desc->HasOwnProperty(weight_prop)) free((char*)weight); - if (desc->HasOwnProperty(style_prop)) free((char*)style); + free((char *)family); + if (desc->HasOwnProperty(weight_prop)) free((char *)weight); + if (desc->HasOwnProperty(style_prop)) free((char *)style); face.user_desc = d; _font_face_list.push_back(face); @@ -757,31 +757,48 @@ Canvas::GetWeightFromCSSString(const char *weight) { } /* - * Tries to find a matching font given to registerFont + * Given a user description, return a description that will select the + * font either from the system or @font-face */ PangoFontDescription * -Canvas::FindCustomFace(PangoFontDescription *desc) { - PangoFontDescription* best_match = NULL; - PangoFontDescription* best_match_target = NULL; - std::vector::iterator it = _font_face_list.begin(); - - while (it != _font_face_list.end()) { - FontFace f = *it; - - if (g_ascii_strcasecmp(pango_font_description_get_family(desc), - pango_font_description_get_family(f.user_desc)) == 0) { - - if (best_match == NULL || pango_font_description_better_match(desc, best_match, f.user_desc)) { - best_match = f.user_desc; - best_match_target = f.target_desc; +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; + } } } - ++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); } - return best_match_target; + 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; } /* diff --git a/src/Canvas.h b/src/Canvas.h index b782e7bb6..9411a1863 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -40,14 +40,14 @@ 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 *target_desc; - PangoFontDescription *user_desc; + PangoFontDescription *sys_desc = NULL; + PangoFontDescription *user_desc = NULL; }; /* @@ -89,7 +89,7 @@ class Canvas: public Nan::ObjectWrap { #endif static PangoWeight GetWeightFromCSSString(const char *weight); static PangoStyle GetStyleFromCSSString(const char *style); - static PangoFontDescription* FindCustomFace(PangoFontDescription *desc); + static PangoFontDescription *ResolveFontDescription(const PangoFontDescription *desc); inline bool isPDF(){ return CANVAS_TYPE_PDF == type; } inline bool isSVG(){ return CANVAS_TYPE_SVG == type; } diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 5d404aef2..c570541ed 100755 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -1840,17 +1840,14 @@ NAN_METHOD(Context2d::SetFont) { if (strlen(*family) > 0) pango_font_description_set_family(desc, *family); - PangoFontDescription *target_desc; - if ((target_desc = Canvas::FindCustomFace(desc))) { - pango_font_description_free(desc); - desc = pango_font_description_copy(target_desc); - } + PangoFontDescription *sys_desc = Canvas::ResolveFontDescription(desc); + pango_font_description_free(desc); - if (size > 0) pango_font_description_set_absolute_size(desc, size * PANGO_SCALE); + if (size > 0) pango_font_description_set_absolute_size(sys_desc, size * PANGO_SCALE); - context->state->fontDescription = desc; + context->state->fontDescription = sys_desc; - pango_layout_set_font_description(context->_layout, desc); + pango_layout_set_font_description(context->_layout, sys_desc); } /* 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' From 50b7c12004d3fae3d3b60072c36f488ca2e0123f Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sat, 27 Aug 2016 03:38:40 +0000 Subject: [PATCH 14/14] handle double-registering fonts. expand font paths this should make it much less likely registering a font will fail: 1. paths to fonts with .., sym links, etc are resolved before passed to the OS and freetype 2. when you register a font with identical metadata to one you have already registered, its description will be adjusted accordingly (if you call registerFont with name = 'a' then name = 'b', you will then need to set ctx.font = 'b' to select it) this commit also fixes some minor memory leaks --- lib/canvas.js | 8 ++++ src/Canvas.cc | 107 ++++++++++++++++++++++--------------------- src/register_font.cc | 24 ++++++---- src/register_font.h | 3 +- 4 files changed, 79 insertions(+), 63 deletions(-) diff --git a/lib/canvas.js b/lib/canvas.js index 1403fabac..8272c9b86 100644 --- a/lib/canvas.js +++ b/lib/canvas.js @@ -75,6 +75,14 @@ exports.JPEGStream = JPEGStream; exports.Image = Image; exports.ImageData = canvas.ImageData; +/** + * Resolve paths for registerFont + */ + +Canvas.registerFont = function(src, fontFace){ + return Canvas._registerFont(fs.realpathSync(src), fontFace); +}; + /** * Context2d implementation. */ diff --git a/src/Canvas.cc b/src/Canvas.cc index 6dd5ba3f9..c7415e246 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -66,7 +66,7 @@ Canvas::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { Nan::SetTemplate(proto, "PNG_ALL_FILTERS", Nan::New(PNG_ALL_FILTERS)); // Class methods - Nan::SetMethod(ctor, "registerFont", RegisterFont); + Nan::SetMethod(ctor, "_registerFont", RegisterFont); Nan::Set(target, Nan::New("Canvas").ToLocalChecked(), ctor->GetFunction()); } @@ -575,72 +575,75 @@ NAN_METHOD(Canvas::StreamJPEGSync) { #endif -NAN_METHOD(Canvas::RegisterFont) { - FontFace face; +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 (!register_font((unsigned char *) *filePath, &face.sys_desc)) { - Nan::ThrowError("Could not load font to the system's font host"); - } else { - PangoFontDescription *d = pango_font_description_new(); - - if (!info[1]->IsObject()) { - Nan::ThrowError(GENERIC_FACE_ERROR); - } else { // now check the attrs, there are many ways to be wrong - Local 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(); - - const char *family; - const char *weight = "normal"; - const char *style = "normal"; - - Local family_val = desc->Get(family_prop); - if (family_val->IsString()) { - family = strdup(*String::Utf8Value(family_val)); - } else { - Nan::ThrowError(GENERIC_FACE_ERROR); - return; - } + if (!sys_desc) return Nan::ThrowError("Could not parse font file"); - if (desc->HasOwnProperty(weight_prop)) { - Local weight_val = desc->Get(weight_prop); - if (weight_val->IsString() || weight_val->IsNumber()) { - weight = strdup(*String::Utf8Value(weight_val)); - } else { - Nan::ThrowError(GENERIC_FACE_ERROR); - return; - } - } + PangoFontDescription *user_desc = pango_font_description_new(); - if (desc->HasOwnProperty(style_prop)) { - Local style_val = desc->Get(style_prop); - if (style_val->IsString()) { - style = strdup(*String::Utf8Value(style_val)); - } else { - Nan::ThrowError(GENERIC_FACE_ERROR); - return; - } - } + // 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); - pango_font_description_set_weight(d, Canvas::GetWeightFromCSSString(weight)); - pango_font_description_set_style(d, Canvas::GetStyleFromCSSString(style)); - pango_font_description_set_family(d, family); + 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; - free((char *)family); - if (desc->HasOwnProperty(weight_prop)) free((char *)weight); - if (desc->HasOwnProperty(style_prop)) free((char *)style); + for (; it != _font_face_list.end() && !already_registered; ++it) { + if (pango_font_description_equal(it->sys_desc, sys_desc)) { + already_registered = &(*it); + } + } - face.user_desc = d; + 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); } /* diff --git a/src/register_font.cc b/src/register_font.cc index f98a66fce..75fc87cc8 100644 --- a/src/register_font.cc +++ b/src/register_font.cc @@ -191,6 +191,10 @@ get_pango_style(FT_Long flags) { } } +/* + * Return a PangoFontDescription that will resolve to the font file + */ + PangoFontDescription * get_pango_font_description(unsigned char* filepath) { FT_Library library; @@ -199,16 +203,18 @@ get_pango_font_description(unsigned char* filepath) { 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); - char *family = get_family_name(face); + 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)); + 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); + FT_Done_Face(face); - return desc; + return desc; + } } pango_font_description_free(desc); @@ -221,7 +227,7 @@ get_pango_font_description(unsigned char* filepath) { */ bool -register_font(unsigned char *filepath, PangoFontDescription **desc) { +register_font(unsigned char *filepath) { bool success; #ifdef __APPLE__ @@ -235,8 +241,6 @@ register_font(unsigned char *filepath, PangoFontDescription **desc) { if (!success) return false; - *desc = get_pango_font_description(filepath); - // 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. diff --git a/src/register_font.h b/src/register_font.h index 60ace17dd..33d006bab 100644 --- a/src/register_font.h +++ b/src/register_font.h @@ -1,4 +1,5 @@ #include -bool register_font(unsigned char *filepath, PangoFontDescription** desc); +PangoFontDescription *get_pango_font_description(unsigned char *filepath); +bool register_font(unsigned char *filepath);