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