diff --git a/Tests/LibWeb/Text/expected/all-window-properties.txt b/Tests/LibWeb/Text/expected/all-window-properties.txt index 5a7600be3bdb6..b46ac93d60008 100644 --- a/Tests/LibWeb/Text/expected/all-window-properties.txt +++ b/Tests/LibWeb/Text/expected/all-window-properties.txt @@ -287,6 +287,7 @@ SVGGElement SVGGeometryElement SVGGradientElement SVGGraphicsElement +SVGImageElement SVGLength SVGLineElement SVGLinearGradientElement diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index 42a67b025b3b4..8290dd4bcf34d 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -525,6 +525,7 @@ set(SOURCES Layout/SVGForeignObjectBox.cpp Layout/SVGGeometryBox.cpp Layout/SVGGraphicsBox.cpp + Layout/SVGImageBox.cpp Layout/SVGSVGBox.cpp Layout/SVGMaskBox.cpp Layout/SVGClipBox.cpp @@ -659,6 +660,7 @@ set(SOURCES SVG/SVGGeometryElement.cpp SVG/SVGGraphicsElement.cpp SVG/SVGGradientElement.cpp + SVG/SVGImageElement.cpp SVG/SVGPathElement.cpp SVG/SVGCircleElement.cpp SVG/SVGEllipseElement.cpp diff --git a/Userland/Libraries/LibWeb/DOM/ElementFactory.cpp b/Userland/Libraries/LibWeb/DOM/ElementFactory.cpp index 68510812e6db9..3511272edc12d 100644 --- a/Userland/Libraries/LibWeb/DOM/ElementFactory.cpp +++ b/Userland/Libraries/LibWeb/DOM/ElementFactory.cpp @@ -92,6 +92,7 @@ #include #include #include +#include #include #include #include @@ -488,6 +489,8 @@ static JS::GCPtr create_svg_element(JS::Realm& realm, Document& return realm.heap().allocate(realm, document, move(qualified_name)); if (local_name == SVG::TagNames::a) return realm.heap().allocate(realm, document, move(qualified_name)); + if (local_name == SVG::TagNames::image) + return realm.heap().allocate(realm, document, move(qualified_name)); return nullptr; } diff --git a/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp b/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp index 99dd9fdcbae16..681f9785c4d61 100644 --- a/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp +++ b/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp @@ -16,11 +16,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -405,6 +407,8 @@ void SVGFormattingContext::layout_graphics_element(SVGGraphicsBox const& graphic // 5.2. Grouping: the ‘g’ element // The ‘g’ element is a container element for grouping together related graphics elements. layout_container_element(graphics_box); + } else if (is(graphics_box)) { + layout_image_element(static_cast(graphics_box)); } else { // Assume this is a path-like element. layout_path_like_element(graphics_box); @@ -417,6 +421,18 @@ void SVGFormattingContext::layout_graphics_element(SVGGraphicsBox const& graphic layout_mask_or_clip(*clip_box); } +void SVGFormattingContext::layout_image_element(SVGImageBox const& image_box) +{ + auto& box_state = m_state.get_mutable(image_box); + auto bounding_box = const_cast(image_box.dom_node()).bounding_box(); + box_state.set_content_x(bounding_box.x()); + box_state.set_content_y(bounding_box.y()); + box_state.set_content_width(bounding_box.width()); + box_state.set_content_height(bounding_box.height()); + box_state.set_has_definite_width(true); + box_state.set_has_definite_height(true); +} + void SVGFormattingContext::layout_mask_or_clip(SVGBox const& mask_or_clip) { SVG::SVGUnits content_units {}; diff --git a/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.h b/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.h index 7eea6ce41e8da..29a07c3f07f5d 100644 --- a/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.h +++ b/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -31,6 +32,7 @@ class SVGFormattingContext : public FormattingContext { void layout_graphics_element(SVGGraphicsBox const&); void layout_path_like_element(SVGGraphicsBox const&); void layout_mask_or_clip(SVGBox const&); + void layout_image_element(SVGImageBox const& image_box); [[nodiscard]] Gfx::Path compute_path_for_text(SVGTextBox const&); [[nodiscard]] Gfx::Path compute_path_for_text_path(SVGTextPathBox const&); diff --git a/Userland/Libraries/LibWeb/Layout/SVGImageBox.cpp b/Userland/Libraries/LibWeb/Layout/SVGImageBox.cpp new file mode 100644 index 0000000000000..e79b66da6d8b3 --- /dev/null +++ b/Userland/Libraries/LibWeb/Layout/SVGImageBox.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024, Tim Ledbetter + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include + +namespace Web::Layout { + +SVGImageBox::SVGImageBox(DOM::Document& document, SVG::SVGGraphicsElement& element, NonnullRefPtr properties) + : SVGGraphicsBox(document, element, properties) +{ +} + +JS::GCPtr SVGImageBox::create_paintable() const +{ + return Painting::ImagePaintable::create(*this); +} + +} diff --git a/Userland/Libraries/LibWeb/Layout/SVGImageBox.h b/Userland/Libraries/LibWeb/Layout/SVGImageBox.h new file mode 100644 index 0000000000000..7163eda6b829a --- /dev/null +++ b/Userland/Libraries/LibWeb/Layout/SVGImageBox.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024, Tim Ledbetter + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace Web::Layout { + +class SVGImageBox : public SVGGraphicsBox { + JS_CELL(SVGImageBox, SVGGraphicsBox); + +public: + SVGImageBox(DOM::Document&, SVG::SVGGraphicsElement&, NonnullRefPtr); + virtual ~SVGImageBox() override = default; + + SVG::SVGImageElement& dom_node() { return static_cast(SVGGraphicsBox::dom_node()); } + SVG::SVGImageElement const& dom_node() const { return static_cast(SVGGraphicsBox::dom_node()); } + + virtual JS::GCPtr create_paintable() const override; +}; + +} diff --git a/Userland/Libraries/LibWeb/Painting/ImagePaintable.cpp b/Userland/Libraries/LibWeb/Painting/ImagePaintable.cpp index 0f9a0cda06f15..c200137ee6ebf 100644 --- a/Userland/Libraries/LibWeb/Painting/ImagePaintable.cpp +++ b/Userland/Libraries/LibWeb/Painting/ImagePaintable.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include #include @@ -19,17 +18,23 @@ namespace Web::Painting { JS_DEFINE_ALLOCATOR(ImagePaintable); +JS::NonnullGCPtr ImagePaintable::create(Layout::SVGImageBox const& layout_box) +{ + return layout_box.heap().allocate_without_realm(layout_box, layout_box.dom_node(), false, String {}, true); +} + JS::NonnullGCPtr ImagePaintable::create(Layout::ImageBox const& layout_box) { auto alt = layout_box.dom_node().get_attribute_value(HTML::AttributeNames::alt); - return layout_box.heap().allocate_without_realm(layout_box, move(alt)); + return layout_box.heap().allocate_without_realm(layout_box, layout_box.image_provider(), layout_box.renders_as_alt_text(), move(alt), false); } -ImagePaintable::ImagePaintable(Layout::ImageBox const& layout_box, String alt_text) +ImagePaintable::ImagePaintable(Layout::Box const& layout_box, Layout::ImageProvider const& image_provider, bool renders_as_alt_text, String alt_text, bool is_svg) : PaintableBox(layout_box) - , m_renders_as_alt_text(layout_box.renders_as_alt_text()) + , m_renders_as_alt_text(renders_as_alt_text) , m_alt_text(move(alt_text)) - , m_image_provider(layout_box.image_provider()) + , m_image_provider(image_provider) + , m_is_svg(is_svg) { const_cast(layout_box.document()).register_viewport_client(*this); } @@ -74,7 +79,8 @@ void ImagePaintable::paint(PaintContext& context, PaintPhase phase) const auto scale_y = 0.0f; Gfx::IntRect bitmap_intersect = bitmap_rect; - switch (computed_values().object_fit()) { + auto object_fit = m_is_svg ? CSS::ObjectFit::Contain : computed_values().object_fit(); + switch (object_fit) { case CSS::ObjectFit::Fill: scale_x = (float)image_int_rect.width() / bitmap_rect.width(); scale_y = (float)image_int_rect.height() / bitmap_rect.height(); diff --git a/Userland/Libraries/LibWeb/Painting/ImagePaintable.h b/Userland/Libraries/LibWeb/Painting/ImagePaintable.h index 6a46499fe7803..2354203eb423c 100644 --- a/Userland/Libraries/LibWeb/Painting/ImagePaintable.h +++ b/Userland/Libraries/LibWeb/Painting/ImagePaintable.h @@ -7,6 +7,7 @@ #pragma once #include +#include #include namespace Web::Painting { @@ -18,7 +19,8 @@ class ImagePaintable final JS_DECLARE_ALLOCATOR(ImagePaintable); public: - static JS::NonnullGCPtr create(Layout::ImageBox const&); + static JS::NonnullGCPtr create(Layout::ImageBox const& layout_box); + static JS::NonnullGCPtr create(Layout::SVGImageBox const& layout_box); virtual void paint(PaintContext&, PaintPhase) const override; @@ -30,12 +32,14 @@ class ImagePaintable final // ^Document::ViewportClient virtual void did_set_viewport_rect(CSSPixelRect const&) final; - ImagePaintable(Layout::ImageBox const&, String alt_text); + ImagePaintable(Layout::Box const& layout_box, Layout::ImageProvider const& image_provider, bool renders_as_alt_text, String alt_text, bool is_svg); bool m_renders_as_alt_text { false }; String m_alt_text; Layout::ImageProvider const& m_image_provider; + + bool m_is_svg { false }; }; } diff --git a/Userland/Libraries/LibWeb/SVG/SVGImageElement.cpp b/Userland/Libraries/LibWeb/SVG/SVGImageElement.cpp new file mode 100644 index 0000000000000..74479b9267fb9 --- /dev/null +++ b/Userland/Libraries/LibWeb/SVG/SVGImageElement.cpp @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2024, Tim Ledbetter + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "SVGImageElement.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web::SVG { + +SVGImageElement::SVGImageElement(DOM::Document& document, DOM::QualifiedName qualified_name) + : SVGGraphicsElement(document, move(qualified_name)) +{ +} + +void SVGImageElement::initialize(JS::Realm& realm) +{ + Base::initialize(realm); + WEB_SET_PROTOTYPE_FOR_INTERFACE(SVGImageElement); + + // The shadow tree is open (inspectable by script), but read-only. + auto shadow_root = heap().allocate(realm, document(), *this, Bindings::ShadowRootMode::Open); + + // The user agent must create a use-element shadow tree whose host is the ‘use’ element itself + set_shadow_root(shadow_root); + + m_document_observer = realm.heap().allocate(realm, realm, document()); + m_document_observer->set_document_completely_loaded([this]() mutable { + document().invalidate_layout(); + }); +} + +void SVGImageElement::visit_edges(Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + SVGURIReferenceMixin::visit_edges(visitor); + visitor.visit(m_x); + visitor.visit(m_y); + visitor.visit(m_width); + visitor.visit(m_height); + visitor.visit(m_resource_request); + visitor.visit(m_document_observer); +} + +void SVGImageElement::attribute_changed(FlyString const& name, Optional const& old_value, Optional const& value) +{ + SVGGraphicsElement::attribute_changed(name, old_value, value); + if (name == SVG::AttributeNames::x) { + auto parsed_value = AttributeParser::parse_coordinate(value.value_or(String {})); + MUST(x()->base_val()->set_value(parsed_value.value_or(0))); + } else if (name == SVG::AttributeNames::y) { + auto parsed_value = AttributeParser::parse_coordinate(value.value_or(String {})); + MUST(y()->base_val()->set_value(parsed_value.value_or(0))); + } else if (name == SVG::AttributeNames::width) { + auto parsed_value = AttributeParser::parse_coordinate(value.value_or(String {})); + MUST(width()->base_val()->set_value(parsed_value.value_or(0))); + } else if (name == SVG::AttributeNames::height) { + auto parsed_value = AttributeParser::parse_coordinate(value.value_or(String {})); + MUST(height()->base_val()->set_value(parsed_value.value_or(0))); + } else if (name == SVG::AttributeNames::href) { + process_the_url(value); + } +} + +JS::NonnullGCPtr SVGImageElement::x() +{ + if (!m_x) { + auto& realm = this->realm(); + m_x = SVGAnimatedLength::create(realm, SVGLength::create(realm, 0, 0), SVGLength::create(realm, 0, 0)); + } + + return *m_x; +} + +JS::NonnullGCPtr SVGImageElement::y() +{ + if (!m_y) { + auto& realm = this->realm(); + m_y = SVGAnimatedLength::create(realm, SVGLength::create(realm, 0, 0), SVGLength::create(realm, 0, 0)); + } + + return *m_y; +} + +JS::NonnullGCPtr SVGImageElement::width() +{ + if (!m_width) { + auto& realm = this->realm(); + m_width = SVGAnimatedLength::create(realm, SVGLength::create(realm, 0, intrinsic_width().value_or(0).to_double()), SVGLength::create(realm, 0, 0)); + } + + return *m_width; +} + +JS::NonnullGCPtr SVGImageElement::height() +{ + if (!m_height) { + auto& realm = this->realm(); + m_height = SVGAnimatedLength::create(realm, SVGLength::create(realm, 0, intrinsic_height().value_or(0).to_double()), SVGLength::create(realm, 0, 0)); + } + + return *m_height; +} + +Gfx::Rect SVGImageElement::bounding_box() const +{ + Optional width; + if (attribute(HTML::AttributeNames::width).has_value()) + width = CSSPixels { m_width->base_val()->value() }; + Optional height; + if (attribute(HTML::AttributeNames::height).has_value()) + height = CSSPixels { m_height->base_val()->value() }; + + if (!height.has_value() && width.has_value() && intrinsic_aspect_ratio().has_value()) + height = width.value() / intrinsic_aspect_ratio().value(); + + if (!width.has_value() && height.has_value() && intrinsic_aspect_ratio().has_value()) + width = height.value() * intrinsic_aspect_ratio().value(); + + if (!width.has_value() && intrinsic_width().has_value()) + width = intrinsic_width(); + + if (!height.has_value() && intrinsic_height().has_value()) + height = intrinsic_height(); + + return { + CSSPixels { m_x ? m_x->base_val()->value() : 0 }, + CSSPixels { m_y ? m_y->base_val()->value() : 0 }, + width.value_or(0), + height.value_or(0), + }; +} + +// https://www.w3.org/TR/SVG2/linking.html#processingURL +void SVGImageElement::process_the_url(Optional const& href) +{ + m_href = document().url().complete_url(href.value_or(String {})); + if (!m_href.is_valid()) + return; + + fetch_the_document(m_href); +} + +// https://svgwg.org/svg2-draft/linking.html#processingURL-fetch +void SVGImageElement::fetch_the_document(URL::URL const& url) +{ + m_load_event_delayer.emplace(document()); + m_resource_request = HTML::SharedResourceRequest::get_or_create(realm(), document().page(), url); + m_resource_request->add_callbacks( + [this] { + m_load_event_delayer.clear(); + }, + [this] { + m_load_event_delayer.clear(); + }); + + if (m_resource_request->needs_fetching()) { + auto request = HTML::create_potential_CORS_request(vm(), url, Fetch::Infrastructure::Request::Destination::Image, HTML::CORSSettingAttribute::NoCORS); + request->set_client(&document().relevant_settings_object()); + m_resource_request->fetch_resource(realm(), request); + } +} + +JS::GCPtr SVGImageElement::create_layout_node(NonnullRefPtr style) +{ + return heap().allocate_without_realm(document(), *this, move(style)); +} + +bool SVGImageElement::is_image_available() const +{ + return m_resource_request && m_resource_request->image_data(); +} + +Optional SVGImageElement::intrinsic_width() const +{ + if (!m_resource_request) + return {}; + if (auto image_data = m_resource_request->image_data()) + return image_data->intrinsic_width(); + return {}; +} + +Optional SVGImageElement::intrinsic_height() const +{ + if (!m_resource_request) + return {}; + if (auto image_data = m_resource_request->image_data()) + return image_data->intrinsic_height(); + return {}; +} + +Optional SVGImageElement::intrinsic_aspect_ratio() const +{ + if (auto image_data = m_resource_request->image_data()) + return image_data->intrinsic_aspect_ratio(); + return {}; +} + +RefPtr SVGImageElement::current_image_bitmap(Gfx::IntSize size) const +{ + if (auto data = m_resource_request->image_data()) { + return data->bitmap(0, size); + } + return nullptr; +} + +void SVGImageElement::set_visible_in_viewport(bool) +{ +} + +} diff --git a/Userland/Libraries/LibWeb/SVG/SVGImageElement.h b/Userland/Libraries/LibWeb/SVG/SVGImageElement.h new file mode 100644 index 0000000000000..b69e07afd09d1 --- /dev/null +++ b/Userland/Libraries/LibWeb/SVG/SVGImageElement.h @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2024, Tim Ledbetter + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include + +namespace Web::SVG { + +class SVGImageElement + : public SVGGraphicsElement + , public SVGURIReferenceMixin + , public Layout::ImageProvider { + WEB_PLATFORM_OBJECT(SVGImageElement, SVGElement); + +public: + virtual void attribute_changed(FlyString const& name, Optional const& old_value, Optional const& value) override; + + JS::NonnullGCPtr x(); + JS::NonnullGCPtr y(); + JS::NonnullGCPtr width(); + JS::NonnullGCPtr height(); + + Gfx::Rect bounding_box() const; + + // ^Layout::ImageProvider + virtual bool is_image_available() const override; + virtual Optional intrinsic_width() const override; + virtual Optional intrinsic_height() const override; + virtual Optional intrinsic_aspect_ratio() const override; + virtual RefPtr current_image_bitmap(Gfx::IntSize = {}) const override; + virtual void set_visible_in_viewport(bool) override; + virtual JS::NonnullGCPtr to_html_element() const override { return *this; } + +protected: + SVGImageElement(DOM::Document&, DOM::QualifiedName); + + virtual void initialize(JS::Realm&) override; + virtual void visit_edges(Cell::Visitor&) override; + + void process_the_url(Optional const& href); + void fetch_the_document(URL::URL const& url); + +private: + virtual JS::GCPtr create_layout_node(NonnullRefPtr) override; + + JS::GCPtr m_x; + JS::GCPtr m_y; + JS::GCPtr m_width; + JS::GCPtr m_height; + + URL::URL m_href; + + JS::GCPtr m_document_observer; + JS::GCPtr m_resource_request; + Optional m_load_event_delayer; +}; + +} diff --git a/Userland/Libraries/LibWeb/SVG/SVGImageElement.idl b/Userland/Libraries/LibWeb/SVG/SVGImageElement.idl new file mode 100644 index 0000000000000..5447941d6395c --- /dev/null +++ b/Userland/Libraries/LibWeb/SVG/SVGImageElement.idl @@ -0,0 +1,15 @@ +#import +#import + +// https://svgwg.org/svg2-draft/embedded.html#InterfaceSVGImageElement +[Exposed=Window] +interface SVGImageElement : SVGGraphicsElement { + [SameObject] readonly attribute SVGAnimatedLength x; + [SameObject] readonly attribute SVGAnimatedLength y; + [SameObject] readonly attribute SVGAnimatedLength width; + [SameObject] readonly attribute SVGAnimatedLength height; + [FIXME, SameObject] readonly attribute SVGAnimatedPreserveAspectRatio preserveAspectRatio; + [Reflect=crossorigin, Enumerated=CORSSettingsAttribute] attribute DOMString? crossOrigin; +}; + +SVGImageElement includes SVGURIReference; diff --git a/Userland/Libraries/LibWeb/SVG/TagNames.h b/Userland/Libraries/LibWeb/SVG/TagNames.h index fdf1655fefa65..b5368f565b2e6 100644 --- a/Userland/Libraries/LibWeb/SVG/TagNames.h +++ b/Userland/Libraries/LibWeb/SVG/TagNames.h @@ -16,6 +16,7 @@ namespace Web::SVG::TagNames { __ENUMERATE_SVG_TAG(circle) \ __ENUMERATE_SVG_TAG(ellipse) \ __ENUMERATE_SVG_TAG(g) \ + __ENUMERATE_SVG_TAG(image) \ __ENUMERATE_SVG_TAG(line) \ __ENUMERATE_SVG_TAG(path) \ __ENUMERATE_SVG_TAG(polygon) \ diff --git a/Userland/Libraries/LibWeb/idl_files.cmake b/Userland/Libraries/LibWeb/idl_files.cmake index 7b0f6c96ffe40..795e5274efd83 100644 --- a/Userland/Libraries/LibWeb/idl_files.cmake +++ b/Userland/Libraries/LibWeb/idl_files.cmake @@ -278,6 +278,7 @@ libweb_js_bindings(SVG/SVGGElement) libweb_js_bindings(SVG/SVGGeometryElement) libweb_js_bindings(SVG/SVGGradientElement) libweb_js_bindings(SVG/SVGGraphicsElement) +libweb_js_bindings(SVG/SVGImageElement) libweb_js_bindings(SVG/SVGCircleElement) libweb_js_bindings(SVG/SVGEllipseElement) libweb_js_bindings(SVG/SVGForeignObjectElement)