diff --git a/Userland/Libraries/LibWeb/HTML/HTMLImageElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLImageElement.cpp
index 7a78f14aa0..d8311e5e85 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLImageElement.cpp
+++ b/Userland/Libraries/LibWeb/HTML/HTMLImageElement.cpp
@@ -47,7 +47,7 @@ JS::ThrowCompletionOr HTMLImageElement::initialize(JS::Realm& realm)
MUST_OR_THROW_OOM(Base::initialize(realm));
set_prototype(&Bindings::ensure_web_prototype(realm, "HTMLImageElement"));
- m_current_request = TRY_OR_THROW_OOM(vm(), ImageRequest::create());
+ m_current_request = TRY_OR_THROW_OOM(vm(), ImageRequest::create(*document().page()));
return {};
}
@@ -329,7 +329,7 @@ ErrorOr HTMLImageElement::update_the_image_data(bool restart_animations, b
m_pending_request = nullptr;
// 4. Let current request be a new image request whose image data is that of the entry and whose state is completely available.
- m_current_request = ImageRequest::create().release_value_but_fixme_should_propagate_errors();
+ m_current_request = ImageRequest::create(*document().page()).release_value_but_fixme_should_propagate_errors();
m_current_request->set_image_data(entry->image_data);
m_current_request->set_state(ImageRequest::State::CompletelyAvailable);
@@ -452,9 +452,12 @@ after_step_7:
// 15. If the pending request is not null, then abort the image request for the pending request.
abort_the_image_request(realm(), m_pending_request);
+ // AD-HOC: At this point we start deviating from the spec in order to allow sharing ImageRequest between
+ // multiple image elements (as well as CSS background-images, etc.)
+
// 16. Set image request to a new image request whose current URL is urlString.
- auto image_request = ImageRequest::create().release_value_but_fixme_should_propagate_errors();
- image_request->set_current_url(url_string);
+ // AD-HOC: Note that the ImageRequest "created" here may be an already existing one.
+ auto image_request = ImageRequest::get_shareable_or_create(*document().page(), url_string).release_value_but_fixme_should_propagate_errors();
// 17. If current request's state is unavailable or broken, then set the current request to image request.
// Otherwise, set the pending request to image request.
@@ -463,6 +466,77 @@ after_step_7:
else
m_pending_request = image_request;
+ // 23. 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() == LazyLoading::Eager;
+
+ // When delay load event is true, fetching the image must delay the load event of the element's node document
+ // until the task that is queued by the networking task source once the resource has been fetched (defined below) has been run.
+ if (delay_load_event)
+ m_load_event_delayer.emplace(document());
+
+ image_request->add_callbacks(
+ [this, image_request, maybe_omit_events, url_string, previous_url]() {
+ auto image_data = image_request->image_data();
+
+ ListOfAvailableImages::Key key;
+ key.url = url_string;
+ key.mode = m_cors_setting;
+ key.origin = document().origin();
+
+ // 1. If image request is the pending request, abort the image request for the current request,
+ // upgrade the pending request to the current request
+ // and prepare image request for presentation given the img element.
+ if (image_request == m_pending_request) {
+ abort_the_image_request(realm(), m_current_request);
+ upgrade_pending_request_to_current_request();
+ image_request->prepare_for_presentation(*this);
+ }
+
+ // 3. Add the image to the list of available images using the key key, with the ignore higher-layer caching flag set.
+ document().list_of_available_images().add(key, *image_data, true).release_value_but_fixme_should_propagate_errors();
+
+ // 4. If maybe omit events is not set or previousURL is not equal to urlString, then fire an event named load at the img element.
+ if (!maybe_omit_events || previous_url != url_string)
+ dispatch_event(DOM::Event::create(realm(), HTML::EventNames::load).release_value_but_fixme_should_propagate_errors());
+
+ set_needs_style_update(true);
+ document().set_needs_layout();
+
+ 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();
+ }
+
+ m_load_event_delayer.clear();
+ },
+ [this, image_request, maybe_omit_events, url_string, previous_url, selected_source]() {
+ // The image data is not in a supported file format;
+
+ // the user agent must set image request's state to broken,
+ image_request->set_state(ImageRequest::State::Broken);
+
+ // abort the image request for the current request and the pending request,
+ abort_the_image_request(realm(), m_current_request);
+ abort_the_image_request(realm(), m_pending_request);
+
+ // upgrade the pending request to the current request if image request is the pending request,
+ if (image_request == m_pending_request)
+ upgrade_pending_request_to_current_request();
+
+ // and then, if maybe omit events is not set or previousURL is not equal to urlString,
+ // queue an element task on the DOM manipulation task source given the img element
+ // to fire an event named error at the img element.
+ if (!maybe_omit_events || previous_url != url_string)
+ dispatch_event(DOM::Event::create(realm(), HTML::EventNames::error).release_value_but_fixme_should_propagate_errors());
+
+ m_load_event_delayer.clear();
+ });
+
+ // AD-HOC: If the image request is already available or fetching, no need to start another fetch.
+ if (image_request->is_available() || image_request->fetch_controller())
+ return;
+
// 18. Let request be the result of creating a potential-CORS request given urlString, "image",
// and the current state of the element's crossorigin content attribute.
auto request = create_potential_CORS_request(vm(), url_string, Fetch::Infrastructure::Request::Destination::Image, m_cors_setting);
@@ -479,157 +553,16 @@ after_step_7:
// FIXME: 22. Set request's priority to the current state of the element's fetchpriority attribute.
- // 23. 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() == LazyLoading::Eager;
-
// FIXME: 24. If the will lazy load element steps given the img return true, then:
// FIXME: 1. Set the img's lazy load resumption steps to the rest of this algorithm starting with the step labeled fetch the image.
// FIXME: 2. Start intersection-observing a lazy loading element for the img element.
// FIXME: 3. Return.
- Fetch::Infrastructure::FetchAlgorithms::Input fetch_algorithms_input {};
- fetch_algorithms_input.process_response = [this, image_request, url_string, maybe_omit_events, previous_url](JS::NonnullGCPtr response) {
- // FIXME: If the response is CORS cross-origin, we must use its internal response to query any of its data. See:
- // https://github.com/whatwg/html/issues/9355
- response = response->unsafe_response();
-
- // 26. As soon as possible, jump to the first applicable entry from the following list:
-
- // FIXME: - If the resource type is multipart/x-mixed-replace
-
- // - If the resource type and data corresponds to a supported image format, as described below
- // - The next task that is queued by the networking task source while the image is being fetched must run the following steps:
- queue_an_element_task(HTML::Task::Source::Networking, [this, response, image_request, url_string, maybe_omit_events, previous_url] {
- auto process_body = [response, image_request, url_string, maybe_omit_events, previous_url, this](ByteBuffer data) {
- auto extracted_mime_type = response->header_list()->extract_mime_type().release_value_but_fixme_should_propagate_errors();
- auto mime_type = extracted_mime_type.has_value() ? extracted_mime_type.value().essence().bytes_as_string_view() : StringView {};
- handle_successful_fetch(url_string, mime_type, image_request, move(data), maybe_omit_events, previous_url);
- };
- auto process_body_error = [this](auto) {
- handle_failed_fetch();
- };
-
- if (response->body().has_value())
- response->body().value().fully_read(realm(), move(process_body), move(process_body_error), JS::NonnullGCPtr { realm().global_object() }).release_value_but_fixme_should_propagate_errors();
- });
- };
-
- // 25. Fetch the image: Fetch request.
- // Return from this algorithm, and run the remaining steps as part of the fetch's processResponse for the response response.
-
- // When delay load event is true, fetching the image must delay the load event of the element's node document
- // until the task that is queued by the networking task source once the resource has been fetched (defined below) has been run.
- if (delay_load_event)
- m_load_event_delayer.emplace(document());
-
- auto fetch_controller = Fetch::Fetching::fetch(
- realm(),
- request,
- Fetch::Infrastructure::FetchAlgorithms::create(vm(), move(fetch_algorithms_input)))
- .release_value_but_fixme_should_propagate_errors();
-
- image_request->set_fetch_controller(fetch_controller);
+ image_request->fetch_image(realm(), request);
});
return {};
}
-void HTMLImageElement::handle_successful_fetch(AK::URL const& url_string, StringView mime_type, ImageRequest& image_request, ByteBuffer data, bool maybe_omit_events, AK::URL const& previous_url)
-{
- // AD-HOC: At this point, things gets very ad-hoc.
- // FIXME: Bring this closer to spec.
-
- ScopeGuard undelay_load_event_guard = [this] {
- m_load_event_delayer.clear();
- };
-
- bool is_svg_image = mime_type == "image/svg+xml"sv || url_string.basename().ends_with(".svg"sv);
-
- RefPtr image_data;
-
- auto handle_failed_image_decode = [&] {
- // The image data is not in a supported file format;
-
- // the user agent must set image request's state to broken,
- image_request.set_state(ImageRequest::State::Broken);
-
- // abort the image request for the current request and the pending request,
- abort_the_image_request(realm(), m_current_request);
- abort_the_image_request(realm(), m_pending_request);
-
- // upgrade the pending request to the current request if image request is the pending request,
- if (image_request == m_pending_request)
- upgrade_pending_request_to_current_request();
-
- // and then, if maybe omit events is not set or previousURL is not equal to urlString,
- // queue an element task on the DOM manipulation task source given the img element
- // to fire an event named error at the img element.
- if (!maybe_omit_events || previous_url != url_string)
- dispatch_event(DOM::Event::create(realm(), HTML::EventNames::error).release_value_but_fixme_should_propagate_errors());
- };
-
- if (is_svg_image) {
- VERIFY(document().page());
- auto result = SVG::SVGDecodedImageData::create(*document().page(), url_string, data);
- if (result.is_error()) {
- dbgln("Failed to decode SVG image: {}", result.error());
- handle_failed_image_decode();
- return;
- }
-
- image_data = result.release_value();
- } else {
- auto result = Web::Platform::ImageCodecPlugin::the().decode_image(data.bytes());
- if (!result.has_value()) {
- handle_failed_image_decode();
- return;
- }
-
- Vector frames;
- for (auto& frame : result.value().frames) {
- frames.append(AnimatedBitmapDecodedImageData::Frame {
- .bitmap = frame.bitmap,
- .duration = static_cast(frame.duration),
- });
- }
- image_data = AnimatedBitmapDecodedImageData::create(move(frames), result.value().loop_count, result.value().is_animated).release_value_but_fixme_should_propagate_errors();
- }
-
- image_request.set_image_data(image_data);
-
- ListOfAvailableImages::Key key;
- key.url = url_string;
- key.mode = m_cors_setting;
- key.origin = document().origin();
-
- // 1. If image request is the pending request, abort the image request for the current request,
- // upgrade the pending request to the current request
- // and prepare image request for presentation given the img element.
- if (image_request == m_pending_request) {
- abort_the_image_request(realm(), m_current_request);
- upgrade_pending_request_to_current_request();
- image_request.prepare_for_presentation(*this);
- }
-
- // 2. Set image request to the completely available state.
- image_request.set_state(ImageRequest::State::CompletelyAvailable);
-
- // 3. Add the image to the list of available images using the key key, with the ignore higher-layer caching flag set.
- document().list_of_available_images().add(key, *image_data, true).release_value_but_fixme_should_propagate_errors();
-
- // 4. If maybe omit events is not set or previousURL is not equal to urlString, then fire an event named load at the img element.
- if (!maybe_omit_events || previous_url != url_string)
- dispatch_event(DOM::Event::create(realm(), HTML::EventNames::load).release_value_but_fixme_should_propagate_errors());
-
- set_needs_style_update(true);
- document().set_needs_layout();
-
- 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();
- }
-}
-
// https://html.spec.whatwg.org/multipage/images.html#upgrade-the-pending-request-to-the-current-request
void HTMLImageElement::upgrade_pending_request_to_current_request()
{
diff --git a/Userland/Libraries/LibWeb/HTML/ImageRequest.cpp b/Userland/Libraries/LibWeb/HTML/ImageRequest.cpp
index e9b0a77356..1e2d1af07a 100644
--- a/Userland/Libraries/LibWeb/HTML/ImageRequest.cpp
+++ b/Userland/Libraries/LibWeb/HTML/ImageRequest.cpp
@@ -4,21 +4,53 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
+#include
#include
+#include
+#include
#include
+#include
+#include
#include
#include
+#include
+#include
+#include
namespace Web::HTML {
-ErrorOr> ImageRequest::create()
+static HashTable& shareable_image_requests()
{
- return adopt_nonnull_ref_or_enomem(new (nothrow) ImageRequest);
+ static HashTable requests;
+ return requests;
}
-ImageRequest::ImageRequest() = default;
+ErrorOr> ImageRequest::create(Page& page)
+{
+ return adopt_nonnull_ref_or_enomem(new (nothrow) ImageRequest(page));
+}
-ImageRequest::~ImageRequest() = default;
+ErrorOr> ImageRequest::get_shareable_or_create(Page& page, AK::URL const& url)
+{
+ for (auto& it : shareable_image_requests()) {
+ if (it->current_url() == url)
+ return *it;
+ }
+ auto request = TRY(create(page));
+ request->set_current_url(url);
+ return request;
+}
+
+ImageRequest::ImageRequest(Page& page)
+ : m_page(page)
+{
+ shareable_image_requests().set(this);
+}
+
+ImageRequest::~ImageRequest()
+{
+ shareable_image_requests().remove(this);
+}
// https://html.spec.whatwg.org/multipage/images.html#img-available
bool ImageRequest::is_available() const
@@ -106,4 +138,115 @@ void ImageRequest::set_fetch_controller(JS::GCPtr request)
+{
+ Fetch::Infrastructure::FetchAlgorithms::Input fetch_algorithms_input {};
+ fetch_algorithms_input.process_response = [this, &realm, request](JS::NonnullGCPtr response) {
+ // FIXME: If the response is CORS cross-origin, we must use its internal response to query any of its data. See:
+ // https://github.com/whatwg/html/issues/9355
+ response = response->unsafe_response();
+
+ // 26. As soon as possible, jump to the first applicable entry from the following list:
+
+ // FIXME: - If the resource type is multipart/x-mixed-replace
+
+ // - If the resource type and data corresponds to a supported image format, as described below
+ // - The next task that is queued by the networking task source while the image is being fetched must run the following steps:
+ auto process_body = [this, request, response](ByteBuffer data) {
+ auto extracted_mime_type = response->header_list()->extract_mime_type().release_value_but_fixme_should_propagate_errors();
+ auto mime_type = extracted_mime_type.has_value() ? extracted_mime_type.value().essence().bytes_as_string_view() : StringView {};
+ handle_successful_fetch(request->url(), mime_type, move(data));
+ };
+ auto process_body_error = [this](auto) {
+ handle_failed_fetch();
+ };
+
+ if (response->body().has_value())
+ response->body().value().fully_read(realm, move(process_body), move(process_body_error), JS::NonnullGCPtr { realm.global_object() }).release_value_but_fixme_should_propagate_errors();
+ };
+
+ // 25. Fetch the image: Fetch request.
+ // Return from this algorithm, and run the remaining steps as part of the fetch's processResponse for the response response.
+ auto fetch_controller = Fetch::Fetching::fetch(
+ realm,
+ request,
+ Fetch::Infrastructure::FetchAlgorithms::create(realm.vm(), move(fetch_algorithms_input)))
+ .release_value_but_fixme_should_propagate_errors();
+
+ set_fetch_controller(fetch_controller);
+}
+
+void ImageRequest::add_callbacks(JS::SafeFunction on_finish, JS::SafeFunction on_fail)
+{
+ if (is_available()) {
+ if (on_finish)
+ on_finish();
+ return;
+ }
+
+ if (state() == ImageRequest::State::Broken) {
+ if (on_fail)
+ on_fail();
+ return;
+ }
+
+ m_callbacks.append({ move(on_finish), move(on_fail) });
+}
+
+void ImageRequest::handle_successful_fetch(AK::URL const& url_string, StringView mime_type, ByteBuffer data)
+{
+ // AD-HOC: At this point, things gets very ad-hoc.
+ // FIXME: Bring this closer to spec.
+
+ bool const is_svg_image = mime_type == "image/svg+xml"sv || url_string.basename().ends_with(".svg"sv);
+
+ RefPtr image_data;
+
+ auto handle_failed_decode = [&] {
+ for (auto& callback : m_callbacks) {
+ if (callback.on_fail)
+ callback.on_fail();
+ }
+ };
+
+ if (is_svg_image) {
+ auto result = SVG::SVGDecodedImageData::create(m_page, url_string, data);
+ if (result.is_error())
+ return handle_failed_decode();
+
+ image_data = result.release_value();
+ } else {
+ auto result = Web::Platform::ImageCodecPlugin::the().decode_image(data.bytes());
+ if (!result.has_value())
+ return handle_failed_decode();
+
+ Vector frames;
+ for (auto& frame : result.value().frames) {
+ frames.append(AnimatedBitmapDecodedImageData::Frame {
+ .bitmap = frame.bitmap,
+ .duration = static_cast(frame.duration),
+ });
+ }
+ image_data = AnimatedBitmapDecodedImageData::create(move(frames), result.value().loop_count, result.value().is_animated).release_value_but_fixme_should_propagate_errors();
+ }
+
+ set_image_data(image_data);
+
+ // 2. Set image request to the completely available state.
+ set_state(ImageRequest::State::CompletelyAvailable);
+
+ for (auto& callback : m_callbacks) {
+ if (callback.on_finish)
+ callback.on_finish();
+ }
+}
+
+void ImageRequest::handle_failed_fetch()
+{
+ for (auto& callback : m_callbacks) {
+ if (callback.on_fail)
+ callback.on_fail();
+ }
+}
+
}
diff --git a/Userland/Libraries/LibWeb/HTML/ImageRequest.h b/Userland/Libraries/LibWeb/HTML/ImageRequest.h
index 7556cdea13..4515e28900 100644
--- a/Userland/Libraries/LibWeb/HTML/ImageRequest.h
+++ b/Userland/Libraries/LibWeb/HTML/ImageRequest.h
@@ -18,7 +18,8 @@ namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/images.html#image-request
class ImageRequest : public RefCounted {
public:
- static ErrorOr> create();
+ static ErrorOr> create(Page&);
+ static ErrorOr> get_shareable_or_create(Page&, AK::URL const&);
~ImageRequest();
// https://html.spec.whatwg.org/multipage/images.html#img-req-state
@@ -52,8 +53,23 @@ public:
[[nodiscard]] JS::GCPtr fetch_controller();
void set_fetch_controller(JS::GCPtr);
+ void fetch_image(JS::Realm&, JS::NonnullGCPtr);
+
+ void add_callbacks(JS::SafeFunction on_finish, JS::SafeFunction on_fail);
+
private:
- ImageRequest();
+ explicit ImageRequest(Page&);
+
+ void handle_successful_fetch(AK::URL const&, StringView mime_type, ByteBuffer data);
+ void handle_failed_fetch();
+
+ Page& m_page;
+
+ struct Callbacks {
+ JS::SafeFunction on_finish;
+ JS::SafeFunction on_fail;
+ };
+ Vector m_callbacks;
// https://html.spec.whatwg.org/multipage/images.html#img-req-state
// An image request's state is initially unavailable.
diff --git a/Userland/Libraries/LibWeb/SVG/SVGDecodedImageData.h b/Userland/Libraries/LibWeb/SVG/SVGDecodedImageData.h
index d2a34e9fd8..7eb8305235 100644
--- a/Userland/Libraries/LibWeb/SVG/SVGDecodedImageData.h
+++ b/Userland/Libraries/LibWeb/SVG/SVGDecodedImageData.h
@@ -6,6 +6,7 @@
#pragma once
+#include
#include
namespace Web::SVG {