Skip to main content

layout/
replaced.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 std::sync::Arc;
6use std::sync::atomic::{AtomicBool, Ordering};
7
8use app_units::{Au, MAX_AU};
9use data_url::DataUrl;
10use embedder_traits::ViewportDetails;
11use euclid::{Scale, Size2D};
12use layout_api::{IFrameSize, LayoutElement, LayoutImageDestination, LayoutNode, SVGElementData};
13use malloc_size_of_derive::MallocSizeOf;
14use net_traits::image_cache::{Image, ImageOrMetadataAvailable, VectorImage};
15use net_traits::request::InternalRequest;
16use script::layout_dom::ServoLayoutNode;
17use servo_arc::Arc as ServoArc;
18use servo_base::cross_process_instant::CrossProcessInstant;
19use servo_base::id::{BrowsingContextId, PipelineId};
20use servo_url::ServoUrl;
21use style::Zero;
22use style::attr::AttrValue;
23use style::computed_values::object_fit::T as ObjectFit;
24use style::context::TreeCountingCaches;
25use style::dom::DummyElementContext;
26use style::logical_geometry::{LogicalAxis, WritingMode};
27use style::properties::{ComputedValues, StyleBuilder};
28use style::rule_cache::RuleCacheConditions;
29use style::rule_tree::RuleCascadeFlags;
30use style::stylesheets::container_rule::ContainerSizeQuery;
31use style::url::ComputedUrl;
32use style::values::CSSFloat;
33use style::values::computed::image::Image as ComputedImage;
34use style::values::computed::{Content, Context, ToComputedValue};
35use style::values::generics::counters::{GenericContentItem, GenericContentItems};
36use url::Url;
37use web_atoms::local_name;
38use webrender_api::ImageKey;
39
40use crate::context::{LayoutContext, LayoutImageCacheResult};
41use crate::dom::NodeExt;
42use crate::fragment_tree::{
43    BaseFragment, BaseFragmentInfo, CollapsedBlockMargins, Fragment, IFrameFragment, ImageFragment,
44};
45use crate::geom::{LogicalVec2, PhysicalPoint, PhysicalRect, PhysicalSize};
46use crate::layout_box_base::{IndependentFormattingContextLayoutResult, LayoutBoxBase};
47use crate::sizing::{
48    ComputeInlineContentSizes, InlineContentSizesResult, LazySize, SizeConstraint,
49};
50use crate::style_ext::{AspectRatio, Clamp, ComputedValuesExt, LayoutStyle};
51use crate::{ConstraintSpace, ContainingBlock, SharedStyle};
52
53#[derive(Debug, MallocSizeOf)]
54pub(crate) struct ReplacedContents {
55    base_fragment_info: BaseFragmentInfo,
56    pub kind: ReplacedContentKind,
57    /// Whether or not this [`ReplacedContents`] is due to content replacement, i.e.
58    /// `content: <image>` in style.
59    pub is_content_replacement: bool,
60    /// Whether or not this replaced element is selected by the document selection.
61    #[conditional_malloc_size_of]
62    pub selected: Arc<AtomicBool>,
63    /// The style to use for selection overlays on top of this replaced content.
64    pub selected_style: SharedStyle,
65    /// The natural size of this replaced content.
66    natural_size: NaturalSizes,
67}
68
69/// The natural dimensions of a replaced element, including a height, width, and
70/// aspect ratio.
71///
72/// * Raster images always have an natural width and height, with 1 image pixel = 1px.
73///   The natural ratio should be based on dividing those.
74///   See <https://github.com/w3c/csswg-drafts/issues/4572> for the case where either is zero.
75///   PNG specifically disallows this but I (SimonSapin) am not sure about other formats.
76///
77/// * Form controls have both natural width and height **but no natural ratio**.
78///   See <https://github.com/w3c/csswg-drafts/issues/1044> and
79///   <https://drafts.csswg.org/css-images/#natural-dimensions> “In general, […]”
80///
81/// * For SVG, see <https://svgwg.org/svg2-draft/coords.html#SizingSVGInCSS>
82///   and again <https://github.com/w3c/csswg-drafts/issues/4572>.
83///
84/// * IFrames do not have natural width and height or natural ratio according
85///   to <https://drafts.csswg.org/css-images/#intrinsic-dimensions>.
86#[derive(Debug, MallocSizeOf)]
87pub(crate) struct NaturalSizes {
88    pub width: Option<Au>,
89    pub height: Option<Au>,
90    pub ratio: Option<CSSFloat>,
91}
92
93impl NaturalSizes {
94    pub(crate) fn from_width_and_height(width: f32, height: f32) -> Self {
95        // https://drafts.csswg.org/css-images/#natural-aspect-ratio:
96        // "If an object has a degenerate natural aspect ratio (at least one part being
97        // zero or infinity), it is treated as having no natural aspect ratio.""
98        let ratio = if width.is_normal() && height.is_normal() {
99            Some(width / height)
100        } else {
101            None
102        };
103
104        Self {
105            width: Some(Au::from_f32_px(width)),
106            height: Some(Au::from_f32_px(height)),
107            ratio,
108        }
109    }
110
111    pub(crate) fn from_natural_size_in_dots(natural_size_in_dots: PhysicalSize<f64>) -> Self {
112        // FIXME: should 'image-resolution' (when implemented) be used *instead* of
113        // `script::dom::htmlimageelement::ImageRequest::current_pixel_density`?
114        // https://drafts.csswg.org/css-images-4/#the-image-resolution
115        let dppx = 1.0;
116        let width = natural_size_in_dots.width as f32 / dppx;
117        let height = natural_size_in_dots.height as f32 / dppx;
118        Self::from_width_and_height(width, height)
119    }
120
121    pub(crate) fn empty() -> Self {
122        Self {
123            width: None,
124            height: None,
125            ratio: None,
126        }
127    }
128}
129
130#[derive(Debug, MallocSizeOf)]
131pub(crate) struct CanvasInfo {
132    pub source: Option<ImageKey>,
133}
134
135#[derive(Debug, MallocSizeOf)]
136pub(crate) struct IFrameInfo {
137    pub pipeline_id: PipelineId,
138    pub browsing_context_id: BrowsingContextId,
139}
140
141#[derive(Debug, MallocSizeOf)]
142pub(crate) struct ImageInfo {
143    pub image: Option<Image>,
144    pub showing_broken_image_icon: bool,
145    pub url: Option<ServoUrl>,
146    pub load_time: Option<CrossProcessInstant>,
147}
148
149#[derive(Debug, MallocSizeOf)]
150pub(crate) struct VideoInfo {
151    pub image_key: Option<ImageKey>,
152    pub poster_or_src_url: Option<ServoUrl>,
153}
154
155#[derive(Debug, MallocSizeOf)]
156pub(crate) enum ReplacedContentKind {
157    Image(ImageInfo),
158    IFrame(IFrameInfo),
159    Canvas(CanvasInfo),
160    Video(VideoInfo),
161    SVGElement {
162        vector_image: Option<VectorImage>,
163        has_viewbox: bool,
164    },
165    Audio,
166}
167
168impl ReplacedContents {
169    pub fn for_element(node: ServoLayoutNode<'_>, context: &LayoutContext) -> Option<Self> {
170        if let Some(ref data_attribute_string) = node.as_typeless_object_with_data_attribute() &&
171            let Some(url) = try_to_parse_image_data_url(data_attribute_string)
172        {
173            return Self::from_image_url(node, context, &ComputedUrl::Valid(ServoArc::new(url)));
174        }
175
176        let (kind, natural_size) = {
177            if let Some((image_info, natural_size_in_dots)) = node.as_image() {
178                if let Some(content_image) = Self::from_content_property(node, context) {
179                    return Some(content_image);
180                }
181                (
182                    ReplacedContentKind::Image(image_info),
183                    NaturalSizes::from_natural_size_in_dots(natural_size_in_dots),
184                )
185            } else if let Some((canvas_info, natural_size_in_dots)) = node.as_canvas() {
186                (
187                    ReplacedContentKind::Canvas(canvas_info),
188                    NaturalSizes::from_natural_size_in_dots(natural_size_in_dots),
189                )
190            } else if let Some(iframe_info) = node.as_iframe() {
191                (
192                    ReplacedContentKind::IFrame(iframe_info),
193                    NaturalSizes::empty(),
194                )
195            } else if let Some((video_info, natural_size_in_dots)) = node.as_video() {
196                (
197                    ReplacedContentKind::Video(video_info),
198                    natural_size_in_dots
199                        .map_or_else(NaturalSizes::empty, NaturalSizes::from_natural_size_in_dots),
200                )
201            } else if let Some(svg_data) = node.as_svg() {
202                Self::svg_kind_size(svg_data, context, node)
203            } else if node
204                .as_html_element()
205                .is_some_and(|element| element.local_name() == &local_name!("audio"))
206            {
207                let natural_size = NaturalSizes {
208                    width: None,
209                    // 40px is the height of the controls.
210                    // See /components/script/resources/media-controls.css
211                    height: Some(Au::from_px(40)),
212                    ratio: None,
213                };
214                (ReplacedContentKind::Audio, natural_size)
215            } else {
216                return Self::from_content_property(node, context);
217            }
218        };
219
220        if let ReplacedContentKind::Image(ImageInfo {
221            image: Some(Image::Raster(ref image)),
222            ..
223        }) = kind
224        {
225            context
226                .image_resolver
227                .handle_animated_image(node.opaque(), image.clone());
228        }
229
230        Some(Self {
231            base_fragment_info: node.into(),
232            kind,
233            is_content_replacement: false,
234            selected: Arc::new(AtomicBool::new(node.replaced_is_selected())),
235            selected_style: SharedStyle::new(node.selected_style(&context.style_context)),
236            natural_size,
237        })
238    }
239
240    fn svg_kind_size(
241        svg_data: SVGElementData,
242        context: &LayoutContext,
243        node: ServoLayoutNode<'_>,
244    ) -> (ReplacedContentKind, NaturalSizes) {
245        let rule_cache_conditions = &mut RuleCacheConditions::default();
246        let mut tree_counting_caches = TreeCountingCaches::default();
247
248        let parent_style = node.style(&context.style_context);
249        let style_builder = StyleBuilder::new(
250            context.style_context.stylist.device(),
251            Some(context.style_context.stylist),
252            Some(&parent_style),
253            None,
254            None,
255            false,
256        );
257
258        // TODO: use the correct element context in order to properly resolve
259        // `sibling-index()`, like Blink. Or maybe do it like Gecko, and only
260        // accept literals, see https://github.com/w3c/csswg-drafts/issues/14117
261        let element_context = &DummyElementContext;
262
263        let to_computed_context = Context::new(
264            style_builder,
265            context.style_context.quirks_mode(),
266            rule_cache_conditions,
267            ContainerSizeQuery::none(),
268            RuleCascadeFlags::empty(),
269            element_context,
270            &mut tree_counting_caches,
271        );
272
273        let attr_to_computed = |attr_val: &AttrValue| {
274            if let AttrValue::LengthPercentage(_, length_percentage) = attr_val {
275                length_percentage
276                    .to_computed_value(&to_computed_context)?
277                    .to_length()
278            } else {
279                None
280            }
281        };
282        let width = svg_data.width.and_then(attr_to_computed);
283        let height = svg_data.height.and_then(attr_to_computed);
284
285        let ratio = match (width, height) {
286            (Some(width), Some(height)) if !width.is_zero() && !height.is_zero() => {
287                Some(width.px() / height.px())
288            },
289            _ => svg_data.ratio_from_view_box(),
290        };
291
292        let natural_size = NaturalSizes {
293            width: width.map(|w| Au::from_f32_px(w.px())),
294            height: height.map(|h| Au::from_f32_px(h.px())),
295            ratio,
296        };
297
298        let svg_source = match svg_data.source {
299            None => {
300                // The SVGSVGElement is not yet serialized, so we add it to a list
301                // and hand it over to script to peform the serialization.
302                context
303                    .image_resolver
304                    .queue_svg_element_for_serialization(node);
305                None
306            },
307            // If `svg_source_result` is `Err()`, it means that the previous attempt
308            // had errored, then don't attempt to serialize again.
309            Some(svg_source_result) => svg_source_result.ok(),
310        };
311
312        let cached_image = svg_source.and_then(|svg_source| {
313            context
314                .image_resolver
315                .get_cached_image_for_url(
316                    node.opaque(),
317                    svg_source,
318                    LayoutImageDestination::BoxTreeConstruction,
319                    InternalRequest::Yes,
320                )
321                .ok()
322        });
323
324        let vector_image = cached_image.map(|image| match image {
325            Image::Vector(mut vector_image) => {
326                vector_image.svg_id = Some(svg_data.svg_id);
327                vector_image
328            },
329            _ => unreachable!("SVG element can't contain a raster image."),
330        });
331
332        (
333            ReplacedContentKind::SVGElement {
334                vector_image,
335                has_viewbox: svg_data.view_box.is_some(),
336            },
337            natural_size,
338        )
339    }
340
341    fn from_content_property(node: ServoLayoutNode<'_>, context: &LayoutContext) -> Option<Self> {
342        // If the `content` property is a single image URL, non-replaced boxes
343        // and images get replaced with the given image.
344        if let Content::Items(GenericContentItems { items, .. }) =
345            node.style(&context.style_context).get_content() &&
346            let [GenericContentItem::Image(image)] = items.as_slice()
347        {
348            // Invalid images are treated as zero-sized.
349            let mut replaced_contents = Self::from_image(node, context, image)
350                .unwrap_or_else(|| Self::zero_sized_invalid_image(node, context));
351
352            replaced_contents.is_content_replacement = true;
353            node.clear_fragments_and_dirty_fragment_caches_of_descendants();
354            return Some(replaced_contents);
355        }
356        None
357    }
358
359    pub fn from_image_url(
360        node: ServoLayoutNode<'_>,
361        context: &LayoutContext,
362        image_url: &ComputedUrl,
363    ) -> Option<Self> {
364        let ComputedUrl::Valid(image_url) = image_url else {
365            return None;
366        };
367        let (image, width, height) = match context.image_resolver.get_or_request_image_or_meta(
368            node.opaque(),
369            image_url.clone().into(),
370            LayoutImageDestination::BoxTreeConstruction,
371            InternalRequest::No,
372        ) {
373            LayoutImageCacheResult::DataAvailable(img_or_meta) => match img_or_meta {
374                ImageOrMetadataAvailable::ImageAvailable { image, .. } => {
375                    if let Image::Raster(image) = &image {
376                        context
377                            .image_resolver
378                            .handle_animated_image(node.opaque(), image.clone());
379                    }
380                    let metadata = image.metadata();
381                    (Some(image), metadata.width as f32, metadata.height as f32)
382                },
383                ImageOrMetadataAvailable::MetadataAvailable(metadata, _id) => {
384                    (None, metadata.width as f32, metadata.height as f32)
385                },
386            },
387            LayoutImageCacheResult::Pending | LayoutImageCacheResult::LoadError => return None,
388        };
389        Some(Self {
390            base_fragment_info: node.into(),
391            kind: ReplacedContentKind::Image(ImageInfo {
392                load_time: image.as_ref().and_then(|image| image.load_time()),
393                image,
394                showing_broken_image_icon: false,
395                url: Some(image_url.clone().into()),
396            }),
397            is_content_replacement: false,
398            selected: Arc::new(AtomicBool::new(node.replaced_is_selected())),
399            selected_style: SharedStyle::new(node.selected_style(&context.style_context)),
400            natural_size: NaturalSizes::from_width_and_height(width, height),
401        })
402    }
403
404    pub fn from_image(
405        element: ServoLayoutNode<'_>,
406        context: &LayoutContext,
407        image: &ComputedImage,
408    ) -> Option<Self> {
409        match image {
410            ComputedImage::Url(image_url) => Self::from_image_url(element, context, image_url),
411            _ => None, // TODO
412        }
413    }
414
415    pub(crate) fn zero_sized_invalid_image(
416        node: ServoLayoutNode<'_>,
417        context: &LayoutContext,
418    ) -> Self {
419        Self {
420            base_fragment_info: node.into(),
421            kind: ReplacedContentKind::Image(ImageInfo {
422                image: None,
423                showing_broken_image_icon: false,
424                url: None,
425                load_time: None,
426            }),
427            is_content_replacement: false,
428            selected: Default::default(),
429            selected_style: SharedStyle::new(node.selected_style(&context.style_context)),
430            natural_size: NaturalSizes::from_width_and_height(0., 0.),
431        }
432    }
433
434    #[inline]
435    fn is_broken_image(&self) -> bool {
436        matches!(&self.kind, ReplacedContentKind::Image(image_info) if image_info.showing_broken_image_icon)
437    }
438
439    #[inline]
440    fn content_size(
441        &self,
442        axis: LogicalAxis,
443        preferred_aspect_ratio: Option<AspectRatio>,
444        get_size_in_opposite_axis: &dyn Fn() -> SizeConstraint,
445        get_fallback_size: &dyn Fn() -> Au,
446    ) -> Au {
447        let Some(ratio) = preferred_aspect_ratio else {
448            return get_fallback_size();
449        };
450        let transfer = |size| ratio.compute_dependent_size(axis, size);
451        match get_size_in_opposite_axis() {
452            SizeConstraint::Definite(size) => transfer(size),
453            SizeConstraint::MinMax(min_size, max_size) => get_fallback_size()
454                .clamp_between_extremums(transfer(min_size), max_size.map(transfer)),
455        }
456    }
457
458    fn calculate_fragment_rect(
459        &self,
460        style: &ServoArc<ComputedValues>,
461        size: PhysicalSize<Au>,
462    ) -> (PhysicalSize<Au>, PhysicalRect<Au>) {
463        if let ReplacedContentKind::Image(ImageInfo {
464            image: Some(Image::Raster(image)),
465            showing_broken_image_icon: true,
466            ..
467        }) = &self.kind
468        {
469            let size = Size2D::new(
470                Au::from_f32_px(image.metadata.width as f32),
471                Au::from_f32_px(image.metadata.height as f32),
472            )
473            .min(size);
474            return (PhysicalSize::zero(), size.into());
475        }
476
477        let natural_size = PhysicalSize::new(
478            self.natural_size.width.unwrap_or(size.width),
479            self.natural_size.height.unwrap_or(size.height),
480        );
481
482        let object_fit_size = self.natural_size.ratio.map_or(size, |width_over_height| {
483            let preserve_aspect_ratio_with_comparison =
484                |size: PhysicalSize<Au>, comparison: fn(&Au, &Au) -> bool| {
485                    let candidate_width = size.height.scale_by(width_over_height);
486                    if comparison(&candidate_width, &size.width) {
487                        return PhysicalSize::new(candidate_width, size.height);
488                    }
489
490                    let candidate_height = size.width.scale_by(1. / width_over_height);
491                    debug_assert!(comparison(&candidate_height, &size.height));
492                    PhysicalSize::new(size.width, candidate_height)
493                };
494
495            match style.get_object_fit() {
496                ObjectFit::Fill => size,
497                ObjectFit::Contain => preserve_aspect_ratio_with_comparison(size, PartialOrd::le),
498                ObjectFit::Cover => preserve_aspect_ratio_with_comparison(size, PartialOrd::ge),
499                ObjectFit::None => natural_size,
500                ObjectFit::ScaleDown => {
501                    preserve_aspect_ratio_with_comparison(size.min(natural_size), PartialOrd::le)
502                },
503            }
504        });
505
506        let object_position = style.get_object_position();
507        let horizontal_position = object_position
508            .horizontal
509            .to_used_value(size.width - object_fit_size.width);
510        let vertical_position = object_position
511            .vertical
512            .to_used_value(size.height - object_fit_size.height);
513
514        let object_position = PhysicalPoint::new(horizontal_position, vertical_position);
515        (
516            object_fit_size,
517            PhysicalRect::new(object_position, object_fit_size),
518        )
519    }
520
521    pub fn make_fragments(
522        &self,
523        layout_context: &LayoutContext,
524        style: &ServoArc<ComputedValues>,
525        size: PhysicalSize<Au>,
526    ) -> Vec<Fragment> {
527        let (object_fit_size, rect) = self.calculate_fragment_rect(style, size);
528        let clip = PhysicalRect::new(PhysicalPoint::origin(), size);
529
530        let base = BaseFragment::new(self.base_fragment_info, rect);
531        match &self.kind {
532            ReplacedContentKind::Image(image_info) => image_info
533                .image
534                .as_ref()
535                .and_then(|image| {
536                    let scale = layout_context.style_context.device_pixel_ratio();
537                    let size = Size2D::new(
538                        object_fit_size.width.scale_by(scale.0).to_px(),
539                        object_fit_size.height.scale_by(scale.0).to_px(),
540                    );
541                    layout_context.image_resolver.image_key_from_cached_image(
542                        image,
543                        size,
544                        self.base_fragment_info.tag.map(|tag| tag.node),
545                    )
546                })
547                .map(|image_key| {
548                    Fragment::Image(Arc::new(ImageFragment {
549                        base,
550                        style: style.clone().into(),
551                        selected_style: self.selected_style.clone(),
552                        clip,
553                        image_key: Some(image_key),
554                        showing_broken_image_icon: image_info.showing_broken_image_icon,
555                        url: image_info.url.clone(),
556                        load_time: image_info.load_time,
557                        natural_width: self.natural_size.width,
558                        natural_height: self.natural_size.height,
559                        selected: self.selected.clone(),
560                    }))
561                })
562                .into_iter()
563                .collect(),
564            ReplacedContentKind::Video(video_info) => {
565                vec![Fragment::Image(Arc::new(ImageFragment {
566                    base,
567                    style: style.clone().into(),
568                    selected_style: self.selected_style.clone(),
569                    clip,
570                    image_key: video_info.image_key,
571                    showing_broken_image_icon: false,
572                    url: video_info.poster_or_src_url.clone(),
573                    load_time: None,
574                    natural_width: self.natural_size.width,
575                    natural_height: self.natural_size.height,
576                    selected: self.selected.clone(),
577                }))]
578            },
579            ReplacedContentKind::IFrame(iframe) => {
580                let size = Size2D::new(rect.size.width.to_f32_px(), rect.size.height.to_f32_px());
581                let hidpi_scale_factor = layout_context.style_context.device_pixel_ratio();
582
583                layout_context.iframe_sizes.lock().insert(
584                    iframe.browsing_context_id,
585                    IFrameSize {
586                        browsing_context_id: iframe.browsing_context_id,
587                        pipeline_id: iframe.pipeline_id,
588                        viewport_details: ViewportDetails {
589                            size,
590                            hidpi_scale_factor: Scale::new(hidpi_scale_factor.0),
591                            device_size: layout_context.device_size.cast_unit(),
592                        },
593                    },
594                );
595                vec![Fragment::IFrame(Arc::new(IFrameFragment {
596                    base,
597                    style: style.clone().into(),
598                    pipeline_id: iframe.pipeline_id,
599                }))]
600            },
601            ReplacedContentKind::Canvas(canvas_info) => {
602                if self.natural_size.width == Some(Au::zero()) ||
603                    self.natural_size.height == Some(Au::zero())
604                {
605                    return vec![];
606                }
607
608                let Some(image_key) = canvas_info.source else {
609                    return vec![];
610                };
611
612                vec![Fragment::Image(Arc::new(ImageFragment {
613                    base,
614                    style: style.clone().into(),
615                    selected_style: self.selected_style.clone(),
616                    clip,
617                    image_key: Some(image_key),
618                    showing_broken_image_icon: false,
619                    url: None,
620                    load_time: None,
621                    natural_width: self.natural_size.width,
622                    natural_height: self.natural_size.height,
623                    selected: self.selected.clone(),
624                }))]
625            },
626            ReplacedContentKind::SVGElement {
627                vector_image,
628                has_viewbox,
629            } => {
630                let Some(vector_image) = vector_image else {
631                    return vec![];
632                };
633
634                if !has_viewbox {
635                    base.set_rect(
636                        PhysicalSize::new(
637                            vector_image
638                                .metadata
639                                .width
640                                .try_into()
641                                .map_or(MAX_AU, Au::from_px),
642                            vector_image
643                                .metadata
644                                .height
645                                .try_into()
646                                .map_or(MAX_AU, Au::from_px),
647                        )
648                        .into(),
649                    );
650                }
651
652                let scale = layout_context.style_context.device_pixel_ratio();
653                let content_size = base.rect().size;
654                let raster_size = Size2D::new(
655                    content_size.width.scale_by(scale.0).to_px(),
656                    content_size.height.scale_by(scale.0).to_px(),
657                );
658
659                let tag = self.base_fragment_info.tag.unwrap();
660                layout_context
661                    .image_resolver
662                    .rasterize_vector_image(
663                        vector_image.id,
664                        raster_size,
665                        tag.node,
666                        vector_image.svg_id,
667                    )
668                    .and_then(|image| image.id)
669                    .map(|image_key| {
670                        Fragment::Image(Arc::new(ImageFragment {
671                            base,
672                            style: style.clone().into(),
673                            selected_style: self.selected_style.clone(),
674                            clip,
675                            image_key: Some(image_key),
676                            showing_broken_image_icon: false,
677                            url: None,
678                            load_time: None,
679                            natural_width: self.natural_size.width,
680                            natural_height: self.natural_size.height,
681                            selected: self.selected.clone(),
682                        }))
683                    })
684                    .into_iter()
685                    .collect()
686            },
687            ReplacedContentKind::Audio => vec![],
688        }
689    }
690
691    pub(crate) fn preferred_aspect_ratio(
692        &self,
693        style: &ComputedValues,
694        padding_border_sums: &LogicalVec2<Au>,
695    ) -> Option<AspectRatio> {
696        if matches!(self.kind, ReplacedContentKind::Audio) {
697            // This isn't specified, but other browsers don't support `aspect-ratio` on `<audio>`.
698            // See <https://phabricator.services.mozilla.com/D118245>
699            return None;
700        }
701        if self.is_broken_image() {
702            // This isn't specified, but when an image is broken, we should prefer to the aspect
703            // ratio from the style, rather than the aspect ratio from the broken image icon.
704            // Note that the broken image icon *does* affect the content size of the image
705            // though as we want the image to be as big as the icon if the size was not specified
706            // in the style.
707            style.preferred_aspect_ratio(None, padding_border_sums)
708        } else {
709            style.preferred_aspect_ratio(self.natural_size.ratio, padding_border_sums)
710        }
711    }
712
713    /// The inline size that would result from combining the natural size
714    /// and the default object size, but disregarding the specified size.
715    /// <https://drafts.csswg.org/css-images-3/#natural-dimensions>
716    /// <https://drafts.csswg.org/css-images-3/#default-object-size>
717    /// <https://drafts.csswg.org/css-images-3/#specified-size>
718    pub(crate) fn fallback_inline_size(&self, writing_mode: WritingMode) -> Au {
719        if writing_mode.is_horizontal() {
720            self.natural_size.width.unwrap_or_else(|| Au::from_px(300))
721        } else {
722            self.natural_size.height.unwrap_or_else(|| Au::from_px(150))
723        }
724    }
725
726    /// The block size that would result from combining the natural size
727    /// and the default object size, but disregarding the specified size.
728    /// <https://drafts.csswg.org/css-images-3/#natural-dimensions>
729    /// <https://drafts.csswg.org/css-images-3/#default-object-size>
730    /// <https://drafts.csswg.org/css-images-3/#specified-size>
731    pub(crate) fn fallback_block_size(&self, writing_mode: WritingMode) -> Au {
732        if writing_mode.is_horizontal() {
733            self.natural_size.height.unwrap_or_else(|| Au::from_px(150))
734        } else {
735            self.natural_size.width.unwrap_or_else(|| Au::from_px(300))
736        }
737    }
738
739    pub(crate) fn logical_natural_sizes(
740        &self,
741        writing_mode: WritingMode,
742    ) -> LogicalVec2<Option<Au>> {
743        if writing_mode.is_horizontal() {
744            LogicalVec2 {
745                inline: self.natural_size.width,
746                block: self.natural_size.height,
747            }
748        } else {
749            LogicalVec2 {
750                inline: self.natural_size.height,
751                block: self.natural_size.width,
752            }
753        }
754    }
755
756    #[inline]
757    pub(crate) fn layout_style<'a>(&self, base: &'a LayoutBoxBase) -> LayoutStyle<'a> {
758        LayoutStyle::Default(&base.style)
759    }
760
761    pub(crate) fn layout(
762        &self,
763        layout_context: &LayoutContext,
764        containing_block_for_children: &ContainingBlock,
765        preferred_aspect_ratio: Option<AspectRatio>,
766        base: &LayoutBoxBase,
767        lazy_block_size: &LazySize,
768    ) -> IndependentFormattingContextLayoutResult {
769        let writing_mode = base.style.writing_mode;
770        let inline_size = containing_block_for_children.size.inline;
771        let content_block_size = self.content_size(
772            LogicalAxis::Block,
773            preferred_aspect_ratio,
774            &|| SizeConstraint::Definite(inline_size),
775            &|| self.fallback_block_size(writing_mode),
776        );
777        let size = LogicalVec2 {
778            inline: inline_size,
779            block: lazy_block_size.resolve(|| content_block_size),
780        }
781        .to_physical_size(writing_mode);
782        IndependentFormattingContextLayoutResult {
783            baselines: Default::default(),
784            collapsible_margins_in_children: CollapsedBlockMargins::zero(),
785            content_block_size,
786            content_inline_size_for_table: None,
787            // The result doesn't depend on `containing_block_for_children.size.block`,
788            // but it depends on `lazy_block_size`, which is probably tied to that.
789            depends_on_block_constraints: true,
790            fragments: self.make_fragments(layout_context, &base.style, size),
791            specific_layout_info: None,
792        }
793    }
794
795    /// Set whether or not this [`ReplacedContents`] is selected. Returns `true` if anything
796    /// changed.
797    pub(crate) fn set_selection(&self, selected: bool) -> bool {
798        // Only build a display list if the value changed and if this isn't an `<iframe>`
799        // or `<audio>` element as they do not paint a selection tint.
800        self.selected.swap(selected, Ordering::Relaxed) != selected &&
801            !matches!(
802                self.kind,
803                ReplacedContentKind::Audio | ReplacedContentKind::IFrame(..)
804            )
805    }
806}
807
808impl ComputeInlineContentSizes for ReplacedContents {
809    fn compute_inline_content_sizes(
810        &self,
811        _: &LayoutContext,
812        constraint_space: &ConstraintSpace,
813    ) -> InlineContentSizesResult {
814        let inline_content_size = self.content_size(
815            LogicalAxis::Inline,
816            constraint_space.preferred_aspect_ratio,
817            &|| constraint_space.block_size,
818            &|| self.fallback_inline_size(constraint_space.style.writing_mode),
819        );
820        InlineContentSizesResult {
821            sizes: inline_content_size.into(),
822            depends_on_block_constraints: constraint_space.preferred_aspect_ratio.is_some(),
823        }
824    }
825}
826
827fn try_to_parse_image_data_url(string: &str) -> Option<Url> {
828    if !string.starts_with("data:") {
829        return None;
830    }
831    let data_url = DataUrl::process(string).ok()?;
832    let mime_type = data_url.mime_type();
833    if mime_type.type_ != "image" {
834        return None;
835    }
836
837    // TODO: Find a better way to test for supported image formats. Currently this type of check is
838    // repeated several places in Servo, but should be centralized somehow.
839    if !matches!(
840        mime_type.subtype.as_str(),
841        "png" | "jpeg" | "gif" | "webp" | "bmp" | "ico"
842    ) {
843        return None;
844    }
845
846    Url::parse(string).ok()
847}