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.
This commit is contained in:
Callum Law 2026-06-14 22:04:42 +12:00 committed by Alexander Kalenik
parent 74e04ed258
commit a7881ca3eb
10 changed files with 100 additions and 15 deletions

View file

@ -43,6 +43,8 @@ ImageStyleValueResource::ImageStyleValueResource(GC::Ref<HTML::SharedResourceReq
ImageStyleValueResource::~ImageStyleValueResource()
{
stop_animation_timer();
VERIFY(m_image_style_values.is_empty());
unregister_with_decoded_image_data_if_needed();
}
void ImageStyleValueResource::visit_edges(JS::Cell::Visitor& visitor)
@ -55,23 +57,26 @@ void ImageStyleValueResource::register_image_style_value(DOM::Document& document
{
m_image_style_values.set(&image_style_value);
start_animation_timer_if_needed(document);
register_with_decoded_image_data_if_needed();
}
void ImageStyleValueResource::unregister_image_style_value(ImageStyleValue const& image_style_value)
{
m_image_style_values.remove(&image_style_value);
if (m_image_style_values.is_empty())
if (m_image_style_values.is_empty()) {
stop_animation_timer();
unregister_with_decoded_image_data_if_needed();
}
}
GC::Ptr<HTML::DecodedImageData> ImageStyleValueResource::image_data() const
GC::Ptr<HTML::DecodedImageData> ImageStyleValueResource::decoded_image_data() const
{
return m_resource_request->image_data();
}
Optional<Gfx::DecodedImageFrame> ImageStyleValueResource::frame(size_t frame_index, Gfx::IntSize size) const
{
if (auto image_data = this->image_data())
if (auto image_data = this->decoded_image_data())
return image_data->frame(frame_index, size);
return {};
}
@ -85,6 +90,8 @@ void ImageStyleValueResource::on_decoded_image_data_loaded(DOM::Document& docume
{
notify_image_style_values_did_update();
start_animation_timer_if_needed(document);
if (!m_image_style_values.is_empty())
register_with_decoded_image_data_if_needed();
}
void ImageStyleValueResource::notify_image_style_values_did_update()
@ -122,7 +129,7 @@ void ImageStyleValueResource::stop_animation_timer()
bool ImageStyleValueResource::is_animatable() const
{
auto image_data = this->image_data();
auto image_data = this->decoded_image_data();
if (!image_data || !image_data->is_animated() || image_data->frame_count() <= 1)
return false;
@ -131,13 +138,13 @@ bool ImageStyleValueResource::is_animatable() const
bool ImageStyleValueResource::animation_has_completed() const
{
auto image_data = this->image_data();
auto image_data = this->decoded_image_data();
return image_data && image_data->loop_count() > 0 && m_loops_completed == image_data->loop_count();
}
int ImageStyleValueResource::current_frame_duration() const
{
auto image_data = this->image_data();
auto image_data = this->decoded_image_data();
if (!image_data)
return 0;
@ -289,7 +296,7 @@ GC::Ptr<HTML::DecodedImageData> ImageStyleValue::image_data(DOM::Document const&
// resolved URL.
VERIFY(resource);
return resource->image_data();
return resource->decoded_image_data();
}
Optional<Gfx::Color> ImageStyleValue::color_if_single_pixel_bitmap(DOM::Document const& document) const

View file

@ -19,12 +19,13 @@
#include <LibWeb/CSS/StyleValues/AbstractImageStyleValue.h>
#include <LibWeb/CSS/URL.h>
#include <LibWeb/Forward.h>
#include <LibWeb/HTML/DecodedImageData.h>
namespace Web::CSS {
class ImageStyleValue;
class ImageStyleValueResource {
class ImageStyleValueResource final : public HTML::DecodedImageData::Client {
public:
explicit ImageStyleValueResource(GC::Ref<HTML::SharedResourceRequest>, GC::Ref<DOM::Document> const&);
~ImageStyleValueResource();
@ -35,7 +36,7 @@ public:
void unregister_image_style_value(ImageStyleValue const&);
bool can_be_removed() const { return m_image_style_values.is_empty(); }
[[nodiscard]] GC::Ptr<HTML::DecodedImageData> image_data() const;
[[nodiscard]] virtual GC::Ptr<HTML::DecodedImageData> decoded_image_data() const override;
[[nodiscard]] Optional<Gfx::DecodedImageFrame> frame(size_t frame_index, Gfx::IntSize = {}) const;
[[nodiscard]] size_t current_frame_index() const { return m_current_frame_index; }
[[nodiscard]] bool has_active_animation_timer() const;
@ -43,6 +44,8 @@ public:
void animate(DOM::Document&);
private:
virtual void decoded_image_data_did_update() override { notify_image_style_values_did_update(); }
void on_decoded_image_data_loaded(DOM::Document&);
void notify_image_style_values_did_update();
void start_animation_timer_if_needed(DOM::Document&);

View file

@ -6821,7 +6821,7 @@ void Document::prune_image_resource_caches()
auto is_used_by_css_image_resource = [&](URL::URL const& url, HTML::SharedResourceRequest const& request) {
auto* css_image_resource = this->css_image_resource(url);
return css_image_resource && css_image_resource->image_data() == request.image_data();
return css_image_resource && css_image_resource->decoded_image_data() == request.image_data();
};
struct CacheSize {

View file

@ -8,8 +8,34 @@
namespace Web::HTML {
void DecodedImageData::Client::register_with_decoded_image_data_if_needed()
{
auto const& image_data = decoded_image_data();
if (!image_data)
return;
image_data->m_clients.set(this);
}
void DecodedImageData::Client::unregister_with_decoded_image_data_if_needed()
{
auto const& image_data = decoded_image_data();
if (!image_data)
return;
image_data->m_clients.remove(this);
}
DecodedImageData::DecodedImageData() = default;
DecodedImageData::~DecodedImageData() = default;
void DecodedImageData::notify_clients_did_update()
{
for (auto* client : m_clients)
client->decoded_image_data_did_update();
}
}

View file

@ -22,6 +22,16 @@ class DecodedImageData : public JS::Cell {
GC_CELL(DecodedImageData, JS::Cell);
public:
class Client {
public:
virtual GC::Ptr<DecodedImageData> decoded_image_data() const = 0;
virtual void decoded_image_data_did_update() = 0;
protected:
void register_with_decoded_image_data_if_needed();
void unregister_with_decoded_image_data_if_needed();
};
virtual ~DecodedImageData();
[[nodiscard]] bool is_cors_cross_origin() const { return m_is_cors_cross_origin; }
@ -46,7 +56,10 @@ public:
protected:
DecodedImageData();
void notify_clients_did_update();
private:
HashTable<Client*> m_clients;
bool m_is_cors_cross_origin { false };
};

View file

@ -159,6 +159,7 @@ HTMLImageElement::~HTMLImageElement() = default;
void HTMLImageElement::finalize()
{
Base::finalize();
unregister_with_decoded_image_data_if_needed();
document().unregister_viewport_client(*this);
}
@ -687,6 +688,7 @@ void HTMLImageElement::update_the_image_data_impl(bool restart_animations, bool
entry->ignore_higher_layer_caching = true;
// 2. Abort the image request for the current request and the pending request.
unregister_with_decoded_image_data_if_needed();
abort_the_image_request(realm(), m_current_request);
abort_the_image_request(realm(), m_pending_request);
@ -698,6 +700,7 @@ void HTMLImageElement::update_the_image_data_impl(bool restart_animations, bool
m_current_request->set_image_data(entry->image_data);
m_current_request->set_state(ImageRequest::State::CompletelyAvailable);
m_current_frame_index = 0;
register_with_decoded_image_data_if_needed();
// 5. Prepare the current request for presentation given the img element.
m_current_request->prepare_for_presentation(*this);
@ -765,6 +768,7 @@ after_step_7:
// abort the image request for the current request and the pending request,
// and set the pending request to null.
m_current_request->set_state(ImageRequest::State::Broken);
unregister_with_decoded_image_data_if_needed();
abort_the_image_request(realm(), m_current_request);
abort_the_image_request(realm(), m_pending_request);
m_pending_request = nullptr;
@ -801,6 +805,7 @@ after_step_7:
// 13. If urlString is failure, then:
if (!url_string.has_value()) {
// 1. Abort the image request for the current request and the pending request.
unregister_with_decoded_image_data_if_needed();
abort_the_image_request(realm(), m_current_request);
abort_the_image_request(realm(), m_pending_request);
@ -865,10 +870,13 @@ after_step_7:
// 18. If the current request's state is unavailable or broken, then set the current request to image request.
// Otherwise, set the pending request to image request.
if (m_current_request->state() == ImageRequest::State::Unavailable || m_current_request->state() == ImageRequest::State::Broken)
if (m_current_request->state() == ImageRequest::State::Unavailable || m_current_request->state() == ImageRequest::State::Broken) {
unregister_with_decoded_image_data_if_needed();
m_current_request = image_request;
else
register_with_decoded_image_data_if_needed();
} else {
m_pending_request = image_request;
}
// 24. Let delay load event be true if the img's lazy loading attribute is in the Eager state, or if scripting is disabled for the img, and false otherwise.
auto delay_load_event = lazy_loading_attribute() == LazyLoading::Eager;
@ -955,11 +963,14 @@ void HTMLImageElement::add_callbacks_to_image_request(GC::Ref<ImageRequest> imag
// upgrade the pending request to the current request
// and prepare image request for presentation given the img element.
if (image_request == m_pending_request) {
unregister_with_decoded_image_data_if_needed();
abort_the_image_request(realm(), m_current_request);
upgrade_pending_request_to_current_request();
image_request->prepare_for_presentation(*this);
}
register_with_decoded_image_data_if_needed();
// 2. Set image request to the completely available state.
image_request->set_state(ImageRequest::State::CompletelyAvailable);
@ -1005,6 +1016,7 @@ void HTMLImageElement::add_callbacks_to_image_request(GC::Ref<ImageRequest> imag
image_request->set_state(ImageRequest::State::Broken);
// abort the image request for the current request and the pending request,
unregister_with_decoded_image_data_if_needed();
abort_the_image_request(realm(), m_current_request);
abort_the_image_request(realm(), m_pending_request);
@ -1200,7 +1212,10 @@ void HTMLImageElement::upgrade_pending_request_to_current_request()
{
// 1. Set the img element's current request to the pending request.
VERIFY(m_pending_request);
unregister_with_decoded_image_data_if_needed();
m_current_request = m_pending_request;
register_with_decoded_image_data_if_needed();
// 2. Set the img element's pending request to null.
m_pending_request = nullptr;

View file

@ -14,6 +14,7 @@
#include <LibWeb/DOM/DocumentLoadEventDelayer.h>
#include <LibWeb/DOM/ViewportClient.h>
#include <LibWeb/HTML/CORSSettingAttribute.h>
#include <LibWeb/HTML/DecodedImageData.h>
#include <LibWeb/HTML/HTMLElement.h>
#include <LibWeb/HTML/LazyLoadingElement.h>
#include <LibWeb/HTML/SourceSet.h>
@ -25,7 +26,8 @@ class HTMLImageElement final
: public HTMLElement
, public LazyLoadingElement<HTMLImageElement>
, public Layout::ImageProvider
, public DOM::ViewportClient {
, public DOM::ViewportClient
, public DecodedImageData::Client {
WEB_PLATFORM_OBJECT(HTMLImageElement, HTMLElement);
GC_DECLARE_ALLOCATOR(HTMLImageElement);
LAZY_LOADING_ELEMENT(HTMLImageElement);
@ -136,6 +138,8 @@ private:
void handle_failed_fetch();
void add_callbacks_to_image_request(GC::Ref<ImageRequest>, bool maybe_omit_events, String const& url_string, String const& previous_url, u64 update_the_image_data_count);
virtual void decoded_image_data_did_update() override { set_needs_repaint(); }
bool current_request_has_running_animation() const;
void start_animation_timer_if_visible();
void animate();

View file

@ -15,6 +15,7 @@
namespace Web::Layout {
// FIXME: Update all ImageProviders to be DecodedImageData::Clients (i.e. support animated images)
class ImageProvider {
public:
virtual ~ImageProvider() { }

View file

@ -32,6 +32,12 @@ SVGImageElement::SVGImageElement(DOM::Document& document, DOM::QualifiedName qua
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);
@ -185,6 +191,7 @@ void SVGImageElement::process_the_url(Optional<String> const& href)
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 }] {
@ -195,6 +202,7 @@ void SVGImageElement::fetch_the_document(URL::URL const& url)
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);

View file

@ -8,6 +8,7 @@
#include <LibGC/Ptr.h>
#include <LibWeb/DOM/DocumentLoadEventDelayer.h>
#include <LibWeb/HTML/DecodedImageData.h>
#include <LibWeb/Layout/ImageProvider.h>
#include <LibWeb/SVG/SVGAnimatedLength.h>
#include <LibWeb/SVG/SVGGraphicsElement.h>
@ -17,12 +18,15 @@ namespace Web::SVG {
class SVGImageElement final
: public SVGGraphicsElement
, public SVGURIReferenceMixin<SupportsXLinkHref::Yes>
, public Layout::ImageProvider {
, public Layout::ImageProvider
, public HTML::DecodedImageData::Client {
WEB_PLATFORM_OBJECT(SVGImageElement, SVGGraphicsElement);
GC_DECLARE_ALLOCATOR(SVGImageElement);
public:
~SVGImageElement();
static constexpr bool OVERRIDES_FINALIZE = true;
virtual ~SVGImageElement() override;
virtual void attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_) override;
@ -48,7 +52,11 @@ protected:
void fetch_the_document(URL::URL const& url);
private:
virtual void finalize() override;
virtual RefPtr<Layout::Node> create_layout_node(CSS::ComputedProperties const&) override;
virtual void decoded_image_data_did_update() override { set_needs_repaint(); }
void animate();
GC::Ptr<SVG::SVGAnimatedLength> m_x;