DecodedImageData::paint() used to take both a destination and a clip rectangle even though most callers passed the same value. SVG image painting used that API to wrap every nested SVG display list in save/add-clip/restore, which put an unbounded command in front of the bounded nested-list command and made offscreen SVG image content harder to cull. Move clipping to ImagePaintable, where the object-fit destination can be compared with the replaced element box. CSS image and marker painting continue to draw into their destination rect, while repeated background images keep their explicit tile clip. The scaled decoded image display-list command now stores only its destination rect and uses that as its bounds; playback still clips decoded images to that rect so bitmap rendering stays unchanged.
322 lines
13 KiB
C++
322 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 {},
|
|
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;
|
|
|
|
switch (m_page_client->display_list_player_type()) {
|
|
case DisplayListPlayerType::SkiaGPUIfAvailable:
|
|
case DisplayListPlayerType::SkiaCPU: {
|
|
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);
|
|
break;
|
|
}
|
|
default:
|
|
VERIFY_NOT_REACHED();
|
|
}
|
|
|
|
m_cached_rendered_surfaces.set(size, *surface);
|
|
return surface;
|
|
}
|
|
|
|
Optional<Gfx::DecodedImageFrame> SVGDecodedImageData::frame(size_t, 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<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);
|
|
}
|
|
|
|
Optional<Gfx::IntRect> SVGDecodedImageData::frame_rect(size_t) const
|
|
{
|
|
return {};
|
|
}
|
|
|
|
RefPtr<Gfx::PaintingSurface> SVGDecodedImageData::surface(size_t, Gfx::IntSize size) const
|
|
{
|
|
return render_to_surface(size);
|
|
}
|
|
|
|
void SVGDecodedImageData::paint(DisplayListRecordingContext& context, size_t, Gfx::IntRect dst_rect, Gfx::ScalingMode) 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);
|
|
}
|
|
|
|
}
|