ladybird/Libraries/LibWeb/SVG/SVGDecodedImageData.cpp
Callum Law 8ebdaeab69 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-18 10:44:25 +02:00

312 lines
13 KiB
C++

/*
* Copyright (c) 2023-2025, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Checked.h>
#include <AK/NumericLimits.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/DecodedImageFrame.h>
#include <LibGfx/PaintingSurface.h>
#include <LibJS/Runtime/ExternalMemory.h>
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Responses.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/DocumentState.h>
#include <LibWeb/HTML/Navigable.h>
#include <LibWeb/HTML/NavigationParams.h>
#include <LibWeb/HTML/Parser/HTMLParser.h>
#include <LibWeb/HTML/TraversableNavigable.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/HTML/WindowProxy.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Painting/DisplayListPlayerSkia.h>
#include <LibWeb/Painting/DisplayListRecordingContext.h>
#include <LibWeb/Painting/DisplayListResourceStorage.h>
#include <LibWeb/Painting/ViewportPaintable.h>
#include <LibWeb/SVG/SVGDecodedImageData.h>
#include <LibWeb/SVG/SVGSVGElement.h>
#include <LibWeb/XML/XMLDocumentBuilder.h>
#include <LibXML/Parser/Parser.h>
namespace Web::SVG {
GC_DEFINE_ALLOCATOR(SVGDecodedImageData);
GC_DEFINE_ALLOCATOR(SVGDecodedImageData::SVGPageClient);
ErrorOr<GC::Ref<SVGDecodedImageData>> SVGDecodedImageData::create(JS::Realm& realm, GC::Ref<Page> host_page, URL::URL const& url, ReadonlyBytes data)
{
auto page_client = SVGPageClient::create(Bindings::main_thread_vm(), host_page);
auto page = Page::create(Bindings::main_thread_vm(), *page_client);
page->set_is_scripting_enabled(false);
page_client->m_svg_page = page.ptr();
page->set_top_level_traversable(Web::HTML::TraversableNavigable::create_a_new_top_level_traversable(*page, nullptr, {}));
GC::Ref<HTML::Navigable> navigable = page->top_level_traversable();
auto response = Fetch::Infrastructure::Response::create(navigable->vm());
response->url_list().append(url);
auto origin = URL::Origin::create_opaque();
auto navigation_params = navigable->heap().allocate<HTML::NavigationParams>(OptionalNone {},
navigable,
nullptr,
response,
nullptr,
nullptr,
HTML::OpenerPolicyEnforcementResult { .url = url, .origin = origin, .opener_policy = HTML::OpenerPolicy {} },
nullptr,
origin,
navigable->heap().allocate<HTML::PolicyContainer>(realm.heap()),
HTML::SandboxingFlagSet {},
ReferrerPolicy::ReferrerPolicy::EmptyString,
HTML::OpenerPolicy {},
OptionalNone {},
HTML::UserNavigationInvolvement::None);
// FIXME: Use Navigable::navigate() instead of manually replacing the navigable's document.
auto document = MUST(DOM::Document::create_and_initialize(DOM::Document::Type::XML, "image/svg+xml"_string, navigation_params));
navigable->set_ongoing_navigation({});
navigable->active_document()->destroy();
navigable->set_active_document(document);
auto& window = as<HTML::Window>(HTML::relevant_global_object(document));
document->browsing_context()->window_proxy()->set_window(window);
XML::Parser parser(data, { .resolve_named_html_entity = resolve_named_html_entity });
XMLDocumentBuilder builder { document, XMLScriptingSupport::Disabled };
auto result = parser.parse_with_listener(builder);
if (result.is_error())
dbgln("SVGDecodedImageData: Failed to parse SVG: {}", result.error());
// Mark the document as completely loaded so that <use> elements
// (which defer cloning until the document is complete) resolve
// forward references to elements parsed after them.
document->completely_finish_loading();
auto* svg_root = document->first_child_of_type<SVG::SVGSVGElement>();
if (!svg_root) {
dbgln("SVGDecodedImageData: Invalid SVG input (no SVGSVGElement found)");
return Error::from_string_literal("SVGDecodedImageData: Invalid SVG input");
}
return realm.create<SVGDecodedImageData>(page, page_client, document, *svg_root);
}
SVGDecodedImageData::SVGDecodedImageData(GC::Ref<Page> page, GC::Ref<SVGPageClient> page_client, GC::Ref<DOM::Document> document, GC::Ref<SVG::SVGSVGElement> root_element)
: m_page(page)
, m_page_client(page_client)
, m_document(document)
, m_root_element(root_element)
{
}
SVGDecodedImageData::~SVGDecodedImageData() = default;
void SVGDecodedImageData::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_page);
visitor.visit(m_document);
visitor.visit(m_page_client);
visitor.visit(m_root_element);
}
static size_t surface_external_memory_size(Gfx::PaintingSurface const& surface)
{
auto surface_size = surface.size();
if (surface_size.is_empty())
return 0;
Checked<size_t> pixel_size = static_cast<size_t>(surface_size.width());
pixel_size *= static_cast<size_t>(surface_size.height());
pixel_size *= sizeof(u32);
if (pixel_size.has_overflow())
return NumericLimits<size_t>::max();
return pixel_size.value();
}
size_t SVGDecodedImageData::external_memory_size() const
{
size_t size = Base::external_memory_size();
size = JS::saturating_add_external_memory_size(size, JS::hash_map_external_memory_size(m_cached_rendered_frames));
for (auto const& cached_frame : m_cached_rendered_frames)
size = JS::saturating_add_external_memory_size(size, cached_frame.value.bitmap().data_size());
size = JS::saturating_add_external_memory_size(size, JS::hash_map_external_memory_size(m_cached_rendered_surfaces));
for (auto const& cached_surface : m_cached_rendered_surfaces)
size = JS::saturating_add_external_memory_size(size, surface_external_memory_size(*cached_surface.value));
return size;
}
static void copy_referenced_resources_to(
Painting::DisplayListResourceStorage& destination,
Painting::DisplayListResourceStorage const& source,
Painting::DisplayListResourceSet const& referenced_resources)
{
Painting::DisplayListResourceSet empty_resource_set;
destination.apply_transaction(source.create_transaction(empty_resource_set, referenced_resources));
}
void SVGDecodedImageData::prune_cached_display_list_resources() const
{
auto& resource_storage = m_document->navigable()->display_list_resource_storage();
Painting::DisplayListResourceSet retained_resources;
for (auto const& cached_display_list : m_cached_display_lists) {
retained_resources.include(
resource_storage.collect_referenced_resources(*cached_display_list.value.display_list));
}
resource_storage.retain_only(retained_resources);
}
Optional<Painting::DisplayListResource> SVGDecodedImageData::record_display_list(Gfx::IntSize size, Painting::DisplayListResourceStorage& destination_resource_storage) const
{
auto& resource_storage = m_document->navigable()->display_list_resource_storage();
if (auto it = m_cached_display_lists.find(size); it != m_cached_display_lists.end()) {
auto referenced_resources = resource_storage.collect_referenced_resources(*it->value.display_list);
copy_referenced_resources_to(destination_resource_storage, resource_storage, referenced_resources);
return Painting::DisplayListResource { *it->value.display_list, it->value.visual_context_tree };
}
// FIXME: Evict least used entries.
if (m_cached_display_lists.size() > 10) {
m_cached_display_lists.remove(m_cached_display_lists.begin());
prune_cached_display_list_resources();
}
m_document->navigable()->set_viewport_size(size.to_type<CSSPixels>());
m_document->update_layout(DOM::UpdateLayoutReason::SVGDecodedImageDataRender);
auto display_list = m_document->record_display_list({}, resource_storage);
if (!display_list)
return {};
auto referenced_resources = resource_storage.collect_referenced_resources(*display_list);
copy_referenced_resources_to(destination_resource_storage, resource_storage, referenced_resources);
auto document_paintable = m_document->paintable();
VERIFY(document_paintable);
auto visual_context_tree = document_paintable->visual_context_tree();
auto display_list_resource = Painting::DisplayListResource { *display_list, visual_context_tree };
m_cached_display_lists.set(size, CachedDisplayList { NonnullRefPtr<Painting::DisplayList> { *display_list }, move(visual_context_tree) });
prune_cached_display_list_resources();
return display_list_resource;
}
RefPtr<Gfx::PaintingSurface> SVGDecodedImageData::render_to_surface(Gfx::IntSize size) const
{
VERIFY(m_document->navigable());
if (size.is_empty())
return {};
if (auto it = m_cached_rendered_surfaces.find(size); it != m_cached_rendered_surfaces.end())
return it->value;
// Prevent the cache from growing too big.
// FIXME: Evict least used entries.
if (m_cached_rendered_surfaces.size() > 10)
m_cached_rendered_surfaces.remove(m_cached_rendered_surfaces.begin());
auto surface = Gfx::PaintingSurface::create_with_size(size, Gfx::BitmapFormat::BGRA8888, Gfx::AlphaType::Premultiplied);
Painting::DisplayListResourceStorage resource_storage;
auto display_list = record_display_list(size, resource_storage);
if (!display_list.has_value())
return nullptr;
Painting::DisplayListPlayerSkia display_list_player;
display_list_player.execute(*display_list->display_list, display_list->visual_context_tree, resource_storage, {}, surface);
display_list_player.flush(*surface);
m_cached_rendered_surfaces.set(size, *surface);
return surface;
}
Optional<Gfx::DecodedImageFrame> SVGDecodedImageData::current_frame(Gfx::IntSize size) const
{
if (size.is_empty())
return {};
if (auto it = m_cached_rendered_frames.find(size); it != m_cached_rendered_frames.end())
return it->value;
// Prevent the cache from growing too big.
// FIXME: Evict least used entries.
if (m_cached_rendered_frames.size() > 10)
m_cached_rendered_frames.remove(m_cached_rendered_frames.begin());
auto decoded_frame = Gfx::DecodedImageFrame { *render_to_surface(size)->snapshot_bitmap() };
m_cached_rendered_frames.set(size, decoded_frame);
return decoded_frame;
}
Optional<Gfx::DecodedImageFrame> SVGDecodedImageData::default_frame(Gfx::IntSize size) const
{
// FIXME: Implement this properly once we support animated SVGs, potentially by creating a temporary internal
// document which has animations disabled.
return current_frame(size);
}
Optional<CSSPixels> SVGDecodedImageData::intrinsic_width() const
{
// https://www.w3.org/TR/SVG2/coords.html#SizingSVGInCSS
m_document->update_style();
auto const root_element_style = m_root_element->computed_properties();
VERIFY(root_element_style);
auto const& width_value = root_element_style->size_value(CSS::PropertyID::Width);
if (width_value.is_length() && width_value.length().is_absolute())
return width_value.length().absolute_length_to_px();
return {};
}
Optional<CSSPixels> SVGDecodedImageData::intrinsic_height() const
{
// https://www.w3.org/TR/SVG2/coords.html#SizingSVGInCSS
m_document->update_style();
auto const root_element_style = m_root_element->computed_properties();
VERIFY(root_element_style);
auto const& height_value = root_element_style->size_value(CSS::PropertyID::Height);
if (height_value.is_length() && height_value.length().is_absolute())
return height_value.length().absolute_length_to_px();
return {};
}
Optional<CSSPixelFraction> SVGDecodedImageData::intrinsic_aspect_ratio() const
{
// https://www.w3.org/TR/SVG2/coords.html#SizingSVGInCSS
auto width = intrinsic_width();
auto height = intrinsic_height();
if (width.has_value() && height.has_value() && *width > 0 && *height > 0)
return *width / *height;
if (auto const& viewbox = m_root_element->view_box(); viewbox.has_value()) {
auto viewbox_width = CSSPixels::nearest_value_for(viewbox->width);
if (viewbox_width == 0)
return {};
auto viewbox_height = CSSPixels::nearest_value_for(viewbox->height);
if (viewbox_height == 0)
return {};
return viewbox_width / viewbox_height;
}
return {};
}
void SVGDecodedImageData::SVGPageClient::visit_edges(Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_host_page);
visitor.visit(m_svg_page);
}
void SVGDecodedImageData::paint(DisplayListRecordingContext& context, Gfx::IntRect dst_rect, CSS::ImageRendering) const
{
auto display_list = record_display_list(dst_rect.size(), context.display_list_recorder().resource_storage());
if (!display_list.has_value())
return;
context.display_list_recorder().paint_nested_display_list(*display_list, dst_rect);
}
}