diff --git a/CHANGELOG.md b/CHANGELOG.md index 92e63fbfa..a0a91d6ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ project adheres to [Semantic Versioning](http://semver.org/). * Fix compile errors with cairo * Fix Image#complete if the image failed to load. * Upgrade node-pre-gyp to v0.15.0 to use latest version of needle to fix error when downloading prebuilds. +* The small-caps variant setting is now honored if included in a font string +* Letterspacing can now be controlled by setting the context's textTracking attribute 2.6.1 ================== diff --git a/Readme.md b/Readme.md index b27549962..032bc352d 100644 --- a/Readme.md +++ b/Readme.md @@ -88,6 +88,7 @@ This project is an implementation of the Web Canvas API and implements that API * [Canvas#createJPEGStream()](#canvascreatejpegstream) * [Canvas#createPDFStream()](#canvascreatepdfstream) * [Canvas#toDataURL()](#canvastodataurl) +* [CanvasRenderingContext2D#textTracking](#canvasrenderingcontext2dtexttracking) * [CanvasRenderingContext2D#patternQuality](#canvasrenderingcontext2dpatternquality) * [CanvasRenderingContext2D#quality](#canvasrenderingcontext2dquality) * [CanvasRenderingContext2D#textDrawingMode](#canvasrenderingcontext2dtextdrawingmode) @@ -392,6 +393,14 @@ canvas.toDataURL('image/jpeg', {...opts}, (err, jpeg) => { }) // see Canvas#crea canvas.toDataURL('image/jpeg', quality, (err, jpeg) => { }) // spec-following; quality from 0 to 1 ``` +### CanvasRenderingContext2D#textTracking + +> ```ts +> context.textTracking: Number +> ``` + +Defaults to 0. Sets the additional amount of space between characters to be added or subtracted when drawing text to the canvas. The amount of space is measured in integer units of thousandths-of-an-em, meaning a value of 1000 will separate characters by 1 'em' (a.k.a., the current point-size of the font). Positive values will increase the letter-spacing and negative values will tighten it. + ### CanvasRenderingContext2D#patternQuality > ```ts diff --git a/examples/crimsonFont/Crimson-Bold.ttf b/examples/crimsonFont/Crimson-Bold.ttf new file mode 100755 index 000000000..8399d75fc Binary files /dev/null and b/examples/crimsonFont/Crimson-Bold.ttf differ diff --git a/examples/crimsonFont/Crimson-BoldItalic.ttf b/examples/crimsonFont/Crimson-BoldItalic.ttf new file mode 100755 index 000000000..d9b4af4c3 Binary files /dev/null and b/examples/crimsonFont/Crimson-BoldItalic.ttf differ diff --git a/examples/crimsonFont/Crimson-Italic.ttf b/examples/crimsonFont/Crimson-Italic.ttf new file mode 100755 index 000000000..b3afa6b7f Binary files /dev/null and b/examples/crimsonFont/Crimson-Italic.ttf differ diff --git a/examples/crimsonFont/Crimson-Roman.ttf b/examples/crimsonFont/Crimson-Roman.ttf new file mode 100755 index 000000000..ab0f0ed6c Binary files /dev/null and b/examples/crimsonFont/Crimson-Roman.ttf differ diff --git a/examples/crimsonFont/Crimson-Semibold.ttf b/examples/crimsonFont/Crimson-Semibold.ttf new file mode 100755 index 000000000..0b92fd02f Binary files /dev/null and b/examples/crimsonFont/Crimson-Semibold.ttf differ diff --git a/examples/crimsonFont/Crimson-SemiboldItalic.ttf b/examples/crimsonFont/Crimson-SemiboldItalic.ttf new file mode 100755 index 000000000..4d20e036b Binary files /dev/null and b/examples/crimsonFont/Crimson-SemiboldItalic.ttf differ diff --git a/index.js b/index.js index c54bfa36b..e9c9cbf1f 100644 --- a/index.js +++ b/index.js @@ -81,6 +81,8 @@ module.exports = { gifVersion: bindings.gifVersion ? bindings.gifVersion.replace(/[^.\d]/g, '') : undefined, /** freetype version. */ freetypeVersion: bindings.freetypeVersion, + /** pango version. */ + pangoVersion: bindings.pangoVersion, /** rsvg version. */ rsvgVersion: bindings.rsvgVersion } diff --git a/package.json b/package.json index 46567b3ec..ae8bddc18 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "express": "^4.16.3", "mocha": "^5.2.0", "pixelmatch": "^4.0.2", + "semver": "^7.3.2", "standard": "^12.0.1" }, "engines": { diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 774612708..edeb9a857 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -172,6 +172,7 @@ Context2d::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { SetProtoAccessor(proto, Nan::New("fillStyle").ToLocalChecked(), GetFillStyle, SetFillStyle, ctor); SetProtoAccessor(proto, Nan::New("strokeStyle").ToLocalChecked(), GetStrokeStyle, SetStrokeStyle, ctor); SetProtoAccessor(proto, Nan::New("font").ToLocalChecked(), GetFont, SetFont, ctor); + SetProtoAccessor(proto, Nan::New("textTracking").ToLocalChecked(), GetTextTracking, SetTextTracking, ctor); SetProtoAccessor(proto, Nan::New("textBaseline").ToLocalChecked(), GetTextBaseline, SetTextBaseline, ctor); SetProtoAccessor(proto, Nan::New("textAlign").ToLocalChecked(), GetTextAlign, SetTextAlign, ctor); Local ctx = Nan::GetCurrentContext(); @@ -199,6 +200,7 @@ Context2d::Context2d(Canvas *canvas) { Context2d::~Context2d() { while(stateno >= 0) { pango_font_description_free(states[stateno]->fontDescription); + pango_attr_list_unref(states[stateno]->textAttributes); free(states[stateno--]); } g_object_unref(_layout); @@ -213,6 +215,7 @@ Context2d::~Context2d() { void Context2d::resetState(bool init) { if (!init) { pango_font_description_free(state->fontDescription); + pango_attr_list_unref(states[stateno]->textAttributes); } state->shadowBlur = 0; @@ -224,6 +227,7 @@ void Context2d::resetState(bool init) { state->fillGradient = nullptr; state->strokeGradient = nullptr; state->textBaseline = TEXT_BASELINE_ALPHABETIC; + state->textTracking = 0; rgba_t transparent = { 0, 0, 0, 1 }; rgba_t transparent_black = { 0, 0, 0, 0 }; state->fill = transparent; @@ -235,6 +239,8 @@ void Context2d::resetState(bool init) { 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); + state->textAttributes = pango_attr_list_new(); + pango_layout_set_attributes(_layout, state->textAttributes); _resetPersistentHandles(); } @@ -244,6 +250,7 @@ void Context2d::_resetPersistentHandles() { _strokeStyle.Reset(); _font.Reset(); _textBaseline.Reset(); + _textTracking.Reset(); _textAlign.Reset(); } @@ -258,6 +265,8 @@ Context2d::save() { states[++stateno] = (canvas_state_t *) malloc(sizeof(canvas_state_t)); memcpy(states[stateno], state, sizeof(canvas_state_t)); states[stateno]->fontDescription = pango_font_description_copy(states[stateno-1]->fontDescription); + states[stateno]->textAttributes = pango_attr_list_copy(states[stateno-1]->textAttributes); + pango_layout_set_attributes(_layout, states[stateno]->textAttributes); state = states[stateno]; } } @@ -271,10 +280,12 @@ Context2d::restore() { if (stateno > 0) { cairo_restore(_context); pango_font_description_free(states[stateno]->fontDescription); + pango_attr_list_unref(states[stateno]->textAttributes); free(states[stateno]); states[stateno] = NULL; state = states[--stateno]; pango_layout_set_font_description(_layout, state->fontDescription); + pango_layout_set_attributes(_layout, state->textAttributes); } } @@ -2499,6 +2510,7 @@ NAN_GETTER(Context2d::GetFont) { /* * Set font: + * - variant * - weight * - style * - size @@ -2523,6 +2535,7 @@ NAN_SETTER(Context2d::SetFont) { if (mparsed->IsUndefined()) return; Local font = Nan::To(mparsed).ToLocalChecked(); + Nan::Utf8String variant(Nan::Get(font, Nan::New("variant").ToLocalChecked()).ToLocalChecked()); Nan::Utf8String weight(Nan::Get(font, Nan::New("weight").ToLocalChecked()).ToLocalChecked()); Nan::Utf8String style(Nan::Get(font, Nan::New("style").ToLocalChecked()).ToLocalChecked()); double size = Nan::To(Nan::Get(font, Nan::New("size").ToLocalChecked()).ToLocalChecked()).FromMaybe(0); @@ -2547,9 +2560,46 @@ NAN_SETTER(Context2d::SetFont) { context->state->fontDescription = sys_desc; pango_layout_set_font_description(context->_layout, sys_desc); + #if PANGO_VERSION >= PANGO_VERSION_ENCODE(1, 37, 1) + PangoAttribute *features; + if (strlen(*variant) > 0 && strcmp("small-caps", *variant) == 0) { + features = pango_attr_font_features_new("smcp 1, onum 1"); + } else { + features = pango_attr_font_features_new(""); + } + pango_attr_list_change(context->state->textAttributes, features); + #endif + + int oneEm = pango_font_description_get_size(context->state->fontDescription); + int perEm = context->state->textTracking; + PangoAttribute *tracking = pango_attr_letter_spacing_new(oneEm * perEm / 1000.0); + pango_attr_list_change(context->state->textAttributes, tracking); + context->_font.Reset(value); } +/* + * Get text tracking. + */ + +NAN_GETTER(Context2d::GetTextTracking) { + Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); + info.GetReturnValue().Set(Nan::New(context->state->textTracking)); +} + +/* + * Set text tracking. + */ + +NAN_SETTER(Context2d::SetTextTracking) { + Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); + int oneEm = pango_font_description_get_size(context->state->fontDescription); + int perEm = Nan::To(value).FromMaybe(0); + PangoAttribute *tracking = pango_attr_letter_spacing_new(oneEm * perEm / 1000.0); + pango_attr_list_change(context->state->textAttributes, tracking); + context->state->textTracking = perEm; +} + /* * Get text baseline. */ diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index 59d6f6d11..1402a1538 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -31,12 +31,14 @@ typedef struct { float globalAlpha; short textAlignment; short textBaseline; + int textTracking; rgba_t shadow; int shadowBlur; double shadowOffsetX; double shadowOffsetY; canvas_draw_mode_t textDrawingMode; PangoFontDescription *fontDescription; + PangoAttrList *textAttributes; bool imageSmoothingEnabled; } canvas_state_t; @@ -133,6 +135,7 @@ class Context2d: public Nan::ObjectWrap { static NAN_GETTER(GetFillStyle); static NAN_GETTER(GetStrokeStyle); static NAN_GETTER(GetFont); + static NAN_GETTER(GetTextTracking); static NAN_GETTER(GetTextBaseline); static NAN_GETTER(GetTextAlign); static NAN_SETTER(SetPatternQuality); @@ -155,6 +158,7 @@ class Context2d: public Nan::ObjectWrap { static NAN_SETTER(SetFillStyle); static NAN_SETTER(SetStrokeStyle); static NAN_SETTER(SetFont); + static NAN_SETTER(SetTextTracking); static NAN_SETTER(SetTextBaseline); static NAN_SETTER(SetTextAlign); inline void setContext(cairo_t *ctx) { _context = ctx; } @@ -193,6 +197,7 @@ class Context2d: public Nan::ObjectWrap { Nan::Persistent _fillStyle; Nan::Persistent _strokeStyle; Nan::Persistent _font; + Nan::Persistent _textTracking; Nan::Persistent _textBaseline; Nan::Persistent _textAlign; Canvas *_canvas; diff --git a/src/init.cc b/src/init.cc index 816ba5837..2a4045840 100644 --- a/src/init.cc +++ b/src/init.cc @@ -87,6 +87,10 @@ NAN_MODULE_INIT(init) { char freetype_version[10]; snprintf(freetype_version, 10, "%d.%d.%d", FREETYPE_MAJOR, FREETYPE_MINOR, FREETYPE_PATCH); Nan::Set(target, Nan::New("freetypeVersion").ToLocalChecked(), Nan::New(freetype_version).ToLocalChecked()).Check(); + + char pango_version[10]; + snprintf(pango_version, 10, "%d.%d.%d", PANGO_VERSION_MAJOR, PANGO_VERSION_MINOR, PANGO_VERSION_MICRO); + Nan::Set(target, Nan::New("pangoVersion").ToLocalChecked(), Nan::New(pango_version).ToLocalChecked()).Check(); } NODE_MODULE(canvas, init); diff --git a/test/canvas.test.js b/test/canvas.test.js index b28a33990..31d394730 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -11,10 +11,12 @@ const createImageData = require('../').createImageData const loadImage = require('../').loadImage const parseFont = require('../').parseFont const registerFont = require('../').registerFont +const pangoVersion = require('../').pangoVersion const assert = require('assert') const os = require('os') const Readable = require('stream').Readable +const semver = require('semver') describe('Canvas', function () { // Run with --expose-gc and uncomment this line to help find memory problems: @@ -446,6 +448,38 @@ describe('Canvas', function () { assert.equal(ctx.font, '15px Arial, sans-serif') }); + it('Context2d#font=small-caps', function () { + if (!semver.satisfies(pangoVersion, '>=1.37.1')){ + this.skip(); + } + + registerFont('./examples/crimsonFont/Crimson-Bold.ttf', {family: 'Crimson'}) + let canvas = createCanvas(200, 200), + ctx = canvas.getContext('2d'); + + // here is where the dot will appear above a lower-case 'i' (and be missing for small-caps) + let offsetX = 15, offsetY = -115; + + ctx.font = "180px Crimson"; + ctx.fillText('i', 20, 180); + let lcDottedI = ctx.getImageData(20 + offsetX, 180 + offsetY, 20, 20).data; + assert.equal(lcDottedI.some(p => p > 0), true); + + ctx.save() + + ctx.font = "small-caps 180px Crimson"; + ctx.fillText('i', 80, 180); + let scEmptySpace = ctx.getImageData(80 + offsetX, 180 + offsetY, 20, 20).data; + assert.equal(scEmptySpace.every(p => p == 0), true); + + ctx.restore() + + ctx.fillText('i', 140, 180); + let lcDottedAgain = ctx.getImageData(140 + offsetX, 180 + offsetY, 20, 20).data; + assert.equal(lcDottedAgain.some(p => p > 0), true); + }); + + it('Context2d#lineWidth=', function () { var canvas = createCanvas(200, 200) , ctx = canvas.getContext('2d'); @@ -540,6 +574,31 @@ describe('Canvas', function () { assert.equal('end', ctx.textAlign); }); + it('Context2d#textTracking', function () { + var canvas = createCanvas(200,200) + , ctx = canvas.getContext('2d') + , measureWidth = () => ctx.measureText('MMMMMMMMMMMMM').width; + + let normalWidth = measureWidth(); + assert.equal(0, ctx.textTracking); + + ctx.save(); + + ctx.textTracking = -500; + assert.equal(-500, ctx.textTracking); + assert.ok(measureWidth() < normalWidth / 2 ); + + ctx.textTracking = 1000; + assert.equal(1000, ctx.textTracking); + assert.ok(measureWidth() > normalWidth * 2 ); + + ctx.restore(); + + assert.equal(0, ctx.textTracking); + assert.equal(measureWidth(), normalWidth ); + }); + + describe('#toBuffer', function () { it('Canvas#toBuffer()', function () { var buf = createCanvas(200,200).toBuffer();