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