| /* | |
| * Copyright 2022 Rive | |
| */ | |
| #include "testing_window.hpp" | |
| #if !defined(RIVE_SVG) || !defined(__EMSCRIPTEN__) | |
| TestingWindow* TestingWindow::MakeSVG() { return nullptr; } | |
| #else | |
| #include "rive/decoders/bitmap_decoder.hpp" | |
| #include "utils/svg_factory.hpp" | |
| #include "utils/svg_renderer.hpp" | |
| #include <limits> | |
| #include <string> | |
| #include <vector> | |
| #include <emscripten/emscripten.h> | |
| #include <emscripten/html5.h> | |
| namespace rive::gpu | |
| { | |
| EM_JS(void, testingWindowSVGResetCanvas, (), { | |
| var canvas = document["getElementById"]("canvas"); | |
| var ctx = canvas["getContext"]("2d"); | |
| ctx["reset"](); | |
| }); | |
| EM_JS(bool, | |
| testingWindowSVGGetCanvasPixels, | |
| (uint8_t* outBuffer, int width, int height), | |
| { | |
| var canvas = document["getElementById"]("canvas"); | |
| var ctx = canvas["getContext"]("2d"); | |
| var imageData = | |
| ctx["getImageData"](0, 0, canvas.width, canvas.height); | |
| var pixelArray = imageData["data"]; | |
| if (pixelArray.length != width * height * 4) | |
| { | |
| return false; | |
| } | |
| // TestHarness::savePNG() flips vertically on the way out, since every | |
| // other backend fills this buffer bottom-up (the GL convention). | |
| // getImageData() hands back rows top-down, so reverse them here and | |
| // let the two cancel. | |
| var stride = width * 4; | |
| for (var y = 0; y < height; ++y) | |
| { | |
| Module["HEAPU8"]["set"]( | |
| pixelArray["subarray"]((height - 1 - y) * stride, | |
| (height - y) * stride), | |
| outBuffer + y * stride); | |
| } | |
| return true; | |
| }); | |
| // Rasterizing SVG means handing it to an <img> and blitting that; the browser | |
| // offers no synchronous path. EM_ASYNC_JS relies on the ASYNCIFY that the wasm | |
| // test targets already link with. | |
| EM_ASYNC_JS(bool, | |
| testingWindowSVGDrawSVGToCanvas, | |
| (const char* svg, int width, int height), | |
| { | |
| // clang-format off | |
| var url = URL["createObjectURL"](new Blob([UTF8ToString(svg)], { | |
| type: "image/svg+xml" | |
| })); | |
| try | |
| { | |
| var img = new Image(); | |
| img["src"] = url; | |
| await img["decode"](); | |
| var ctx = document["getElementById"]("canvas")["getContext"]("2d"); | |
| ctx["drawImage"](img, 0, 0, width, height); | |
| return true; | |
| } | |
| catch (e) | |
| { | |
| console["error"](e); | |
| return false; | |
| } | |
| finally | |
| { | |
| URL["revokeObjectURL"](url); | |
| } | |
| // clang-format on | |
| }); | |
| // This wrapper exists so that we can return a std::unique_ptr<Renderer> from | |
| // beginFrame() while still holding a pointer to the returned object for use in | |
| // endFrame(). | |
| class SVGRendererWrapper : public rive::Renderer | |
| { | |
| public: | |
| SVGRendererWrapper(std::shared_ptr<rive::SVGRenderer>& renderer) : | |
| m_renderer(renderer) | |
| {} | |
| void save() override { m_renderer->save(); } | |
| void restore() override { m_renderer->restore(); } | |
| void transform(const Mat2D& transform) override | |
| { | |
| m_renderer->transform(transform); | |
| } | |
| void drawPath(RenderPath* path, RenderPaint* paint) override | |
| { | |
| m_renderer->drawPath(path, paint); | |
| } | |
| void clipPath(RenderPath* path) override { m_renderer->clipPath(path); } | |
| void drawImage(const RenderImage* image, | |
| ImageSampler sampler, | |
| BlendMode blendMode, | |
| float opacity) override | |
| { | |
| m_renderer->drawImage(image, sampler, blendMode, opacity); | |
| } | |
| void drawImageMesh(const RenderImage* image, | |
| ImageSampler sampler, | |
| rcp<RenderBuffer> vertices_f32, | |
| rcp<RenderBuffer> uvCoords_f32, | |
| rcp<RenderBuffer> indices_u16, | |
| uint32_t vertexCount, | |
| uint32_t indexCount, | |
| BlendMode blendMode, | |
| float opacity) override | |
| { | |
| m_renderer->drawImageMesh(image, | |
| sampler, | |
| vertices_f32, | |
| uvCoords_f32, | |
| indices_u16, | |
| vertexCount, | |
| indexCount, | |
| blendMode, | |
| opacity); | |
| } | |
| void modulateOpacity(float opacity) override | |
| { | |
| m_renderer->modulateOpacity(opacity); | |
| } | |
| private: | |
| std::shared_ptr<rive::SVGRenderer> m_renderer; | |
| }; | |
| // SVGFactory embeds encoded images verbatim as data URIs, so it only needs | |
| // their dimensions. rive_decoders has no header-only sizing entry point, so | |
| // pay for a full decode and throw the pixels away (fine since these are just | |
| // tests). | |
| static bool decodeImageSize(rive::Span<const uint8_t> encodedBytes, | |
| uint32_t* width, | |
| uint32_t* height) | |
| { | |
| auto bitmap = Bitmap::decode(encodedBytes.data(), encodedBytes.size()); | |
| if (bitmap == nullptr) | |
| { | |
| return false; | |
| } | |
| *width = bitmap->width(); | |
| *height = bitmap->height(); | |
| return true; | |
| } | |
| class TestingWindowSVG : public TestingWindow | |
| { | |
| public: | |
| TestingWindowSVG() | |
| { | |
| int w, h; | |
| emscripten_get_canvas_element_size("#canvas", &w, &h); | |
| m_width = w; | |
| m_height = h; | |
| m_factory = std::make_unique<SVGFactory>(decodeImageSize); | |
| } | |
| rive::Factory* factory() override { return m_factory.get(); } | |
| void resize(int width, int height) override | |
| { | |
| if (m_width != (uint32_t)width || m_height != (uint32_t)height) | |
| { | |
| TestingWindow::resize(width, height); | |
| emscripten_set_canvas_element_size("#canvas", width, height); | |
| } | |
| } | |
| std::unique_ptr<rive::Renderer> beginFrame( | |
| const FrameOptions& options) override | |
| { | |
| // Because the SVGRenderer is a stateful object that accumulates all | |
| // draws, we treat doCleaer as a "reset" flag. If doClear is true, we | |
| // reset the renderer to start fresh; if false, we keep the existing | |
| // state and continue drawing on top of it. | |
| // For the above reason, we always reset the canvas at the start of a | |
| // frame; endFrame will include previous frames' contents if doClear was | |
| // not set. | |
| testingWindowSVGResetCanvas(); | |
| if (options.doClear) | |
| { | |
| m_renderer.reset(); | |
| } | |
| if (!m_renderer) | |
| { | |
| // Goldens compare pixels, so emit enough digits to round-trip | |
| // a float rather than the renderer's compact default. | |
| m_renderer = std::make_shared<SVGRenderer>( | |
| std::numeric_limits<float>::max_digits10); | |
| } | |
| if (options.doClear) | |
| { | |
| RawPath rawPath; | |
| rawPath.addRect(AABB(0, 0, (float)m_width, (float)m_height)); | |
| auto path = m_factory->makeRenderPath(rawPath, FillRule::nonZero); | |
| auto paint = m_factory->makeRenderPaint(); | |
| paint->style(RenderPaintStyle::fill); | |
| paint->color(options.clearColor); | |
| m_renderer->drawPath(path.get(), paint.get()); | |
| } | |
| return std::make_unique<SVGRendererWrapper>(m_renderer); | |
| } | |
| void endFrame(std::vector<uint8_t>* pixelData) override | |
| { | |
| std::string svg = m_renderer->finalize(m_width, m_height); | |
| if (!testingWindowSVGDrawSVGToCanvas(svg.c_str(), m_width, m_height)) | |
| { | |
| printf("Failed to rasterize SVG\n"); | |
| } | |
| if (!pixelData) | |
| { | |
| return; | |
| } | |
| pixelData->resize(m_width * m_height * 4); | |
| if (!testingWindowSVGGetCanvasPixels(pixelData->data(), | |
| m_width, | |
| m_height)) | |
| { | |
| printf("Canvas size mismatch, read failed\n"); | |
| pixelData->assign(pixelData->size(), 0); | |
| } | |
| m_renderer.reset(); | |
| } | |
| private: | |
| std::unique_ptr<SVGFactory> m_factory; | |
| std::shared_ptr<SVGRenderer> m_renderer; | |
| }; | |
| }; // namespace rive::gpu | |
| TestingWindow* TestingWindow::MakeSVG() | |
| { | |
| return new rive::gpu::TestingWindowSVG(); | |
| } | |
| #endif |