diff --git a/Apps/UnitTests/CMakeLists.txt b/Apps/UnitTests/CMakeLists.txt index 3b4d70a89..5a35ac2e3 100644 --- a/Apps/UnitTests/CMakeLists.txt +++ b/Apps/UnitTests/CMakeLists.txt @@ -32,6 +32,7 @@ set_property(TARGET UnitTests PROPERTY UNITY_BUILD false) target_link_libraries(UnitTests PRIVATE AppRuntime + PRIVATE Blob PRIVATE Canvas PRIVATE Console PRIVATE GraphicsDevice diff --git a/Apps/UnitTests/Scripts/tests.ts b/Apps/UnitTests/Scripts/tests.ts index b9f004325..e95992f5c 100644 --- a/Apps/UnitTests/Scripts/tests.ts +++ b/Apps/UnitTests/Scripts/tests.ts @@ -295,8 +295,9 @@ describe("PostProcesses", function () { describe("NativeEncoding", function () { this.timeout(0); - function expectValidPNG(arrayBuffer: ArrayBuffer) { - expect(arrayBuffer).to.be.instanceOf(ArrayBuffer); + async function expectValidPNG(blob: Blob) { + expect(blob).to.be.instanceOf(Blob); + const arrayBuffer = await blob.arrayBuffer(); expect(arrayBuffer.byteLength).to.be.greaterThan(0); const pngSignature = new Uint8Array(arrayBuffer.slice(0, 4)); @@ -309,7 +310,7 @@ describe("NativeEncoding", function () { it("should encode a PNG", async function () { const pixelData = new Uint8Array(4).fill(255); const result = await _native.EncodeImageAsync(pixelData, 1, 1, "image/png", false); - expectValidPNG(result); + await expectValidPNG(result); }); it("should handle multiple concurrent encoding tasks", async function () { @@ -320,17 +321,7 @@ describe("NativeEncoding", function () { const results = await Promise.all(pixelDatas.map((pixelData) => _native.EncodeImageAsync(pixelData, 1, 1, "image/png", false) )); - results.forEach(expectValidPNG); - }); - - it("should reject if MIME type not supported", async function () { - const pixelData = new Uint8Array([255, 0, 0, 255]); - try { - await _native.EncodeImageAsync(pixelData, 1, 1, "bad-mimetype", false); - expect.fail("Expected promise to reject with unsupported mime type"); - } catch (error) { - expect(error).to.exist; - } + await Promise.all(results.map(b => expectValidPNG(b))); }); }); diff --git a/Apps/UnitTests/Shared/Shared.cpp b/Apps/UnitTests/Shared/Shared.cpp index 7ab708136..871582001 100644 --- a/Apps/UnitTests/Shared/Shared.cpp +++ b/Apps/UnitTests/Shared/Shared.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -73,6 +74,7 @@ TEST(JavaScript, All) std::cout.flush(); }); Babylon::Polyfills::Window::Initialize(env); + Babylon::Polyfills::Blob::Initialize(env); nativeCanvas.emplace(Babylon::Polyfills::Canvas::Initialize(env)); Babylon::Plugins::NativeEngine::Initialize(env); Babylon::Plugins::NativeEncoding::Initialize(env); diff --git a/Install/Install.cmake b/Install/Install.cmake index 459e3d8cc..c1d1b9f97 100644 --- a/Install/Install.cmake +++ b/Install/Install.cmake @@ -187,3 +187,8 @@ if(TARGET XMLHttpRequest) install_targets(XMLHttpRequest) install_include_for_targets(XMLHttpRequest) endif() + +if(TARGET Blob) + install_targets(Blob) + install_include_for_targets(Blob) +endif() diff --git a/Plugins/NativeEncoding/README.md b/Plugins/NativeEncoding/README.md index 211559273..ee8164cef 100644 --- a/Plugins/NativeEncoding/README.md +++ b/Plugins/NativeEncoding/README.md @@ -4,26 +4,36 @@ The NativeEncoding plugin provides native image encoding capabilities to Babylon, allowing raw pixel data to be encoded into standard image formats (PNG, JPEG, WebP, etc.). +## Prerequisites + +A **Blob implementation** must be registered before using this plugin. The Babylon polyfill provides one that can be initialized from C++ using `Babylon::Polyfills::Blob::Initialize()`. + ## Limitations -Currently, **only PNG encoding** is supported. +Currently, **only PNG encoding** is supported. If a different MIME type is requested (e.g., "image/jpeg"), it will be ignored. -## Design +## Design Unlike a traditional polyfill which would implement Canvas's `toBlob()` or `toDataURL()` methods, NativeEncoding exists as a plugin because: + 1. **No standard Web API exists** for general-purpose image encoding separate from Canvas 2. **Simplicity** - Exposes only what Babylon actually needs: direct pixel-to-bytes encoding 3. **Efficiency** - Avoids extra routing through the Canvas API via intermediate data structures 4. **Modularity** - Image encoding is a separate concern from 2D canvas rendering 5. **Extensibility** - New codecs can be added in the future without bloating other components -An encoding function is exposed on the `_native` global object, similar to NativeOptimizations. +An encoding function is exposed on the `_native` global object, similar to NativeOptimizations. ```typescript interface INative { - EncodeImageAsync: (pixelData: Uint8Array, width: number, height: number, mimeType: string, invertY: boolean) => Promise; + EncodeImageAsync: ( + pixelData: ArrayBufferView, + width: number, + height: number, + mimeType?: string, + invertY?: boolean + ) => Promise; } ``` It should be wrapped by higher-level Babylon.js APIs (e.g., DumpTools) for common workflows like asset exports and screenshots. - diff --git a/Plugins/NativeEncoding/Source/NativeEncoding.cpp b/Plugins/NativeEncoding/Source/NativeEncoding.cpp index 88b7d4730..19c3628a0 100644 --- a/Plugins/NativeEncoding/Source/NativeEncoding.cpp +++ b/Plugins/NativeEncoding/Source/NativeEncoding.cpp @@ -15,7 +15,7 @@ namespace Babylon::Plugins { namespace { - std::shared_ptr> EncodePNG(const std::vector& pixelData, uint32_t width, uint32_t height, bool invertY) + std::shared_ptr> EncodePNG(const std::vector& pixelData, uint32_t width, uint32_t height, bool invertY) { auto memoryBlock{bx::MemoryBlock(&Graphics::DeviceContext::GetDefaultAllocator())}; auto writer{bx::MemoryWriter(&memoryBlock)}; @@ -35,7 +35,7 @@ namespace Babylon::Plugins throw std::runtime_error("Failed to encode PNG image: output is empty"); } - auto result{std::make_shared>(byteLength)}; + auto result{std::make_shared>(byteLength)}; std::memcpy(result->data(), memoryBlock.more(0), byteLength); return result; @@ -43,20 +43,14 @@ namespace Babylon::Plugins Napi::Value EncodeImageAsync(const Napi::CallbackInfo& info) { - auto buffer{info[0].As()}; + auto buffer{info[0].As()}; // ArrayBufferView auto width{info[1].As().Uint32Value()}; auto height{info[2].As().Uint32Value()}; - auto mimeType{info[3].As().Utf8Value()}; - auto invertY{info[4].As().Value()}; - + auto mimeType{info.Length() > 3 && !info[3].IsUndefined() ? info[3].As().Utf8Value() : "image/png"}; + auto invertY{info.Length() > 4 && !info[4].IsUndefined() ? info[4].As().Value() : false}; + auto env{info.Env()}; auto deferred{Napi::Promise::Deferred::New(env)}; - - if (mimeType != "image/png") - { - deferred.Reject(Napi::Error::New(env, "Unsupported mime type: " + mimeType + ". Only image/png is currently supported.").Value()); - return deferred.Promise(); - } if (buffer.ByteLength() != width * height * 4) { @@ -65,14 +59,15 @@ namespace Babylon::Plugins } auto runtimeScheduler{std::make_shared(JsRuntime::GetFromJavaScript(env))}; - auto pixelData{std::vector(buffer.Data(), buffer.Data() + buffer.ByteLength())}; + auto start = static_cast(buffer.ArrayBuffer().Data()) + buffer.ByteOffset(); + auto pixelData{std::vector(start, start + buffer.ByteLength())}; arcana::make_task(arcana::threadpool_scheduler, arcana::cancellation_source::none(), [pixelData{std::move(pixelData)}, width, height, invertY]() { return EncodePNG(pixelData, width, height, invertY); }) .then(*runtimeScheduler, arcana::cancellation_source::none(), - [runtimeScheduler, deferred, env](const arcana::expected>, std::exception_ptr>& result) { + [runtimeScheduler, deferred, env](const arcana::expected>, std::exception_ptr>& result) { // TODO: Crash risk on JS teardown - this async work isn't tied to any JS object lifetime, // unlike other plugins that cancel / clean up pending work in their destructors. if (result.has_error()) @@ -83,8 +78,17 @@ namespace Babylon::Plugins auto& imageData = result.value(); auto arrayBuffer{Napi::ArrayBuffer::New(env, imageData->data(), imageData->size(), [imageData](Napi::Env, void*) {})}; - - deferred.Resolve(arrayBuffer); + + auto blobCtor = env.Global().Get("Blob").As(); + auto blobParts = Napi::Array::New(env, 1); + blobParts.Set(uint32_t{0}, arrayBuffer); + + auto options = Napi::Object::New(env); + options.Set("type", Napi::String::New(env, "image/png")); + + auto blob = blobCtor.New({blobParts, options}); + + deferred.Resolve(blob); }); return deferred.Promise();