/* * Copyright (c) 2023-2025, Andreas Kling * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Web::SVG { GC_DEFINE_ALLOCATOR(SVGDecodedImageData); GC_DEFINE_ALLOCATOR(SVGDecodedImageData::SVGPageClient); ErrorOr> SVGDecodedImageData::create(JS::Realm& realm, GC::Ref 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 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(OptionalNone {}, navigable, nullptr, response, nullptr, nullptr, HTML::OpenerPolicyEnforcementResult { .url = url, .origin = origin, .opener_policy = HTML::OpenerPolicy {} }, nullptr, origin, navigable->heap().allocate(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::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 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(); if (!svg_root) { dbgln("SVGDecodedImageData: Invalid SVG input (no SVGSVGElement found)"); return Error::from_string_literal("SVGDecodedImageData: Invalid SVG input"); } return realm.create(page, page_client, document, *svg_root); } SVGDecodedImageData::SVGDecodedImageData(GC::Ref page, GC::Ref page_client, GC::Ref document, GC::Ref 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 pixel_size = static_cast(surface_size.width()); pixel_size *= static_cast(surface_size.height()); pixel_size *= sizeof(u32); if (pixel_size.has_overflow()) return NumericLimits::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 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()); 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 { *display_list }, move(visual_context_tree) }); prune_cached_display_list_resources(); return display_list_resource; } RefPtr 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 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 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 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 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 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); } }