diff --git a/Apps/UnitTests/JavaScript/dist/tests.javaScript.all.js b/Apps/UnitTests/JavaScript/dist/tests.javaScript.all.js index a5d9cda5bb..da059e5559 100644 --- a/Apps/UnitTests/JavaScript/dist/tests.javaScript.all.js +++ b/Apps/UnitTests/JavaScript/dist/tests.javaScript.all.js @@ -29075,6 +29075,129 @@ describe("Canvas2D", function () { (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(ctx.strokeStyle).to.equal("#00ff00"); }); + it("rejects a prototype-spoofed object in place of a Path2D", function () { + // instanceof only walks the prototype chain, and a prototype is assignable, so an + // InstanceOf gate accepted any object wearing Path2D.prototype and then unwrapped it. + // Handing fill() a CanvasGradient this way was an access violation, not a wrong answer. + // Object.create(Path2D.prototype) is the same hole with no native wrap behind it at all. + var ctx = createContext(); + var spoofedGradient = ctx.createLinearGradient(0, 0, 10, 10); + Object.setPrototypeOf(spoofedGradient, Path2D.prototype); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(spoofedGradient instanceof Path2D).to.equal(true); + + var bare = Object.create(Path2D.prototype); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(bare instanceof Path2D).to.equal(true); + + var realPath = new Path2D();var _loop = function _loop() + {var impostor = _arr[_i]; + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.fill(impostor);}).to.throw(); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.stroke(impostor);}).to.throw(); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {realPath.addPath(impostor);}).to.throw(); + // The Path2D() argument is a (Path2D or DOMString) union, so a non-Path2D is string + // data rather than an error. It must not be unwrapped on the way there. + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {new Path2D(impostor);}).to.not.throw(); + };for (var _i = 0, _arr = [spoofedGradient, bare]; _i < _arr.length; _i++) {_loop();} + }); + + it("ignores a prototype-spoofed object assigned to fillStyle or strokeStyle", function () { + // Same defect on the gradient side: the assignment gate accepted anything wearing the + // gradient prototype and stored it, and the next fill unwrapped it as a CanvasGradient. + var ctx = createContext(); + var gradient = ctx.createLinearGradient(0, 0, 10, 10); + var spoofedPath = new Path2D(); + Object.setPrototypeOf(spoofedPath, Object.getPrototypeOf(gradient)); + + ctx.fillStyle = "#ff0000"; + ctx.strokeStyle = "#00ff00"; + ctx.fillStyle = spoofedPath; + ctx.strokeStyle = spoofedPath; + + // Per spec an unusable assignment leaves the previous value in place. + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(ctx.fillStyle).to.equal("#ff0000"); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(ctx.strokeStyle).to.equal("#00ff00"); + + // The drawing path must stay usable rather than unwrapping the impostor. + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.fillRect(0, 0, 10, 10);}).to.not.throw(); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.strokeRect(0, 0, 10, 10);}).to.not.throw(); + }); + + it("rejects a non-Path2D argument to fill and stroke", function () { + // Neither call type-checked the argument before handing it to ObjectWrap::Unwrap, + // which does no checking of its own, so an unrelated object was reinterpreted as a + // NativeCanvasPath2D. stroke() was the worse of the two: it had no gate at all, so + // even a string got there. + // The thrown type is deliberately not asserted: a C++ Napi::TypeError surfaces as a + // JS TypeError on some engines and as an InternalError on the QuickJS Node-API port, + // so only the fact that it throws is portable. Every other throw test here does the same. + var ctx = createContext(); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.stroke("x");}).to.throw(); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.stroke({});}).to.throw(); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.stroke(5);}).to.throw(); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.fill({});}).to.throw(); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.fill(5);}).to.throw(); + }); + + it("still accepts the valid fill and stroke argument forms", function () { + var ctx = createContext(); + var path = new Path2D("M0 0 L10 10"); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.fill();}).to.not.throw(); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.stroke();}).to.not.throw(); + // undefined selects the no-argument overload rather than being a bad Path2D. + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.fill(undefined);}).to.not.throw(); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.stroke(undefined);}).to.not.throw(); + // fill() also takes a fill rule string. + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.fill("evenodd");}).to.not.throw(); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.fill(new String("evenodd"));}).to.not.throw(); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () { + ctx.fill({ toString: function toString() {return "nonzero";} }); + }).to.not.throw(); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.fill(path);}).to.not.throw(); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.fill(path, "nonzero");}).to.not.throw(); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.fill(path, new String("evenodd"));}).to.not.throw(); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.stroke(path);}).to.not.throw(); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.fill("invalid");}).to.throw(); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.fill(path, "invalid");}).to.throw(); + }); + + it("rejects a non-Path2D argument to Path2D.addPath", function () { + // addPath had neither a type check nor an arity check, so addPath() unwrapped a + // missing argument and addPath("x") unwrapped a string. See above for why the + // thrown type is not asserted. + var path = new Path2D(); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {path.addPath();}).to.throw(); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {path.addPath("x");}).to.throw(); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {path.addPath({});}).to.throw(); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {path.addPath(new Path2D("M0 0 L5 5"));}).to.not.throw(); + }); + + it("rejects an invalid native source argument to drawImage", function () { + // Native-looking impostors must throw, not AV via an unchecked Unwrap. + var ctx = createContext(); + var realCanvas = new _native.Canvas(); + var spoofedCanvas = Object.create(Object.getPrototypeOf(realCanvas)); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(spoofedCanvas instanceof _native.Canvas).to.equal(true); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.drawImage({}, 0, 0);}).to.throw(); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.drawImage(new Path2D(), 0, 0);}).to.throw(); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.drawImage(spoofedCanvas, 0, 0);}).to.throw(); + realCanvas.dispose(); + }); + + it("treats a non-Path2D Path2D() argument as path data", function () { + // The constructor routed on IsObject(), so any object was unwrapped as a Path2D. + // Per the (Path2D or DOMString) union a non-Path2D is stringified instead. + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {new Path2D({});}).to.not.throw(); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {new Path2D(5);}).to.not.throw(); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {new Path2D();}).to.not.throw(); + // A copy of a real Path2D still copies, and an object that stringifies to path + // data is still parsed as such. + var source = new Path2D("M0 0 L10 10"); + var ctx = createContext(); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.fill(new Path2D(source));}).to.not.throw(); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () { + ctx.fill(new Path2D({ toString: function toString() {return "M0 0 L10 10";} })); + }).to.not.throw(); + }); + it("accepts a CanvasGradient as fillStyle", function () { var ctx = createContext(); var gradient = ctx.createLinearGradient(0, 0, 64, 64); diff --git a/Apps/UnitTests/JavaScript/src/tests.javaScript.all.ts b/Apps/UnitTests/JavaScript/src/tests.javaScript.all.ts index d861bf035d..8b451d90df 100644 --- a/Apps/UnitTests/JavaScript/src/tests.javaScript.all.ts +++ b/Apps/UnitTests/JavaScript/src/tests.javaScript.all.ts @@ -573,6 +573,129 @@ describe("Canvas2D", function () { expect(ctx.strokeStyle).to.equal("#00ff00"); }); + it("rejects a prototype-spoofed object in place of a Path2D", function () { + // instanceof only walks the prototype chain, and a prototype is assignable, so an + // InstanceOf gate accepted any object wearing Path2D.prototype and then unwrapped it. + // Handing fill() a CanvasGradient this way was an access violation, not a wrong answer. + // Object.create(Path2D.prototype) is the same hole with no native wrap behind it at all. + const ctx = createContext(); + const spoofedGradient: any = ctx.createLinearGradient(0, 0, 10, 10); + Object.setPrototypeOf(spoofedGradient, Path2D.prototype); + expect(spoofedGradient instanceof Path2D).to.equal(true); + + const bare: any = Object.create(Path2D.prototype); + expect(bare instanceof Path2D).to.equal(true); + + const realPath = new Path2D(); + for (const impostor of [spoofedGradient, bare]) { + expect(function () { ctx.fill(impostor); }).to.throw(); + expect(function () { ctx.stroke(impostor); }).to.throw(); + expect(function () { realPath.addPath(impostor); }).to.throw(); + // The Path2D() argument is a (Path2D or DOMString) union, so a non-Path2D is string + // data rather than an error. It must not be unwrapped on the way there. + expect(function () { new Path2D(impostor); }).to.not.throw(); + } + }); + + it("ignores a prototype-spoofed object assigned to fillStyle or strokeStyle", function () { + // Same defect on the gradient side: the assignment gate accepted anything wearing the + // gradient prototype and stored it, and the next fill unwrapped it as a CanvasGradient. + const ctx = createContext(); + const gradient = ctx.createLinearGradient(0, 0, 10, 10); + const spoofedPath: any = new Path2D(); + Object.setPrototypeOf(spoofedPath, Object.getPrototypeOf(gradient)); + + ctx.fillStyle = "#ff0000"; + ctx.strokeStyle = "#00ff00"; + ctx.fillStyle = spoofedPath; + ctx.strokeStyle = spoofedPath; + + // Per spec an unusable assignment leaves the previous value in place. + expect(ctx.fillStyle).to.equal("#ff0000"); + expect(ctx.strokeStyle).to.equal("#00ff00"); + + // The drawing path must stay usable rather than unwrapping the impostor. + expect(function () { ctx.fillRect(0, 0, 10, 10); }).to.not.throw(); + expect(function () { ctx.strokeRect(0, 0, 10, 10); }).to.not.throw(); + }); + + it("rejects a non-Path2D argument to fill and stroke", function () { + // Neither call type-checked the argument before handing it to ObjectWrap::Unwrap, + // which does no checking of its own, so an unrelated object was reinterpreted as a + // NativeCanvasPath2D. stroke() was the worse of the two: it had no gate at all, so + // even a string got there. + // The thrown type is deliberately not asserted: a C++ Napi::TypeError surfaces as a + // JS TypeError on some engines and as an InternalError on the QuickJS Node-API port, + // so only the fact that it throws is portable. Every other throw test here does the same. + const ctx = createContext(); + expect(function () { ctx.stroke("x"); }).to.throw(); + expect(function () { ctx.stroke({}); }).to.throw(); + expect(function () { ctx.stroke(5); }).to.throw(); + expect(function () { ctx.fill({}); }).to.throw(); + expect(function () { ctx.fill(5); }).to.throw(); + }); + + it("still accepts the valid fill and stroke argument forms", function () { + const ctx = createContext(); + const path = new Path2D("M0 0 L10 10"); + expect(function () { ctx.fill(); }).to.not.throw(); + expect(function () { ctx.stroke(); }).to.not.throw(); + // undefined selects the no-argument overload rather than being a bad Path2D. + expect(function () { ctx.fill(undefined); }).to.not.throw(); + expect(function () { ctx.stroke(undefined); }).to.not.throw(); + // fill() also takes a fill rule string. + expect(function () { ctx.fill("evenodd"); }).to.not.throw(); + expect(function () { ctx.fill(new String("evenodd") as any); }).to.not.throw(); + expect(function () { + ctx.fill({ toString: function () { return "nonzero"; } } as any); + }).to.not.throw(); + expect(function () { ctx.fill(path); }).to.not.throw(); + expect(function () { ctx.fill(path, "nonzero"); }).to.not.throw(); + expect(function () { ctx.fill(path, new String("evenodd") as any); }).to.not.throw(); + expect(function () { ctx.stroke(path); }).to.not.throw(); + expect(function () { ctx.fill("invalid"); }).to.throw(); + expect(function () { ctx.fill(path, "invalid"); }).to.throw(); + }); + + it("rejects a non-Path2D argument to Path2D.addPath", function () { + // addPath had neither a type check nor an arity check, so addPath() unwrapped a + // missing argument and addPath("x") unwrapped a string. See above for why the + // thrown type is not asserted. + const path = new Path2D(); + expect(function () { path.addPath(); }).to.throw(); + expect(function () { path.addPath("x"); }).to.throw(); + expect(function () { path.addPath({}); }).to.throw(); + expect(function () { path.addPath(new Path2D("M0 0 L5 5")); }).to.not.throw(); + }); + + it("rejects an invalid native source argument to drawImage", function () { + // Native-looking impostors must throw, not AV via an unchecked Unwrap. + const ctx = createContext(); + const realCanvas = new _native.Canvas(); + const spoofedCanvas = Object.create(Object.getPrototypeOf(realCanvas)); + expect(spoofedCanvas instanceof _native.Canvas).to.equal(true); + expect(function () { ctx.drawImage({}, 0, 0); }).to.throw(); + expect(function () { ctx.drawImage(new Path2D(), 0, 0); }).to.throw(); + expect(function () { ctx.drawImage(spoofedCanvas, 0, 0); }).to.throw(); + realCanvas.dispose(); + }); + + it("treats a non-Path2D Path2D() argument as path data", function () { + // The constructor routed on IsObject(), so any object was unwrapped as a Path2D. + // Per the (Path2D or DOMString) union a non-Path2D is stringified instead. + expect(function () { new Path2D({}); }).to.not.throw(); + expect(function () { new Path2D(5); }).to.not.throw(); + expect(function () { new Path2D(); }).to.not.throw(); + // A copy of a real Path2D still copies, and an object that stringifies to path + // data is still parsed as such. + const source = new Path2D("M0 0 L10 10"); + const ctx = createContext(); + expect(function () { ctx.fill(new Path2D(source)); }).to.not.throw(); + expect(function () { + ctx.fill(new Path2D({ toString: function () { return "M0 0 L10 10"; } })); + }).to.not.throw(); + }); + it("accepts a CanvasGradient as fillStyle", function () { const ctx = createContext(); const gradient = ctx.createLinearGradient(0, 0, 64, 64); diff --git a/Polyfills/Canvas/CMakeLists.txt b/Polyfills/Canvas/CMakeLists.txt index 3103b004cf..22affaf88d 100644 --- a/Polyfills/Canvas/CMakeLists.txt +++ b/Polyfills/Canvas/CMakeLists.txt @@ -22,6 +22,7 @@ set(SOURCES "Source/Context.h" "Source/MeasureText.cpp" "Source/MeasureText.h" + "Source/NativeInstanceRegistry.h" "Source/Gradient.cpp" "Source/Gradient.h" "Source/Font.cpp" diff --git a/Polyfills/Canvas/Source/Canvas.cpp b/Polyfills/Canvas/Source/Canvas.cpp index e08b443d16..4df3fbfd46 100644 --- a/Polyfills/Canvas/Source/Canvas.cpp +++ b/Polyfills/Canvas/Source/Canvas.cpp @@ -11,6 +11,7 @@ #include "Colors.h" #include "Gradient.h" #include "Font.h" +#include "NativeInstanceRegistry.h" #include #include #include @@ -47,15 +48,23 @@ namespace Babylon::Polyfills::Internal JsRuntime::NativeObject::GetFromJavaScript(env).Set(JS_CONSTRUCTOR_NAME, func); } + NativeCanvas* NativeCanvas::TryUnwrap(Napi::Env env, const Napi::Value& value) + { + return NativeInstanceRegistry::TryUnwrap(env, value); + } + NativeCanvas::NativeCanvas(const Napi::CallbackInfo& info) : Napi::ObjectWrap{info} , m_graphicsContext{Graphics::DeviceContext::GetFromJavaScript(info.Env())} , Polyfills::Canvas::Impl::MonitoredResource{Polyfills::Canvas::Impl::GetFromJavaScript(info.Env())} { + NativeInstanceRegistry::Add(info, this); } NativeCanvas::~NativeCanvas() { + NativeInstanceRegistry::Remove(this); + // Canvas and Context form a JS cycle; finalizer order is not guaranteed. // Clear the reverse pointer first so Context::~Context cannot touch us. if (m_context != nullptr) diff --git a/Polyfills/Canvas/Source/Canvas.h b/Polyfills/Canvas/Source/Canvas.h index 6cc504a90f..6d4d6e792b 100644 --- a/Polyfills/Canvas/Source/Canvas.h +++ b/Polyfills/Canvas/Source/Canvas.h @@ -61,6 +61,8 @@ namespace Babylon::Polyfills::Internal public: static void Initialize(Napi::Env env); + static NativeCanvas* TryUnwrap(Napi::Env env, const Napi::Value& value); + explicit NativeCanvas(const Napi::CallbackInfo& info); virtual ~NativeCanvas(); diff --git a/Polyfills/Canvas/Source/Context.cpp b/Polyfills/Canvas/Source/Context.cpp index b26d762f51..e65fba0909 100644 --- a/Polyfills/Canvas/Source/Context.cpp +++ b/Polyfills/Canvas/Source/Context.cpp @@ -62,6 +62,15 @@ namespace Babylon::Polyfills::Internal value <= static_cast(std::numeric_limits::max()) && value == std::trunc(value); } + + void ValidateFillRule(Napi::Env env, const Napi::Value& value) + { + const auto fillRule = value.ToString().Utf8Value(); + if (fillRule != "nonzero" && fillRule != "evenodd") + { + throw Napi::TypeError::New(env, "Context2D.fill: the fill rule must be \"nonzero\" or \"evenodd\"."); + } + } } void Context::Initialize(Napi::Env env) @@ -201,12 +210,16 @@ namespace Babylon::Polyfills::Internal } else if (std::holds_alternative(m_state.fillStyle)) { - CanvasGradient* gradient = CanvasGradient::Unwrap(std::get(m_state.fillStyle)->Value()); + CanvasGradient* gradient = CanvasGradient::TryUnwrap(info.Env(), std::get(m_state.fillStyle)->Value()); + if (gradient == nullptr) + { + throw Napi::TypeError::New(info.Env(), "fillStyle must be a string or CanvasGradient."); + } nvgFillPaint(*m_nvg, gradient->Paint()); } else { - throw Napi::Error::New(info.Env(), "Fillstyle is not a color string or a gradient."); + throw Napi::TypeError::New(info.Env(), "fillStyle must be a string or CanvasGradient."); } } @@ -223,12 +236,16 @@ namespace Babylon::Polyfills::Internal } else if (std::holds_alternative(m_state.strokeStyle)) { - CanvasGradient* gradient = CanvasGradient::Unwrap(std::get(m_state.strokeStyle)->Value()); + CanvasGradient* gradient = CanvasGradient::TryUnwrap(info.Env(), std::get(m_state.strokeStyle)->Value()); + if (gradient == nullptr) + { + throw Napi::TypeError::New(info.Env(), "strokeStyle must be a string or CanvasGradient."); + } nvgStrokePaint(*m_nvg, gradient->Paint()); } else { - throw Napi::Error::New(info.Env(), "Strokestyle is not a color string or a gradient."); + throw Napi::TypeError::New(info.Env(), "strokeStyle must be a string or CanvasGradient."); } } @@ -345,10 +362,21 @@ namespace Babylon::Polyfills::Internal { SetFilterStack(); - const NativeCanvasPath2D* path = info.Length() >= 1 && info[0].IsObject() - ? NativeCanvasPath2D::Unwrap(info[0].As()) - : nullptr; // TODO: handle fillRule: nonzero, evenodd + // fill(path?, fillRule?) — distinguish Path2D from the string-converted enum overload. + const NativeCanvasPath2D* path = nullptr; + if (info.Length() >= 1 && !info[0].IsUndefined()) + { + path = NativeCanvasPath2D::TryUnwrap(info.Env(), info[0]); + if (path == nullptr) + { + ValidateFillRule(info.Env(), info[0]); + } + } + if (path != nullptr && info.Length() >= 2 && !info[1].IsUndefined()) + { + ValidateFillRule(info.Env(), info[1]); + } // draw Path2D if exists if (path != nullptr) @@ -654,8 +682,18 @@ namespace Babylon::Polyfills::Internal void Context::Stroke(const Napi::CallbackInfo& info) { + // stroke(path?) — reject non-Path2D first arg. + const NativeCanvasPath2D* path = nullptr; + if (info.Length() >= 1 && !info[0].IsUndefined()) + { + path = NativeCanvasPath2D::TryUnwrap(info.Env(), info[0]); + if (path == nullptr) + { + throw Napi::TypeError::New(info.Env(), "Context2D.stroke: the first argument is not a Path2D."); + } + } + // draw Path2D if exists - const NativeCanvasPath2D* path = info.Length() == 1 ? NativeCanvasPath2D::Unwrap(info[0].As()) : nullptr; if (path != nullptr) { PlayPath2D(path); @@ -1255,11 +1293,15 @@ namespace Babylon::Polyfills::Internal Napi::Object imageObj = info[0].As(); // Retain the source kind before coercion can change its prototype or properties. // Canvas takes precedence over the structural ImageBitmap shape. - const auto canvasCtorVal = JsRuntime::NativeObject::GetFromJavaScript(info.Env()).Get("Canvas"); - NativeCanvas* const srcCanvas = canvasCtorVal.IsFunction() && imageObj.InstanceOf(canvasCtorVal.As()) - ? NativeCanvas::Unwrap(imageObj) + NativeCanvas* const srcCanvas = NativeCanvas::TryUnwrap(info.Env(), imageObj); + const NativeCanvasImage* const canvasImage = srcCanvas == nullptr + ? NativeCanvasImage::TryUnwrap(info.Env(), imageObj) : nullptr; - const bool isImageBitmap = srcCanvas == nullptr && imageObj.Has("data") && imageObj.Get("data").IsTypedArray(); + const bool isImageBitmap = srcCanvas == nullptr && canvasImage == nullptr && imageObj.Has("data") && imageObj.Get("data").IsTypedArray(); + if (srcCanvas == nullptr && canvasImage == nullptr && !isImageBitmap) + { + throw Napi::TypeError::New(info.Env(), "drawImage: first argument must be a Canvas, Image, or ImageBitmap-like object."); + } // Coercion can also resize the source. Do it once, before reading dimensions // or creating any graphics resources. @@ -1386,7 +1428,6 @@ namespace Babylon::Polyfills::Internal #endif } - const NativeCanvasImage* canvasImage = NativeCanvasImage::Unwrap(imageObj); const auto rectangles = ParseDrawImageRectangles(arguments, canvasImage->GetWidth(), canvasImage->GetHeight()); if (!rectangles) { diff --git a/Polyfills/Canvas/Source/Gradient.cpp b/Polyfills/Canvas/Source/Gradient.cpp index e237157a08..1f7d533561 100644 --- a/Polyfills/Canvas/Source/Gradient.cpp +++ b/Polyfills/Canvas/Source/Gradient.cpp @@ -3,6 +3,7 @@ #include "Context.h" #include "Gradient.h" #include "Colors.h" +#include "NativeInstanceRegistry.h" #include #include @@ -120,13 +121,12 @@ namespace Babylon::Polyfills::Internal bool CanvasGradient::IsInstance(Napi::Env env, const Napi::Value& value) { - if (!value.IsObject()) - { - return false; - } + return TryUnwrap(env, value) != nullptr; + } - const auto constructor = JsRuntime::NativeObject::GetFromJavaScript(env).Get(JS_CANVAS_GRADIENT_CONSTRUCTOR_NAME); - return constructor.IsFunction() && value.As().InstanceOf(constructor.As()); + CanvasGradient* CanvasGradient::TryUnwrap(Napi::Env env, const Napi::Value& value) + { + return NativeInstanceRegistry::TryUnwrap(env, value); } Napi::Object CanvasGradient::CreateLinear(Napi::Env env, const std::shared_ptr& context, float x0, float y0, float x1, float y1) @@ -163,10 +163,14 @@ namespace Babylon::Polyfills::Internal r0 = info[4].As().FloatValue(); r1 = info[5].As().FloatValue(); } + + // Register after successful construction only. + NativeInstanceRegistry::Add(info, this); } CanvasGradient::~CanvasGradient() { + NativeInstanceRegistry::Remove(this); Dispose(); } diff --git a/Polyfills/Canvas/Source/Gradient.h b/Polyfills/Canvas/Source/Gradient.h index 913a257b5d..c69cd19af2 100644 --- a/Polyfills/Canvas/Source/Gradient.h +++ b/Polyfills/Canvas/Source/Gradient.h @@ -16,12 +16,13 @@ namespace Babylon::Polyfills::Internal static Napi::Object CreateLinear(Napi::Env env, const std::shared_ptr& context, float x0, float y0, float x1, float y1); static Napi::Object CreateRadial(Napi::Env env, const std::shared_ptr& context, float x0, float y0, float r0, float x1, float y1, float r1); - // True only for objects this polyfill's own constructor produced. Unwrap() is a - // napi_unwrap, which dereferences whatever the object's slot happens to hold, so - // callers must establish the type first: `ctx.strokeStyle = {}` is reachable from - // script and would otherwise unwrap an object that was never wrapped. + // True only for objects this polyfill's own constructor produced. Never use + // ObjectWrap::Unwrap on an unchecked object; see NativeInstanceRegistry. static bool IsInstance(Napi::Env env, const Napi::Value& value); + // Returns the wrapped gradient, or nullptr when `value` is not one. + static CanvasGradient* TryUnwrap(Napi::Env env, const Napi::Value& value); + explicit CanvasGradient(const Napi::CallbackInfo& info); virtual ~CanvasGradient(); diff --git a/Polyfills/Canvas/Source/Image.cpp b/Polyfills/Canvas/Source/Image.cpp index 306a5b7815..af8489bacd 100644 --- a/Polyfills/Canvas/Source/Image.cpp +++ b/Polyfills/Canvas/Source/Image.cpp @@ -3,6 +3,7 @@ #include "Canvas.h" #include "Image.h" #include "Context.h" +#include "NativeInstanceRegistry.h" #include #include #include @@ -43,15 +44,23 @@ namespace Babylon::Polyfills::Internal JsRuntime::NativeObject::GetFromJavaScript(env).Set(JS_IMAGE_CONSTRUCTOR_NAME, func); } + NativeCanvasImage* NativeCanvasImage::TryUnwrap(Napi::Env env, const Napi::Value& value) + { + return NativeInstanceRegistry::TryUnwrap(env, value); + } + NativeCanvasImage::NativeCanvasImage(const Napi::CallbackInfo& info) : Napi::ObjectWrap{info} , m_runtimeScheduler{JsRuntime::GetFromJavaScript(info.Env())} , m_cancellationSource{std::make_shared()} { + // Register after successful construction only. + NativeInstanceRegistry::Add(info, this); } NativeCanvasImage::~NativeCanvasImage() { + NativeInstanceRegistry::Remove(this); Dispose(); } diff --git a/Polyfills/Canvas/Source/Image.h b/Polyfills/Canvas/Source/Image.h index bc40b60257..5c3b28a604 100644 --- a/Polyfills/Canvas/Source/Image.h +++ b/Polyfills/Canvas/Source/Image.h @@ -19,6 +19,8 @@ namespace Babylon::Polyfills::Internal public: static void Initialize(Napi::Env env); + static NativeCanvasImage* TryUnwrap(Napi::Env env, const Napi::Value& value); + explicit NativeCanvasImage(const Napi::CallbackInfo& info); virtual ~NativeCanvasImage(); diff --git a/Polyfills/Canvas/Source/NativeInstanceRegistry.h b/Polyfills/Canvas/Source/NativeInstanceRegistry.h new file mode 100644 index 0000000000..ae58741ca0 --- /dev/null +++ b/Polyfills/Canvas/Source/NativeInstanceRegistry.h @@ -0,0 +1,89 @@ +#pragma once + +#include +#include +#include +#include + +namespace Babylon::Polyfills::Internal +{ + // Brands a native ObjectWrap instance so TryUnwrap can reject forgeable instanceof / + // unchecked ObjectWrap::Unwrap (V8 AV; QuickJS wrong pointer). Temporary until + // JsRuntimeHost type tags land on all ports; then swap to CheckTypeTag. + template + class NativeInstanceRegistry + { + public: + // Register at end of ctor only — a throwing ctor never reaches the dtor. + static void Add(const Napi::CallbackInfo& info, T* instance) + { + // Non-enumerable brand so it does not show up in Object.keys / for..in. + // Object.defineProperty is supported by every JsRuntimeHost port, unlike the + // Napi::PropertyDescriptor and C Node-API surfaces. Omitted flags default to false. + Napi::Object self = info.This().As(); + Napi::Object descriptor = Napi::Object::New(info.Env()); + descriptor.Set("value", Napi::External::New(info.Env(), instance)); + Napi::Object object = info.Env().Global().Get("Object").As(); + object.Get("defineProperty").As().Call( + object, + {self, Napi::String::New(info.Env(), BRAND_NAME), descriptor}); + + const std::scoped_lock lock{Mutex()}; + Instances().insert(instance); + } + + static void Remove(const T* instance) + { + const std::scoped_lock lock{Mutex()}; + Instances().erase(instance); + } + + static T* TryUnwrap(Napi::Env env, const Napi::Value& value) + { + if (!value.IsObject()) + { + return nullptr; + } + + Napi::Value brand{env.Undefined()}; + try + { + brand = value.As().Get(BRAND_NAME); + } + catch (...) + { + } + + if (env.IsExceptionPending()) + { + (void)env.GetAndClearPendingException(); + } + + if (!brand.IsExternal()) + { + return nullptr; + } + + T* const candidate = brand.As>().Data(); + + const std::scoped_lock lock{Mutex()}; + return Instances().count(candidate) != 0 ? candidate : nullptr; + } + + private: + static constexpr const char* BRAND_NAME{"__nativeInstance"}; + + // Heap-allocated so a late finalizer during static teardown cannot touch a dead set. + static std::mutex& Mutex() + { + static auto* mutex{new std::mutex{}}; + return *mutex; + } + + static std::unordered_set& Instances() + { + static auto* instances{new std::unordered_set{}}; + return *instances; + } + }; +} \ No newline at end of file diff --git a/Polyfills/Canvas/Source/Path2D.cpp b/Polyfills/Canvas/Source/Path2D.cpp index 2060e76ebe..0c524b853c 100644 --- a/Polyfills/Canvas/Source/Path2D.cpp +++ b/Polyfills/Canvas/Source/Path2D.cpp @@ -1,368 +1,399 @@ -#include -#include -#include -#include "Canvas.h" -#include "Path2D.h" -#include - -#ifdef __GNUC__ -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wpedantic" -#endif - -#define NANOSVG_IMPLEMENTATION // Expands implementation -#include "nanosvg.h" - -#ifdef __GNUC__ -#pragma GCC diagnostic pop -#endif - -namespace Babylon::Polyfills::Internal -{ - static constexpr auto JS_PATH2D_CONSTRUCTOR_NAME = "Path2D"; - - void NativeCanvasPath2D::Initialize(Napi::Env env) - { - Napi::HandleScope scope{env}; - - Napi::Function func = DefineClass( - env, - JS_PATH2D_CONSTRUCTOR_NAME, - { - InstanceMethod("addPath", &NativeCanvasPath2D::AddPath), - InstanceMethod("closePath", &NativeCanvasPath2D::ClosePath), - InstanceMethod("moveTo", &NativeCanvasPath2D::MoveTo), - InstanceMethod("lineTo", &NativeCanvasPath2D::LineTo), - InstanceMethod("bezierCurveTo", &NativeCanvasPath2D::BezierCurveTo), - InstanceMethod("quadraticCurveTo", &NativeCanvasPath2D::QuadraticCurveTo), - InstanceMethod("arc", &NativeCanvasPath2D::Arc), - InstanceMethod("arcTo", &NativeCanvasPath2D::ArcTo), - InstanceMethod("ellipse", &NativeCanvasPath2D::Ellipse), - InstanceMethod("rect", &NativeCanvasPath2D::Rect), - InstanceMethod("roundRect", &NativeCanvasPath2D::RoundRect), - }); - - JsRuntime::NativeObject::GetFromJavaScript(env).Set(JS_PATH2D_CONSTRUCTOR_NAME, func); - - // Browsers expose Path2D as a global constructor, and Babylon.js relies on that: the generic - // engines call `new Path2D(d)` from AbstractEngine.createCanvasPath2D. Only NativeEngine - // overrides that method to use `_native.Path2D`, so without the global any portable browser - // code doing `new Path2D(...)` fails with "Path2D is not defined". - auto global = env.Global(); - - // Claim the global unconditionally. A Path2D from anywhere else cannot be drawn by this - // Context -- Context::Fill unwraps whatever object it is handed as a NativeCanvasPath2D -- - // so deferring to a foreign constructor would only guarantee that `ctx.fill(new Path2D(d))` - // unwraps an object that was never wrapped. Canvas::Initialize calls this while the - // polyfill is still installing, so anything already there came from outside it. - global.Set(JS_PATH2D_CONSTRUCTOR_NAME, func); - } - - NativeCanvasPath2D::NativeCanvasPath2D(const Napi::CallbackInfo& info) - : Napi::ObjectWrap{info} - , m_commands{std::deque()} - { - const NativeCanvasPath2D* path = info.Length() == 1 && info[0].IsObject() - ? NativeCanvasPath2D::Unwrap(info[0].As()) - : nullptr; - const std::string d = info.Length() == 1 && info[0].IsString() ? info[0].As().Utf8Value() : ""; - - if (path != nullptr) - { - for (const auto& command : *path) - { - m_commands.push_back(command); - } - } - - if (!d.empty()) - { - NSVGparser* parser = nsvg__createParser(); - const char* path[] = {"d", d.c_str(), NULL}; - const char** attr = {path}; - - assert(strcmp(attr[0], "d") == 0); - assert(!attr[2]); // nsvg__parsePath terminates attr parsing on falsy - - nsvg__parsePath(parser, attr); - - for (NSVGshape *shape = parser->image->shapes; shape != NULL; shape = shape->next) { - for (NSVGpath *path = shape->paths; path != NULL; path = path->next) { - for (int i = 0; i < path->npts-1; i += 3) { - float* p = &path->pts[i*2]; - - auto x0 = p[0]; // start x, same as end x of previous - auto y0 = p[1]; // start y, same as end y of previous - auto cpx1 = p[2]; - auto cpy1 = p[3]; - auto cpx2 = p[4]; - auto cpy2 = p[5]; - auto x1 = p[6]; // end x - auto y1 = p[7]; // end y - - // Only need to move on new shape - if (i == 0) - { - Path2DCommandArgs moveArgs = {}; - moveArgs.moveTo = {x0, y0}; - AppendCommand(P2D_MOVETO, moveArgs); - } - - Path2DCommandArgs args = {}; - args.bezierTo = {cpx1, cpy1, cpx2, cpy2, x1, y1}; - AppendCommand(P2D_BEZIERTO, args); - } - } - } - - nsvg__deleteParser(parser); - } - } - - typename std::deque::iterator NativeCanvasPath2D::begin() - { - return m_commands.begin(); - } - - typename std::deque::iterator NativeCanvasPath2D::end() - { - return m_commands.end(); - } - - typename std::deque::const_iterator NativeCanvasPath2D::begin() const - { - return m_commands.begin(); - } - - typename std::deque::const_iterator NativeCanvasPath2D::end() const - { - return m_commands.end(); - } - - void NativeCanvasPath2D::AppendCommand(Path2DCommandTypes type, Path2DCommandArgs args) - { - m_commands.push_back({type, args}); - } - - void NativeCanvasPath2D::AddPath(const Napi::CallbackInfo& info) - { - const NativeCanvasPath2D* path = NativeCanvasPath2D::Unwrap(info[0].As()); - - // optional transform arg - bool xformInvReady{false}; - float xformInv[6]; - if (info.Length() == 2) - { - Napi::Object transform = info[1].As(); - auto a = transform.Get("a").As().FloatValue(); - auto b = transform.Get("b").As().FloatValue(); - auto c = transform.Get("c").As().FloatValue(); - auto d = transform.Get("d").As().FloatValue(); - auto e = transform.Get("e").As().FloatValue(); - auto f = transform.Get("f").As().FloatValue(); - - float xform[6] = {a, b, c, d, e, f}; - nsvg__xformInverse(xformInv, xform); - xformInvReady = true; - - Path2DCommandArgs args = {}; - args.transform = { xform[0], xform[1], xform[2], xform[3], xform[4], xform[5] }; - AppendCommand(P2D_TRANSFORM, args); - } - - for (const auto& command : *path) - { - m_commands.push_back(command); - } - - // invert transform after all commands played - if (info.Length() == 2) - { - assert(xformInvReady); - Path2DCommandArgs argsInv = {}; - argsInv.transform = {xformInv[0], xformInv[1], xformInv[2], xformInv[3], xformInv[4], xformInv[5]}; - AppendCommand(P2D_TRANSFORM, argsInv); - } - } - - void NativeCanvasPath2D::ClosePath(const Napi::CallbackInfo& info) - { - AppendCommand(P2D_CLOSE, {}); - } - - void NativeCanvasPath2D::MoveTo(const Napi::CallbackInfo& info) - { - const auto x = static_cast(info[0].As().DoubleValue()); - const auto y = static_cast(info[1].As().DoubleValue()); - - Path2DCommandArgs args = {}; - args.moveTo = {x, y}; - AppendCommand(P2D_MOVETO, args); - } - - void NativeCanvasPath2D::LineTo(const Napi::CallbackInfo& info) - { - const auto x = static_cast(info[0].As().DoubleValue()); - const auto y = static_cast(info[1].As().DoubleValue()); - - Path2DCommandArgs args = {}; - args.lineTo = {x, y}; - AppendCommand(P2D_LINETO, args); - } - - void NativeCanvasPath2D::BezierCurveTo(const Napi::CallbackInfo& info) - { - const auto cp1x = static_cast(info[0].As().DoubleValue()); - const auto cp1y = static_cast(info[1].As().DoubleValue()); - const auto cp2x = static_cast(info[2].As().DoubleValue()); - const auto cp2y = static_cast(info[3].As().DoubleValue()); - const auto x = static_cast(info[4].As().DoubleValue()); - const auto y = static_cast(info[5].As().DoubleValue()); - - Path2DCommandArgs args = {}; - args.bezierTo = {cp1x, cp1y, cp2x, cp2y, x, y}; - AppendCommand(P2D_BEZIERTO, args); - } - - void NativeCanvasPath2D::QuadraticCurveTo(const Napi::CallbackInfo& info) - { - const auto cpx = static_cast(info[0].As().DoubleValue()); - const auto cpy = static_cast(info[1].As().DoubleValue()); - const auto x = static_cast(info[2].As().DoubleValue()); - const auto y = static_cast(info[3].As().DoubleValue()); - - Path2DCommandArgs args = {}; - args.quadTo = {cpx, cpy, x, y}; - AppendCommand(P2D_QUADTO, args); - } - - void NativeCanvasPath2D::Arc(const Napi::CallbackInfo& info) - { - const auto x = static_cast(info[0].As().DoubleValue()); - const auto y = static_cast(info[1].As().DoubleValue()); - const auto radius = static_cast(info[2].As().DoubleValue()); - const auto startAngle = static_cast(info[3].As().DoubleValue()); - const auto endAngle = static_cast(info[4].As().DoubleValue()); - const auto counterclockwise = info.Length() == 6 ? info[5].As() : false; - - Path2DCommandArgs args = {}; - args.arc = {x, y, radius, startAngle, endAngle, counterclockwise}; - AppendCommand(P2D_ARC, args); - } - - void NativeCanvasPath2D::ArcTo(const Napi::CallbackInfo& info) - { - const auto x1 = static_cast(info[0].As().DoubleValue()); - const auto y1 = static_cast(info[1].As().DoubleValue()); - const auto x2 = static_cast(info[2].As().DoubleValue()); - const auto y2 = static_cast(info[3].As().DoubleValue()); - const auto radius = static_cast(info[4].As().DoubleValue()); - - Path2DCommandArgs args = {}; - args.arcTo = {x1, y1, x2, y2, radius}; - AppendCommand(P2D_ARCTO, args); - } - - void NativeCanvasPath2D::Ellipse(const Napi::CallbackInfo& info) - { - const auto x = static_cast(info[0].As().DoubleValue()); - const auto y = static_cast(info[1].As().DoubleValue()); - const auto radiusX = static_cast(info[2].As().DoubleValue()); - const auto radiusY = static_cast(info[3].As().DoubleValue()); - const auto rotation = static_cast(info[4].As().DoubleValue()); - const auto startAngle = static_cast(info[5].As().DoubleValue()); - const auto endAngle = static_cast(info[6].As().DoubleValue()); - const auto counterclockwise = info.Length() == 8 ? info[7].As() : false; - - Path2DCommandArgs args = {}; - args.ellipse = {x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterclockwise}; - AppendCommand(P2D_ELLIPSE, args); - } - - void NativeCanvasPath2D::Rect(const Napi::CallbackInfo& info) - { - const auto x = static_cast(info[0].As().DoubleValue()); - const auto y = static_cast(info[1].As().DoubleValue()); - const auto width = static_cast(info[2].As().DoubleValue()); - const auto height = static_cast(info[3].As().DoubleValue()); - - Path2DCommandArgs args = {}; - args.rect = {x, y, width, height}; - AppendCommand(P2D_RECT, args); - } - - void NativeCanvasPath2D::RoundRect(const Napi::CallbackInfo& info) - { - const auto x = static_cast(info[0].As().DoubleValue()); - const auto y = static_cast(info[1].As().DoubleValue()); - const auto width = static_cast(info[2].As().DoubleValue()); - const auto height = static_cast(info[3].As().DoubleValue()); - const auto radii = info[4]; - - if (radii.IsNumber()) - { - const auto radius = radii.As().FloatValue(); - Path2DCommandArgs args = {}; - args.roundRect = {x, y, width, height, radius}; - AppendCommand(P2D_ROUNDRECT, args); - } - else if (radii.IsArray()) - { - const auto radiiArray = radii.As(); - const auto radiiArrayLength = radiiArray.Length(); - if (radiiArrayLength == 1) - { - const auto radius = radiiArray[0u].As().FloatValue(); - Path2DCommandArgs args = {}; - args.roundRect = {x, y, width, height, radius}; - AppendCommand(P2D_ROUNDRECT, args); - } - else if (radiiArrayLength == 2) - { - const auto topLeftBottomRight = radiiArray[0u].As().FloatValue(); - const auto topRightBottomLeft = radiiArray[1u].As().FloatValue(); - Path2DCommandArgs args = {}; - args.roundRectVarying = {x, y, width, height, topLeftBottomRight, topRightBottomLeft, topLeftBottomRight, topRightBottomLeft}; - AppendCommand(P2D_ROUNDRECTVARYING, args); - } - else if (radiiArrayLength == 3) - { - const auto topLeft = radiiArray[0u].As().FloatValue(); - const auto topRightBottomLeft = radiiArray[1u].As().FloatValue(); - const auto bottomRight = radiiArray[2u].As().FloatValue(); - Path2DCommandArgs args = {}; - args.roundRectVarying = {x, y, width, height, topLeft, topRightBottomLeft, bottomRight, topRightBottomLeft}; - AppendCommand(P2D_ROUNDRECTVARYING, args); - } - else if (radiiArrayLength == 4) - { - const auto topLeft = radiiArray[0u].As().FloatValue(); - const auto topRight = radiiArray[1u].As().FloatValue(); - const auto bottomRight = radiiArray[2u].As().FloatValue(); - const auto bottomLeft = radiiArray[3u].As().FloatValue(); - Path2DCommandArgs args = {}; - args.roundRectVarying = {x, y, width, height, topLeft, topRight, bottomRight, bottomLeft}; - AppendCommand(P2D_ROUNDRECTVARYING, args); - } - else - { - throw Napi::Error::New(info.Env(), "Invalid number of parameters for radii"); - } - } - // DOMPoint - // TODO: move duplicate Path2D & Context args parsing into a utils.cpp - else if (radii.IsObject()) - { - const auto dompoint = radii.As(); - const auto radiusX = dompoint.Get("x").As().FloatValue(); - const auto radiusY = dompoint.Get("y").As().FloatValue(); - Path2DCommandArgs args = {}; - args.roundRectElliptic = {x, y, width, height, radiusX, radiusY, radiusX, radiusY, radiusX, radiusY, radiusX, radiusY}; - AppendCommand(P2D_ROUNDRECTELLIPTIC, args); - } - else - { - throw Napi::Error::New(info.Env(), "Invalid radii parameter"); - } - } -} +#include +#include +#include +#include "Canvas.h" +#include "NativeInstanceRegistry.h" +#include "Path2D.h" +#include + +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpedantic" +#endif + +#define NANOSVG_IMPLEMENTATION // Expands implementation +#include "nanosvg.h" + +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + +namespace Babylon::Polyfills::Internal +{ + static constexpr auto JS_PATH2D_CONSTRUCTOR_NAME = "Path2D"; + + void NativeCanvasPath2D::Initialize(Napi::Env env) + { + Napi::HandleScope scope{env}; + + Napi::Function func = DefineClass( + env, + JS_PATH2D_CONSTRUCTOR_NAME, + { + InstanceMethod("addPath", &NativeCanvasPath2D::AddPath), + InstanceMethod("closePath", &NativeCanvasPath2D::ClosePath), + InstanceMethod("moveTo", &NativeCanvasPath2D::MoveTo), + InstanceMethod("lineTo", &NativeCanvasPath2D::LineTo), + InstanceMethod("bezierCurveTo", &NativeCanvasPath2D::BezierCurveTo), + InstanceMethod("quadraticCurveTo", &NativeCanvasPath2D::QuadraticCurveTo), + InstanceMethod("arc", &NativeCanvasPath2D::Arc), + InstanceMethod("arcTo", &NativeCanvasPath2D::ArcTo), + InstanceMethod("ellipse", &NativeCanvasPath2D::Ellipse), + InstanceMethod("rect", &NativeCanvasPath2D::Rect), + InstanceMethod("roundRect", &NativeCanvasPath2D::RoundRect), + }); + + JsRuntime::NativeObject::GetFromJavaScript(env).Set(JS_PATH2D_CONSTRUCTOR_NAME, func); + + // Browsers expose Path2D as a global constructor, and Babylon.js relies on that: the generic + // engines call `new Path2D(d)` from AbstractEngine.createCanvasPath2D. Only NativeEngine + // overrides that method to use `_native.Path2D`, so without the global any portable browser + // code doing `new Path2D(...)` fails with "Path2D is not defined". + auto global = env.Global(); + + // Claim the global unconditionally. A Path2D from anywhere else cannot be drawn by this + // Context -- Context::Fill unwraps whatever object it is handed as a NativeCanvasPath2D -- + // so deferring to a foreign constructor would only guarantee that `ctx.fill(new Path2D(d))` + // unwraps an object that was never wrapped. Canvas::Initialize calls this while the + // polyfill is still installing, so anything already there came from outside it. + global.Set(JS_PATH2D_CONSTRUCTOR_NAME, func); + } + + bool NativeCanvasPath2D::IsInstance(Napi::Env env, const Napi::Value& value) + { + return TryUnwrap(env, value) != nullptr; + } + + NativeCanvasPath2D* NativeCanvasPath2D::TryUnwrap(Napi::Env env, const Napi::Value& value) + { + return NativeInstanceRegistry::TryUnwrap(env, value); + } + + NativeCanvasPath2D::NativeCanvasPath2D(const Napi::CallbackInfo& info) + : Napi::ObjectWrap{info} + , m_commands{std::deque()} + { + // Path2D | DOMString union: only a real Path2D is copied; everything else stringifies. + const NativeCanvasPath2D* path = info.Length() >= 1 + ? NativeCanvasPath2D::TryUnwrap(info.Env(), info[0]) + : nullptr; + const std::string d = path == nullptr && info.Length() >= 1 && !info[0].IsUndefined() + ? info[0].ToString().Utf8Value() + : ""; + + if (path != nullptr) + { + for (const auto& command : *path) + { + m_commands.push_back(command); + } + } + + if (!d.empty()) + { + NSVGparser* parser = nsvg__createParser(); + const char* path[] = {"d", d.c_str(), NULL}; + const char** attr = {path}; + + assert(strcmp(attr[0], "d") == 0); + assert(!attr[2]); // nsvg__parsePath terminates attr parsing on falsy + + nsvg__parsePath(parser, attr); + + for (NSVGshape *shape = parser->image->shapes; shape != NULL; shape = shape->next) { + for (NSVGpath *path = shape->paths; path != NULL; path = path->next) { + for (int i = 0; i < path->npts-1; i += 3) { + float* p = &path->pts[i*2]; + + auto x0 = p[0]; // start x, same as end x of previous + auto y0 = p[1]; // start y, same as end y of previous + auto cpx1 = p[2]; + auto cpy1 = p[3]; + auto cpx2 = p[4]; + auto cpy2 = p[5]; + auto x1 = p[6]; // end x + auto y1 = p[7]; // end y + + // Only need to move on new shape + if (i == 0) + { + Path2DCommandArgs moveArgs = {}; + moveArgs.moveTo = {x0, y0}; + AppendCommand(P2D_MOVETO, moveArgs); + } + + Path2DCommandArgs args = {}; + args.bezierTo = {cpx1, cpy1, cpx2, cpy2, x1, y1}; + AppendCommand(P2D_BEZIERTO, args); + } + } + } + + nsvg__deleteParser(parser); + } + + // Register after successful construction only. + NativeInstanceRegistry::Add(info, this); + } + + NativeCanvasPath2D::~NativeCanvasPath2D() + { + NativeInstanceRegistry::Remove(this); + } + + typename std::deque::iterator NativeCanvasPath2D::begin() + { + return m_commands.begin(); + } + + typename std::deque::iterator NativeCanvasPath2D::end() + { + return m_commands.end(); + } + + typename std::deque::const_iterator NativeCanvasPath2D::begin() const + { + return m_commands.begin(); + } + + typename std::deque::const_iterator NativeCanvasPath2D::end() const + { + return m_commands.end(); + } + + void NativeCanvasPath2D::AppendCommand(Path2DCommandTypes type, Path2DCommandArgs args) + { + m_commands.push_back({type, args}); + } + + void NativeCanvasPath2D::AddPath(const Napi::CallbackInfo& info) + { + if (info.Length() < 1) + { + throw Napi::TypeError::New(info.Env(), "Path2D.addPath: requires at least 1 argument (path)."); + } + + NativeCanvasPath2D* const path = NativeCanvasPath2D::TryUnwrap(info.Env(), info[0]); + if (path == nullptr) + { + throw Napi::TypeError::New(info.Env(), "Path2D.addPath: the first argument is not a Path2D."); + } + + // optional transform arg + bool xformInvReady{false}; + float xformInv[6]; + if (info.Length() == 2) + { + Napi::Object transform = info[1].As(); + auto a = transform.Get("a").As().FloatValue(); + auto b = transform.Get("b").As().FloatValue(); + auto c = transform.Get("c").As().FloatValue(); + auto d = transform.Get("d").As().FloatValue(); + auto e = transform.Get("e").As().FloatValue(); + auto f = transform.Get("f").As().FloatValue(); + + float xform[6] = {a, b, c, d, e, f}; + nsvg__xformInverse(xformInv, xform); + xformInvReady = true; + + Path2DCommandArgs args = {}; + args.transform = { xform[0], xform[1], xform[2], xform[3], xform[4], xform[5] }; + AppendCommand(P2D_TRANSFORM, args); + } + + for (const auto& command : *path) + { + m_commands.push_back(command); + } + + // invert transform after all commands played + if (info.Length() == 2) + { + assert(xformInvReady); + Path2DCommandArgs argsInv = {}; + argsInv.transform = {xformInv[0], xformInv[1], xformInv[2], xformInv[3], xformInv[4], xformInv[5]}; + AppendCommand(P2D_TRANSFORM, argsInv); + } + } + + void NativeCanvasPath2D::ClosePath(const Napi::CallbackInfo& info) + { + AppendCommand(P2D_CLOSE, {}); + } + + void NativeCanvasPath2D::MoveTo(const Napi::CallbackInfo& info) + { + const auto x = static_cast(info[0].As().DoubleValue()); + const auto y = static_cast(info[1].As().DoubleValue()); + + Path2DCommandArgs args = {}; + args.moveTo = {x, y}; + AppendCommand(P2D_MOVETO, args); + } + + void NativeCanvasPath2D::LineTo(const Napi::CallbackInfo& info) + { + const auto x = static_cast(info[0].As().DoubleValue()); + const auto y = static_cast(info[1].As().DoubleValue()); + + Path2DCommandArgs args = {}; + args.lineTo = {x, y}; + AppendCommand(P2D_LINETO, args); + } + + void NativeCanvasPath2D::BezierCurveTo(const Napi::CallbackInfo& info) + { + const auto cp1x = static_cast(info[0].As().DoubleValue()); + const auto cp1y = static_cast(info[1].As().DoubleValue()); + const auto cp2x = static_cast(info[2].As().DoubleValue()); + const auto cp2y = static_cast(info[3].As().DoubleValue()); + const auto x = static_cast(info[4].As().DoubleValue()); + const auto y = static_cast(info[5].As().DoubleValue()); + + Path2DCommandArgs args = {}; + args.bezierTo = {cp1x, cp1y, cp2x, cp2y, x, y}; + AppendCommand(P2D_BEZIERTO, args); + } + + void NativeCanvasPath2D::QuadraticCurveTo(const Napi::CallbackInfo& info) + { + const auto cpx = static_cast(info[0].As().DoubleValue()); + const auto cpy = static_cast(info[1].As().DoubleValue()); + const auto x = static_cast(info[2].As().DoubleValue()); + const auto y = static_cast(info[3].As().DoubleValue()); + + Path2DCommandArgs args = {}; + args.quadTo = {cpx, cpy, x, y}; + AppendCommand(P2D_QUADTO, args); + } + + void NativeCanvasPath2D::Arc(const Napi::CallbackInfo& info) + { + const auto x = static_cast(info[0].As().DoubleValue()); + const auto y = static_cast(info[1].As().DoubleValue()); + const auto radius = static_cast(info[2].As().DoubleValue()); + const auto startAngle = static_cast(info[3].As().DoubleValue()); + const auto endAngle = static_cast(info[4].As().DoubleValue()); + const auto counterclockwise = info.Length() == 6 ? info[5].As() : false; + + Path2DCommandArgs args = {}; + args.arc = {x, y, radius, startAngle, endAngle, counterclockwise}; + AppendCommand(P2D_ARC, args); + } + + void NativeCanvasPath2D::ArcTo(const Napi::CallbackInfo& info) + { + const auto x1 = static_cast(info[0].As().DoubleValue()); + const auto y1 = static_cast(info[1].As().DoubleValue()); + const auto x2 = static_cast(info[2].As().DoubleValue()); + const auto y2 = static_cast(info[3].As().DoubleValue()); + const auto radius = static_cast(info[4].As().DoubleValue()); + + Path2DCommandArgs args = {}; + args.arcTo = {x1, y1, x2, y2, radius}; + AppendCommand(P2D_ARCTO, args); + } + + void NativeCanvasPath2D::Ellipse(const Napi::CallbackInfo& info) + { + const auto x = static_cast(info[0].As().DoubleValue()); + const auto y = static_cast(info[1].As().DoubleValue()); + const auto radiusX = static_cast(info[2].As().DoubleValue()); + const auto radiusY = static_cast(info[3].As().DoubleValue()); + const auto rotation = static_cast(info[4].As().DoubleValue()); + const auto startAngle = static_cast(info[5].As().DoubleValue()); + const auto endAngle = static_cast(info[6].As().DoubleValue()); + const auto counterclockwise = info.Length() == 8 ? info[7].As() : false; + + Path2DCommandArgs args = {}; + args.ellipse = {x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterclockwise}; + AppendCommand(P2D_ELLIPSE, args); + } + + void NativeCanvasPath2D::Rect(const Napi::CallbackInfo& info) + { + const auto x = static_cast(info[0].As().DoubleValue()); + const auto y = static_cast(info[1].As().DoubleValue()); + const auto width = static_cast(info[2].As().DoubleValue()); + const auto height = static_cast(info[3].As().DoubleValue()); + + Path2DCommandArgs args = {}; + args.rect = {x, y, width, height}; + AppendCommand(P2D_RECT, args); + } + + void NativeCanvasPath2D::RoundRect(const Napi::CallbackInfo& info) + { + const auto x = static_cast(info[0].As().DoubleValue()); + const auto y = static_cast(info[1].As().DoubleValue()); + const auto width = static_cast(info[2].As().DoubleValue()); + const auto height = static_cast(info[3].As().DoubleValue()); + const auto radii = info[4]; + + if (radii.IsNumber()) + { + const auto radius = radii.As().FloatValue(); + Path2DCommandArgs args = {}; + args.roundRect = {x, y, width, height, radius}; + AppendCommand(P2D_ROUNDRECT, args); + } + else if (radii.IsArray()) + { + const auto radiiArray = radii.As(); + const auto radiiArrayLength = radiiArray.Length(); + if (radiiArrayLength == 1) + { + const auto radius = radiiArray[0u].As().FloatValue(); + Path2DCommandArgs args = {}; + args.roundRect = {x, y, width, height, radius}; + AppendCommand(P2D_ROUNDRECT, args); + } + else if (radiiArrayLength == 2) + { + const auto topLeftBottomRight = radiiArray[0u].As().FloatValue(); + const auto topRightBottomLeft = radiiArray[1u].As().FloatValue(); + Path2DCommandArgs args = {}; + args.roundRectVarying = {x, y, width, height, topLeftBottomRight, topRightBottomLeft, topLeftBottomRight, topRightBottomLeft}; + AppendCommand(P2D_ROUNDRECTVARYING, args); + } + else if (radiiArrayLength == 3) + { + const auto topLeft = radiiArray[0u].As().FloatValue(); + const auto topRightBottomLeft = radiiArray[1u].As().FloatValue(); + const auto bottomRight = radiiArray[2u].As().FloatValue(); + Path2DCommandArgs args = {}; + args.roundRectVarying = {x, y, width, height, topLeft, topRightBottomLeft, bottomRight, topRightBottomLeft}; + AppendCommand(P2D_ROUNDRECTVARYING, args); + } + else if (radiiArrayLength == 4) + { + const auto topLeft = radiiArray[0u].As().FloatValue(); + const auto topRight = radiiArray[1u].As().FloatValue(); + const auto bottomRight = radiiArray[2u].As().FloatValue(); + const auto bottomLeft = radiiArray[3u].As().FloatValue(); + Path2DCommandArgs args = {}; + args.roundRectVarying = {x, y, width, height, topLeft, topRight, bottomRight, bottomLeft}; + AppendCommand(P2D_ROUNDRECTVARYING, args); + } + else + { + throw Napi::Error::New(info.Env(), "Invalid number of parameters for radii"); + } + } + // DOMPoint + // TODO: move duplicate Path2D & Context args parsing into a utils.cpp + else if (radii.IsObject()) + { + const auto dompoint = radii.As(); + const auto radiusX = dompoint.Get("x").As().FloatValue(); + const auto radiusY = dompoint.Get("y").As().FloatValue(); + Path2DCommandArgs args = {}; + args.roundRectElliptic = {x, y, width, height, radiusX, radiusY, radiusX, radiusY, radiusX, radiusY, radiusX, radiusY}; + AppendCommand(P2D_ROUNDRECTELLIPTIC, args); + } + else + { + throw Napi::Error::New(info.Env(), "Invalid radii parameter"); + } + } +} diff --git a/Polyfills/Canvas/Source/Path2D.h b/Polyfills/Canvas/Source/Path2D.h index 9f00bb558e..0ec53c8d08 100644 --- a/Polyfills/Canvas/Source/Path2D.h +++ b/Polyfills/Canvas/Source/Path2D.h @@ -65,8 +65,15 @@ namespace Babylon::Polyfills::Internal public: static void Initialize(Napi::Env env); + // True only for objects actually produced by this class's constructor. Never use + // ObjectWrap::Unwrap on an unchecked object; see NativeInstanceRegistry. + static bool IsInstance(Napi::Env env, const Napi::Value& value); + + // Returns the wrapped path, or nullptr when `value` is not one of ours. + static NativeCanvasPath2D* TryUnwrap(Napi::Env env, const Napi::Value& value); + explicit NativeCanvasPath2D(const Napi::CallbackInfo& info); - // virtual ~NativeCanvasPath2D(); // TODO: destructor? empty queue? + ~NativeCanvasPath2D(); typename std::deque::iterator begin(); typename std::deque::iterator end();