From 5c07aeb78e7c87241c6172dc9405f76c0beb253a Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Sun, 11 Jun 2023 08:08:44 +0200 Subject: [PATCH] LibWeb: Update HTML image loading algorithm for recent spec changes Changes: https://github.com/whatwg/html/commit/8a2375aac23f88137922c820cf12650364b72715 Note that we don't support the full lazy loading machinery yet, so this isn't actually hooked up. --- .../LibWeb/HTML/HTMLImageElement.cpp | 121 +++++++++++------- .../Libraries/LibWeb/HTML/HTMLImageElement.h | 4 +- 2 files changed, 78 insertions(+), 47 deletions(-) diff --git a/Userland/Libraries/LibWeb/HTML/HTMLImageElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLImageElement.cpp index 6941daa62a..7a78f14aa0 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLImageElement.cpp +++ b/Userland/Libraries/LibWeb/HTML/HTMLImageElement.cpp @@ -266,7 +266,7 @@ bool HTMLImageElement::uses_srcset_or_picture() const } // https://html.spec.whatwg.org/multipage/images.html#update-the-image-data -ErrorOr HTMLImageElement::update_the_image_data(bool restart_animations) +ErrorOr HTMLImageElement::update_the_image_data(bool restart_animations, bool maybe_omit_events) { // 1. If the element's node document is not fully active, then: if (!document().is_fully_active()) { @@ -281,11 +281,14 @@ ErrorOr HTMLImageElement::update_the_image_data(bool restart_animations) // then abort the image request for the current request and the pending request, // set current request's state to unavailable, set pending request to null, and return. - // 3. Let selected source be null and selected pixel density be undefined. + // 3. Let previous URL be the current request's current URL. + auto previous_url = m_current_request->current_url(); + + // 4. Let selected source be null and selected pixel density be undefined. Optional selected_source; Optional selected_pixel_density; - // 4. If the element does not use srcset or picture + // 5. If the element does not use srcset or picture // and it has a src attribute specified whose value is not the empty string, // then set selected source to the value of the element's src attribute // and set selected pixel density to 1.0. @@ -294,17 +297,17 @@ ErrorOr HTMLImageElement::update_the_image_data(bool restart_animations) selected_pixel_density = 1.0f; } - // 5. Set the element's last selected source to selected source. + // 6. Set the element's last selected source to selected source. m_last_selected_source = selected_source; - // 6. If selected source is not null, then: + // 7. If selected source is not null, then: if (selected_source.has_value()) { // 1. Parse selected source, relative to the element's node document. // If that is not successful, then abort this inner set of steps. // Otherwise, let urlString be the resulting URL string. auto url_string = document().parse_url(selected_source.value().to_deprecated_string()); if (!url_string.is_valid()) - goto after_step_6; + goto after_step_7; // 2. Let key be a tuple consisting of urlString, the img element's crossorigin attribute's mode, // and, if that mode is not No CORS, the node document's origin. @@ -339,7 +342,7 @@ ErrorOr HTMLImageElement::update_the_image_data(bool restart_animations) m_current_request->set_current_pixel_density(selected_pixel_density.value_or(1.0f)); // 7. Queue an element task on the DOM manipulation task source given the img element and following steps: - queue_an_element_task(HTML::Task::Source::DOMManipulation, [this, restart_animations, url_string] { + queue_an_element_task(HTML::Task::Source::DOMManipulation, [this, restart_animations, maybe_omit_events, url_string, previous_url] { // 1. If restart animation is set, then restart the animation. if (restart_animations) restart_the_animation(); @@ -347,21 +350,22 @@ ErrorOr HTMLImageElement::update_the_image_data(bool restart_animations) // 2. Set current request's current URL to urlString. m_current_request->set_current_url(url_string); - // 3. Fire an event named load at the img element. - dispatch_event(DOM::Event::create(realm(), HTML::EventNames::load).release_value_but_fixme_should_propagate_errors()); + // 3. 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()); }); // 8. Abort the update the image data algorithm. return {}; } } -after_step_6: - // 7. Queue a microtask to perform the rest of this algorithm, allowing the task that invoked this algorithm to continue. - queue_a_microtask(&document(), [this, restart_animations]() mutable { - // FIXME: 8. If another instance of this algorithm for this img element was started after this instance +after_step_7: + // 8. Queue a microtask to perform the rest of this algorithm, allowing the task that invoked this algorithm to continue. + queue_a_microtask(&document(), [this, restart_animations, maybe_omit_events, previous_url]() mutable { + // FIXME: 9. If another instance of this algorithm for this img element was started after this instance // (even if it aborted and is no longer running), then return. - // 9. Let selected source and selected pixel density be + // 10. Let selected source and selected pixel density be // the URL and pixel density that results from selecting an image source, respectively. Optional selected_source; Optional pixel_density; @@ -370,7 +374,7 @@ after_step_6: pixel_density = result.value().pixel_density; } - // 10. If selected source is null, then: + // 11. If selected source is null, then: if (!selected_source.has_value()) { // 1. Set the current request's state to broken, // abort the image request for the current request and the pending request, @@ -381,12 +385,16 @@ after_step_6: m_pending_request = nullptr; // 2. Queue an element task on the DOM manipulation task source given the img element and the following steps: - queue_an_element_task(HTML::Task::Source::DOMManipulation, [this] { + queue_an_element_task(HTML::Task::Source::DOMManipulation, [this, maybe_omit_events, previous_url] { // 1. Change the current request's current URL to the empty string. m_current_request->set_current_url(""sv); - // 2. If the element has a src attribute or it uses srcset or picture, fire an event named error at the img element. - if (has_attribute(HTML::AttributeNames::src) || uses_srcset_or_picture()) { + // 2. If all of the following conditions are true: + // - the element has a src attribute or it uses srcset or picture; and + // - maybe omit events is not set or previousURL is not the empty string + if ( + (has_attribute(HTML::AttributeNames::src) || uses_srcset_or_picture()) + && (!maybe_omit_events || m_current_request->current_url() != ""sv)) { dispatch_event(DOM::Event::create(realm(), HTML::EventNames::error).release_value_but_fixme_should_propagate_errors()); } }); @@ -395,7 +403,7 @@ after_step_6: return; } - // 11. Parse selected source, relative to the element's node document, and let urlString be the resulting URL string. + // 12. Parse selected source, relative to the element's node document, and let urlString be the resulting URL string. auto url_string = document().parse_url(selected_source.value().url.to_deprecated_string()); // If that is not successful, then: if (!url_string.is_valid()) { @@ -410,23 +418,24 @@ after_step_6: m_pending_request = nullptr; // 4. Queue an element task on the DOM manipulation task source given the img element and the following steps: - queue_an_element_task(HTML::Task::Source::DOMManipulation, [this, selected_source] { + queue_an_element_task(HTML::Task::Source::DOMManipulation, [this, selected_source, maybe_omit_events, previous_url] { // 1. Change the current request's current URL to selected source. m_current_request->set_current_url(selected_source.value().url); - // 2. Fire an event named error at the img element. - dispatch_event(DOM::Event::create(realm(), HTML::EventNames::error).release_value_but_fixme_should_propagate_errors()); + // 2. If maybe omit events is not set or previousURL is not equal to selected source, then fire an event named error at the img element. + if (!maybe_omit_events || previous_url != selected_source.value().url) + dispatch_event(DOM::Event::create(realm(), HTML::EventNames::error).release_value_but_fixme_should_propagate_errors()); }); // 5. Return. return; } - // 12. If the pending request is not null and urlString is the same as the pending request's current URL, then return. + // 13. If the pending request is not null and urlString is the same as the pending request's current URL, then return. if (m_pending_request && url_string == m_pending_request->current_url()) return; - // 13. If urlString is the same as the current request's current URL and current request's state is partially available, + // 14. If urlString is the same as the current request's current URL and current request's state is partially available, // then abort the image request for the pending request, // queue an element task on the DOM manipulation task source given the img element // to restart the animation if restart animation is set, and return. @@ -440,61 +449,61 @@ after_step_6: return; } - // 14. If the pending request is not null, then abort the image request for the pending request. + // 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); - // 15. Set image request to a new image request whose current URL is urlString. + // 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); - // 16. If current request's state is unavailable or broken, then set the current request to image request. + // 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. if (m_current_request->state() == ImageRequest::State::Unavailable || m_current_request->state() == ImageRequest::State::Broken) m_current_request = image_request; else m_pending_request = image_request; - // 17. Let request be the result of creating a potential-CORS request given urlString, "image", + // 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); - // 18. Set request's client to the element's node document's relevant settings object. + // 19. Set request's client to the element's node document's relevant settings object. request->set_client(&document().relevant_settings_object()); - // 19. If the element uses srcset or picture, set request's initiator to "imageset". + // 20. If the element uses srcset or picture, set request's initiator to "imageset". if (uses_srcset_or_picture()) request->set_initiator(Fetch::Infrastructure::Request::Initiator::ImageSet); - // 20. Set request's referrer policy to the current state of the element's referrerpolicy attribute. + // 21. Set request's referrer policy to the current state of the element's referrerpolicy attribute. request->set_referrer_policy(ReferrerPolicy::from_string(attribute(HTML::AttributeNames::referrerpolicy))); - // FIXME: 21. Set request's priority to the current state of the element's fetchpriority attribute. + // FIXME: 22. Set request's priority to the current state of the element's fetchpriority attribute. - // 22. 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. + // 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: 23. If the will lazy load element steps given the img return true, then: + // 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](JS::NonnullGCPtr response) { + 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(); - // 25. As soon as possible, jump to the first applicable entry from the following list: + // 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] { - auto process_body = [response, image_request, url_string, this](ByteBuffer data) { + 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)); + 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(); @@ -505,7 +514,7 @@ after_step_6: }); }; - // 24. Fetch the image: Fetch request. + // 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 @@ -524,7 +533,7 @@ after_step_6: return {}; } -void HTMLImageElement::handle_successful_fetch(AK::URL const& url_string, StringView mime_type, ImageRequest& image_request, ByteBuffer data) +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. @@ -537,12 +546,33 @@ void HTMLImageElement::handle_successful_fetch(AK::URL const& url_string, String 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()); - dispatch_event(DOM::Event::create(realm(), HTML::EventNames::error).release_value_but_fixme_should_propagate_errors()); + handle_failed_image_decode(); return; } @@ -550,7 +580,7 @@ void HTMLImageElement::handle_successful_fetch(AK::URL const& url_string, String } else { auto result = Web::Platform::ImageCodecPlugin::the().decode_image(data.bytes()); if (!result.has_value()) { - dispatch_event(DOM::Event::create(realm(), HTML::EventNames::error).release_value_but_fixme_should_propagate_errors()); + handle_failed_image_decode(); return; } @@ -586,8 +616,9 @@ void HTMLImageElement::handle_successful_fetch(AK::URL const& url_string, String // 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. Fire an event named load at the img element. - dispatch_event(DOM::Event::create(realm(), HTML::EventNames::load).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(); diff --git a/Userland/Libraries/LibWeb/HTML/HTMLImageElement.h b/Userland/Libraries/LibWeb/HTML/HTMLImageElement.h index 8f5be7d320..3ee78d53d3 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLImageElement.h +++ b/Userland/Libraries/LibWeb/HTML/HTMLImageElement.h @@ -51,7 +51,7 @@ public: virtual Optional default_role() const override; // https://html.spec.whatwg.org/multipage/images.html#update-the-image-data - ErrorOr update_the_image_data(bool restart_the_animations = false); + ErrorOr update_the_image_data(bool restart_the_animations = false, bool maybe_omit_events = false); // https://html.spec.whatwg.org/multipage/images.html#use-srcset-or-picture [[nodiscard]] bool uses_srcset_or_picture() const; @@ -93,7 +93,7 @@ private: virtual JS::GCPtr create_layout_node(NonnullRefPtr) override; - void handle_successful_fetch(AK::URL const&, StringView mime_type, ImageRequest&, ByteBuffer); + void handle_successful_fetch(AK::URL const&, StringView mime_type, ImageRequest&, ByteBuffer, bool maybe_omit_events, AK::URL const& previous_url); void handle_failed_fetch(); void animate();