mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2026-06-21 08:35:24 +00:00
ImageStyleValue stored GC-managed request, stylesheet, and animation timer references as strong GC::Ptr fields even though image style values are refcounted objects. When such a style value outlives the GC object that normally visits it, those fields can keep stale pointers after GC collects the referents. On Steam this allowed a stale image resource request to be read as unrelated image data, making carousel SVG arrows render at the wrong size. Store these back references as GC::Weak instead. Reachable style values still use live requests, stylesheets, and timers normally, but detached values observe null after the GC collects the referent and can reload or skip the now-dead association instead of dereferencing reused GC memory. Keep a local timer handle while installing the timeout callback so setup does not rely on the weak member. With the image style values no longer hiding strong GC edges, remove the obsolete IGNORE_GC annotation from CSSStyleSheet's pending image list.
338 lines
10 KiB
C++
338 lines
10 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 <LibGfx/DecodedImageFrame.h>
|
|
#include <LibWeb/CSS/CSSStyleSheet.h>
|
|
#include <LibWeb/CSS/ComputedValues.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 {
|
|
|
|
static HashTable<ImageStyleValue const*> s_active_animation_timers;
|
|
|
|
ValueComparingNonnullRefPtr<ImageStyleValue const> ImageStyleValue::create(URL const& url)
|
|
{
|
|
return adopt_ref(*new (nothrow) ImageStyleValue(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)
|
|
: AbstractImageStyleValue(Type::Image)
|
|
, m_url(url)
|
|
{
|
|
}
|
|
|
|
ImageStyleValue::~ImageStyleValue() = default;
|
|
|
|
u64 ImageStyleValue::active_animation_timer_count(DOM::Document const& document)
|
|
{
|
|
u64 count = 0;
|
|
for (auto const* image_style_value : s_active_animation_timers) {
|
|
if (any_of(image_style_value->m_clients, [&](auto const* client) {
|
|
return client->document() == &document;
|
|
}))
|
|
++count;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
void ImageStyleValue::visit_edges(JS::Cell::Visitor& visitor) const
|
|
{
|
|
Base::visit_edges(visitor);
|
|
// FIXME: visit_edges in non-GC allocated classes is confusing pattern.
|
|
// Consider making StyleValue to be GC allocated instead.
|
|
visitor.visit(m_resource_request);
|
|
visitor.visit(m_style_sheet);
|
|
visitor.visit(m_timer);
|
|
}
|
|
|
|
void ImageStyleValue::load_any_resources(DOM::Document& document)
|
|
{
|
|
if (m_resource_request)
|
|
return;
|
|
m_document = &document;
|
|
|
|
RuleOrDeclaration rule_or_declaration {
|
|
.environment_settings_object = document.relevant_settings_object(),
|
|
.value = RuleOrDeclaration::Rule {
|
|
.parent_style_sheet = m_style_sheet.ptr(),
|
|
}
|
|
};
|
|
|
|
m_resource_request = fetch_an_external_image_for_a_stylesheet(m_url, rule_or_declaration, m_style_sheet ? *m_style_sheet->owning_document() : document);
|
|
|
|
if (m_resource_request) {
|
|
m_resource_request->add_callbacks(
|
|
weak_callback(*this, [](auto& self) {
|
|
if (!self.m_document)
|
|
return;
|
|
|
|
for (auto* client : self.m_clients)
|
|
client->image_style_value_did_update(self);
|
|
|
|
auto image_data = self.m_resource_request->image_data();
|
|
if (image_data->is_animated() && image_data->frame_count() > 1) {
|
|
for (auto* client : self.m_clients) {
|
|
if (auto document = client->document()) {
|
|
self.start_animation_timer_if_needed(*document);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}),
|
|
nullptr);
|
|
}
|
|
}
|
|
|
|
void ImageStyleValue::start_animation_timer_if_needed(DOM::Document& document) const
|
|
{
|
|
if (m_clients.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;
|
|
auto weak_self = make_weak_ptr<ImageStyleValue>();
|
|
timer->on_timeout = GC::create_function(document.heap(), [weak_self] {
|
|
if (weak_self)
|
|
weak_self->animate();
|
|
});
|
|
}
|
|
|
|
m_timer->set_interval(current_frame_duration());
|
|
m_timer->start();
|
|
s_active_animation_timers.set(this);
|
|
}
|
|
|
|
void ImageStyleValue::stop_animation_timer() const
|
|
{
|
|
if (m_timer && m_timer->is_active()) {
|
|
m_timer->stop();
|
|
s_active_animation_timers.remove(this);
|
|
}
|
|
}
|
|
|
|
bool ImageStyleValue::is_animatable() const
|
|
{
|
|
auto image_data = this->image_data();
|
|
if (!image_data || !image_data->is_animated() || image_data->frame_count() <= 1)
|
|
return false;
|
|
|
|
return !animation_has_completed();
|
|
}
|
|
|
|
bool ImageStyleValue::animation_has_completed() const
|
|
{
|
|
auto image_data = this->image_data();
|
|
return image_data && image_data->loop_count() > 0 && m_loops_completed == image_data->loop_count();
|
|
}
|
|
|
|
int ImageStyleValue::current_frame_duration() const
|
|
{
|
|
auto image_data = this->image_data();
|
|
if (!image_data)
|
|
return 0;
|
|
|
|
return image_data->frame_duration(m_current_frame_index);
|
|
}
|
|
|
|
void ImageStyleValue::animate()
|
|
{
|
|
if (!m_resource_request)
|
|
return;
|
|
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();
|
|
}
|
|
|
|
if (on_animate)
|
|
on_animate();
|
|
}
|
|
|
|
bool ImageStyleValue::is_paintable() const
|
|
{
|
|
return image_data();
|
|
}
|
|
|
|
Optional<Gfx::DecodedImageFrame> ImageStyleValue::frame(size_t frame_index, Gfx::IntSize size) const
|
|
{
|
|
if (auto image_data = this->image_data())
|
|
return image_data->frame(frame_index, size);
|
|
return {};
|
|
}
|
|
|
|
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() const
|
|
{
|
|
if (auto image_data = this->image_data())
|
|
return image_data->intrinsic_width();
|
|
return {};
|
|
}
|
|
|
|
Optional<CSSPixels> ImageStyleValue::natural_height() const
|
|
{
|
|
if (auto image_data = this->image_data())
|
|
return image_data->intrinsic_height();
|
|
return {};
|
|
}
|
|
|
|
Optional<CSSPixelFraction> ImageStyleValue::natural_aspect_ratio() const
|
|
{
|
|
if (auto image_data = this->image_data())
|
|
return image_data->intrinsic_aspect_ratio();
|
|
return {};
|
|
}
|
|
|
|
void ImageStyleValue::paint(DisplayListRecordingContext& context, DevicePixelRect const& dest_rect, CSS::ImageRendering image_rendering) const
|
|
{
|
|
auto image_data = this->image_data();
|
|
if (!image_data)
|
|
return;
|
|
|
|
auto dest_int_rect = dest_rect.to_type<int>();
|
|
auto rect = image_data->frame_rect(m_current_frame_index).value_or(dest_int_rect);
|
|
auto scaling_mode = to_gfx_scaling_mode(image_rendering, rect.size(), dest_int_rect.size());
|
|
image_data->paint(context, m_current_frame_index, dest_int_rect, dest_int_rect, scaling_mode);
|
|
}
|
|
|
|
Optional<Gfx::DecodedImageFrame> ImageStyleValue::current_frame(DevicePixelRect const& dest_rect) const
|
|
{
|
|
return frame(m_current_frame_index, dest_rect.size().to_type<int>());
|
|
}
|
|
|
|
GC::Ptr<HTML::DecodedImageData> ImageStyleValue::image_data() const
|
|
{
|
|
if (!m_resource_request)
|
|
return nullptr;
|
|
return m_resource_request->image_data();
|
|
}
|
|
|
|
Optional<Gfx::Color> ImageStyleValue::color_if_single_pixel_bitmap() const
|
|
{
|
|
if (auto decoded_frame = frame(m_current_frame_index); 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_sheet = style_sheet;
|
|
|
|
if (m_style_sheet)
|
|
m_style_sheet->register_pending_image_value(*this);
|
|
}
|
|
|
|
ValueComparingNonnullRefPtr<StyleValue const> ImageStyleValue::absolutized(ComputationContext const&) 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 = [&]() -> Optional<::URL::URL> {
|
|
if (m_style_sheet) {
|
|
return m_style_sheet->base_url()
|
|
.value_or_lazy_evaluated_optional([&]() { return m_style_sheet->location(); })
|
|
.value_or_lazy_evaluated_optional([&]() { return HTML::relevant_settings_object(*m_style_sheet).api_base_url(); });
|
|
}
|
|
|
|
if (m_document)
|
|
return m_document->base_url();
|
|
|
|
return {};
|
|
}();
|
|
|
|
if (base_url.has_value()) {
|
|
if (auto resolved_url = DOMURL::parse(m_url.url(), *base_url); resolved_url.has_value())
|
|
return ImageStyleValue::create(*resolved_url);
|
|
}
|
|
|
|
return *this;
|
|
}
|
|
|
|
void ImageStyleValue::register_client(Client& client) const
|
|
{
|
|
auto result = m_clients.set(&client);
|
|
VERIFY(result == AK::HashSetResult::InsertedNewEntry);
|
|
if (auto document = client.document())
|
|
start_animation_timer_if_needed(*document);
|
|
}
|
|
|
|
void ImageStyleValue::unregister_client(Client& client) const
|
|
{
|
|
auto did_remove = m_clients.remove(&client);
|
|
VERIFY(did_remove);
|
|
|
|
if (m_clients.is_empty())
|
|
stop_animation_timer();
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
}
|