mirror of
https://github.com/RGBCube/serenity
synced 2025-07-10 03:57:35 +00:00
LibWeb: Use a separate class for shared image requests
As it turns out, making everyone piggyback on HTML::ImageRequest had some major flaws, as HTMLImageElement may decide to abort an ongoing fetch or wipe out image data, even when someone else is using the same image request. To avoid this issue, this patch introduces SharedImageRequest, and then implements ImageRequest on top of that. Other clients of the ImageRequest API are moved to SharedImageRequest as well, and ImageRequest is now only used by HTMLImageElement. This fixes an issue with image data disappearing and leading to asserts and/or visually absent images.
This commit is contained in:
parent
cdec23a68c
commit
34591ff3d9
11 changed files with 287 additions and 174 deletions
|
@ -19,37 +19,18 @@
|
|||
|
||||
namespace Web::HTML {
|
||||
|
||||
static HashTable<ImageRequest*>& shareable_image_requests()
|
||||
{
|
||||
static HashTable<ImageRequest*> requests;
|
||||
return requests;
|
||||
}
|
||||
|
||||
ErrorOr<NonnullRefPtr<ImageRequest>> ImageRequest::create(Page& page)
|
||||
{
|
||||
return adopt_nonnull_ref_or_enomem(new (nothrow) ImageRequest(page));
|
||||
}
|
||||
|
||||
ErrorOr<NonnullRefPtr<ImageRequest>> 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<DecodedImageData const> 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<Fetch::Infrastructure::FetchController> ImageRequest::fetch_controller()
|
||||
{
|
||||
return m_fetch_controller.ptr();
|
||||
}
|
||||
|
||||
void ImageRequest::set_fetch_controller(JS::GCPtr<Fetch::Infrastructure::FetchController> fetch_controller)
|
||||
{
|
||||
m_fetch_controller = move(fetch_controller);
|
||||
}
|
||||
|
||||
void ImageRequest::fetch_image(JS::Realm& realm, JS::NonnullGCPtr<Fetch::Infrastructure::Request> request)
|
||||
{
|
||||
Fetch::Infrastructure::FetchAlgorithms::Input fetch_algorithms_input {};
|
||||
fetch_algorithms_input.process_response = [this, &realm, request](JS::NonnullGCPtr<Fetch::Infrastructure::Response> 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<void()> on_finish, JS::SafeFunction<void()> 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<DecodedImageData> 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<AnimatedBitmapDecodedImageData::Frame> frames;
|
||||
for (auto& frame : result.value().frames) {
|
||||
frames.append(AnimatedBitmapDecodedImageData::Frame {
|
||||
.bitmap = frame.bitmap,
|
||||
.duration = static_cast<int>(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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue