| /* |
| * 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 |