script/dom/svg/
svgsvgelement.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use base64::Engine as _;
6use cssparser::{Parser, ParserInput};
7use dom_struct::dom_struct;
8use html5ever::{LocalName, Prefix, local_name, ns};
9use js::rust::HandleObject;
10use layout_api::SVGElementData;
11use servo_url::ServoUrl;
12use style::attr::AttrValue;
13use style::parser::ParserContext;
14use style::stylesheets::Origin;
15use style::values::specified::Length;
16use style_traits::ParsingMode;
17use uuid::Uuid;
18use xml5ever::serialize::TraversalScope;
19
20use crate::dom::attr::Attr;
21use crate::dom::bindings::cell::DomRefCell;
22use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
23use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
24use crate::dom::bindings::inheritance::Castable;
25use crate::dom::bindings::root::{DomRoot, LayoutDom};
26use crate::dom::bindings::str::DOMString;
27use crate::dom::document::Document;
28use crate::dom::element::{AttributeMutation, Element, LayoutElementHelpers};
29use crate::dom::node::{
30    ChildrenMutation, CloneChildrenFlag, Node, NodeDamage, NodeTraits, ShadowIncluding,
31    UnbindContext,
32};
33use crate::dom::svg::svggraphicselement::SVGGraphicsElement;
34use crate::dom::virtualmethods::VirtualMethods;
35use crate::script_runtime::CanGc;
36
37#[dom_struct]
38pub(crate) struct SVGSVGElement {
39    svggraphicselement: SVGGraphicsElement,
40    uuid: String,
41    // The XML source of subtree rooted at this SVG element, serialized into
42    // a base64 encoded `data:` url. This is cached to avoid recomputation
43    // on each layout and must be invalidated when the subtree changes.
44    #[no_trace]
45    cached_serialized_data_url: DomRefCell<Option<Result<ServoUrl, ()>>>,
46}
47
48impl SVGSVGElement {
49    fn new_inherited(
50        local_name: LocalName,
51        prefix: Option<Prefix>,
52        document: &Document,
53    ) -> SVGSVGElement {
54        SVGSVGElement {
55            svggraphicselement: SVGGraphicsElement::new_inherited(local_name, prefix, document),
56            uuid: Uuid::new_v4().to_string(),
57            cached_serialized_data_url: Default::default(),
58        }
59    }
60
61    #[cfg_attr(crown, allow(crown::unrooted_must_root))]
62    pub(crate) fn new(
63        local_name: LocalName,
64        prefix: Option<Prefix>,
65        document: &Document,
66        proto: Option<HandleObject>,
67        can_gc: CanGc,
68    ) -> DomRoot<SVGSVGElement> {
69        Node::reflect_node_with_proto(
70            Box::new(SVGSVGElement::new_inherited(local_name, prefix, document)),
71            document,
72            proto,
73            can_gc,
74        )
75    }
76
77    pub(crate) fn serialize_and_cache_subtree(&self) {
78        let can_gc = CanGc::note();
79        let cloned_nodes = self.process_use_elements(can_gc);
80
81        let serialize_result = self
82            .upcast::<Node>()
83            .xml_serialize(TraversalScope::IncludeNode);
84
85        self.cleanup_cloned_nodes(&cloned_nodes, can_gc);
86
87        let Ok(xml_source) = serialize_result else {
88            *self.cached_serialized_data_url.borrow_mut() = Some(Err(()));
89            return;
90        };
91
92        let xml_source: String = xml_source.into();
93        let base64_encoded_source = base64::engine::general_purpose::STANDARD.encode(xml_source);
94        let data_url = format!("data:image/svg+xml;base64,{}", base64_encoded_source);
95        match ServoUrl::parse(&data_url) {
96            Ok(url) => *self.cached_serialized_data_url.borrow_mut() = Some(Ok(url)),
97            Err(error) => error!("Unable to parse serialized SVG data url: {error}"),
98        };
99    }
100
101    fn process_use_elements(&self, can_gc: CanGc) -> Vec<DomRoot<Node>> {
102        let mut cloned_nodes = Vec::new();
103        let root_node = self.upcast::<Node>();
104
105        for node in root_node.traverse_preorder(ShadowIncluding::No) {
106            if let Some(element) = node.downcast::<Element>() {
107                if element.local_name() == &local_name!("use") {
108                    if let Some(cloned) = self.process_single_use_element(element, can_gc) {
109                        cloned_nodes.push(cloned);
110                    }
111                }
112            }
113        }
114
115        cloned_nodes
116    }
117
118    fn process_single_use_element(
119        &self,
120        use_element: &Element,
121        can_gc: CanGc,
122    ) -> Option<DomRoot<Node>> {
123        let href = use_element.get_string_attribute(&local_name!("href"));
124        let href_view = href.str();
125        let id_str = href_view.strip_prefix("#")?;
126        let id = DOMString::from(id_str);
127        let document = self.upcast::<Node>().owner_doc();
128        let referenced_element = document.GetElementById(id)?;
129        let referenced_node = referenced_element.upcast::<Node>();
130        let has_svg_ancestor = referenced_node
131            .inclusive_ancestors(ShadowIncluding::No)
132            .any(|ancestor| ancestor.is::<SVGSVGElement>());
133        if !has_svg_ancestor {
134            return None;
135        }
136        let cloned_node = Node::clone(
137            referenced_node,
138            None,
139            CloneChildrenFlag::CloneChildren,
140            None,
141            can_gc,
142        );
143        let root_node = self.upcast::<Node>();
144        let _ = root_node.AppendChild(&cloned_node, can_gc);
145
146        Some(cloned_node)
147    }
148
149    fn cleanup_cloned_nodes(&self, cloned_nodes: &[DomRoot<Node>], can_gc: CanGc) {
150        if cloned_nodes.is_empty() {
151            return;
152        }
153        let root_node = self.upcast::<Node>();
154
155        for cloned_node in cloned_nodes {
156            let _ = root_node.RemoveChild(cloned_node, can_gc);
157        }
158    }
159
160    fn invalidate_cached_serialized_subtree(&self) {
161        *self.cached_serialized_data_url.borrow_mut() = None;
162        self.upcast::<Node>().dirty(NodeDamage::Other);
163    }
164}
165
166pub(crate) trait LayoutSVGSVGElementHelpers<'dom> {
167    fn data(self) -> SVGElementData<'dom>;
168}
169
170impl<'dom> LayoutSVGSVGElementHelpers<'dom> for LayoutDom<'dom, SVGSVGElement> {
171    #[expect(unsafe_code)]
172    fn data(self) -> SVGElementData<'dom> {
173        let svg_id = self.unsafe_get().uuid.clone();
174        let element = self.upcast::<Element>();
175        let width = element.get_attr_for_layout(&ns!(), &local_name!("width"));
176        let height = element.get_attr_for_layout(&ns!(), &local_name!("height"));
177        let view_box = element.get_attr_for_layout(&ns!(), &local_name!("viewBox"));
178        SVGElementData {
179            source: unsafe {
180                self.unsafe_get()
181                    .cached_serialized_data_url
182                    .borrow_for_layout()
183                    .clone()
184            },
185            width,
186            height,
187            view_box,
188            svg_id,
189        }
190    }
191}
192
193impl VirtualMethods for SVGSVGElement {
194    fn super_type(&self) -> Option<&dyn VirtualMethods> {
195        Some(self.upcast::<SVGGraphicsElement>() as &dyn VirtualMethods)
196    }
197
198    fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation, can_gc: CanGc) {
199        self.super_type()
200            .unwrap()
201            .attribute_mutated(attr, mutation, can_gc);
202
203        self.invalidate_cached_serialized_subtree();
204    }
205
206    fn attribute_affects_presentational_hints(&self, attr: &Attr) -> bool {
207        match attr.local_name() {
208            &local_name!("width") | &local_name!("height") => true,
209            _ => self
210                .super_type()
211                .unwrap()
212                .attribute_affects_presentational_hints(attr),
213        }
214    }
215
216    fn parse_plain_attribute(&self, name: &LocalName, value: DOMString) -> AttrValue {
217        match *name {
218            local_name!("width") | local_name!("height") => {
219                let value = &value.str();
220                let parser_input = &mut ParserInput::new(value);
221                let parser = &mut Parser::new(parser_input);
222                let doc = self.owner_document();
223                let url = doc.url().into_url().into();
224                let context = ParserContext::new(
225                    Origin::Author,
226                    &url,
227                    None,
228                    ParsingMode::ALLOW_UNITLESS_LENGTH,
229                    doc.quirks_mode(),
230                    /* namespaces = */ Default::default(),
231                    None,
232                    None,
233                );
234                let val = Length::parse_quirky(
235                    &context,
236                    parser,
237                    style::values::specified::AllowQuirks::Always,
238                );
239                AttrValue::Length(value.to_string(), val.ok())
240            },
241            _ => self
242                .super_type()
243                .unwrap()
244                .parse_plain_attribute(name, value),
245        }
246    }
247
248    fn children_changed(&self, mutation: &ChildrenMutation, can_gc: CanGc) {
249        if let Some(super_type) = self.super_type() {
250            super_type.children_changed(mutation, can_gc);
251        }
252
253        self.invalidate_cached_serialized_subtree();
254    }
255
256    fn unbind_from_tree(&self, context: &UnbindContext<'_>, can_gc: CanGc) {
257        if let Some(s) = self.super_type() {
258            s.unbind_from_tree(context, can_gc);
259        }
260        let owner_window = self.owner_window();
261        self.owner_window()
262            .image_cache()
263            .evict_rasterized_image(&self.uuid);
264        let data_url = self.cached_serialized_data_url.borrow().clone();
265        if let Some(Ok(url)) = data_url {
266            owner_window.layout_mut().remove_cached_image(&url);
267            owner_window.image_cache().evict_completed_image(
268                &url,
269                owner_window.origin().immutable(),
270                &None,
271            );
272        }
273        self.invalidate_cached_serialized_subtree();
274    }
275}