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