ladybird/Libraries/LibWeb/HTML/SharedResourceRequest.cpp

286 lines
9.7 KiB
C++
Raw Permalink Normal View History

/*
* Copyright (c) 2023, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibGC/Weak.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/DecodedImageFrame.h>
#include <LibWeb/Bindings/PrincipalHostDefined.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/Fetch/Fetching/Fetching.h>
#include <LibWeb/Fetch/Infrastructure/FetchAlgorithms.h>
#include <LibWeb/Fetch/Infrastructure/FetchController.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/MIME.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Requests.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Responses.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Statuses.h>
#include <LibWeb/HTML/AnimatedBitmapDecodedImageData.h>
#include <LibWeb/HTML/BitmapDecodedImageData.h>
#include <LibWeb/HTML/DecodedImageData.h>
#include <LibWeb/HTML/SharedResourceRequest.h>
#include <LibWeb/Loader/ResourceLoader.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Platform/ImageCodecPlugin.h>
#include <LibWeb/SVG/SVGDecodedImageData.h>
namespace Web::HTML {
GC_DEFINE_ALLOCATOR(SharedResourceRequest);
static u64 s_next_memory_cache_touch_serial;
GC::Ref<SharedResourceRequest> SharedResourceRequest::get_or_create(JS::Realm& realm, GC::Ref<Page> page, URL::URL const& url)
{
auto document = Bindings::principal_host_defined_environment_settings_object(realm).responsible_document();
VERIFY(document);
auto& shared_resource_requests = document->shared_resource_requests();
if (auto it = shared_resource_requests.find(url); it != shared_resource_requests.end()) {
it->value->touch_memory_cache_entry();
return *it->value;
}
auto request = realm.create<SharedResourceRequest>(page, url, *document);
shared_resource_requests.set(url, request);
return request;
}
SharedResourceRequest::SharedResourceRequest(GC::Ref<Page> page, URL::URL url, GC::Ref<DOM::Document> document)
: m_page(page)
, m_url(move(url))
, m_document(document)
{
touch_memory_cache_entry();
}
SharedResourceRequest::~SharedResourceRequest() = default;
void SharedResourceRequest::finalize()
{
Base::finalize();
m_callbacks.clear();
m_load_event_delayer.clear();
m_image_data = nullptr;
m_fetch_controller = nullptr;
if (m_document) {
auto& shared_resource_requests = m_document->shared_resource_requests();
if (auto it = shared_resource_requests.find(m_url);
it != shared_resource_requests.end() && it->value.ptr() == this)
shared_resource_requests.remove(it);
m_document = nullptr;
}
}
void SharedResourceRequest::visit_edges(JS::Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_fetch_controller);
visitor.visit(m_document);
visitor.visit(m_page);
for (auto& callback : m_callbacks) {
visitor.visit(callback.on_finish);
visitor.visit(callback.on_fail);
}
visitor.visit(m_image_data);
}
GC::Ptr<DecodedImageData> SharedResourceRequest::image_data() const
{
return m_image_data;
}
void SharedResourceRequest::touch_memory_cache_entry()
{
m_cache_touch_serial = ++s_next_memory_cache_touch_serial;
}
GC::Ptr<Fetch::Infrastructure::FetchController> SharedResourceRequest::fetch_controller()
{
return m_fetch_controller.ptr();
}
void SharedResourceRequest::set_fetch_controller(GC::Ptr<Fetch::Infrastructure::FetchController> fetch_controller)
{
m_fetch_controller = move(fetch_controller);
}
void SharedResourceRequest::fetch_resource(JS::Realm& realm, GC::Ref<Fetch::Infrastructure::Request> request)
{
VERIFY(needs_fetching());
if (!ResourceLoader::is_initialized()) {
handle_failed_fetch();
return;
}
GC::Weak weak_this { *this };
Fetch::Infrastructure::FetchAlgorithms::Input fetch_algorithms_input {};
fetch_algorithms_input.process_response = [weak_this, &realm, request](GC::Ref<Fetch::Infrastructure::Response> response) {
auto self = weak_this.ptr();
if (!self)
return;
auto image_data_is_cors_cross_origin = response->is_cors_cross_origin();
// 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 = GC::create_function(self->heap(), [weak_this, request, response, image_data_is_cors_cross_origin](ByteBuffer data) {
auto self = weak_this.ptr();
if (!self)
return;
auto extracted_mime_type = Fetch::Infrastructure::extract_mime_type(response->header_list());
auto mime_type = extracted_mime_type.has_value() ? extracted_mime_type.value().essence().bytes_as_string_view() : StringView {};
self->handle_successful_fetch(request->url(), mime_type, move(data), image_data_is_cors_cross_origin);
});
auto process_body_error = GC::create_function(self->heap(), [weak_this](JS::Value) {
auto self = weak_this.ptr();
if (!self)
return;
self->handle_failed_fetch();
});
// Check for failed fetch response
if (!Fetch::Infrastructure::is_ok_status(response->status()) || !response->body()) {
self->handle_failed_fetch();
return;
}
response->body()->fully_read(realm, process_body, process_body_error, GC::Ref { realm.global_object() });
};
m_state = State::Fetching;
m_load_event_delayer.emplace(*m_document);
auto fetch_controller = Fetch::Fetching::fetch(
realm,
request,
Fetch::Infrastructure::FetchAlgorithms::create(realm.vm(), move(fetch_algorithms_input)));
set_fetch_controller(fetch_controller);
}
void SharedResourceRequest::add_callbacks(Function<void()> on_finish, Function<void()> on_fail)
{
if (m_state == State::Finished) {
if (on_finish)
on_finish();
return;
}
if (m_state == State::Failed) {
if (on_fail)
on_fail();
return;
}
Callbacks callbacks;
if (on_finish)
callbacks.on_finish = GC::create_function(vm().heap(), move(on_finish));
if (on_fail)
callbacks.on_fail = GC::create_function(vm().heap(), move(on_fail));
m_callbacks.append(move(callbacks));
}
void SharedResourceRequest::handle_successful_fetch(URL::URL const& url_string, StringView mime_type, ByteBuffer data, bool image_data_is_cors_cross_origin)
{
// 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
|| (mime_type.is_empty() && url_string.basename().ends_with(".svg"sv));
if (is_svg_image) {
auto result = SVG::SVGDecodedImageData::create(m_document->realm(), m_page, url_string, data);
if (result.is_error()) {
handle_failed_fetch();
} else {
m_image_data = result.release_value();
m_image_data->set_is_cors_cross_origin(image_data_is_cors_cross_origin);
handle_successful_resource_load();
}
return;
}
auto handle_successful_bitmap_decode = [strong_this = GC::Root(*this), image_data_is_cors_cross_origin](Web::Platform::DecodedImage& result) -> ErrorOr<void> {
if (result.session_id != 0) {
// Streaming animated decode: create AnimatedBitmapDecodedImageData.
Vector<NonnullRefPtr<Gfx::Bitmap>> initial_bitmaps;
initial_bitmaps.ensure_capacity(result.frames.size());
for (auto& frame : result.frames)
initial_bitmaps.unchecked_append(*frame.bitmap);
auto first_bitmap = result.frames.first().bitmap;
auto size = first_bitmap->size();
strong_this->m_image_data = AnimatedBitmapDecodedImageData::create(
strong_this->m_document->realm(),
LibWeb: Transfer animation ownership to `AnimatedBitmapDecodedImageData` Previously animation ownership was a messy split between `AnimatedBitmapDecodedImageData` and the consumers (i.e. `ImageStyleValueResource`, `HTMLImageElement`, and `SVGImageElement`) with `AnimatedBitmapDecodedImageData` owning the frames and a current frame index, and the consumers owning the rest of the state (e.g. loop count, timers to drive the animation forward, their own current index). This had a couple of main issues: - While `AnimatedDecodedImageData` partially synchronized animations by dropping unexpected advancement notifications, this didn't apply to other animation state which meant, for instance, that a later started consumer could drive the animation of an earlier one past the max loop count (albeit without invalidating the earlier consumer). - Multiple consumers didn't share frame timings, meaning animations could be up to a full frame out of sync visually. - Animations were paused depending on whether there were any consumers, this is different to the behavior in other browsers (where they continue regardless of whether there are any consumers). - It was an overgeneralization of how animations need to work - only `AnimatedBitmapDecodedImageData` works with an indexed frame model, with animated SVGs (although not yet implemented) relying on their internal event loop to be driven forward. Given the above the new approach implemented in this commit is: - The API for `DecodedImageData` is animation system agnostic, only exposing `default_frame`, `current_frame`, and `restart_animation` methods not reliant on providing a specific frame index. - `AnimatedBitmapDecodedImageData` owns its own timer, loop count, etc. The animation starts when the first consumer registers and ends when the document is hidden or becomes inactive (or completes in the case of finite animations). - Consumers are invalidated by `AnimatedBitmapDecodedImageData` when required. Tests have been added for: - Animations being paused when the document becomes inactive and restarted when it becomes active again. - Frame timings being synchronized across consumers. - Restarts triggered by `HTMLImageElement` applying to all consumers. - Processing ending once a non-infinite animation plays to completion. The tests to ensure animations are cancelled when consumers are removed (e.g. `animated-background-image-timer-stops-when-hidden.html`) have been updated to assert the inverse since animation state is now per resource not per consumer.
2026-06-15 19:56:00 -03:00
*strong_this->m_document,
result.session_id,
result.frame_count,
result.loop_count,
size,
move(result.color_space),
move(result.all_durations),
move(initial_bitmaps));
} else {
// Non animated decode: create a single framed BitmapDecodedImageData.
VERIFY(result.frames.size() == 1);
strong_this->m_image_data = BitmapDecodedImageData::create(strong_this->m_document->realm(), { *result.frames[0].bitmap, result.color_space });
}
strong_this->m_image_data->set_is_cors_cross_origin(image_data_is_cors_cross_origin);
strong_this->handle_successful_resource_load();
return {};
};
auto handle_failed_decode = [strong_this = GC::Root(*this)](Error&) -> void {
strong_this->handle_failed_fetch();
};
(void)Web::Platform::ImageCodecPlugin::the().decode_image(data.bytes(), move(handle_successful_bitmap_decode), move(handle_failed_decode));
}
void SharedResourceRequest::handle_failed_fetch()
{
m_state = State::Failed;
m_load_event_delayer.clear();
m_fetch_controller = nullptr;
for (auto& callback : m_callbacks) {
if (callback.on_fail)
callback.on_fail->function()();
}
m_callbacks.clear();
}
void SharedResourceRequest::handle_successful_resource_load()
{
m_state = State::Finished;
m_load_event_delayer.clear();
m_fetch_controller = nullptr;
for (auto& callback : m_callbacks) {
if (callback.on_finish)
callback.on_finish->function()();
}
m_callbacks.clear();
m_document->prune_image_resource_caches();
}
bool SharedResourceRequest::needs_fetching() const
{
return m_state == State::New;
}
bool SharedResourceRequest::is_fetching() const
{
return m_state == State::Fetching;
}
}