diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3c92ea1c8..c21812da6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -7,18 +7,18 @@ on: paths-ignore: - ".github/workflows/prebuild.yaml" -jobs: +jobs: Linux: name: Test on Linux runs-on: ubuntu-latest strategy: matrix: - node: [10, 12, 14, 16] + node: [10, 12, 14, 16, 18, 20] steps: - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install Dependencies run: | sudo apt update @@ -33,12 +33,12 @@ jobs: runs-on: windows-2019 strategy: matrix: - node: [10, 12, 14, 16] + node: [10, 12, 14, 16, 18, 20] steps: - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install Dependencies run: | Invoke-WebRequest "https://ftp-osl.osuosl.org/pub/gnome/binaries/win64/gtk+/2.22/gtk+-bundle_2.22.1-20101229_win64.zip" -OutFile "gtk.zip" @@ -57,15 +57,16 @@ jobs: runs-on: macos-latest strategy: matrix: - node: [10, 12, 14, 16] + node: [10, 12, 14, 16, 18, 20] steps: - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install Dependencies run: | brew update + brew install python3 || : # python doesn't need to be linked brew install pkg-config cairo pango libpng jpeg giflib librsvg - name: Install run: npm install --build-from-source @@ -79,7 +80,7 @@ jobs: - uses: actions/setup-node@v3 with: node-version: 14 - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install run: npm install --ignore-scripts - name: Lint diff --git a/.github/workflows/prebuild.yaml b/.github/workflows/prebuild.yaml index b10cc1522..784069e06 100644 --- a/.github/workflows/prebuild.yaml +++ b/.github/workflows/prebuild.yaml @@ -24,7 +24,7 @@ jobs: Linux: strategy: matrix: - node: [8, 9, 10, 11, 12, 13, 14] + node: [8, 9, 10, 11, 12, 13, 14, 16, 18, 20] canvas_tag: [] # e.g. "v2.6.1" name: ${{ matrix.canvas_tag}}, Node.js ${{ matrix.node }}, Linux runs-on: ubuntu-latest @@ -97,7 +97,7 @@ jobs: macOS: strategy: matrix: - node: [8, 9, 10, 11, 12, 13, 14] + node: [8, 9, 10, 11, 12, 13, 14, 16, 18, 20] canvas_tag: [] # e.g. "v2.6.1" name: ${{ matrix.canvas_tag}}, Node.js ${{ matrix.node }}, macOS runs-on: macos-latest @@ -163,7 +163,7 @@ jobs: Win: strategy: matrix: - node: [8, 9, 10, 11, 12, 13, 14] + node: [8, 9, 10, 11, 12, 13, 14, 16, 18, 20] canvas_tag: [] # e.g. "v2.6.1" name: ${{ matrix.canvas_tag}}, Node.js ${{ matrix.node }}, Windows runs-on: windows-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 513643483..e0768f8c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,45 @@ project adheres to [Semantic Versioning](http://semver.org/). (Unreleased) ================== ### Changed +* Defer the initialization of the `op` variable to the `default` switch case to avoid a compiler warning. (#2229) +* Use a `default` switch case with a null statement if some enum values aren't suppsed to be handled, this avoids a compiler warning. (#2229) +* Migrate from librsvg's deprecated `rsvg_handle_get_dimensions()` and `rsvg_handle_render_cairo()` functions to the new `rsvg_handle_get_intrinsic_size_in_pixels()` and `rsvg_handle_render_document()` respectively. (#2229) +* Avoid calling virtual methods in constructors/destructors to avoid bypassing virtual dispatch. (#2229) +* Remove unused private field `backend` in the `Backend` class. (#2229) +* Add Node.js v20 to CI. (#2237) ### Added +* Added string tags to support class detection ### Fixed +* Fix a case of use-after-free. (#2229) +* Fix usage of garbage value by filling the allocated memory entirely with zeros if it's not modified. (#2229) +* Fix a potential memory leak. (#2229) + +2.11.2 +================== +### Fixed +* Building on Windows in CI (and maybe other Windows configurations?) (#2216) + +2.11.1 +================== +### Fixed +* Add missing property `canvas` to the `CanvasRenderingContext2D` type +* Fixed glyph positions getting rounded, resulting text having a slight `letter-spacing` effect +* Fixed `ctx.font` not being restored correctly after `ctx.restore()` (#1946) + +2.11.0 +================== +### Fixed +* Replace triple-slash directive in types with own types to avoid polluting TS modules with globals ([#1656](https://github.com/Automattic/node-canvas/issues/1656)) + +2.10.2 +================== +### Fixed +* Fix `Assertion failed: (object->InternalFieldCount() > 0), function Unwrap, file nan_object_wrap.h, line 32.` ([#2025](https://github.com/Automattic/node-canvas/issues/2025)) +* `textBaseline` and `textAlign` were not saved/restored by `save()`/`restore()`. ([#1936](https://github.com/Automattic/node-canvas/issues/2029)) +* Update nan to v2.17.0 to ensure Node.js v18+ support. +### Changed +* Improve performance and memory usage of `save()`/`restore()`. +* `save()`/`restore()` no longer have a maximum depth (previously 64 states). 2.10.1 ================== diff --git a/Readme.md b/Readme.md index 992904ce3..cc945aa9a 100644 --- a/Readme.md +++ b/Readme.md @@ -91,7 +91,7 @@ This project is an implementation of the Web Canvas API and implements that API * [CanvasRenderingContext2D#patternQuality](#canvasrenderingcontext2dpatternquality) * [CanvasRenderingContext2D#quality](#canvasrenderingcontext2dquality) * [CanvasRenderingContext2D#textDrawingMode](#canvasrenderingcontext2dtextdrawingmode) -* [CanvasRenderingContext2D#globalCompositeOperator = 'saturate'](#canvasrenderingcontext2dglobalcompositeoperator--saturate) +* [CanvasRenderingContext2D#globalCompositeOperation = 'saturate'](#canvasrenderingcontext2dglobalcompositeoperation--saturate) * [CanvasRenderingContext2D#antialias](#canvasrenderingcontext2dantialias) ### createCanvas() diff --git a/benchmarks/run.js b/benchmarks/run.js index a6954da87..14f4db379 100644 --- a/benchmarks/run.js +++ b/benchmarks/run.js @@ -64,6 +64,18 @@ function done (benchmark, times, start, isAsync) { // node-canvas +bm('save/restore', function () { + for (let i = 0; i < 1000; i++) { + const max = i & 15 + for (let j = 0; j < max; ++j) { + ctx.save() + } + for (let j = 0; j < max; ++j) { + ctx.restore() + } + } +}) + bm('fillStyle= name', function () { for (let i = 0; i < 10000; i++) { ctx.fillStyle = '#fefefe' diff --git a/lib/bindings.js b/lib/bindings.js index c638a5878..40cef3c69 100644 --- a/lib/bindings.js +++ b/lib/bindings.js @@ -4,10 +4,40 @@ const bindings = require('../build/Release/canvas.node') module.exports = bindings +Object.defineProperty(bindings.Canvas.prototype, Symbol.toStringTag, { + value: 'HTMLCanvasElement', + configurable: true +}) + +Object.defineProperty(bindings.Image.prototype, Symbol.toStringTag, { + value: 'HTMLImageElement', + configurable: true +}) + bindings.ImageData.prototype.toString = function () { return '[object ImageData]' } +Object.defineProperty(bindings.ImageData.prototype, Symbol.toStringTag, { + value: 'ImageData', + configurable: true +}) + bindings.CanvasGradient.prototype.toString = function () { return '[object CanvasGradient]' } + +Object.defineProperty(bindings.CanvasGradient.prototype, Symbol.toStringTag, { + value: 'CanvasGradient', + configurable: true +}) + +Object.defineProperty(bindings.CanvasPattern.prototype, Symbol.toStringTag, { + value: 'CanvasPattern', + configurable: true +}) + +Object.defineProperty(bindings.CanvasRenderingContext2d.prototype, Symbol.toStringTag, { + value: 'CanvasRenderingContext2d', + configurable: true +}) diff --git a/package.json b/package.json index 034783003..77e72328c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.10.1", + "version": "2.11.2", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", @@ -51,7 +51,7 @@ "types": "types/index.d.ts", "dependencies": { "@mapbox/node-pre-gyp": "^1.0.0", - "nan": "^2.15.0", + "nan": "^2.17.0", "simple-get": "^3.0.3" }, "devDependencies": { diff --git a/src/Canvas.cc b/src/Canvas.cc index 3e339f033..0cfe750d6 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -35,6 +35,12 @@ "with at least a family (string) and optionally weight (string/number) " \ "and style (string)." +#define CHECK_RECEIVER(prop) \ + if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { \ + Nan::ThrowTypeError("Method " #prop " called on incompatible receiver"); \ + return; \ + } + using namespace v8; using namespace std; @@ -64,10 +70,10 @@ Canvas::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { #ifdef HAVE_JPEG Nan::SetPrototypeMethod(ctor, "streamJPEGSync", StreamJPEGSync); #endif - SetProtoAccessor(proto, Nan::New("type").ToLocalChecked(), GetType, NULL, ctor); - SetProtoAccessor(proto, Nan::New("stride").ToLocalChecked(), GetStride, NULL, ctor); - SetProtoAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, SetWidth, ctor); - SetProtoAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, SetHeight, ctor); + Nan::SetAccessor(proto, Nan::New("type").ToLocalChecked(), GetType); + Nan::SetAccessor(proto, Nan::New("stride").ToLocalChecked(), GetStride); + Nan::SetAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, SetWidth); + Nan::SetAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, SetHeight); Nan::SetTemplate(proto, "PNG_NO_FILTERS", Nan::New(PNG_NO_FILTERS)); Nan::SetTemplate(proto, "PNG_FILTER_NONE", Nan::New(PNG_FILTER_NONE)); @@ -127,8 +133,9 @@ NAN_METHOD(Canvas::New) { } if (!backend->isSurfaceValid()) { + const char *error = backend->getError(); delete backend; - return Nan::ThrowError(backend->getError()); + return Nan::ThrowError(error); } Canvas* canvas = new Canvas(backend); @@ -144,6 +151,7 @@ NAN_METHOD(Canvas::New) { */ NAN_GETTER(Canvas::GetType) { + CHECK_RECEIVER(Canvas.GetType); Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(canvas->backend()->getName()).ToLocalChecked()); } @@ -152,6 +160,7 @@ NAN_GETTER(Canvas::GetType) { * Get stride. */ NAN_GETTER(Canvas::GetStride) { + CHECK_RECEIVER(Canvas.GetStride); Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(canvas->stride())); } @@ -161,6 +170,7 @@ NAN_GETTER(Canvas::GetStride) { */ NAN_GETTER(Canvas::GetWidth) { + CHECK_RECEIVER(Canvas.GetWidth); Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(canvas->getWidth())); } @@ -170,6 +180,7 @@ NAN_GETTER(Canvas::GetWidth) { */ NAN_SETTER(Canvas::SetWidth) { + CHECK_RECEIVER(Canvas.SetWidth); if (value->IsNumber()) { Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); canvas->backend()->setWidth(Nan::To(value).FromMaybe(0)); @@ -182,6 +193,7 @@ NAN_SETTER(Canvas::SetWidth) { */ NAN_GETTER(Canvas::GetHeight) { + CHECK_RECEIVER(Canvas.GetHeight); Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(canvas->getHeight())); } @@ -191,6 +203,7 @@ NAN_GETTER(Canvas::GetHeight) { */ NAN_SETTER(Canvas::SetHeight) { + CHECK_RECEIVER(Canvas.SetHeight); if (value->IsNumber()) { Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); canvas->backend()->setHeight(Nan::To(value).FromMaybe(0)); @@ -773,13 +786,13 @@ NAN_METHOD(Canvas::RegisterFont) { NAN_METHOD(Canvas::DeregisterAllFonts) { // Unload all fonts from pango to free up memory bool success = true; - + std::for_each(font_face_list.begin(), font_face_list.end(), [&](FontFace& f) { if (!deregister_font( (unsigned char *)f.file_path )) success = false; pango_font_description_free(f.user_desc); pango_font_description_free(f.sys_desc); }); - + font_face_list.clear(); if (!success) Nan::ThrowError("Could not deregister one or more fonts"); } @@ -949,3 +962,5 @@ Local Canvas::Error(cairo_status_t status) { return Exception::Error(Nan::New(cairo_status_to_string(status)).ToLocalChecked()); } + +#undef CHECK_RECEIVER diff --git a/src/Canvas.h b/src/Canvas.h index f356af035..60d3b4216 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -11,15 +11,6 @@ #include #include -/* - * Maxmimum states per context. - * TODO: remove/resize - */ - -#ifndef CANVAS_MAX_STATES -#define CANVAS_MAX_STATES 64 -#endif - /* * 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) @@ -31,6 +22,29 @@ class FontFace { unsigned char file_path[1024]; }; +enum text_baseline_t : uint8_t { + TEXT_BASELINE_ALPHABETIC = 0, + TEXT_BASELINE_TOP = 1, + TEXT_BASELINE_BOTTOM = 2, + TEXT_BASELINE_MIDDLE = 3, + TEXT_BASELINE_IDEOGRAPHIC = 4, + TEXT_BASELINE_HANGING = 5 +}; + +enum text_align_t : int8_t { + TEXT_ALIGNMENT_LEFT = -1, + TEXT_ALIGNMENT_CENTER = 0, + TEXT_ALIGNMENT_RIGHT = 1, + // Currently same as LEFT and RIGHT without RTL support: + TEXT_ALIGNMENT_START = -2, + TEXT_ALIGNMENT_END = 2 +}; + +enum canvas_draw_mode_t : uint8_t { + TEXT_DRAW_PATHS, + TEXT_DRAW_GLYPHS +}; + /* * Canvas. */ diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 60a86cb3b..5bfe08d6a 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -36,20 +36,13 @@ Nan::Persistent Context2d::constructor; double width = args[2]; \ double height = args[3]; -constexpr double twoPi = M_PI * 2.; - -/* - * Text baselines. - */ +#define CHECK_RECEIVER(prop) \ + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { \ + Nan::ThrowTypeError("Method " #prop " called on incompatible receiver"); \ + return; \ + } -enum { - TEXT_BASELINE_ALPHABETIC - , TEXT_BASELINE_TOP - , TEXT_BASELINE_BOTTOM - , TEXT_BASELINE_MIDDLE - , TEXT_BASELINE_IDEOGRAPHIC - , TEXT_BASELINE_HANGING -}; +constexpr double twoPi = M_PI * 2.; /* * Simple helper macro for a rather verbose function call. @@ -142,29 +135,29 @@ Context2d::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { Nan::SetPrototypeMethod(ctor, "createPattern", CreatePattern); Nan::SetPrototypeMethod(ctor, "createLinearGradient", CreateLinearGradient); Nan::SetPrototypeMethod(ctor, "createRadialGradient", CreateRadialGradient); - SetProtoAccessor(proto, Nan::New("pixelFormat").ToLocalChecked(), GetFormat, NULL, ctor); - SetProtoAccessor(proto, Nan::New("patternQuality").ToLocalChecked(), GetPatternQuality, SetPatternQuality, ctor); - SetProtoAccessor(proto, Nan::New("imageSmoothingEnabled").ToLocalChecked(), GetImageSmoothingEnabled, SetImageSmoothingEnabled, ctor); - SetProtoAccessor(proto, Nan::New("globalCompositeOperation").ToLocalChecked(), GetGlobalCompositeOperation, SetGlobalCompositeOperation, ctor); - SetProtoAccessor(proto, Nan::New("globalAlpha").ToLocalChecked(), GetGlobalAlpha, SetGlobalAlpha, ctor); - SetProtoAccessor(proto, Nan::New("shadowColor").ToLocalChecked(), GetShadowColor, SetShadowColor, ctor); - SetProtoAccessor(proto, Nan::New("miterLimit").ToLocalChecked(), GetMiterLimit, SetMiterLimit, ctor); - SetProtoAccessor(proto, Nan::New("lineWidth").ToLocalChecked(), GetLineWidth, SetLineWidth, ctor); - SetProtoAccessor(proto, Nan::New("lineCap").ToLocalChecked(), GetLineCap, SetLineCap, ctor); - SetProtoAccessor(proto, Nan::New("lineJoin").ToLocalChecked(), GetLineJoin, SetLineJoin, ctor); - SetProtoAccessor(proto, Nan::New("lineDashOffset").ToLocalChecked(), GetLineDashOffset, SetLineDashOffset, ctor); - SetProtoAccessor(proto, Nan::New("shadowOffsetX").ToLocalChecked(), GetShadowOffsetX, SetShadowOffsetX, ctor); - SetProtoAccessor(proto, Nan::New("shadowOffsetY").ToLocalChecked(), GetShadowOffsetY, SetShadowOffsetY, ctor); - SetProtoAccessor(proto, Nan::New("shadowBlur").ToLocalChecked(), GetShadowBlur, SetShadowBlur, ctor); - SetProtoAccessor(proto, Nan::New("antialias").ToLocalChecked(), GetAntiAlias, SetAntiAlias, ctor); - SetProtoAccessor(proto, Nan::New("textDrawingMode").ToLocalChecked(), GetTextDrawingMode, SetTextDrawingMode, ctor); - SetProtoAccessor(proto, Nan::New("quality").ToLocalChecked(), GetQuality, SetQuality, ctor); - SetProtoAccessor(proto, Nan::New("currentTransform").ToLocalChecked(), GetCurrentTransform, SetCurrentTransform, ctor); - 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("textBaseline").ToLocalChecked(), GetTextBaseline, SetTextBaseline, ctor); - SetProtoAccessor(proto, Nan::New("textAlign").ToLocalChecked(), GetTextAlign, SetTextAlign, ctor); + Nan::SetAccessor(proto, Nan::New("pixelFormat").ToLocalChecked(), GetFormat); + Nan::SetAccessor(proto, Nan::New("patternQuality").ToLocalChecked(), GetPatternQuality, SetPatternQuality); + Nan::SetAccessor(proto, Nan::New("imageSmoothingEnabled").ToLocalChecked(), GetImageSmoothingEnabled, SetImageSmoothingEnabled); + Nan::SetAccessor(proto, Nan::New("globalCompositeOperation").ToLocalChecked(), GetGlobalCompositeOperation, SetGlobalCompositeOperation); + Nan::SetAccessor(proto, Nan::New("globalAlpha").ToLocalChecked(), GetGlobalAlpha, SetGlobalAlpha); + Nan::SetAccessor(proto, Nan::New("shadowColor").ToLocalChecked(), GetShadowColor, SetShadowColor); + Nan::SetAccessor(proto, Nan::New("miterLimit").ToLocalChecked(), GetMiterLimit, SetMiterLimit); + Nan::SetAccessor(proto, Nan::New("lineWidth").ToLocalChecked(), GetLineWidth, SetLineWidth); + Nan::SetAccessor(proto, Nan::New("lineCap").ToLocalChecked(), GetLineCap, SetLineCap); + Nan::SetAccessor(proto, Nan::New("lineJoin").ToLocalChecked(), GetLineJoin, SetLineJoin); + Nan::SetAccessor(proto, Nan::New("lineDashOffset").ToLocalChecked(), GetLineDashOffset, SetLineDashOffset); + Nan::SetAccessor(proto, Nan::New("shadowOffsetX").ToLocalChecked(), GetShadowOffsetX, SetShadowOffsetX); + Nan::SetAccessor(proto, Nan::New("shadowOffsetY").ToLocalChecked(), GetShadowOffsetY, SetShadowOffsetY); + Nan::SetAccessor(proto, Nan::New("shadowBlur").ToLocalChecked(), GetShadowBlur, SetShadowBlur); + Nan::SetAccessor(proto, Nan::New("antialias").ToLocalChecked(), GetAntiAlias, SetAntiAlias); + Nan::SetAccessor(proto, Nan::New("textDrawingMode").ToLocalChecked(), GetTextDrawingMode, SetTextDrawingMode); + Nan::SetAccessor(proto, Nan::New("quality").ToLocalChecked(), GetQuality, SetQuality); + Nan::SetAccessor(proto, Nan::New("currentTransform").ToLocalChecked(), GetCurrentTransform, SetCurrentTransform); + Nan::SetAccessor(proto, Nan::New("fillStyle").ToLocalChecked(), GetFillStyle, SetFillStyle); + Nan::SetAccessor(proto, Nan::New("strokeStyle").ToLocalChecked(), GetStrokeStyle, SetStrokeStyle); + Nan::SetAccessor(proto, Nan::New("font").ToLocalChecked(), GetFont, SetFont); + Nan::SetAccessor(proto, Nan::New("textBaseline").ToLocalChecked(), GetTextBaseline, SetTextBaseline); + Nan::SetAccessor(proto, Nan::New("textAlign").ToLocalChecked(), GetTextAlign, SetTextAlign); Local ctx = Nan::GetCurrentContext(); Nan::Set(target, Nan::New("CanvasRenderingContext2d").ToLocalChecked(), ctor->GetFunction(ctx).ToLocalChecked()); Nan::Set(target, Nan::New("CanvasRenderingContext2dInit").ToLocalChecked(), Nan::New(SaveExternalModules)); @@ -178,9 +171,16 @@ Context2d::Context2d(Canvas *canvas) { _canvas = canvas; _context = canvas->createCairoContext(); _layout = pango_cairo_create_layout(_context); - state = states[stateno = 0] = (canvas_state_t *) malloc(sizeof(canvas_state_t)); - resetState(true); + // As of January 2023, Pango rounds glyph positions which renders text wider + // or narrower than the browser. See #2184 for more information +#if PANGO_VERSION_CHECK(1, 44, 0) + pango_context_set_round_glyph_positions(pango_layout_get_context(_layout), FALSE); +#endif + + states.emplace(); + state = &states.top(); + pango_layout_set_font_description(_layout, state->fontDescription); } /* @@ -188,10 +188,6 @@ Context2d::Context2d(Canvas *canvas) { */ Context2d::~Context2d() { - while(stateno >= 0) { - pango_font_description_free(states[stateno]->fontDescription); - free(states[stateno--]); - } g_object_unref(_layout); cairo_destroy(_context); _resetPersistentHandles(); @@ -201,41 +197,16 @@ Context2d::~Context2d() { * Reset canvas state. */ -void Context2d::resetState(bool init) { - if (!init) { - pango_font_description_free(state->fontDescription); - } - - state->shadowBlur = 0; - state->shadowOffsetX = state->shadowOffsetY = 0; - state->globalAlpha = 1; - state->textAlignment = -1; - state->fillPattern = nullptr; - state->strokePattern = nullptr; - state->fillGradient = nullptr; - state->strokeGradient = nullptr; - state->textBaseline = TEXT_BASELINE_ALPHABETIC; - rgba_t transparent = { 0, 0, 0, 1 }; - rgba_t transparent_black = { 0, 0, 0, 0 }; - state->fill = transparent; - state->stroke = transparent; - state->shadow = transparent_black; - state->patternQuality = CAIRO_FILTER_GOOD; - state->imageSmoothingEnabled = true; - state->textDrawingMode = TEXT_DRAW_PATHS; - state->fontDescription = pango_font_description_from_string("sans"); - pango_font_description_set_absolute_size(state->fontDescription, 10 * PANGO_SCALE); +void Context2d::resetState() { + states.pop(); + states.emplace(); pango_layout_set_font_description(_layout, state->fontDescription); - _resetPersistentHandles(); } void Context2d::_resetPersistentHandles() { _fillStyle.Reset(); _strokeStyle.Reset(); - _font.Reset(); - _textBaseline.Reset(); - _textAlign.Reset(); } /* @@ -244,13 +215,9 @@ void Context2d::_resetPersistentHandles() { void Context2d::save() { - if (stateno < CANVAS_MAX_STATES) { - cairo_save(_context); - 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); - state = states[stateno]; - } + cairo_save(_context); + states.emplace(states.top()); + state = &states.top(); } /* @@ -259,12 +226,10 @@ Context2d::save() { void Context2d::restore() { - if (stateno > 0) { + if (states.size() > 1) { cairo_restore(_context); - pango_font_description_free(states[stateno]->fontDescription); - free(states[stateno]); - states[stateno] = NULL; - state = states[--stateno]; + states.pop(); + state = &states.top(); pango_layout_set_font_description(_layout, state->fontDescription); } } @@ -642,8 +607,8 @@ Context2d::blur(cairo_surface_t *surface, int radius) { // get width, height int width = cairo_image_surface_get_width( surface ); int height = cairo_image_surface_get_height( surface ); - unsigned* precalc = - (unsigned*)malloc(width*height*sizeof(unsigned)); + const unsigned int size = width * height * sizeof(unsigned); + unsigned* precalc = (unsigned*)malloc(size); cairo_surface_flush( surface ); unsigned char* src = cairo_image_surface_get_data( surface ); double mul=1.f/((radius*2)*(radius*2)); @@ -662,6 +627,8 @@ Context2d::blur(cairo_surface_t *surface, int radius) { unsigned char* pix = src; unsigned* pre = precalc; + bool modified = false; + pix += channel; for (y=0;y0) tot+=pre[-width]; if (x>0 && y>0) tot-=pre[-width-1]; *pre++=tot; + if (!modified) modified = true; pix += 4; } } + if (!modified) { + memset(precalc, 0, size); + } + // blur step. pix = src + (int)radius * width * 4 + (int)radius * 4 + channel; for (y=radius;y(info.This()); std::string pixelFormatString; switch (context->canvas()->backend()->getFormat()) { @@ -1439,6 +1412,7 @@ NAN_METHOD(Context2d::DrawImage) { */ NAN_GETTER(Context2d::GetGlobalAlpha) { + CHECK_RECEIVER(Context2d.GetGlobalAlpha); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(context->state->globalAlpha)); } @@ -1448,6 +1422,7 @@ NAN_GETTER(Context2d::GetGlobalAlpha) { */ NAN_SETTER(Context2d::SetGlobalAlpha) { + CHECK_RECEIVER(Context2d.SetGlobalAlpha); double n = Nan::To(value).FromMaybe(0); if (n >= 0 && n <= 1) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -1460,10 +1435,11 @@ NAN_SETTER(Context2d::SetGlobalAlpha) { */ NAN_GETTER(Context2d::GetGlobalCompositeOperation) { + CHECK_RECEIVER(Context2d.GetGlobalCompositeOperation); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); - const char *op = "source-over"; + const char *op{}; switch (cairo_get_operator(ctx)) { // composite modes: case CAIRO_OPERATOR_CLEAR: op = "clear"; break; @@ -1500,6 +1476,7 @@ NAN_GETTER(Context2d::GetGlobalCompositeOperation) { case CAIRO_OPERATOR_HSL_LUMINOSITY: op = "luminosity"; break; // non-standard: case CAIRO_OPERATOR_SATURATE: op = "saturate"; break; + default: op = "source-over"; } info.GetReturnValue().Set(Nan::New(op).ToLocalChecked()); @@ -1510,6 +1487,7 @@ NAN_GETTER(Context2d::GetGlobalCompositeOperation) { */ NAN_SETTER(Context2d::SetPatternQuality) { + CHECK_RECEIVER(Context2d.SetPatternQuality); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Nan::Utf8String quality(Nan::To(value).ToLocalChecked()); if (0 == strcmp("fast", *quality)) { @@ -1530,6 +1508,7 @@ NAN_SETTER(Context2d::SetPatternQuality) { */ NAN_GETTER(Context2d::GetPatternQuality) { + CHECK_RECEIVER(Context2d.GetPatternQuality); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *quality; switch (context->state->patternQuality) { @@ -1547,6 +1526,7 @@ NAN_GETTER(Context2d::GetPatternQuality) { */ NAN_SETTER(Context2d::SetImageSmoothingEnabled) { + CHECK_RECEIVER(Context2d.SetImageSmoothingEnabled); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); context->state->imageSmoothingEnabled = Nan::To(value).FromMaybe(false); } @@ -1556,6 +1536,7 @@ NAN_SETTER(Context2d::SetImageSmoothingEnabled) { */ NAN_GETTER(Context2d::GetImageSmoothingEnabled) { + CHECK_RECEIVER(Context2d.GetImageSmoothingEnabled); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(context->state->imageSmoothingEnabled)); } @@ -1565,6 +1546,7 @@ NAN_GETTER(Context2d::GetImageSmoothingEnabled) { */ NAN_SETTER(Context2d::SetGlobalCompositeOperation) { + CHECK_RECEIVER(Context2d.SetGlobalCompositeOperation); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); Nan::Utf8String opStr(Nan::To(value).ToLocalChecked()); // Unlike CSS colors, this *is* case-sensitive @@ -1612,6 +1594,7 @@ NAN_SETTER(Context2d::SetGlobalCompositeOperation) { */ NAN_GETTER(Context2d::GetShadowOffsetX) { + CHECK_RECEIVER(Context2d.GetShadowOffsetX); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(context->state->shadowOffsetX)); } @@ -1621,6 +1604,7 @@ NAN_GETTER(Context2d::GetShadowOffsetX) { */ NAN_SETTER(Context2d::SetShadowOffsetX) { + CHECK_RECEIVER(Context2d.SetShadowOffsetX); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); context->state->shadowOffsetX = Nan::To(value).FromMaybe(0); } @@ -1630,6 +1614,7 @@ NAN_SETTER(Context2d::SetShadowOffsetX) { */ NAN_GETTER(Context2d::GetShadowOffsetY) { + CHECK_RECEIVER(Context2d.GetShadowOffsetY); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(context->state->shadowOffsetY)); } @@ -1639,6 +1624,7 @@ NAN_GETTER(Context2d::GetShadowOffsetY) { */ NAN_SETTER(Context2d::SetShadowOffsetY) { + CHECK_RECEIVER(Context2d.SetShadowOffsetY); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); context->state->shadowOffsetY = Nan::To(value).FromMaybe(0); } @@ -1648,6 +1634,7 @@ NAN_SETTER(Context2d::SetShadowOffsetY) { */ NAN_GETTER(Context2d::GetShadowBlur) { + CHECK_RECEIVER(Context2d.GetShadowBlur); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(context->state->shadowBlur)); } @@ -1657,6 +1644,7 @@ NAN_GETTER(Context2d::GetShadowBlur) { */ NAN_SETTER(Context2d::SetShadowBlur) { + CHECK_RECEIVER(Context2d.SetShadowBlur); int n = Nan::To(value).FromMaybe(0); if (n >= 0) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -1669,6 +1657,7 @@ NAN_SETTER(Context2d::SetShadowBlur) { */ NAN_GETTER(Context2d::GetAntiAlias) { + CHECK_RECEIVER(Context2d.GetAntiAlias); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *aa; switch (cairo_get_antialias(context->context())) { @@ -1685,6 +1674,7 @@ NAN_GETTER(Context2d::GetAntiAlias) { */ NAN_SETTER(Context2d::SetAntiAlias) { + CHECK_RECEIVER(Context2d.SetAntiAlias); Nan::Utf8String str(Nan::To(value).ToLocalChecked()); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); @@ -1708,6 +1698,7 @@ NAN_SETTER(Context2d::SetAntiAlias) { */ NAN_GETTER(Context2d::GetTextDrawingMode) { + CHECK_RECEIVER(Context2d.GetTextDrawingMode); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *mode; if (context->state->textDrawingMode == TEXT_DRAW_PATHS) { @@ -1725,6 +1716,7 @@ NAN_GETTER(Context2d::GetTextDrawingMode) { */ NAN_SETTER(Context2d::SetTextDrawingMode) { + CHECK_RECEIVER(Context2d.SetTextDrawingMode); Nan::Utf8String str(Nan::To(value).ToLocalChecked()); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); if (0 == strcmp("path", *str)) { @@ -1739,6 +1731,7 @@ NAN_SETTER(Context2d::SetTextDrawingMode) { */ NAN_GETTER(Context2d::GetQuality) { + CHECK_RECEIVER(Context2d.GetQuality); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *filter; switch (cairo_pattern_get_filter(cairo_get_source(context->context()))) { @@ -1756,6 +1749,7 @@ NAN_GETTER(Context2d::GetQuality) { */ NAN_SETTER(Context2d::SetQuality) { + CHECK_RECEIVER(Context2d.SetQuality); Nan::Utf8String str(Nan::To(value).ToLocalChecked()); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_filter_t filter; @@ -1818,6 +1812,7 @@ void parse_matrix_from_object(cairo_matrix_t &matrix, Local mat) { */ NAN_GETTER(Context2d::GetCurrentTransform) { + CHECK_RECEIVER(Context2d.GetCurrentTransform); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Local instance = get_current_transform(context); @@ -1829,6 +1824,7 @@ NAN_GETTER(Context2d::GetCurrentTransform) { */ NAN_SETTER(Context2d::SetCurrentTransform) { + CHECK_RECEIVER(Context2d.SetCurrentTransform); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Local ctx = Nan::GetCurrentContext(); Local mat = Nan::To(value).ToLocalChecked(); @@ -1850,6 +1846,7 @@ NAN_SETTER(Context2d::SetCurrentTransform) { */ NAN_GETTER(Context2d::GetFillStyle) { + CHECK_RECEIVER(Context2d.GetFillStyle); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Isolate *iso = Isolate::GetCurrent(); Local style; @@ -1867,6 +1864,7 @@ NAN_GETTER(Context2d::GetFillStyle) { */ NAN_SETTER(Context2d::SetFillStyle) { + CHECK_RECEIVER(Context2d.SetFillStyle); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); if (value->IsString()) { @@ -1894,6 +1892,7 @@ NAN_SETTER(Context2d::SetFillStyle) { */ NAN_GETTER(Context2d::GetStrokeStyle) { + CHECK_RECEIVER(Context2d.GetStrokeStyle); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Local style; @@ -1910,6 +1909,7 @@ NAN_GETTER(Context2d::GetStrokeStyle) { */ NAN_SETTER(Context2d::SetStrokeStyle) { + CHECK_RECEIVER(Context2d.SetStrokeStyle); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); if (value->IsString()) { @@ -1937,6 +1937,7 @@ NAN_SETTER(Context2d::SetStrokeStyle) { */ NAN_GETTER(Context2d::GetMiterLimit) { + CHECK_RECEIVER(Context2d.GetMiterLimit); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(cairo_get_miter_limit(context->context()))); } @@ -1946,6 +1947,7 @@ NAN_GETTER(Context2d::GetMiterLimit) { */ NAN_SETTER(Context2d::SetMiterLimit) { + CHECK_RECEIVER(Context2d.SetMiterLimit); double n = Nan::To(value).FromMaybe(0); if (n > 0) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -1958,6 +1960,7 @@ NAN_SETTER(Context2d::SetMiterLimit) { */ NAN_GETTER(Context2d::GetLineWidth) { + CHECK_RECEIVER(Context2d.GetLineWidth); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(cairo_get_line_width(context->context()))); } @@ -1967,6 +1970,7 @@ NAN_GETTER(Context2d::GetLineWidth) { */ NAN_SETTER(Context2d::SetLineWidth) { + CHECK_RECEIVER(Context2d.SetLineWidth); double n = Nan::To(value).FromMaybe(0); if (n > 0 && n != std::numeric_limits::infinity()) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -1979,6 +1983,7 @@ NAN_SETTER(Context2d::SetLineWidth) { */ NAN_GETTER(Context2d::GetLineJoin) { + CHECK_RECEIVER(Context2d.GetLineJoin); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *join; switch (cairo_get_line_join(context->context())) { @@ -1994,6 +1999,7 @@ NAN_GETTER(Context2d::GetLineJoin) { */ NAN_SETTER(Context2d::SetLineJoin) { + CHECK_RECEIVER(Context2d.SetLineJoin); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); Nan::Utf8String type(Nan::To(value).ToLocalChecked()); @@ -2011,6 +2017,7 @@ NAN_SETTER(Context2d::SetLineJoin) { */ NAN_GETTER(Context2d::GetLineCap) { + CHECK_RECEIVER(Context2d.GetLineCap); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *cap; switch (cairo_get_line_cap(context->context())) { @@ -2026,6 +2033,7 @@ NAN_GETTER(Context2d::GetLineCap) { */ NAN_SETTER(Context2d::SetLineCap) { + CHECK_RECEIVER(Context2d.SetLineCap); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); Nan::Utf8String type(Nan::To(value).ToLocalChecked()); @@ -2060,6 +2068,7 @@ NAN_METHOD(Context2d::IsPointInPath) { */ NAN_SETTER(Context2d::SetShadowColor) { + CHECK_RECEIVER(Context2d.SetShadowColor); short ok; Nan::Utf8String str(Nan::To(value).ToLocalChecked()); uint32_t rgba = rgba_from_string(*str, &ok); @@ -2074,6 +2083,7 @@ NAN_SETTER(Context2d::SetShadowColor) { */ NAN_GETTER(Context2d::GetShadowColor) { + CHECK_RECEIVER(Context2d.GetShadowColor); char buf[64]; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); rgba_to_string(context->state->shadow, buf, sizeof(buf)); @@ -2496,16 +2506,16 @@ Context2d::setTextPath(double x, double y) { PangoRectangle logical_rect; switch (state->textAlignment) { - // center - case 0: + case TEXT_ALIGNMENT_CENTER: pango_layout_get_pixel_extents(_layout, NULL, &logical_rect); x -= logical_rect.width / 2; break; - // right - case 1: + case TEXT_ALIGNMENT_END: + case TEXT_ALIGNMENT_RIGHT: pango_layout_get_pixel_extents(_layout, NULL, &logical_rect); x -= logical_rect.width; break; + default: ; } y -= getBaselineAdjustment(_layout, state->textBaseline); @@ -2549,16 +2559,10 @@ NAN_METHOD(Context2d::MoveTo) { */ NAN_GETTER(Context2d::GetFont) { + CHECK_RECEIVER(Context2d.GetFont); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Isolate *iso = Isolate::GetCurrent(); - Local font; - if (context->_font.IsEmpty()) - font = Nan::New("10px sans-serif").ToLocalChecked(); - else - font = context->_font.Get(iso); - - info.GetReturnValue().Set(font); + info.GetReturnValue().Set(Nan::New(context->state->font).ToLocalChecked()); } /* @@ -2571,6 +2575,7 @@ NAN_GETTER(Context2d::GetFont) { */ NAN_SETTER(Context2d::SetFont) { + CHECK_RECEIVER(Context2d.SetFont); if (!value->IsString()) return; Isolate *iso = Isolate::GetCurrent(); @@ -2620,7 +2625,7 @@ NAN_SETTER(Context2d::SetFont) { context->state->fontDescription = sys_desc; pango_layout_set_font_description(context->_layout, sys_desc); - context->_font.Reset(value); + context->state->font = *Nan::Utf8String(value); } /* @@ -2628,16 +2633,19 @@ NAN_SETTER(Context2d::SetFont) { */ NAN_GETTER(Context2d::GetTextBaseline) { + CHECK_RECEIVER(Context2d.GetTextBaseline); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Isolate *iso = Isolate::GetCurrent(); - Local font; - - if (context->_textBaseline.IsEmpty()) - font = Nan::New("alphabetic").ToLocalChecked(); - else - font = context->_textBaseline.Get(iso); - - info.GetReturnValue().Set(font); + const char* baseline; + switch (context->state->textBaseline) { + default: + case TEXT_BASELINE_ALPHABETIC: baseline = "alphabetic"; break; + case TEXT_BASELINE_TOP: baseline = "top"; break; + case TEXT_BASELINE_BOTTOM: baseline = "bottom"; break; + case TEXT_BASELINE_MIDDLE: baseline = "middle"; break; + case TEXT_BASELINE_IDEOGRAPHIC: baseline = "ideographic"; break; + case TEXT_BASELINE_HANGING: baseline = "hanging"; break; + } + info.GetReturnValue().Set(Nan::New(baseline).ToLocalChecked()); } /* @@ -2645,23 +2653,23 @@ NAN_GETTER(Context2d::GetTextBaseline) { */ NAN_SETTER(Context2d::SetTextBaseline) { + CHECK_RECEIVER(Context2d.SetTextBaseline); if (!value->IsString()) return; Nan::Utf8String opStr(Nan::To(value).ToLocalChecked()); - const std::map modes = { - {"alphabetic", 0}, - {"top", 1}, - {"bottom", 2}, - {"middle", 3}, - {"ideographic", 4}, - {"hanging", 5} + const std::map modes = { + {"alphabetic", TEXT_BASELINE_ALPHABETIC}, + {"top", TEXT_BASELINE_TOP}, + {"bottom", TEXT_BASELINE_BOTTOM}, + {"middle", TEXT_BASELINE_MIDDLE}, + {"ideographic", TEXT_BASELINE_IDEOGRAPHIC}, + {"hanging", TEXT_BASELINE_HANGING} }; auto op = modes.find(*opStr); if (op == modes.end()) return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); context->state->textBaseline = op->second; - context->_textBaseline.Reset(value); } /* @@ -2669,16 +2677,19 @@ NAN_SETTER(Context2d::SetTextBaseline) { */ NAN_GETTER(Context2d::GetTextAlign) { + CHECK_RECEIVER(Context2d.GetTextAlign); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Isolate *iso = Isolate::GetCurrent(); - Local font; - - if (context->_textAlign.IsEmpty()) - font = Nan::New("start").ToLocalChecked(); - else - font = context->_textAlign.Get(iso); - - info.GetReturnValue().Set(font); + const char* align; + switch (context->state->textAlignment) { + default: + // TODO the default is supposed to be "start" + case TEXT_ALIGNMENT_LEFT: align = "left"; break; + case TEXT_ALIGNMENT_START: align = "start"; break; + case TEXT_ALIGNMENT_CENTER: align = "center"; break; + case TEXT_ALIGNMENT_RIGHT: align = "right"; break; + case TEXT_ALIGNMENT_END: align = "end"; break; + } + info.GetReturnValue().Set(Nan::New(align).ToLocalChecked()); } /* @@ -2686,22 +2697,22 @@ NAN_GETTER(Context2d::GetTextAlign) { */ NAN_SETTER(Context2d::SetTextAlign) { + CHECK_RECEIVER(Context2d.SetTextAlign); if (!value->IsString()) return; Nan::Utf8String opStr(Nan::To(value).ToLocalChecked()); - const std::map modes = { - {"center", 0}, - {"left", -1}, - {"start", -1}, - {"right", 1}, - {"end", 1} + const std::map modes = { + {"center", TEXT_ALIGNMENT_CENTER}, + {"left", TEXT_ALIGNMENT_LEFT}, + {"start", TEXT_ALIGNMENT_START}, + {"right", TEXT_ALIGNMENT_RIGHT}, + {"end", TEXT_ALIGNMENT_END} }; auto op = modes.find(*opStr); if (op == modes.end()) return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); context->state->textAlignment = op->second; - context->_textAlign.Reset(value); } /* @@ -2747,13 +2758,16 @@ NAN_METHOD(Context2d::MeasureText) { double x_offset; switch (context->state->textAlignment) { - case 0: // center + case TEXT_ALIGNMENT_CENTER: x_offset = logical_rect.width / 2.; break; - case 1: // right + case TEXT_ALIGNMENT_END: + case TEXT_ALIGNMENT_RIGHT: x_offset = logical_rect.width; break; - default: // left + case TEXT_ALIGNMENT_START: + case TEXT_ALIGNMENT_LEFT: + default: x_offset = 0.0; } @@ -2846,6 +2860,7 @@ NAN_METHOD(Context2d::GetLineDash) { * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash */ NAN_SETTER(Context2d::SetLineDashOffset) { + CHECK_RECEIVER(Context2d.SetLineDashOffset); double offset = Nan::To(value).FromMaybe(0); if (!std::isfinite(offset)) return; @@ -2863,6 +2878,7 @@ NAN_SETTER(Context2d::SetLineDashOffset) { * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash */ NAN_GETTER(Context2d::GetLineDashOffset) { + CHECK_RECEIVER(Context2d.GetLineDashOffset); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); double offset; @@ -3349,3 +3365,5 @@ NAN_METHOD(Context2d::Ellipse) { } cairo_set_matrix(ctx, &save_matrix); } + +#undef CHECK_RECEIVER diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index 89c86df67..8ea4d60b8 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -7,11 +7,7 @@ #include "color.h" #include "nan.h" #include - -typedef enum { - TEXT_DRAW_PATHS, - TEXT_DRAW_GLYPHS -} canvas_draw_mode_t; +#include /* * State struct. @@ -20,25 +16,56 @@ typedef enum { * cairo's gstate maintains only a single source pattern at a time. */ -typedef struct { - rgba_t fill; - rgba_t stroke; - cairo_filter_t patternQuality; - cairo_pattern_t *fillPattern; - cairo_pattern_t *strokePattern; - cairo_pattern_t *fillGradient; - cairo_pattern_t *strokeGradient; - float globalAlpha; - short textAlignment; - short textBaseline; - rgba_t shadow; - int shadowBlur; - double shadowOffsetX; - double shadowOffsetY; - canvas_draw_mode_t textDrawingMode; - PangoFontDescription *fontDescription; - bool imageSmoothingEnabled; -} canvas_state_t; +struct canvas_state_t { + rgba_t fill = { 0, 0, 0, 1 }; + rgba_t stroke = { 0, 0, 0, 1 }; + rgba_t shadow = { 0, 0, 0, 0 }; + double shadowOffsetX = 0.; + double shadowOffsetY = 0.; + cairo_pattern_t* fillPattern = nullptr; + cairo_pattern_t* strokePattern = nullptr; + cairo_pattern_t* fillGradient = nullptr; + cairo_pattern_t* strokeGradient = nullptr; + PangoFontDescription* fontDescription = nullptr; + std::string font = "10px sans-serif"; + cairo_filter_t patternQuality = CAIRO_FILTER_GOOD; + float globalAlpha = 1.f; + int shadowBlur = 0; + text_align_t textAlignment = TEXT_ALIGNMENT_LEFT; // TODO default is supposed to be START + text_baseline_t textBaseline = TEXT_BASELINE_ALPHABETIC; + canvas_draw_mode_t textDrawingMode = TEXT_DRAW_PATHS; + bool imageSmoothingEnabled = true; + + canvas_state_t() { + fontDescription = pango_font_description_from_string("sans"); + pango_font_description_set_absolute_size(fontDescription, 10 * PANGO_SCALE); + } + + canvas_state_t(const canvas_state_t& other) { + fill = other.fill; + stroke = other.stroke; + patternQuality = other.patternQuality; + fillPattern = other.fillPattern; + strokePattern = other.strokePattern; + fillGradient = other.fillGradient; + strokeGradient = other.strokeGradient; + globalAlpha = other.globalAlpha; + textAlignment = other.textAlignment; + textBaseline = other.textBaseline; + shadow = other.shadow; + shadowBlur = other.shadowBlur; + shadowOffsetX = other.shadowOffsetX; + shadowOffsetY = other.shadowOffsetY; + textDrawingMode = other.textDrawingMode; + fontDescription = pango_font_description_copy(other.fontDescription); + font = other.font; + imageSmoothingEnabled = other.imageSmoothingEnabled; + } + + ~canvas_state_t() { + pango_font_description_free(fontDescription); + } +}; /* * Equivalent to a PangoRectangle but holds floats instead of ints @@ -54,12 +81,9 @@ typedef struct { float height; } float_rectangle; -void state_assign_fontFamily(canvas_state_t *state, const char *str); - -class Context2d: public Nan::ObjectWrap { +class Context2d : public Nan::ObjectWrap { public: - short stateno; - canvas_state_t *states[CANVAS_MAX_STATES]; + std::stack states; canvas_state_t *state; Context2d(Canvas *canvas); static Nan::Persistent _DOMMatrix; @@ -180,7 +204,7 @@ class Context2d: public Nan::ObjectWrap { void save(); void restore(); void setFontFromState(); - void resetState(bool init = false); + void resetState(); inline PangoLayout *layout(){ return _layout; } private: @@ -194,9 +218,6 @@ class Context2d: public Nan::ObjectWrap { void _setStrokePattern(v8::Local arg); Nan::Persistent _fillStyle; Nan::Persistent _strokeStyle; - Nan::Persistent _font; - Nan::Persistent _textBaseline; - Nan::Persistent _textAlign; Canvas *_canvas; cairo_t *_context; cairo_path_t *_path; diff --git a/src/Image.cc b/src/Image.cc index 103b65ee7..301257769 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -8,7 +8,6 @@ #include #include #include -#include "Util.h" /* Cairo limit: * https://lists.cairographics.org/archives/cairo/2010-December/021422.html @@ -60,12 +59,12 @@ Image::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { // Prototype Local proto = ctor->PrototypeTemplate(); - SetProtoAccessor(proto, Nan::New("complete").ToLocalChecked(), GetComplete, NULL, ctor); - SetProtoAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, SetWidth, ctor); - SetProtoAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, SetHeight, ctor); - SetProtoAccessor(proto, Nan::New("naturalWidth").ToLocalChecked(), GetNaturalWidth, NULL, ctor); - SetProtoAccessor(proto, Nan::New("naturalHeight").ToLocalChecked(), GetNaturalHeight, NULL, ctor); - SetProtoAccessor(proto, Nan::New("dataMode").ToLocalChecked(), GetDataMode, SetDataMode, ctor); + Nan::SetAccessor(proto, Nan::New("complete").ToLocalChecked(), GetComplete); + Nan::SetAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, SetWidth); + Nan::SetAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, SetHeight); + Nan::SetAccessor(proto, Nan::New("naturalWidth").ToLocalChecked(), GetNaturalWidth); + Nan::SetAccessor(proto, Nan::New("naturalHeight").ToLocalChecked(), GetNaturalHeight); + Nan::SetAccessor(proto, Nan::New("dataMode").ToLocalChecked(), GetDataMode, SetDataMode); ctor->Set(Nan::New("MODE_IMAGE").ToLocalChecked(), Nan::New(DATA_IMAGE)); ctor->Set(Nan::New("MODE_MIME").ToLocalChecked(), Nan::New(DATA_MIME)); @@ -108,6 +107,10 @@ NAN_GETTER(Image::GetComplete) { */ NAN_GETTER(Image::GetDataMode) { + if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Image.GetDataMode called on incompatible receiver"); + return; + } Image *img = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(img->data_mode)); } @@ -117,6 +120,10 @@ NAN_GETTER(Image::GetDataMode) { */ NAN_SETTER(Image::SetDataMode) { + if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Image.SetDataMode called on incompatible receiver"); + return; + } if (value->IsNumber()) { Image *img = Nan::ObjectWrap::Unwrap(info.This()); int mode = Nan::To(value).FromMaybe(0); @@ -129,6 +136,10 @@ NAN_SETTER(Image::SetDataMode) { */ NAN_GETTER(Image::GetNaturalWidth) { + if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Image.GetNaturalWidth called on incompatible receiver"); + return; + } Image *img = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(img->naturalWidth)); } @@ -138,6 +149,10 @@ NAN_GETTER(Image::GetNaturalWidth) { */ NAN_GETTER(Image::GetWidth) { + if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Image.GetWidth called on incompatible receiver"); + return; + } Image *img = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(img->width)); } @@ -147,6 +162,10 @@ NAN_GETTER(Image::GetWidth) { */ NAN_SETTER(Image::SetWidth) { + if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Image.SetWidth called on incompatible receiver"); + return; + } if (value->IsNumber()) { Image *img = Nan::ObjectWrap::Unwrap(info.This()); img->width = Nan::To(value).FromMaybe(0); @@ -158,6 +177,10 @@ NAN_SETTER(Image::SetWidth) { */ NAN_GETTER(Image::GetNaturalHeight) { + if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Image.GetNaturalHeight called on incompatible receiver"); + return; + } Image *img = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(img->naturalHeight)); } @@ -167,6 +190,10 @@ NAN_GETTER(Image::GetNaturalHeight) { */ NAN_GETTER(Image::GetHeight) { + if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Image.GetHeight called on incompatible receiver"); + return; + } Image *img = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(img->height)); } @@ -175,6 +202,11 @@ NAN_GETTER(Image::GetHeight) { */ NAN_SETTER(Image::SetHeight) { + if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + // #1534 + Nan::ThrowTypeError("Method Image.SetHeight called on incompatible receiver"); + return; + } if (value->IsNumber()) { Image *img = Nan::ObjectWrap::Unwrap(info.This()); img->height = Nan::To(value).FromMaybe(0); @@ -1160,11 +1192,13 @@ Image::loadSVGFromBuffer(uint8_t *buf, unsigned len) { return CAIRO_STATUS_READ_ERROR; } - RsvgDimensionData *dims = new RsvgDimensionData(); - rsvg_handle_get_dimensions(_rsvg, dims); + double d_width; + double d_height; + + rsvg_handle_get_intrinsic_size_in_pixels(_rsvg, &d_width, &d_height); - width = naturalWidth = dims->width; - height = naturalHeight = dims->height; + width = naturalWidth = d_width; + height = naturalHeight = d_height; status = renderSVGToSurface(); if (status != CAIRO_STATUS_SUCCESS) { @@ -1200,7 +1234,13 @@ Image::renderSVGToSurface() { return status; } - gboolean render_ok = rsvg_handle_render_cairo(_rsvg, cr); + RsvgRectangle viewport = { + .x = 0, + .y = 0, + .width = static_cast(width), + .height = static_cast(height), + }; + gboolean render_ok = rsvg_handle_render_document(_rsvg, cr, &viewport, nullptr); if (!render_ok) { g_object_unref(_rsvg); cairo_destroy(cr); diff --git a/src/ImageData.cc b/src/ImageData.cc index 668733d39..03da2e270 100644 --- a/src/ImageData.cc +++ b/src/ImageData.cc @@ -2,8 +2,6 @@ #include "ImageData.h" -#include "Util.h" - using namespace v8; Nan::Persistent ImageData::constructor; @@ -24,8 +22,8 @@ ImageData::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { // Prototype Local proto = ctor->PrototypeTemplate(); - SetProtoAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, NULL, ctor); - SetProtoAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, NULL, ctor); + Nan::SetAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth); + Nan::SetAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight); Local ctx = Nan::GetCurrentContext(); Nan::Set(target, Nan::New("ImageData").ToLocalChecked(), ctor->GetFunction(ctx).ToLocalChecked()); } @@ -126,6 +124,10 @@ NAN_METHOD(ImageData::New) { */ NAN_GETTER(ImageData::GetWidth) { + if (!ImageData::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method ImageData.GetWidth called on incompatible receiver"); + return; + } ImageData *imageData = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(imageData->width())); } @@ -135,6 +137,10 @@ NAN_GETTER(ImageData::GetWidth) { */ NAN_GETTER(ImageData::GetHeight) { + if (!ImageData::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method ImageData.GetHeight called on incompatible receiver"); + return; + } ImageData *imageData = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(imageData->height())); } diff --git a/src/Point.h b/src/Point.h index 50c7b711c..a61f8b1ba 100644 --- a/src/Point.h +++ b/src/Point.h @@ -1,5 +1,4 @@ // Copyright (c) 2010 LearnBoost - #pragma once template @@ -8,4 +7,5 @@ class Point { T x, y; Point(T x=0, T y=0): x(x), y(y) {} Point(const Point&) = default; + Point& operator=(const Point&) = default; }; diff --git a/src/Util.h b/src/Util.h index dba6883a2..0e6d1d89c 100644 --- a/src/Util.h +++ b/src/Util.h @@ -1,32 +1,7 @@ #pragma once -#include -#include #include -// Wrapper around Nan::SetAccessor that makes it easier to change the last -// argument (signature). Getters/setters must be accessed only when there is -// actually an instance, i.e. MyClass.prototype.getter1 should not try to -// unwrap the non-existent 'this'. See #803, #847, #885, nodejs/node#15099, ... -inline void SetProtoAccessor( - v8::Local tpl, - v8::Local name, - Nan::GetterCallback getter, - Nan::SetterCallback setter, - v8::Local ctor - ) { - Nan::SetAccessor( - tpl, - name, - getter, - setter, - v8::Local(), - v8::DEFAULT, - v8::None, - v8::AccessorSignature::New(v8::Isolate::GetCurrent(), ctor) - ); -} - inline bool streq_casein(std::string& str1, std::string& str2) { return str1.size() == str2.size() && std::equal(str1.begin(), str1.end(), str2.begin(), [](char& c1, char& c2) { return c1 == c2 || std::toupper(c1) == std::toupper(c2); diff --git a/src/backend/Backend.cc b/src/backend/Backend.cc index 528f61a08..9f2b39dd3 100644 --- a/src/backend/Backend.cc +++ b/src/backend/Backend.cc @@ -9,7 +9,7 @@ Backend::Backend(std::string name, int width, int height) Backend::~Backend() { - this->destroySurface(); + Backend::destroySurface(); } void Backend::init(const Nan::FunctionCallbackInfo &info) { @@ -97,8 +97,7 @@ bool Backend::isSurfaceValid(){ BackendOperationNotAvailable::BackendOperationNotAvailable(Backend* backend, std::string operation_name) - : backend(backend) - , operation_name(operation_name) + : operation_name(operation_name) { msg = "operation " + operation_name + " not supported by backend " + backend->getName(); diff --git a/src/backend/Backend.h b/src/backend/Backend.h index df65194b9..f8448c41a 100644 --- a/src/backend/Backend.h +++ b/src/backend/Backend.h @@ -57,7 +57,6 @@ class Backend : public Nan::ObjectWrap class BackendOperationNotAvailable: public std::exception { private: - Backend* backend; std::string operation_name; std::string msg; diff --git a/src/backend/PdfBackend.cc b/src/backend/PdfBackend.cc index d8bd23422..fe831a68d 100644 --- a/src/backend/PdfBackend.cc +++ b/src/backend/PdfBackend.cc @@ -8,7 +8,7 @@ using namespace v8; PdfBackend::PdfBackend(int width, int height) : Backend("pdf", width, height) { - createSurface(); + PdfBackend::createSurface(); } PdfBackend::~PdfBackend() { diff --git a/src/backend/SvgBackend.cc b/src/backend/SvgBackend.cc index 10bf4caa7..7d4181fc2 100644 --- a/src/backend/SvgBackend.cc +++ b/src/backend/SvgBackend.cc @@ -9,7 +9,7 @@ using namespace v8; SvgBackend::SvgBackend(int width, int height) : Backend("svg", width, height) { - createSurface(); + SvgBackend::createSurface(); } SvgBackend::~SvgBackend() { diff --git a/src/register_font.cc b/src/register_font.cc index ae44c9aba..37182c0ac 100644 --- a/src/register_font.cc +++ b/src/register_font.cc @@ -264,7 +264,7 @@ get_pango_font_description(unsigned char* filepath) { FILE_SHARE_READ, NULL, OPEN_EXISTING, - NULL, + FILE_ATTRIBUTE_NORMAL, NULL ); if(!hFile){ @@ -304,6 +304,7 @@ get_pango_font_description(unsigned char* filepath) { } pango_font_description_set_family_static(desc, family); + free(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)); diff --git a/test/canvas.test.js b/test/canvas.test.js index 670127783..9573688f5 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -547,6 +547,8 @@ describe('Canvas', function () { const canvas = createCanvas(200, 200) const ctx = canvas.getContext('2d') + assert.equal('left', ctx.textAlign) // default TODO wrong default + ctx.textAlign = 'start' assert.equal('start', ctx.textAlign) ctx.textAlign = 'center' assert.equal('center', ctx.textAlign) @@ -1408,6 +1410,14 @@ describe('Canvas', function () { assert.strictEqual(pattern.toString(), '[object CanvasPattern]') }) + it('CanvasPattern has class string of `CanvasPattern`', async function () { + const img = await loadImage(path.join(__dirname, '/fixtures/checkers.png')); + const canvas = createCanvas(20, 20) + const ctx = canvas.getContext('2d') + const pattern = ctx.createPattern(img) + assert.strictEqual(Object.prototype.toString.call(pattern), '[object CanvasPattern]') + }) + it('Context2d#createLinearGradient()', function () { const canvas = createCanvas(20, 1) const ctx = canvas.getContext('2d') @@ -1437,6 +1447,11 @@ describe('Canvas', function () { assert.equal(0, imageData.data[i + 2]) assert.equal(255, imageData.data[i + 3]) }) + it('Canvas has class string of `HTMLCanvasElement`', function () { + const canvas = createCanvas(20, 1) + + assert.strictEqual(Object.prototype.toString.call(canvas), '[object HTMLCanvasElement]') + }) it('CanvasGradient stringifies as [object CanvasGradient]', function () { const canvas = createCanvas(20, 1) @@ -1445,6 +1460,13 @@ describe('Canvas', function () { assert.strictEqual(gradient.toString(), '[object CanvasGradient]') }) + it('CanvasGradient has class string of `CanvasGradient`', function () { + const canvas = createCanvas(20, 1) + const ctx = canvas.getContext('2d') + const gradient = ctx.createLinearGradient(1, 1, 19, 1) + assert.strictEqual(Object.prototype.toString.call(gradient), '[object CanvasGradient]') + }) + describe('Context2d#putImageData()', function () { it('throws for invalid arguments', function () { const canvas = createCanvas(2, 1) @@ -1900,4 +1922,55 @@ describe('Canvas', function () { if (index + 1 & 3) { assert.strictEqual(byte, 128) } else { assert.strictEqual(byte, 255) } }) }) + + describe('Context2d#save()/restore()', function () { + // Based on WPT meta:2d.state.saverestore + const state = [ // non-default values to test with + ['strokeStyle', '#ff0000'], + ['fillStyle', '#ff0000'], + ['globalAlpha', 0.5], + ['lineWidth', 0.5], + ['lineCap', 'round'], + ['lineJoin', 'round'], + ['miterLimit', 0.5], + ['shadowOffsetX', 5], + ['shadowOffsetY', 5], + ['shadowBlur', 5], + ['shadowColor', '#ff0000'], + ['globalCompositeOperation', 'copy'], + ['font', '25px serif'], + ['textAlign', 'center'], + ['textBaseline', 'bottom'], + // Added vs. WPT + ['imageSmoothingEnabled', false], + // ['imageSmoothingQuality', ], // not supported by node-canvas, #2114 + ['lineDashOffset', 1.0], + // Non-standard properties: + ['patternQuality', 'best'], + // ['quality', 'best'], // doesn't do anything, TODO remove + ['textDrawingMode', 'glyph'], + ['antialias', 'gray'] + ] + + for (const [k, v] of state) { + it(`2d.state.saverestore.${k}`, function () { + const canvas = createCanvas(0, 0) + const ctx = canvas.getContext('2d') + + // restore() undoes modification: + let old = ctx[k] + ctx.save() + ctx[k] = v + ctx.restore() + assert.strictEqual(ctx[k], old) + + // save() doesn't modify the value: + ctx[k] = v + old = ctx[k] + ctx.save() + assert.strictEqual(ctx[k], old) + ctx.restore() + }) + } + }) }) diff --git a/test/image.test.js b/test/image.test.js index 8d54dd90f..ec1631a10 100644 --- a/test/image.test.js +++ b/test/image.test.js @@ -30,6 +30,11 @@ describe('Image', function () { assert(Image.prototype.hasOwnProperty('width')) }) + it('Image has class string of `HTMLImageElement`', async function () { + const img = new Image() + assert.strictEqual(Object.prototype.toString.call(img), '[object HTMLImageElement]') + }) + it('loads JPEG image', function () { return loadImage(jpgFace).then((img) => { assert.strictEqual(img.onerror, null) diff --git a/test/imageData.test.js b/test/imageData.test.js index d3c84c29a..04b117b45 100644 --- a/test/imageData.test.js +++ b/test/imageData.test.js @@ -17,6 +17,11 @@ describe('ImageData', function () { assert.strictEqual(imageData.toString(), '[object ImageData]') }) + it('gives class string as `ImageData`', function () { + const imageData = createImageData(2, 3) + assert.strictEqual(Object.prototype.toString.call(imageData), '[object ImageData]') + }) + it('should throw with invalid numeric arguments', function () { assert.throws(() => { createImageData(0, 0) }, /width is zero/) assert.throws(() => { createImageData(1, 0) }, /height is zero/) diff --git a/test/public/tests.js b/test/public/tests.js index 651105e36..d24202602 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -2693,6 +2693,11 @@ tests['measureText()'] = function (ctx) { drawWithBBox('right', 195, 195) } +tests['glyph advances (#2184)'] = function (ctx) { + ctx.font = '8px Arial' + ctx.fillText('A float is a box that is shifted to the left or right on the current line.', 0, 8) +} + tests['image sampling (#1084)'] = function (ctx, done) { let loaded1, loaded2 const img1 = new Image() diff --git a/test/wpt/generated/the-canvas-element.js b/test/wpt/generated/the-canvas-element.js index 8b1a6817e..cea4fd9b4 100644 --- a/test/wpt/generated/the-canvas-element.js +++ b/test/wpt/generated/the-canvas-element.js @@ -171,6 +171,12 @@ describe("WPT: the-canvas-element", function () { assert.strictEqual(window.CanvasRenderingContext2D.prototype.fill, undefined, "window.CanvasRenderingContext2D.prototype.fill", "undefined") }); + it("2d.type class string", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + assert.strictEqual(Object.prototype.toString.call(ctx), '[object CanvasRenderingContext2D]') + }) + it("2d.type.replace", function () { // Interface methods can be overridden const canvas = createCanvas(100, 50); diff --git a/types/index.d.ts b/types/index.d.ts index 7b53f4851..8bcfd105e 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,5 +1,4 @@ // TypeScript Version: 3.0 -/// import { Readable } from 'stream' @@ -80,7 +79,7 @@ export class Canvas { constructor(width: number, height: number, type?: 'image'|'pdf'|'svg') - getContext(contextId: '2d', contextAttributes?: NodeCanvasRenderingContext2DSettings): NodeCanvasRenderingContext2D + getContext(contextId: '2d', contextAttributes?: NodeCanvasRenderingContext2DSettings): CanvasRenderingContext2D /** * For image canvases, encodes the canvas as a PNG. For PDF canvases, @@ -128,19 +127,126 @@ export class Canvas { toDataURL(mimeType: 'image/jpeg', quality: number, cb: (err: Error|null, result: string) => void): void } -declare class NodeCanvasRenderingContext2D extends CanvasRenderingContext2D { +export interface TextMetrics { + readonly actualBoundingBoxAscent: number; + readonly actualBoundingBoxDescent: number; + readonly actualBoundingBoxLeft: number; + readonly actualBoundingBoxRight: number; + readonly fontBoundingBoxAscent: number; + readonly fontBoundingBoxDescent: number; + readonly width: number; +} + +export type CanvasFillRule = 'evenodd' | 'nonzero'; + +export type GlobalCompositeOperation = + | 'clear' + | 'copy' + | 'destination' + | 'source-over' + | 'destination-over' + | 'source-in' + | 'destination-in' + | 'source-out' + | 'destination-out' + | 'source-atop' + | 'destination-atop' + | 'xor' + | 'lighter' + | 'normal' + | 'multiply' + | 'screen' + | 'overlay' + | 'darken' + | 'lighten' + | 'color-dodge' + | 'color-burn' + | 'hard-light' + | 'soft-light' + | 'difference' + | 'exclusion' + | 'hue' + | 'saturation' + | 'color' + | 'luminosity' + | 'saturate'; + +export type CanvasLineCap = 'butt' | 'round' | 'square'; + +export type CanvasLineJoin = 'bevel' | 'miter' | 'round'; + +export type CanvasTextBaseline = 'alphabetic' | 'bottom' | 'hanging' | 'ideographic' | 'middle' | 'top'; + +export type CanvasTextAlign = 'center' | 'end' | 'left' | 'right' | 'start'; + +export class CanvasRenderingContext2D { + drawImage(image: Canvas|Image, dx: number, dy: number): void + drawImage(image: Canvas|Image, dx: number, dy: number, dw: number, dh: number): void + drawImage(image: Canvas|Image, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void + putImageData(imagedata: ImageData, dx: number, dy: number): void; + putImageData(imagedata: ImageData, dx: number, dy: number, dirtyX: number, dirtyY: number, dirtyWidth: number, dirtyHeight: number): void; + getImageData(sx: number, sy: number, sw: number, sh: number): ImageData; + createImageData(sw: number, sh: number): ImageData; + createImageData(imagedata: ImageData): ImageData; + /** + * For PDF canvases, adds another page. If width and/or height are omitted, + * the canvas's initial size is used. + */ + addPage(width?: number, height?: number): void + save(): void; + restore(): void; + rotate(angle: number): void; + translate(x: number, y: number): void; + transform(a: number, b: number, c: number, d: number, e: number, f: number): void; + getTransform(): DOMMatrix; + resetTransform(): void; + setTransform(transform?: DOMMatrix): void; + isPointInPath(x: number, y: number, fillRule?: CanvasFillRule): boolean; + scale(x: number, y: number): void; + clip(fillRule?: CanvasFillRule): void; + fill(fillRule?: CanvasFillRule): void; + stroke(): void; + fillText(text: string, x: number, y: number, maxWidth?: number): void; + strokeText(text: string, x: number, y: number, maxWidth?: number): void; + fillRect(x: number, y: number, w: number, h: number): void; + strokeRect(x: number, y: number, w: number, h: number): void; + clearRect(x: number, y: number, w: number, h: number): void; + rect(x: number, y: number, w: number, h: number): void; + roundRect(x: number, y: number, w: number, h: number, radii?: number | number[]): void; + measureText(text: string): TextMetrics; + moveTo(x: number, y: number): void; + lineTo(x: number, y: number): void; + bezierCurveTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): void; + quadraticCurveTo(cpx: number, cpy: number, x: number, y: number): void; + beginPath(): void; + closePath(): void; + arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, counterclockwise?: boolean): void; + arcTo(x1: number, y1: number, x2: number, y2: number, radius: number): void; + ellipse(x: number, y: number, radiusX: number, radiusY: number, rotation: number, startAngle: number, endAngle: number, counterclockwise?: boolean): void; + setLineDash(segments: number[]): void; + getLineDash(): number[]; + createPattern(image: Canvas|Image, repetition: 'repeat' | 'repeat-x' | 'repeat-y' | 'no-repeat' | '' | null): CanvasPattern + createLinearGradient(x0: number, y0: number, x1: number, y1: number): CanvasGradient; + createRadialGradient(x0: number, y0: number, r0: number, x1: number, y1: number, r1: number): CanvasGradient; /** * _Non-standard_. Defaults to 'good'. Affects pattern (gradient, image, * etc.) rendering quality. */ patternQuality: 'fast' | 'good' | 'best' | 'nearest' | 'bilinear' - - /** - * _Non-standard_. Defaults to 'good'. Like `patternQuality`, but applies to - * transformations affecting more than just patterns. - */ - quality: 'fast' | 'good' | 'best' | 'nearest' | 'bilinear' - + imageSmoothingEnabled: boolean; + globalCompositeOperation: GlobalCompositeOperation; + globalAlpha: number; + shadowColor: string; + miterLimit: number; + lineWidth: number; + lineCap: CanvasLineCap; + lineJoin: CanvasLineJoin; + lineDashOffset: number; + shadowOffsetX: number; + shadowOffsetY: number; + shadowBlur: number; + /** _Non-standard_. Sets the antialiasing mode. */ + antialias: 'default' | 'gray' | 'none' | 'subpixel' /** * Defaults to 'path'. The effect depends on the canvas type: * @@ -165,55 +271,28 @@ declare class NodeCanvasRenderingContext2D extends CanvasRenderingContext2D { * (aside from using the stroke and fill style, respectively). */ textDrawingMode: 'path' | 'glyph' - - /** _Non-standard_. Sets the antialiasing mode. */ - antialias: 'default' | 'gray' | 'none' | 'subpixel' - - // Standard, but not in the TS lib and needs node-canvas class return type. - /** Returns or sets a `DOMMatrix` for the current transformation matrix. */ - currentTransform: NodeCanvasDOMMatrix - - // Standard, but need node-canvas class versions: - getTransform(): NodeCanvasDOMMatrix - setTransform(a: number, b: number, c: number, d: number, e: number, f: number): void - setTransform(transform?: NodeCanvasDOMMatrix): void - createImageData(sw: number, sh: number): NodeCanvasImageData - createImageData(imagedata: NodeCanvasImageData): NodeCanvasImageData - getImageData(sx: number, sy: number, sw: number, sh: number): NodeCanvasImageData - putImageData(imagedata: NodeCanvasImageData, dx: number, dy: number): void - putImageData(imagedata: NodeCanvasImageData, dx: number, dy: number, dirtyX: number, dirtyY: number, dirtyWidth: number, dirtyHeight: number): void - drawImage(image: Canvas|Image, dx: number, dy: number): void - drawImage(image: Canvas|Image, dx: number, dy: number, dw: number, dh: number): void - drawImage(image: Canvas|Image, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void /** - * **Do not use this overload. Use one of the other three overloads.** This - * is a catch-all definition required for compatibility with the base - * `CanvasRenderingContext2D` interface. - */ - drawImage(...args: any[]): void - createPattern(image: Canvas|Image, repetition: 'repeat' | 'repeat-x' | 'repeat-y' | 'no-repeat' | '' | null): NodeCanvasCanvasPattern - /** - * **Do not use this overload. Use the other three overload.** This is a - * catch-all definition required for compatibility with the base - * `CanvasRenderingContext2D` interface. - */ - createPattern(...args: any[]): NodeCanvasCanvasPattern - createLinearGradient(x0: number, y0: number, x1: number, y1: number): NodeCanvasCanvasGradient; - createRadialGradient(x0: number, y0: number, r0: number, x1: number, y1: number, r1: number): NodeCanvasCanvasGradient; - - /** - * For PDF canvases, adds another page. If width and/or height are omitted, - * the canvas's initial size is used. + * _Non-standard_. Defaults to 'good'. Like `patternQuality`, but applies to + * transformations affecting more than just patterns. */ - addPage(width?: number, height?: number): void + quality: 'fast' | 'good' | 'best' | 'nearest' | 'bilinear' + /** Returns or sets a `DOMMatrix` for the current transformation matrix. */ + currentTransform: DOMMatrix + fillStyle: string | CanvasGradient | CanvasPattern; + strokeStyle: string | CanvasGradient | CanvasPattern; + font: string; + textBaseline: CanvasTextBaseline; + textAlign: CanvasTextAlign; + canvas: Canvas; } -export { NodeCanvasRenderingContext2D as CanvasRenderingContext2D } -declare class NodeCanvasCanvasGradient extends CanvasGradient {} -export { NodeCanvasCanvasGradient as CanvasGradient } +export class CanvasGradient { + addColorStop(offset: number, color: string): void; +} -declare class NodeCanvasCanvasPattern extends CanvasPattern {} -export { NodeCanvasCanvasPattern as CanvasPattern } +export class CanvasPattern { + setTransform(transform?: DOMMatrix): void; +} // This does not extend HTMLImageElement because there are dozens of inherited // methods and properties that we do not provide. @@ -312,14 +391,79 @@ export class JPEGStream extends Readable {} /** This class must not be constructed directly; use `canvas.createPDFStream()`. */ export class PDFStream extends Readable {} -declare class NodeCanvasDOMMatrix extends DOMMatrix {} -export { NodeCanvasDOMMatrix as DOMMatrix } +export class DOMPoint { + w: number; + x: number; + y: number; + z: number; +} -declare class NodeCanvasDOMPoint extends DOMPoint {} -export { NodeCanvasDOMPoint as DOMPoint } +export class DOMMatrix { + constructor(init: string | number[]); + toString(): string; + multiply(other?: DOMMatrix): DOMMatrix; + multiplySelf(other?: DOMMatrix): DOMMatrix; + preMultiplySelf(other?: DOMMatrix): DOMMatrix; + translate(tx?: number, ty?: number, tz?: number): DOMMatrix; + translateSelf(tx?: number, ty?: number, tz?: number): DOMMatrix; + scale(scaleX?: number, scaleY?: number, scaleZ?: number, originX?: number, originY?: number, originZ?: number): DOMMatrix; + scale3d(scale?: number, originX?: number, originY?: number, originZ?: number): DOMMatrix; + scale3dSelf(scale?: number, originX?: number, originY?: number, originZ?: number): DOMMatrix; + scaleSelf(scaleX?: number, scaleY?: number, scaleZ?: number, originX?: number, originY?: number, originZ?: number): DOMMatrix; + rotateFromVector(x?: number, y?: number): DOMMatrix; + rotateFromVectorSelf(x?: number, y?: number): DOMMatrix; + rotate(rotX?: number, rotY?: number, rotZ?: number): DOMMatrix; + rotateSelf(rotX?: number, rotY?: number, rotZ?: number): DOMMatrix; + rotateAxisAngle(x?: number, y?: number, z?: number, angle?: number): DOMMatrix; + rotateAxisAngleSelf(x?: number, y?: number, z?: number, angle?: number): DOMMatrix; + skewX(sx?: number): DOMMatrix; + skewXSelf(sx?: number): DOMMatrix; + skewY(sy?: number): DOMMatrix; + skewYSelf(sy?: number): DOMMatrix; + flipX(): DOMMatrix; + flipY(): DOMMatrix; + inverse(): DOMMatrix; + invertSelf(): DOMMatrix; + setMatrixValue(transformList: string): DOMMatrix; + transformPoint(point?: DOMPoint): DOMPoint; + toFloat32Array(): Float32Array; + toFloat64Array(): Float64Array; + readonly is2D: boolean; + readonly isIdentity: boolean; + a: number; + b: number; + c: number; + d: number; + e: number; + f: number; + m11: number; + m12: number; + m13: number; + m14: number; + m21: number; + m22: number; + m23: number; + m24: number; + m31: number; + m32: number; + m33: number; + m34: number; + m41: number; + m42: number; + m43: number; + m44: number; + static fromMatrix(other: DOMMatrix): DOMMatrix; + static fromFloat32Array(a: Float32Array): DOMMatrix; + static fromFloat64Array(a: Float64Array): DOMMatrix; +} -declare class NodeCanvasImageData extends ImageData {} -export { NodeCanvasImageData as ImageData } +export class ImageData { + constructor(sw: number, sh: number); + constructor(data: Uint8ClampedArray, sw: number, sh?: number); + readonly data: Uint8ClampedArray; + readonly height: number; + readonly width: number; +} // This is marked private, but is exported... // export function parseFont(description: string): object