Skip to content

Commit 5df06da

Browse files
committed
script: support inline SVG by serializing the subtree
This patch adds support for rendering static inline SVG documents in the DOM tree by serializing the SVGElement's subtree and leveraging the existing resvg based SVG stack for rendering. Serialiing the subtree is necessary as resvg's tree representation (roxmltree) is immutable, so we can't construct the tree incrementally. Few other design choices here: 1. The `SVGSVGElement` is now treated as a replaced element and the layout code is responsible for plumbing the serialized SVG source (encoded as a base64 data: url) into the image cache, much like how background images are handled. 2. The serialization is done on the script thread after an initial layout pass. This is necessary because the serialization code asserts that it is invoked from script thread i.e we can't call it from layout workers. 3. The serialized SVG data: url is cached to avoid recomputing it on subsequent layouts. The cache is invalidated when the SVGSVGElement's subtree is mutated. The original SVGSVGElement code was behind the `dom_svg_enabled` pref. This patch doesn't change that, so the inline SVG rendering will need the pref to be enabled as well. Given this is patch still doesn't add "native" SVG support, it makes sense to still keep it behind the pref, but this can be discussed. Signed-off-by: Mukilan Thiyagarajan <[email protected]>
1 parent ab78a76 commit 5df06da

File tree

10 files changed

+157
-43
lines changed

10 files changed

+157
-43
lines changed

components/layout/context.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use std::sync::Arc;
66

7+
use embedder_traits::UntrustedNodeAddress;
78
use euclid::Size2D;
89
use fnv::FnvHashMap;
910
use fonts::FontContext;
@@ -19,7 +20,7 @@ use parking_lot::{Mutex, RwLock};
1920
use pixels::RasterImage;
2021
use servo_url::{ImmutableOrigin, ServoUrl};
2122
use style::context::SharedStyleContext;
22-
use style::dom::OpaqueNode;
23+
use style::dom::{OpaqueNode, TNode};
2324
use style::values::computed::image::{Gradient, Image};
2425
use webrender_api::units::{DeviceIntSize, DeviceSize};
2526

@@ -86,6 +87,13 @@ pub(crate) struct ImageResolver {
8687
/// size determined by layout. This will be shared with the script thread.
8788
pub pending_rasterization_images: Mutex<Vec<PendingRasterizationImage>>,
8889

90+
/// A list of `SVGSVGElement`s encountered during layout that are not
91+
/// serialized yet. This is needed to support inline SVGs as they are treated
92+
/// as replaced elements and the layout is responsible for triggering the
93+
/// network load for the corresponding serialzed data: urls (similar to
94+
/// background images).
95+
pub pending_svg_elements_for_serialization: Mutex<Vec<UntrustedNodeAddress>>,
96+
8997
/// A shared reference to script's map of DOM nodes with animated images. This is used
9098
/// to manage image animations in script and inform the script about newly animating
9199
/// nodes.
@@ -105,6 +113,11 @@ impl Drop for ImageResolver {
105113
if !std::thread::panicking() {
106114
assert!(self.pending_images.lock().is_empty());
107115
assert!(self.pending_rasterization_images.lock().is_empty());
116+
assert!(
117+
self.pending_svg_elements_for_serialization
118+
.lock()
119+
.is_empty()
120+
);
108121
}
109122
}
110123
}
@@ -174,7 +187,7 @@ impl ImageResolver {
174187
}
175188
}
176189

177-
fn get_cached_image_for_url(
190+
pub(crate) fn get_cached_image_for_url(
178191
&self,
179192
node: OpaqueNode,
180193
url: ServoUrl,
@@ -234,6 +247,15 @@ impl ImageResolver {
234247
result
235248
}
236249

250+
pub(crate) fn queue_svg_element_for_serialization(
251+
&self,
252+
element: script::layout_dom::ServoLayoutNode<'_>,
253+
) {
254+
self.pending_svg_elements_for_serialization
255+
.lock()
256+
.push(element.opaque().into())
257+
}
258+
237259
pub(crate) fn resolve_image<'a>(
238260
&self,
239261
node: Option<OpaqueNode>,

components/layout/dom.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ use layout_api::wrapper_traits::{
1212
LayoutDataTrait, LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode,
1313
};
1414
use layout_api::{
15-
GenericLayoutDataTrait, LayoutDamage, LayoutElementType, LayoutNodeType as ScriptLayoutNodeType,
15+
GenericLayoutDataTrait, LayoutDamage, LayoutElementType,
16+
LayoutNodeType as ScriptLayoutNodeType, SVGElementData,
1617
};
1718
use malloc_size_of_derive::MallocSizeOf;
1819
use net_traits::image_cache::Image;
@@ -232,6 +233,7 @@ pub(crate) trait NodeExt<'dom> {
232233
fn as_canvas(&self) -> Option<(CanvasInfo, PhysicalSize<f64>)>;
233234
fn as_iframe(&self) -> Option<(PipelineId, BrowsingContextId)>;
234235
fn as_video(&self) -> Option<(Option<webrender_api::ImageKey>, Option<PhysicalSize<f64>>)>;
236+
fn as_svg(&self) -> Option<SVGElementData>;
235237
fn as_typeless_object_with_data_attribute(&self) -> Option<String>;
236238
fn style(&self, context: &SharedStyleContext) -> ServoArc<ComputedValues>;
237239

@@ -273,6 +275,11 @@ impl<'dom> NodeExt<'dom> for ServoLayoutNode<'dom> {
273275
Some((resource, PhysicalSize::new(width, height)))
274276
}
275277

278+
fn as_svg(&self) -> Option<SVGElementData> {
279+
let node = self.to_threadsafe();
280+
node.svg_data()
281+
}
282+
276283
fn as_video(&self) -> Option<(Option<webrender_api::ImageKey>, Option<PhysicalSize<f64>>)> {
277284
let node = self.to_threadsafe();
278285
let data = node.media_data()?;

components/layout/layout_impl.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,7 @@ impl LayoutThread {
660660
resolved_images_cache: self.resolved_images_cache.clone(),
661661
pending_images: Mutex::default(),
662662
pending_rasterization_images: Mutex::default(),
663+
pending_svg_elements_for_serialization: Mutex::default(),
663664
node_to_animating_image_map: reflow_request.node_to_animating_image_map.clone(),
664665
animation_timeline_value: reflow_request.animation_timeline_value,
665666
});
@@ -682,11 +683,14 @@ impl LayoutThread {
682683
let pending_images = std::mem::take(&mut *image_resolver.pending_images.lock());
683684
let pending_rasterization_images =
684685
std::mem::take(&mut *image_resolver.pending_rasterization_images.lock());
686+
let pending_svg_elements_for_serialization =
687+
std::mem::take(&mut *image_resolver.pending_svg_elements_for_serialization.lock());
685688

686689
Some(ReflowResult {
687690
built_display_list,
688691
pending_images,
689692
pending_rasterization_images,
693+
pending_svg_elements_for_serialization,
690694
iframe_sizes,
691695
})
692696
}

components/layout/replaced.rs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use embedder_traits::ViewportDetails;
99
use euclid::{Scale, Size2D};
1010
use layout_api::IFrameSize;
1111
use malloc_size_of_derive::MallocSizeOf;
12-
use net_traits::image_cache::{Image, ImageOrMetadataAvailable, UsePlaceholder};
12+
use net_traits::image_cache::{Image, ImageOrMetadataAvailable, UsePlaceholder, VectorImage};
1313
use script::layout_dom::ServoLayoutNode;
1414
use servo_arc::Arc as ServoArc;
1515
use style::Zero;
@@ -115,6 +115,7 @@ pub(crate) enum ReplacedContentKind {
115115
IFrame(IFrameInfo),
116116
Canvas(CanvasInfo),
117117
Video(Option<VideoInfo>),
118+
SVGElement(VectorImage),
118119
}
119120

120121
impl ReplacedContents {
@@ -153,6 +154,31 @@ impl ReplacedContents {
153154
ReplacedContentKind::Video(image_key.map(|key| VideoInfo { image_key: key })),
154155
natural_size_in_dots,
155156
)
157+
} else if let Some(svg_data) = element.as_svg() {
158+
let Some(svg_source) = svg_data.source else {
159+
// If the SVGSVGElement is not yet serialized, we add it to a list
160+
// and hand it over to script to peform the serialization.
161+
context
162+
.image_resolver
163+
.queue_svg_element_for_serialization(element);
164+
return None;
165+
};
166+
let result = context
167+
.image_resolver
168+
.get_cached_image_for_url(element.opaque(), svg_source, UsePlaceholder::No)
169+
.ok()?;
170+
171+
let Image::Vector(vector_image) = result else {
172+
unreachable!("SVG element can't contain a raster image.")
173+
};
174+
let physical_size = PhysicalSize::new(
175+
vector_image.metadata.width as f64,
176+
vector_image.metadata.height as f64,
177+
);
178+
(
179+
ReplacedContentKind::SVGElement(vector_image),
180+
Some(physical_size),
181+
)
156182
} else {
157183
return None;
158184
}
@@ -390,6 +416,28 @@ impl ReplacedContents {
390416
image_key: Some(image_key),
391417
}))]
392418
},
419+
ReplacedContentKind::SVGElement(vector_image) => {
420+
let scale = layout_context.style_context.device_pixel_ratio();
421+
let width = object_fit_size.width.scale_by(scale.0).to_px();
422+
let height = object_fit_size.height.scale_by(scale.0).to_px();
423+
let size = Size2D::new(width, height);
424+
let tag = self.base_fragment_info.tag.unwrap();
425+
layout_context
426+
.image_resolver
427+
.rasterize_vector_image(vector_image.id, size, tag.node)
428+
.and_then(|i| i.id)
429+
.map(|image_key| {
430+
Fragment::Image(ArcRefCell::new(ImageFragment {
431+
base: self.base_fragment_info.into(),
432+
style: style.clone(),
433+
rect,
434+
clip,
435+
image_key: Some(image_key),
436+
}))
437+
})
438+
.into_iter()
439+
.collect()
440+
},
393441
}
394442
}
395443

components/script/dom/node.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use js::rust::HandleObject;
2727
use keyboard_types::Modifiers;
2828
use layout_api::{
2929
GenericLayoutData, HTMLCanvasData, HTMLMediaData, LayoutElementType, LayoutNodeType, QueryMsg,
30-
SVGSVGData, StyleData, TrustedNodeAddress,
30+
SVGElementData, StyleData, TrustedNodeAddress,
3131
};
3232
use libc::{self, c_void, uintptr_t};
3333
use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
@@ -1680,7 +1680,7 @@ pub(crate) trait LayoutNodeHelpers<'dom> {
16801680
fn image_data(self) -> Option<(Option<Image>, Option<ImageMetadata>)>;
16811681
fn canvas_data(self) -> Option<HTMLCanvasData>;
16821682
fn media_data(self) -> Option<HTMLMediaData>;
1683-
fn svg_data(self) -> Option<SVGSVGData>;
1683+
fn svg_data(self) -> Option<SVGElementData>;
16841684
fn iframe_browsing_context_id(self) -> Option<BrowsingContextId>;
16851685
fn iframe_pipeline_id(self) -> Option<PipelineId>;
16861686
fn opaque(self) -> OpaqueNode;
@@ -1969,7 +1969,7 @@ impl<'dom> LayoutNodeHelpers<'dom> for LayoutDom<'dom, Node> {
19691969
.map(|media| media.data())
19701970
}
19711971

1972-
fn svg_data(self) -> Option<SVGSVGData> {
1972+
fn svg_data(self) -> Option<SVGElementData> {
19731973
self.downcast::<SVGSVGElement>().map(|svg| svg.data())
19741974
}
19751975

components/script/dom/svgsvgelement.rs

Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,34 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
44

5+
use std::cell::RefCell;
6+
7+
use base64::Engine as _;
58
use dom_struct::dom_struct;
6-
use html5ever::{LocalName, Prefix, local_name, ns};
9+
use html5ever::{LocalName, Prefix};
710
use js::rust::HandleObject;
8-
use layout_api::SVGSVGData;
9-
use style::attr::AttrValue;
11+
use layout_api::SVGElementData;
12+
use servo_url::ServoUrl;
13+
use xml5ever::serialize::TraversalScope;
1014

1115
use crate::dom::attr::Attr;
1216
use crate::dom::bindings::inheritance::Castable;
1317
use crate::dom::bindings::root::{DomRoot, LayoutDom};
14-
use crate::dom::bindings::str::DOMString;
1518
use crate::dom::document::Document;
16-
use crate::dom::element::{AttributeMutation, Element, LayoutElementHelpers};
19+
use crate::dom::element::AttributeMutation;
1720
use crate::dom::node::Node;
1821
use crate::dom::svggraphicselement::SVGGraphicsElement;
1922
use crate::dom::virtualmethods::VirtualMethods;
2023
use crate::script_runtime::CanGc;
2124

22-
const DEFAULT_WIDTH: u32 = 300;
23-
const DEFAULT_HEIGHT: u32 = 150;
24-
2525
#[dom_struct]
2626
pub(crate) struct SVGSVGElement {
2727
svggraphicselement: SVGGraphicsElement,
28+
// The XML source of subtree rooted at this SVG element, serialized into
29+
// a base64 encoded `data:` url. This is cached to avoid recomputation
30+
// on each layout and must be invalidated when the subtree changes.
31+
#[no_trace]
32+
cached_serialized_data_url: RefCell<Option<ServoUrl>>,
2833
}
2934

3035
impl SVGSVGElement {
@@ -35,6 +40,7 @@ impl SVGSVGElement {
3540
) -> SVGSVGElement {
3641
SVGSVGElement {
3742
svggraphicselement: SVGGraphicsElement::new_inherited(local_name, prefix, document),
43+
cached_serialized_data_url: Default::default(),
3844
}
3945
}
4046

@@ -53,23 +59,37 @@ impl SVGSVGElement {
5359
can_gc,
5460
)
5561
}
62+
63+
pub(crate) fn serialize_and_cache_subtree(&self) {
64+
let xml_source: String = self
65+
.upcast::<Node>()
66+
.xml_serialize(TraversalScope::IncludeNode)
67+
.into();
68+
let base64_encoded_source = base64::engine::general_purpose::STANDARD.encode(xml_source);
69+
let data_url = format!("data:image/svg+xml;base64,{}", base64_encoded_source);
70+
match ServoUrl::parse(&data_url) {
71+
Ok(url) => *self.cached_serialized_data_url.borrow_mut() = Some(url),
72+
Err(error) => error!("Unable to parse serialized SVG data url: {error}"),
73+
};
74+
}
75+
76+
fn invalidate_cached_serialized_subtree(&self) {
77+
*self.cached_serialized_data_url.borrow_mut() = None;
78+
}
5679
}
5780

5881
pub(crate) trait LayoutSVGSVGElementHelpers {
59-
fn data(self) -> SVGSVGData;
82+
fn data(self) -> SVGElementData;
6083
}
6184

6285
impl LayoutSVGSVGElementHelpers for LayoutDom<'_, SVGSVGElement> {
63-
fn data(self) -> SVGSVGData {
64-
let width_attr = self
65-
.upcast::<Element>()
66-
.get_attr_for_layout(&ns!(), &local_name!("width"));
67-
let height_attr = self
68-
.upcast::<Element>()
69-
.get_attr_for_layout(&ns!(), &local_name!("height"));
70-
SVGSVGData {
71-
width: width_attr.map_or(DEFAULT_WIDTH, |val| val.as_uint()),
72-
height: height_attr.map_or(DEFAULT_HEIGHT, |val| val.as_uint()),
86+
fn data(self) -> SVGElementData {
87+
SVGElementData {
88+
source: self
89+
.unsafe_get()
90+
.cached_serialized_data_url
91+
.borrow()
92+
.clone(),
7393
}
7494
}
7595
}
@@ -83,16 +103,15 @@ impl VirtualMethods for SVGSVGElement {
83103
self.super_type()
84104
.unwrap()
85105
.attribute_mutated(attr, mutation, can_gc);
106+
107+
self.invalidate_cached_serialized_subtree();
86108
}
87109

88-
fn parse_plain_attribute(&self, name: &LocalName, value: DOMString) -> AttrValue {
89-
match *name {
90-
local_name!("width") => AttrValue::from_u32(value.into(), DEFAULT_WIDTH),
91-
local_name!("height") => AttrValue::from_u32(value.into(), DEFAULT_HEIGHT),
92-
_ => self
93-
.super_type()
94-
.unwrap()
95-
.parse_plain_attribute(name, value),
110+
fn children_changed(&self, mutation: &super::node::ChildrenMutation) {
111+
if let Some(super_type) = self.super_type() {
112+
super_type.children_changed(mutation);
96113
}
114+
115+
self.invalidate_cached_serialized_subtree();
97116
}
98117
}

components/script/dom/window.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ use dom_struct::dom_struct;
3333
use embedder_traits::user_content_manager::{UserContentManager, UserScript};
3434
use embedder_traits::{
3535
AlertResponse, ConfirmResponse, EmbedderMsg, GamepadEvent, GamepadSupportedHapticEffects,
36-
GamepadUpdateType, PromptResponse, SimpleDialog, Theme, ViewportDetails, WebDriverJSError,
37-
WebDriverJSResult,
36+
GamepadUpdateType, PromptResponse, SimpleDialog, Theme, UntrustedNodeAddress, ViewportDetails,
37+
WebDriverJSError, WebDriverJSResult,
3838
};
3939
use euclid::default::{Point2D as UntypedPoint2D, Rect as UntypedRect, Size2D as UntypedSize2D};
4040
use euclid::{Point2D, Scale, Size2D, Vector2D};
@@ -92,6 +92,7 @@ use webrender_api::units::{DeviceIntSize, DevicePixel, LayoutPixel};
9292

9393
use super::bindings::codegen::Bindings::MessagePortBinding::StructuredSerializeOptions;
9494
use super::bindings::trace::HashMapTracedValues;
95+
use super::types::SVGSVGElement;
9596
use crate::dom::bindings::cell::{DomRefCell, Ref};
9697
use crate::dom::bindings::codegen::Bindings::DocumentBinding::{
9798
DocumentMethods, DocumentReadyState, NamedPropertyValue,
@@ -2258,6 +2259,7 @@ impl Window {
22582259
self.handle_pending_images_post_reflow(
22592260
results.pending_images,
22602261
results.pending_rasterization_images,
2262+
results.pending_svg_elements_for_serialization,
22612263
);
22622264
document
22632265
.iframes_mut()
@@ -2989,6 +2991,7 @@ impl Window {
29892991
&self,
29902992
pending_images: Vec<PendingImage>,
29912993
pending_rasterization_images: Vec<PendingRasterizationImage>,
2994+
pending_svg_element_for_serialization: Vec<UntrustedNodeAddress>,
29922995
) {
29932996
let pipeline_id = self.pipeline_id();
29942997
for image in pending_images {
@@ -3037,6 +3040,13 @@ impl Window {
30373040
nodes.push(Dom::from_ref(&*node));
30383041
}
30393042
}
3043+
3044+
for node in pending_svg_element_for_serialization.into_iter() {
3045+
let node = unsafe { from_untrusted_node_address(node) };
3046+
let svg = node.downcast::<SVGSVGElement>().unwrap();
3047+
svg.serialize_and_cache_subtree();
3048+
node.dirty(NodeDamage::Other);
3049+
}
30403050
}
30413051
}
30423052

0 commit comments

Comments
 (0)