mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2026-06-28 12:10:28 +00:00
`ImageStyleValueResource::frame` was unused and `ImageStyleValue::frame` was only ever used to get the current frame so it can just be inlined.
464 lines
16 KiB
C++
464 lines
16 KiB
C++
/*
|
|
* Copyright (c) 2018-2023, Andreas Kling <andreas@ladybird.org>
|
|
* Copyright (c) 2021, Tobias Christiansen <tobyase@serenityos.org>
|
|
* Copyright (c) 2021-2025, Sam Atkins <sam@ladybird.org>
|
|
* Copyright (c) 2022-2023, MacDue <macdue@dueutil.tech>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <AK/AnyOf.h>
|
|
#include <LibGC/Function.h>
|
|
#include <LibGC/Weak.h>
|
|
#include <LibGfx/DecodedImageFrame.h>
|
|
#include <LibWeb/CSS/CSSStyleSheet.h>
|
|
#include <LibWeb/CSS/Fetch.h>
|
|
#include <LibWeb/CSS/StyleValues/ImageStyleValue.h>
|
|
#include <LibWeb/DOM/Document.h>
|
|
#include <LibWeb/DOMURL/DOMURL.h>
|
|
#include <LibWeb/HTML/DecodedImageData.h>
|
|
#include <LibWeb/HTML/PotentialCORSRequest.h>
|
|
#include <LibWeb/HTML/Scripting/Environments.h>
|
|
#include <LibWeb/HTML/SharedResourceRequest.h>
|
|
#include <LibWeb/Painting/DisplayListRecorder.h>
|
|
#include <LibWeb/Painting/DisplayListRecordingContext.h>
|
|
#include <LibWeb/Platform/Timer.h>
|
|
|
|
namespace Web::CSS {
|
|
|
|
ImageStyleValueResource::ImageStyleValueResource(GC::Ref<HTML::SharedResourceRequest> request, GC::Ref<DOM::Document> const& document)
|
|
: m_resource_request(move(request))
|
|
{
|
|
m_resource_request->add_callbacks(
|
|
[weak_document = GC::Weak(document), url = m_resource_request->url()] {
|
|
// FIXME: Can we directly access the resource (i.e. this) here instead of looking it up in the document?
|
|
if (auto document = weak_document.ptr()) {
|
|
if (auto* resource = document->css_image_resource(url))
|
|
resource->on_decoded_image_data_loaded(*document);
|
|
}
|
|
},
|
|
nullptr);
|
|
}
|
|
|
|
ImageStyleValueResource::~ImageStyleValueResource()
|
|
{
|
|
stop_animation_timer();
|
|
VERIFY(m_image_style_values.is_empty());
|
|
unregister_with_decoded_image_data_if_needed();
|
|
}
|
|
|
|
void ImageStyleValueResource::visit_edges(JS::Cell::Visitor& visitor)
|
|
{
|
|
visitor.visit(m_resource_request);
|
|
visitor.visit(m_timer);
|
|
}
|
|
|
|
void ImageStyleValueResource::register_image_style_value(DOM::Document& document, ImageStyleValue const& image_style_value)
|
|
{
|
|
m_image_style_values.set(&image_style_value);
|
|
start_animation_timer_if_needed(document);
|
|
register_with_decoded_image_data_if_needed();
|
|
}
|
|
|
|
void ImageStyleValueResource::unregister_image_style_value(ImageStyleValue const& image_style_value)
|
|
{
|
|
m_image_style_values.remove(&image_style_value);
|
|
if (m_image_style_values.is_empty()) {
|
|
stop_animation_timer();
|
|
unregister_with_decoded_image_data_if_needed();
|
|
}
|
|
}
|
|
|
|
GC::Ptr<HTML::DecodedImageData> ImageStyleValueResource::decoded_image_data() const
|
|
{
|
|
return m_resource_request->image_data();
|
|
}
|
|
|
|
bool ImageStyleValueResource::has_active_animation_timer() const
|
|
{
|
|
return m_timer && m_timer->is_active();
|
|
}
|
|
|
|
void ImageStyleValueResource::on_decoded_image_data_loaded(DOM::Document& document)
|
|
{
|
|
notify_image_style_values_did_update();
|
|
start_animation_timer_if_needed(document);
|
|
if (!m_image_style_values.is_empty())
|
|
register_with_decoded_image_data_if_needed();
|
|
}
|
|
|
|
void ImageStyleValueResource::notify_image_style_values_did_update()
|
|
{
|
|
for (auto const* image_style_value : m_image_style_values)
|
|
image_style_value->notify_clients_did_update();
|
|
}
|
|
|
|
void ImageStyleValueResource::start_animation_timer_if_needed(DOM::Document& document)
|
|
{
|
|
if (m_image_style_values.is_empty() || !is_animatable())
|
|
return;
|
|
|
|
if (m_timer && m_timer->is_active())
|
|
return;
|
|
|
|
if (!m_timer) {
|
|
auto timer = Platform::Timer::create(document.heap());
|
|
m_timer = timer;
|
|
timer->on_timeout = GC::create_function(document.heap(), [weak_document = GC::Weak(document), url = m_resource_request->url()] {
|
|
if (auto document = weak_document.ptr())
|
|
document->animate_css_image_resource(url);
|
|
});
|
|
}
|
|
|
|
m_timer->set_interval(current_frame_duration());
|
|
m_timer->start();
|
|
}
|
|
|
|
void ImageStyleValueResource::stop_animation_timer()
|
|
{
|
|
if (m_timer && m_timer->is_active())
|
|
m_timer->stop();
|
|
}
|
|
|
|
bool ImageStyleValueResource::is_animatable() const
|
|
{
|
|
auto image_data = this->decoded_image_data();
|
|
if (!image_data || !image_data->is_animated() || image_data->frame_count() <= 1)
|
|
return false;
|
|
|
|
return !animation_has_completed();
|
|
}
|
|
|
|
bool ImageStyleValueResource::animation_has_completed() const
|
|
{
|
|
auto image_data = this->decoded_image_data();
|
|
return image_data && image_data->loop_count() > 0 && m_loops_completed == image_data->loop_count();
|
|
}
|
|
|
|
int ImageStyleValueResource::current_frame_duration() const
|
|
{
|
|
auto image_data = this->decoded_image_data();
|
|
if (!image_data)
|
|
return 0;
|
|
|
|
return image_data->frame_duration(m_current_frame_index);
|
|
}
|
|
|
|
void ImageStyleValueResource::animate(DOM::Document&)
|
|
{
|
|
auto image_data = m_resource_request->image_data();
|
|
if (!image_data)
|
|
return;
|
|
|
|
m_current_frame_index = (m_current_frame_index + 1) % image_data->frame_count();
|
|
m_current_frame_index = image_data->notify_frame_advanced(m_current_frame_index);
|
|
auto current_frame_duration = image_data->frame_duration(m_current_frame_index);
|
|
|
|
if (m_timer && current_frame_duration != m_timer->interval())
|
|
m_timer->restart(current_frame_duration);
|
|
|
|
if (m_current_frame_index == image_data->frame_count() - 1) {
|
|
++m_loops_completed;
|
|
if (animation_has_completed())
|
|
stop_animation_timer();
|
|
}
|
|
|
|
notify_image_style_values_did_update();
|
|
}
|
|
|
|
ValueComparingNonnullRefPtr<ImageStyleValue const> ImageStyleValue::create(URL const& url)
|
|
{
|
|
return adopt_ref(*new (nothrow) ImageStyleValue(url));
|
|
}
|
|
|
|
ValueComparingNonnullRefPtr<ImageStyleValue const> ImageStyleValue::create(URL const& url, Optional<::URL::URL> style_resource_base_url)
|
|
{
|
|
return adopt_ref(*new (nothrow) ImageStyleValue(url, move(style_resource_base_url)));
|
|
}
|
|
|
|
ValueComparingNonnullRefPtr<ImageStyleValue const> ImageStyleValue::create(::URL::URL const& url)
|
|
{
|
|
return adopt_ref(*new (nothrow) ImageStyleValue(URL { url.to_string() }));
|
|
}
|
|
|
|
ImageStyleValue::ImageStyleValue(URL const& url, Optional<::URL::URL> style_resource_base_url)
|
|
: AbstractImageStyleValue(Type::Image)
|
|
, m_url(url)
|
|
, m_style_resource_base_url(move(style_resource_base_url))
|
|
{
|
|
}
|
|
|
|
ImageStyleValue::~ImageStyleValue() = default;
|
|
|
|
u64 ImageStyleValue::active_animation_timer_count(DOM::Document const& document)
|
|
{
|
|
return document.active_css_image_animation_timer_count();
|
|
}
|
|
|
|
GC::Ptr<HTML::SharedResourceRequest> ImageStyleValue::fetch_image(DOM::Document& document) const
|
|
{
|
|
RuleOrDeclaration rule_or_declaration {
|
|
.environment_settings_object = document.relevant_settings_object(),
|
|
.value = RuleOrDeclaration::Rule {},
|
|
.style_resource_base_url = m_style_resource_base_url,
|
|
.parent_style_sheet_origin_clean = m_parent_style_sheet_origin_clean,
|
|
};
|
|
|
|
return fetch_an_external_image_for_a_stylesheet(m_url, rule_or_declaration, document);
|
|
}
|
|
|
|
void ImageStyleValue::load_any_resources(DOM::Document& document)
|
|
{
|
|
fetch_image(document);
|
|
}
|
|
|
|
void ImageStyleValue::serialize(StringBuilder& builder, SerializationMode) const
|
|
{
|
|
builder.append(m_url.to_string());
|
|
}
|
|
|
|
bool ImageStyleValue::equals(StyleValue const& other) const
|
|
{
|
|
if (type() != other.type())
|
|
return false;
|
|
return m_url == other.as_image().m_url;
|
|
}
|
|
|
|
Optional<CSSPixels> ImageStyleValue::natural_width(DOM::Document const& document) const
|
|
{
|
|
if (auto image_data = this->image_data(document))
|
|
return image_data->intrinsic_width();
|
|
return {};
|
|
}
|
|
|
|
Optional<CSSPixels> ImageStyleValue::natural_height(DOM::Document const& document) const
|
|
{
|
|
if (auto image_data = this->image_data(document))
|
|
return image_data->intrinsic_height();
|
|
return {};
|
|
}
|
|
|
|
Optional<CSSPixelFraction> ImageStyleValue::natural_aspect_ratio(DOM::Document const& document) const
|
|
{
|
|
if (auto image_data = this->image_data(document))
|
|
return image_data->intrinsic_aspect_ratio();
|
|
return {};
|
|
}
|
|
|
|
bool ImageStyleValue::is_paintable(DOM::Document const& document) const
|
|
{
|
|
return image_data(document);
|
|
}
|
|
|
|
void ImageStyleValue::paint(DisplayListRecordingContext& context, DOM::Document const& document, DevicePixelRect const& dest_rect, CSS::ImageRendering image_rendering) const
|
|
{
|
|
auto image_data = this->image_data(document);
|
|
if (!image_data)
|
|
return;
|
|
|
|
auto current_frame_index = this->current_frame_index(document);
|
|
auto dest_int_rect = dest_rect.to_type<int>();
|
|
image_data->paint(context, current_frame_index, dest_int_rect, image_rendering);
|
|
}
|
|
|
|
Optional<Gfx::DecodedImageFrame> ImageStyleValue::current_frame(DOM::Document const& document, DevicePixelRect const& dest_rect) const
|
|
{
|
|
if (auto image_data = this->image_data(document))
|
|
return image_data->frame(current_frame_index(document), dest_rect.size().to_type<int>());
|
|
|
|
return {};
|
|
}
|
|
|
|
size_t ImageStyleValue::current_frame_index(DOM::Document const& document) const
|
|
{
|
|
auto resolved_url = this->resolved_url(document);
|
|
if (!resolved_url.has_value())
|
|
return 0;
|
|
|
|
if (auto const* resource = document.css_image_resource(*resolved_url))
|
|
return resource->current_frame_index();
|
|
return 0;
|
|
}
|
|
|
|
GC::Ptr<HTML::DecodedImageData> ImageStyleValue::image_data(DOM::Document const& document) const
|
|
{
|
|
auto resolved_url = this->resolved_url(document);
|
|
if (!resolved_url.has_value())
|
|
return nullptr;
|
|
|
|
auto const* resource = document.css_image_resource(*resolved_url);
|
|
|
|
// NB: We should have registered a client for this before now which ensures a resource exists if we have a valid
|
|
// resolved URL.
|
|
VERIFY(resource);
|
|
|
|
return resource->decoded_image_data();
|
|
}
|
|
|
|
Optional<Gfx::Color> ImageStyleValue::color_if_single_pixel_bitmap(DOM::Document const& document) const
|
|
{
|
|
if (auto decoded_frame = current_frame(document); decoded_frame.has_value()) {
|
|
auto const& bitmap = decoded_frame->bitmap();
|
|
if (bitmap.width() == 1 && bitmap.height() == 1)
|
|
return bitmap.get_pixel(0, 0);
|
|
}
|
|
return {};
|
|
}
|
|
|
|
void ImageStyleValue::set_style_sheet(GC::Ptr<CSSStyleSheet> style_sheet)
|
|
{
|
|
Base::set_style_sheet(style_sheet);
|
|
|
|
m_style_resource_base_url.clear();
|
|
m_parent_style_sheet_origin_clean.clear();
|
|
m_should_absolutize_url_for_computed_value = false;
|
|
|
|
if (style_sheet) {
|
|
update_style_sheet_resource_context(*style_sheet);
|
|
style_sheet->register_pending_image_value(*this);
|
|
}
|
|
}
|
|
|
|
void ImageStyleValue::update_style_sheet_resource_context(CSSStyleSheet const& style_sheet)
|
|
{
|
|
m_style_resource_base_url = style_sheet.base_url()
|
|
.value_or_lazy_evaluated_optional([&]() { return style_sheet.location(); })
|
|
.value_or_lazy_evaluated_optional([&]() { return HTML::relevant_settings_object(style_sheet).api_base_url(); });
|
|
m_parent_style_sheet_origin_clean = style_sheet.is_origin_clean();
|
|
m_should_absolutize_url_for_computed_value = true;
|
|
}
|
|
|
|
ValueComparingNonnullRefPtr<StyleValue const> ImageStyleValue::absolutized(ComputationContext const& context) const
|
|
{
|
|
if (m_url.url().is_empty())
|
|
return *this;
|
|
|
|
// FIXME: The spec has been updated to handle this better. The computation of the base URL here is roughly based on:
|
|
// https://drafts.csswg.org/css-values-4/#style-resource-base-url
|
|
// https://github.com/w3c/csswg-drafts/pull/12261
|
|
auto base_url = m_style_resource_base_url;
|
|
if (!base_url.has_value() && context.abstract_element.has_value())
|
|
base_url = context.abstract_element->document().base_url();
|
|
|
|
if (base_url.has_value()) {
|
|
if (m_should_absolutize_url_for_computed_value) {
|
|
if (DOMURL::parse(m_url.url()).has_value()) {
|
|
auto absolutized_image = adopt_ref(*new (nothrow) ImageStyleValue(m_url, *base_url));
|
|
absolutized_image->m_parent_style_sheet_origin_clean = m_parent_style_sheet_origin_clean;
|
|
absolutized_image->m_should_absolutize_url_for_computed_value = true;
|
|
return absolutized_image;
|
|
}
|
|
|
|
if (auto resolved_url = DOMURL::parse(m_url.url(), *base_url); resolved_url.has_value()) {
|
|
auto absolutized_image = adopt_ref(*new (nothrow) ImageStyleValue(URL { resolved_url->to_string(), m_url.type(), m_url.request_url_modifiers() }, *base_url));
|
|
absolutized_image->m_parent_style_sheet_origin_clean = m_parent_style_sheet_origin_clean;
|
|
absolutized_image->m_should_absolutize_url_for_computed_value = true;
|
|
return absolutized_image;
|
|
}
|
|
|
|
return *this;
|
|
}
|
|
|
|
auto absolutized_image = adopt_ref(*new (nothrow) ImageStyleValue(m_url, *base_url));
|
|
absolutized_image->m_parent_style_sheet_origin_clean = m_parent_style_sheet_origin_clean;
|
|
return absolutized_image;
|
|
}
|
|
|
|
return *this;
|
|
}
|
|
|
|
void ImageStyleValue::register_client(Client& client) const
|
|
{
|
|
auto result = m_clients.set(&client);
|
|
VERIFY(result == AK::HashSetResult::InsertedNewEntry);
|
|
auto document = client.document();
|
|
if (!document)
|
|
return;
|
|
|
|
auto resolved_url = this->resolved_url(*document);
|
|
if (!resolved_url.has_value())
|
|
return;
|
|
|
|
// NB: Store the resolved URL so that we can unregister from the resource later even if the document's base URL
|
|
// changes in the interim.
|
|
client.m_registered_url = *resolved_url;
|
|
|
|
GC::Ptr<CSS::ImageStyleValueResource> resource;
|
|
|
|
if (auto* existing_resource = document->css_image_resource(*resolved_url)) {
|
|
resource = existing_resource;
|
|
} else {
|
|
auto resource_request = fetch_image(*document);
|
|
|
|
// NB: This can only fail if the URL is invalid or ResourceLoader is not initialized, neither of which should be
|
|
// the case here.
|
|
VERIFY(resource_request);
|
|
|
|
resource = document->create_css_image_resource(*resource_request);
|
|
}
|
|
|
|
resource->register_image_style_value(*document, *this);
|
|
}
|
|
|
|
void ImageStyleValue::unregister_client(Client& client) const
|
|
{
|
|
auto document = client.document();
|
|
auto registered_url = move(client.m_registered_url);
|
|
client.m_registered_url.clear();
|
|
|
|
auto did_remove = m_clients.remove(&client);
|
|
VERIFY(did_remove);
|
|
|
|
if (!document || !registered_url.has_value())
|
|
return;
|
|
|
|
if (any_of(m_clients, [&](auto const* remaining_client) {
|
|
return remaining_client->document() == document
|
|
&& remaining_client->m_registered_url.has_value()
|
|
&& *remaining_client->m_registered_url == *registered_url;
|
|
}))
|
|
return;
|
|
|
|
if (auto* resource = document->css_image_resource(*registered_url))
|
|
resource->unregister_image_style_value(*this);
|
|
document->remove_css_image_resource_if_unused(*registered_url);
|
|
}
|
|
|
|
void ImageStyleValue::notify_clients_did_update() const
|
|
{
|
|
for (auto* client : m_clients)
|
|
client->image_style_value_did_update(const_cast<ImageStyleValue&>(*this));
|
|
}
|
|
|
|
Optional<::URL::URL> ImageStyleValue::resolved_url(DOM::Document const& document) const
|
|
{
|
|
if (m_url.url().is_empty())
|
|
return {};
|
|
|
|
return DOMURL::parse(m_url.url(), style_resource_base_url(document));
|
|
}
|
|
|
|
::URL::URL ImageStyleValue::style_resource_base_url(DOM::Document const& document) const
|
|
{
|
|
return m_style_resource_base_url.value_or_lazy_evaluated([&] {
|
|
return document.relevant_settings_object().api_base_url();
|
|
});
|
|
}
|
|
|
|
ImageStyleValue::Client::Client(DOM::Document& document, ImageStyleValue const& image_style_value)
|
|
: m_image_style_value(image_style_value)
|
|
, m_document(document)
|
|
{
|
|
m_image_style_value.register_client(*this);
|
|
}
|
|
|
|
ImageStyleValue::Client::~Client()
|
|
{
|
|
}
|
|
|
|
void ImageStyleValue::Client::image_style_value_finalize()
|
|
{
|
|
m_image_style_value.unregister_client(*this);
|
|
}
|
|
|
|
}
|