ladybird/Libraries/LibWeb/SVG/SVGImageElement.cpp
Callum Law a7881ca3eb LibWeb: Register DecodedImageData consumers as clients
In a future commit, ownership of animation will be transferred from
these clients to `AnimatedDecodedImageData` and we will need a way to
invalidate them for new frames.

This also revealed some `ImageProvider`s which don't yet support
animated images (e.g. `<input type="file">`, `<object>`, etc) but that
is left as a FIXME for now.
2026-06-18 10:44:25 +02:00

261 lines
9 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2024-2025, Tim Ledbetter <tim.ledbetter@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "SVGImageElement.h"
#include <LibCore/Timer.h>
#include <LibGC/Heap.h>
#include <LibGfx/DecodedImageFrame.h>
#include <LibWeb/Bindings/SVGImageElement.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/DocumentObserver.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/HTML/PotentialCORSRequest.h>
#include <LibWeb/HTML/SharedResourceRequest.h>
#include <LibWeb/Layout/SVGImageBox.h>
#include <LibWeb/Namespace.h>
#include <LibWeb/Painting/Paintable.h>
#include <LibWeb/SVG/SVGDecodedImageData.h>
namespace Web::SVG {
GC_DEFINE_ALLOCATOR(SVGImageElement);
SVGImageElement::SVGImageElement(DOM::Document& document, DOM::QualifiedName qualified_name)
: SVGGraphicsElement(document, move(qualified_name))
{
m_animation_timer = Core::Timer::create();
m_animation_timer->on_timeout = [this] { animate(); };
}
SVGImageElement::~SVGImageElement() = default;
void SVGImageElement::finalize()
{
Base::finalize();
unregister_with_decoded_image_data_if_needed();
}
void SVGImageElement::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(SVGImageElement);
Base::initialize(realm);
}
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);
}
void SVGImageElement::adopted_from(DOM::Document& old_document)
{
Base::adopted_from(old_document);
if (m_load_event_delayer.has_value())
m_load_event_delayer.emplace(document());
}
void SVGImageElement::attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_)
{
Base::attribute_changed(name, old_value, value, namespace_);
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) {
// https://svgwg.org/svg2-draft/linking.html#XLinkRefAttrs
// For backwards compatibility, elements with an href attribute also recognize an href attribute in the
// XLink namespace. If the element is in the XLink namespace, it does not recognize an href attribute in the
// SVG namespace. When the href attribute is present in both the XLink namespace and without a namespace, the
// value of the attribute without a namespace shall be used. The attribute in the XLink namespace shall be ignored.
if (namespace_ == Namespace::XLink && has_attribute_ns({}, name))
return;
auto href = value;
if (!namespace_.has_value() && !href.has_value())
href = get_attribute_ns(SVG::AttributeNames::href, Namespace::XLink);
process_the_url(href);
}
}
// https://svgwg.org/svg2-draft/embedded.html#__svg__SVGImageElement__x
GC::Ref<SVG::SVGAnimatedLength> SVGImageElement::x()
{
if (!m_x)
m_x = fake_animated_length_fixme();
return *m_x;
}
// https://svgwg.org/svg2-draft/embedded.html#__svg__SVGImageElement__y
GC::Ref<SVG::SVGAnimatedLength> SVGImageElement::y()
{
if (!m_y)
m_y = fake_animated_length_fixme();
return *m_y;
}
// https://svgwg.org/svg2-draft/embedded.html#__svg__SVGImageElement__width
GC::Ref<SVG::SVGAnimatedLength> 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::ReadOnly::No),
SVGLength::create(realm, 0, 0, SVGLength::ReadOnly::Yes));
}
return *m_width;
}
// https://svgwg.org/svg2-draft/embedded.html#__svg__SVGImageElement__height
GC::Ref<SVG::SVGAnimatedLength> 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::ReadOnly::No),
SVGLength::create(realm, 0, 0, SVGLength::ReadOnly::Yes));
}
return *m_height;
}
Gfx::FloatRect SVGImageElement::bounding_box() const
{
Optional<float> width;
if (attribute(HTML::AttributeNames::width).has_value())
width = m_width->base_val()->value();
Optional<float> height;
if (attribute(HTML::AttributeNames::height).has_value())
height = m_height->base_val()->value();
if (!height.has_value() && width.has_value() && intrinsic_aspect_ratio().has_value())
height = width.value() / intrinsic_aspect_ratio().value().to_float();
if (!width.has_value() && height.has_value() && intrinsic_aspect_ratio().has_value())
width = height.value() * intrinsic_aspect_ratio().value().to_float();
if (!width.has_value() && intrinsic_width().has_value())
width = intrinsic_width()->to_float();
if (!height.has_value() && intrinsic_height().has_value())
height = intrinsic_height()->to_float();
return {
m_x ? m_x->base_val()->value() : 0.0f,
m_y ? m_y->base_val()->value() : 0.0f,
width.value_or(0.0f),
height.value_or(0.0f),
};
}
// https://www.w3.org/TR/SVG2/linking.html#processingURL
void SVGImageElement::process_the_url(Optional<String> const& href)
{
if (!href.has_value()) {
m_href = {};
return;
}
m_href = document().encoding_parse_url(*href);
if (!m_href.has_value())
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());
unregister_with_decoded_image_data_if_needed();
m_resource_request = HTML::SharedResourceRequest::get_or_create(realm(), document().page(), url);
m_resource_request->add_callbacks(
[this, resource_request = GC::Root { m_resource_request }] {
m_load_event_delayer.clear();
auto image_data = resource_request->image_data();
if (image_data->is_animated() && image_data->frame_count() > 1) {
m_current_frame_index = 0;
m_animation_timer->set_interval(image_data->frame_duration(0));
m_animation_timer->start();
}
register_with_decoded_image_data_if_needed();
set_needs_style_update(true);
set_needs_layout_update(DOM::SetNeedsLayoutReason::SVGImageElementFetchTheDocument);
dispatch_event(DOM::Event::create(realm(), HTML::EventNames::load));
},
[this] {
m_load_event_delayer.clear();
dispatch_event(DOM::Event::create(realm(), HTML::EventNames::error));
});
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);
}
}
RefPtr<Layout::Node> SVGImageElement::create_layout_node(CSS::ComputedProperties const& style)
{
return make_ref_counted<Layout::SVGImageBox>(document(), *this, style);
}
void SVGImageElement::animate()
{
auto image_data = m_resource_request->image_data();
if (!image_data) {
return;
}
m_current_frame_index = (m_current_frame_index + 1) % image_data->frame_count();
auto current_frame_duration = image_data->frame_duration(m_current_frame_index);
if (current_frame_duration != m_animation_timer->interval()) {
m_animation_timer->restart(current_frame_duration);
}
if (m_current_frame_index == image_data->frame_count() - 1) {
++m_loops_completed;
if (m_loops_completed > 0 && m_loops_completed == image_data->loop_count()) {
m_animation_timer->stop();
}
}
if (paintable())
paintable()->set_needs_repaint();
}
GC::Ptr<HTML::DecodedImageData> SVGImageElement::decoded_image_data() const
{
if (!m_resource_request)
return nullptr;
return m_resource_request->image_data();
}
}