diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index fbe9009ee7..2bfbc29134 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -360,6 +360,7 @@ set(SOURCES HTML/Scripting/Script.cpp HTML/Scripting/WindowEnvironmentSettingsObject.cpp HTML/SessionHistoryEntry.cpp + HTML/SharedImageRequest.cpp HTML/SourceSet.cpp HTML/Storage.cpp HTML/StructuredSerialize.cpp diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.cpp b/Userland/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.cpp index f7c08d40ef..cba3190eb4 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.cpp +++ b/Userland/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.cpp @@ -31,7 +31,7 @@ void ImageStyleValue::load_any_resources(DOM::Document& document) return; m_document = &document; - m_image_request = HTML::ImageRequest::get_shareable_or_create(*document.page(), m_url).release_value_but_fixme_should_propagate_errors(); + m_image_request = HTML::SharedImageRequest::get_or_create(*document.page(), m_url).release_value_but_fixme_should_propagate_errors(); m_image_request->add_callbacks( [this, weak_this = make_weak_ptr()] { if (!weak_this) @@ -54,13 +54,11 @@ void ImageStyleValue::load_any_resources(DOM::Document& document) }, nullptr); - // If the image request is already available or fetching, no need to start another fetch. - if (m_image_request->is_available() || m_image_request->fetch_controller()) - return; - - auto request = HTML::create_potential_CORS_request(document.vm(), m_url, Fetch::Infrastructure::Request::Destination::Image, HTML::CORSSettingAttribute::NoCORS); - request->set_client(&document.relevant_settings_object()); - m_image_request->fetch_image(document.realm(), request); + if (m_image_request->needs_fetching()) { + auto request = HTML::create_potential_CORS_request(document.vm(), m_url, Fetch::Infrastructure::Request::Destination::Image, HTML::CORSSettingAttribute::NoCORS); + request->set_client(&document.relevant_settings_object()); + m_image_request->fetch_image(document.realm(), request); + } } void ImageStyleValue::animate() diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.h b/Userland/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.h index b5fb47b25f..93462fc03b 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.h +++ b/Userland/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.h @@ -43,7 +43,7 @@ public: private: ImageStyleValue(AK::URL const&); - RefPtr m_image_request; + RefPtr m_image_request; void animate(); Gfx::Bitmap const* bitmap(size_t frame_index, Gfx::IntSize = {}) const; diff --git a/Userland/Libraries/LibWeb/Forward.h b/Userland/Libraries/LibWeb/Forward.h index cdedc0a775..00d9bec100 100644 --- a/Userland/Libraries/LibWeb/Forward.h +++ b/Userland/Libraries/LibWeb/Forward.h @@ -403,6 +403,7 @@ class Path2D; class Plugin; class PluginArray; class PromiseRejectionEvent; +class SharedImageRequest; class Storage; class SubmitEvent; class TextMetrics; diff --git a/Userland/Libraries/LibWeb/HTML/HTMLImageElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLImageElement.cpp index 89592815a4..b04637939a 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLImageElement.cpp +++ b/Userland/Libraries/LibWeb/HTML/HTMLImageElement.cpp @@ -456,8 +456,8 @@ after_step_7: // multiple image elements (as well as CSS background-images, etc.) // 16. Set image request to a new image request whose current URL is urlString. - // 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(); + auto image_request = ImageRequest::create(*document().page()).release_value_but_fixme_should_propagate_errors(); + image_request->set_current_url(url_string); // 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. @@ -476,7 +476,9 @@ after_step_7: image_request->add_callbacks( [this, image_request, maybe_omit_events, url_string, previous_url]() { - auto image_data = image_request->image_data(); + VERIFY(image_request->shared_image_request()); + auto image_data = image_request->shared_image_request()->image_data(); + image_request->set_image_data(image_data); ListOfAvailableImages::Key key; key.url = url_string; @@ -492,6 +494,9 @@ after_step_7: 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(); @@ -534,7 +539,7 @@ after_step_7: }); // 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()) + if (image_request->is_available() || image_request->is_fetching()) return; // 18. Let request be the result of creating a potential-CORS request given urlString, "image", diff --git a/Userland/Libraries/LibWeb/HTML/HTMLObjectElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLObjectElement.cpp index 105c4322cc..b4130572f3 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLObjectElement.cpp +++ b/Userland/Libraries/LibWeb/HTML/HTMLObjectElement.cpp @@ -318,7 +318,7 @@ void HTMLObjectElement::load_image() // NOTE: This currently reloads the image instead of reusing the resource we've already downloaded. auto data = attribute(HTML::AttributeNames::data); auto url = document().parse_url(data); - m_image_request = HTML::ImageRequest::get_shareable_or_create(*document().page(), url).release_value_but_fixme_should_propagate_errors(); + m_image_request = HTML::SharedImageRequest::get_or_create(*document().page(), url).release_value_but_fixme_should_propagate_errors(); m_image_request->add_callbacks( [this] { run_object_representation_completed_steps(Representation::Image); @@ -327,13 +327,11 @@ void HTMLObjectElement::load_image() run_object_representation_fallback_steps(); }); - // If the image request is already available or fetching, no need to start another fetch. - if (m_image_request->is_available() || m_image_request->fetch_controller()) - return; - - 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_image_request->fetch_image(realm(), request); + if (m_image_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_image_request->fetch_image(realm(), request); + } } void HTMLObjectElement::update_layout_and_child_objects(Representation representation) diff --git a/Userland/Libraries/LibWeb/HTML/HTMLObjectElement.h b/Userland/Libraries/LibWeb/HTML/HTMLObjectElement.h index 11f3d904c9..108c54a2ee 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLObjectElement.h +++ b/Userland/Libraries/LibWeb/HTML/HTMLObjectElement.h @@ -80,7 +80,7 @@ private: RefPtr image_data() const; - RefPtr m_image_request; + RefPtr m_image_request; }; } diff --git a/Userland/Libraries/LibWeb/HTML/ImageRequest.cpp b/Userland/Libraries/LibWeb/HTML/ImageRequest.cpp index 1e2d1af07a..399e02b454 100644 --- a/Userland/Libraries/LibWeb/HTML/ImageRequest.cpp +++ b/Userland/Libraries/LibWeb/HTML/ImageRequest.cpp @@ -19,37 +19,18 @@ namespace Web::HTML { -static HashTable& shareable_image_requests() -{ - static HashTable requests; - return requests; -} - ErrorOr> ImageRequest::create(Page& page) { return adopt_nonnull_ref_or_enomem(new (nothrow) ImageRequest(page)); } -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 @@ -59,6 +40,11 @@ bool ImageRequest::is_available() const return m_state == State::PartiallyAvailable || m_state == State::CompletelyAvailable; } +bool ImageRequest::is_fetching() const +{ + return m_shared_image_request && m_shared_image_request->is_fetching(); +} + ImageRequest::State ImageRequest::state() const { return m_state; @@ -77,10 +63,12 @@ AK::URL const& ImageRequest::current_url() const void ImageRequest::set_current_url(AK::URL url) { m_current_url = move(url); + if (m_current_url.is_valid()) + m_shared_image_request = SharedImageRequest::get_or_create(m_page, m_current_url).release_value_but_fixme_should_propagate_errors(); } // https://html.spec.whatwg.org/multipage/images.html#abort-the-image-request -void abort_the_image_request(JS::Realm& realm, ImageRequest* image_request) +void abort_the_image_request(JS::Realm&, ImageRequest* image_request) { // 1. If image request is null, then return. if (!image_request) @@ -91,10 +79,7 @@ void abort_the_image_request(JS::Realm& realm, ImageRequest* image_request) // 3. Abort any instance of the fetching algorithm for image request, // discarding any pending tasks generated by that algorithm. - if (auto fetch_controller = image_request->fetch_controller()) - fetch_controller->abort(realm, {}); - - image_request->set_fetch_controller(nullptr); + // AD-HOC: We simply don't do this, since our SharedImageRequest may be used by someone else. } RefPtr ImageRequest::image_data() const @@ -128,125 +113,16 @@ void ImageRequest::prepare_for_presentation(HTMLImageElement&) // FIXME: 16. Update req's img element's presentation appropriately. } -JS::GCPtr ImageRequest::fetch_controller() -{ - return m_fetch_controller.ptr(); -} - -void ImageRequest::set_fetch_controller(JS::GCPtr fetch_controller) -{ - m_fetch_controller = move(fetch_controller); -} - void ImageRequest::fetch_image(JS::Realm& realm, JS::NonnullGCPtr 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); + VERIFY(m_shared_image_request); + m_shared_image_request->fetch_image(realm, request); } 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(); - } + VERIFY(m_shared_image_request); + m_shared_image_request->add_callbacks(move(on_finish), move(on_fail)); } } diff --git a/Userland/Libraries/LibWeb/HTML/ImageRequest.h b/Userland/Libraries/LibWeb/HTML/ImageRequest.h index 4515e28900..4a7c1cc9c2 100644 --- a/Userland/Libraries/LibWeb/HTML/ImageRequest.h +++ b/Userland/Libraries/LibWeb/HTML/ImageRequest.h @@ -12,6 +12,7 @@ #include #include #include +#include namespace Web::HTML { @@ -19,7 +20,6 @@ namespace Web::HTML { class ImageRequest : public RefCounted { public: 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 @@ -31,6 +31,7 @@ public: }; bool is_available() const; + bool is_fetching() const; State state() const; void set_state(State); @@ -50,27 +51,16 @@ public: // https://html.spec.whatwg.org/multipage/images.html#prepare-an-image-for-presentation void prepare_for_presentation(HTMLImageElement&); - [[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); + SharedImageRequest const* shared_image_request() const { return m_shared_image_request; } + private: 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. State m_state { State::Unavailable }; @@ -91,7 +81,7 @@ private: // which is either a struct consisting of a width and a height or is null. It must initially be null. Optional m_preferred_density_corrected_dimensions; - JS::Handle m_fetch_controller; + RefPtr m_shared_image_request; }; // https://html.spec.whatwg.org/multipage/images.html#abort-the-image-request diff --git a/Userland/Libraries/LibWeb/HTML/SharedImageRequest.cpp b/Userland/Libraries/LibWeb/HTML/SharedImageRequest.cpp new file mode 100644 index 0000000000..235d7f3f69 --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/SharedImageRequest.cpp @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2023, Andreas Kling + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web::HTML { + +static HashMap& shared_image_requests() +{ + static HashMap requests; + return requests; +} + +ErrorOr> SharedImageRequest::get_or_create(Page& page, AK::URL const& url) +{ + if (auto it = shared_image_requests().find(url); it != shared_image_requests().end()) + return *it->value; + auto request = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) SharedImageRequest(page, url))); + shared_image_requests().set(url, request); + return request; +} + +SharedImageRequest::SharedImageRequest(Page& page, AK::URL url) + : m_page(page) + , m_url(move(url)) +{ +} + +SharedImageRequest::~SharedImageRequest() +{ + shared_image_requests().remove(m_url); +} + +RefPtr SharedImageRequest::image_data() const +{ + return m_image_data; +} + +JS::GCPtr SharedImageRequest::fetch_controller() +{ + return m_fetch_controller.ptr(); +} + +void SharedImageRequest::set_fetch_controller(JS::GCPtr fetch_controller) +{ + m_fetch_controller = move(fetch_controller); +} + +void SharedImageRequest::fetch_image(JS::Realm& realm, JS::NonnullGCPtr 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(); + + 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(); + }; + + m_state = State::Fetching; + + 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 SharedImageRequest::add_callbacks(JS::SafeFunction on_finish, JS::SafeFunction on_fail) +{ + if (m_state == State::Finished) { + if (on_finish) + on_finish(); + return; + } + + if (m_state == State::Failed) { + if (on_fail) + on_fail(); + return; + } + + m_callbacks.append({ move(on_finish), move(on_fail) }); +} + +void SharedImageRequest::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 = [&] { + m_state = State::Failed; + 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(); + } + + m_image_data = move(image_data); + + m_state = State::Finished; + + for (auto& callback : m_callbacks) { + if (callback.on_finish) + callback.on_finish(); + } +} + +void SharedImageRequest::handle_failed_fetch() +{ + m_state = State::Failed; + for (auto& callback : m_callbacks) { + if (callback.on_fail) + callback.on_fail(); + } +} + +bool SharedImageRequest::needs_fetching() const +{ + return m_state == State::New; +} + +bool SharedImageRequest::is_fetching() const +{ + return m_state == State::Fetching; +} + +} diff --git a/Userland/Libraries/LibWeb/HTML/SharedImageRequest.h b/Userland/Libraries/LibWeb/HTML/SharedImageRequest.h new file mode 100644 index 0000000000..1cd9dee959 --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/SharedImageRequest.h @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2023, Andreas Kling + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace Web::HTML { + +class SharedImageRequest : public RefCounted { +public: + static ErrorOr> get_or_create(Page&, AK::URL const&); + ~SharedImageRequest(); + + AK::URL const& url() const { return m_url; } + + [[nodiscard]] RefPtr image_data() const; + + [[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); + + bool is_fetching() const; + bool needs_fetching() const; + +private: + explicit SharedImageRequest(Page&, AK::URL); + + void handle_successful_fetch(AK::URL const&, StringView mime_type, ByteBuffer data); + void handle_failed_fetch(); + + enum class State { + New, + Fetching, + Finished, + Failed, + }; + + State m_state { State::New }; + + Page& m_page; + + struct Callbacks { + JS::SafeFunction on_finish; + JS::SafeFunction on_fail; + }; + Vector m_callbacks; + + AK::URL m_url; + RefPtr m_image_data; + JS::Handle m_fetch_controller; +}; + +}