/* * Copyright (c) 2026, Tim Ledbetter * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Web::SVG { GC_DEFINE_ALLOCATOR(SVGPatternElement); SVGPatternElement::SVGPatternElement(DOM::Document& document, DOM::QualifiedName qualified_name) : SVGElement(document, move(qualified_name)) { } void SVGPatternElement::initialize(JS::Realm& realm) { WEB_SET_PROTOTYPE_FOR_INTERFACE(SVGPatternElement); Base::initialize(realm); SVGFitToViewBox::initialize(realm); } void SVGPatternElement::visit_edges(Cell::Visitor& visitor) { Base::visit_edges(visitor); SVGURIReferenceMixin::visit_edges(visitor); SVGFitToViewBox::visit_edges(visitor); } void SVGPatternElement::attribute_changed(FlyString const& name, Optional const& old_value, Optional const& value, Optional const& namespace_) { Base::attribute_changed(name, old_value, value, namespace_); SVGFitToViewBox::attribute_changed(*this, name, value); if (name == AttributeNames::patternUnits) { m_pattern_units = AttributeParser::parse_units(value.value_or(String {})); } else if (name == AttributeNames::patternContentUnits) { m_pattern_content_units = AttributeParser::parse_units(value.value_or(String {})); } else if (name == AttributeNames::patternTransform) { if (auto transform_list = AttributeParser::parse_transform(value.value_or(String {})); transform_list.has_value()) { m_pattern_transform = transform_from_transform_list(*transform_list); } else { m_pattern_transform = {}; } } else if (name == AttributeNames::x) { m_x = AttributeParser::parse_number_percentage(value.value_or(String {})); } else if (name == AttributeNames::y) { m_y = AttributeParser::parse_number_percentage(value.value_or(String {})); } else if (name == AttributeNames::width) { m_width = AttributeParser::parse_number_percentage(value.value_or(String {})); } else if (name == AttributeNames::height) { m_height = AttributeParser::parse_number_percentage(value.value_or(String {})); } } GC::Ptr SVGPatternElement::linked_pattern(HashTable& seen_patterns) const { // FIXME: This can only resolve same-document references. The spec allows cross-document references. auto link = has_attribute(AttributeNames::href) ? get_attribute(AttributeNames::href) : get_attribute("xlink:href"_fly_string); if (!link.has_value() || link->is_empty()) return {}; auto url = document().encoding_parse_url(*link); if (!url.has_value()) return {}; auto id = url->fragment(); if (!id.has_value() || id->is_empty()) return {}; auto element = document().get_element_by_id(id.value()); if (!element) return {}; if (element == this) return {}; auto* pattern = as_if(*element); if (!pattern) return {}; // Detect circular references in the template chain. if (seen_patterns.set(pattern) != AK::HashSetResult::InsertedNewEntry) return {}; return pattern; } GC::Ptr SVGPatternElement::pattern_content_element() const { HashTable seen_patterns; return pattern_content_element_impl(seen_patterns); } GC::Ptr SVGPatternElement::pattern_content_element_impl(HashTable& seen_patterns) const { if (child_element_count() > 0) return this; if (auto pattern = linked_pattern(seen_patterns)) return pattern->pattern_content_element_impl(seen_patterns); return {}; } // https://svgwg.org/svg2-draft/pservers.html#PatternElementPatternUnitsAttribute SVGUnits SVGPatternElement::pattern_units() const { HashTable seen_patterns; return pattern_units_impl(seen_patterns); } SVGUnits SVGPatternElement::pattern_units_impl(HashTable& seen_patterns) const { if (m_pattern_units.has_value()) return *m_pattern_units; if (auto pattern = linked_pattern(seen_patterns)) return pattern->pattern_units_impl(seen_patterns); // Initial value: objectBoundingBox return SVGUnits::ObjectBoundingBox; } // https://svgwg.org/svg2-draft/pservers.html#PatternElementPatternContentUnitsAttribute SVGUnits SVGPatternElement::pattern_content_units() const { HashTable seen_patterns; return pattern_content_units_impl(seen_patterns); } SVGUnits SVGPatternElement::pattern_content_units_impl(HashTable& seen_patterns) const { if (m_pattern_content_units.has_value()) return *m_pattern_content_units; if (auto pattern = linked_pattern(seen_patterns)) return pattern->pattern_content_units_impl(seen_patterns); // Initial value: userSpaceOnUse return SVGUnits::UserSpaceOnUse; } // https://svgwg.org/svg2-draft/pservers.html#PatternElementPatternTransformAttribute Optional SVGPatternElement::pattern_transform() const { HashTable seen_patterns; return pattern_transform_impl(seen_patterns); } Optional SVGPatternElement::pattern_transform_impl(HashTable& seen_patterns) const { if (m_pattern_transform.has_value()) return m_pattern_transform; if (auto pattern = linked_pattern(seen_patterns)) return pattern->pattern_transform_impl(seen_patterns); return {}; } // https://svgwg.org/svg2-draft/pservers.html#PatternElementXAttribute NumberPercentage SVGPatternElement::pattern_x() const { HashTable seen_patterns; return pattern_x_impl(seen_patterns); } NumberPercentage SVGPatternElement::pattern_x_impl(HashTable& seen_patterns) const { if (m_x.has_value()) return *m_x; if (auto pattern = linked_pattern(seen_patterns)) return pattern->pattern_x_impl(seen_patterns); return NumberPercentage::create_number(0); } // https://svgwg.org/svg2-draft/pservers.html#PatternElementYAttribute NumberPercentage SVGPatternElement::pattern_y() const { HashTable seen_patterns; return pattern_y_impl(seen_patterns); } NumberPercentage SVGPatternElement::pattern_y_impl(HashTable& seen_patterns) const { if (m_y.has_value()) return *m_y; if (auto pattern = linked_pattern(seen_patterns)) return pattern->pattern_y_impl(seen_patterns); return NumberPercentage::create_number(0); } // https://svgwg.org/svg2-draft/pservers.html#PatternElementWidthAttribute NumberPercentage SVGPatternElement::pattern_width() const { HashTable seen_patterns; return pattern_width_impl(seen_patterns); } NumberPercentage SVGPatternElement::pattern_width_impl(HashTable& seen_patterns) const { if (m_width.has_value()) return *m_width; if (auto pattern = linked_pattern(seen_patterns)) return pattern->pattern_width_impl(seen_patterns); return NumberPercentage::create_number(0); } // https://svgwg.org/svg2-draft/pservers.html#PatternElementHeightAttribute NumberPercentage SVGPatternElement::pattern_height() const { HashTable seen_patterns; return pattern_height_impl(seen_patterns); } NumberPercentage SVGPatternElement::pattern_height_impl(HashTable& seen_patterns) const { if (m_height.has_value()) return *m_height; if (auto pattern = linked_pattern(seen_patterns)) return pattern->pattern_height_impl(seen_patterns); return NumberPercentage::create_number(0); } Optional SVGPatternElement::to_gfx_paint_style(SVGPaintContext const& paint_context, DisplayListRecordingContext& recording_context, Layout::Node const& target_layout_node) const { auto content_element = pattern_content_element(); if (!content_element) return {}; Layout::SVGPatternBox const* pattern_box = nullptr; target_layout_node.for_each_child_of_type([&](auto const& candidate) { if (&candidate.dom_node() == content_element.ptr()) { pattern_box = &candidate; return IterationDecision::Break; } return IterationDecision::Continue; }); if (!pattern_box) return {}; auto* pattern_paintable = pattern_box->paintable_box(); if (!pattern_paintable) return {}; float tile_x = 0; float tile_y = 0; float tile_width = 0; float tile_height = 0; if (pattern_units() == SVGUnits::ObjectBoundingBox) { // For objectBoundingBox, values are fractions of the bounding box. // NumberPercentage::value() already normalizes percentages to 0-1 range. auto const& bbox = paint_context.path_bounding_box; tile_x = pattern_x().value() * bbox.width() + bbox.x(); tile_y = pattern_y().value() * bbox.height() + bbox.y(); tile_width = pattern_width().value() * bbox.width(); tile_height = pattern_height().value() * bbox.height(); } else { // For userSpaceOnUse, resolve percentages relative to the viewport. auto const& viewport = paint_context.viewport; tile_x = pattern_x().resolve_relative_to(viewport.width()); tile_y = pattern_y().resolve_relative_to(viewport.height()); tile_width = pattern_width().resolve_relative_to(viewport.width()); tile_height = pattern_height().resolve_relative_to(viewport.height()); } if (tile_width <= 0 || tile_height <= 0) return {}; auto tile_rect = paint_context.paint_transform.map(Gfx::FloatRect { tile_x, tile_y, tile_width, tile_height }); if (tile_rect.is_empty()) return {}; auto const* svg_node = target_layout_node.first_ancestor_of_type(); if (!svg_node || !svg_node->paintable_box()) return {}; auto svg_element_rect = svg_node->paintable_box()->absolute_rect(); auto svg_offset = recording_context.rounded_device_point(svg_element_rect.location()).to_type().to_type(); tile_rect.translate_by(svg_offset); auto display_list = Painting::DisplayList::create(Painting::AccumulatedVisualContextTree::create()); Painting::DisplayListRecorder display_list_recorder(*display_list); auto content_origin = paint_context.paint_transform.map(Gfx::FloatPoint { 0, 0 }) + svg_offset; display_list_recorder.translate(-Gfx::IntPoint(content_origin.to_type())); auto paint_context_copy = recording_context.clone(display_list_recorder); Gfx::AffineTransform target_svg_transform; if (auto const* svg_graphics_paintable = as_if(*target_layout_node.first_paintable())) target_svg_transform = svg_graphics_paintable->computed_transforms().svg_transform(); paint_context_copy.set_svg_transform(target_svg_transform); Painting::StackingContext::paint_svg(paint_context_copy, *pattern_paintable, Painting::PaintPhase::Foreground); Optional user_space_pattern_transform; auto css_transformations = computed_properties()->transformations(); if (!css_transformations.is_empty()) { auto matrix = Gfx::FloatMatrix4x4::identity(); bool transform_valid = true; for (auto const& css_transform : css_transformations) { auto result = css_transform->to_matrix(*pattern_paintable); if (result.is_error()) { transform_valid = false; break; } matrix = matrix * result.release_value(); } if (transform_valid) user_space_pattern_transform = extract_2d_affine_transform(matrix); } else { user_space_pattern_transform = pattern_transform(); } Optional device_pattern_transform; if (user_space_pattern_transform.has_value()) { if (!user_space_pattern_transform->inverse().has_value()) return {}; // patternTransform is defined in user space, but the tile rect and shader operate in device pixel space. // Convert by conjugating with paint_transform. if (auto inv = paint_context.paint_transform.inverse(); inv.has_value()) { auto transform = paint_context.paint_transform; device_pattern_transform = transform.multiply(*user_space_pattern_transform).multiply(*inv); } } return Painting::SVGPatternPaintStyle::create(display_list, tile_rect, device_pattern_transform); } // https://svgwg.org/svg2-draft/pservers.html#PatternElementXAttribute GC::Ref SVGPatternElement::x() const { // FIXME: Populate the unit type when it is parsed (0 here is "unknown"). // FIXME: Create a proper animated value when animations are supported. auto base_length = SVGLength::create(realm(), 0, m_x.value_or(NumberPercentage::create_number(0)).value(), SVGLength::ReadOnly::No); auto anim_length = SVGLength::create(realm(), 0, m_x.value_or(NumberPercentage::create_number(0)).value(), SVGLength::ReadOnly::Yes); return SVGAnimatedLength::create(realm(), base_length, anim_length); } // https://svgwg.org/svg2-draft/pservers.html#PatternElementYAttribute GC::Ref SVGPatternElement::y() const { // FIXME: Populate the unit type when it is parsed (0 here is "unknown"). // FIXME: Create a proper animated value when animations are supported. auto base_length = SVGLength::create(realm(), 0, m_y.value_or(NumberPercentage::create_number(0)).value(), SVGLength::ReadOnly::No); auto anim_length = SVGLength::create(realm(), 0, m_y.value_or(NumberPercentage::create_number(0)).value(), SVGLength::ReadOnly::Yes); return SVGAnimatedLength::create(realm(), base_length, anim_length); } // https://svgwg.org/svg2-draft/pservers.html#PatternElementWidthAttribute GC::Ref SVGPatternElement::width() const { // FIXME: Populate the unit type when it is parsed (0 here is "unknown"). // FIXME: Create a proper animated value when animations are supported. auto base_length = SVGLength::create(realm(), 0, m_width.value_or(NumberPercentage::create_number(0)).value(), SVGLength::ReadOnly::No); auto anim_length = SVGLength::create(realm(), 0, m_width.value_or(NumberPercentage::create_number(0)).value(), SVGLength::ReadOnly::Yes); return SVGAnimatedLength::create(realm(), base_length, anim_length); } // https://svgwg.org/svg2-draft/pservers.html#PatternElementHeightAttribute GC::Ref SVGPatternElement::height() const { // FIXME: Populate the unit type when it is parsed (0 here is "unknown"). // FIXME: Create a proper animated value when animations are supported. auto base_length = SVGLength::create(realm(), 0, m_height.value_or(NumberPercentage::create_number(0)).value(), SVGLength::ReadOnly::No); auto anim_length = SVGLength::create(realm(), 0, m_height.value_or(NumberPercentage::create_number(0)).value(), SVGLength::ReadOnly::Yes); return SVGAnimatedLength::create(realm(), base_length, anim_length); } }