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 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::servo::url::ComputedUrl;
26use style::stylesheets::container_rule::ContainerSizeQuery;
27use style::values::CSSFloat;
28use style::values::computed::image::Image as ComputedImage;
29use style::values::computed::{Content, Context, ToComputedValue};
30use style::values::generics::counters::{GenericContentItem, GenericContentItems};
31use url::Url;
32use webrender_api::ImageKey;
33
34use crate::cell::ArcRefCell;
35use crate::context::{LayoutContext, LayoutImageCacheResult};
36use crate::dom::NodeExt;
37use crate::fragment_tree::{
38    BaseFragment, BaseFragmentInfo, CollapsedBlockMargins, Fragment, IFrameFragment, ImageFragment,
39};
40use crate::geom::{LogicalVec2, PhysicalPoint, PhysicalRect, PhysicalSize};
41use crate::layout_box_base::{CacheableLayoutResult, LayoutBoxBase};
42use crate::sizing::{
43    ComputeInlineContentSizes, InlineContentSizesResult, LazySize, SizeConstraint,
44};
45use crate::style_ext::{AspectRatio, Clamp, ComputedValuesExt, LayoutStyle};
46use crate::{ConstraintSpace, ContainingBlock};
47
48#[derive(Debug, MallocSizeOf)]
49pub(crate) struct ReplacedContents {
50    pub kind: ReplacedContentKind,
51    natural_size: NaturalSizes,
52    base_fragment_info: BaseFragmentInfo,
53}
54
55/// The natural dimensions of a replaced element, including a height, width, and
56/// aspect ratio.
57///
58/// * Raster images always have an natural width and height, with 1 image pixel = 1px.
59///   The natural ratio should be based on dividing those.
60///   See <https://github.com/w3c/csswg-drafts/issues/4572> for the case where either is zero.
61///   PNG specifically disallows this but I (SimonSapin) am not sure about other formats.
62///
63/// * Form controls have both natural width and height **but no natural ratio**.
64///   See <https://github.com/w3c/csswg-drafts/issues/1044> and
65///   <https://drafts.csswg.org/css-images/#natural-dimensions> “In general, […]”
66///
67/// * For SVG, see <https://svgwg.org/svg2-draft/coords.html#SizingSVGInCSS>
68///   and again <https://github.com/w3c/csswg-drafts/issues/4572>.
69///
70/// * IFrames do not have natural width and height or natural ratio according
71///   to <https://drafts.csswg.org/css-images/#intrinsic-dimensions>.
72#[derive(Debug, MallocSizeOf)]
73pub(crate) struct NaturalSizes {
74    pub width: Option<Au>,
75    pub height: Option<Au>,
76    pub ratio: Option<CSSFloat>,
77}
78
79impl NaturalSizes {
80    pub(crate) fn from_width_and_height(width: f32, height: f32) -> Self {
81        // https://drafts.csswg.org/css-images/#natural-aspect-ratio:
82        // "If an object has a degenerate natural aspect ratio (at least one part being
83        // zero or infinity), it is treated as having no natural aspect ratio.""
84        let ratio = if width.is_normal() && height.is_normal() {
85            Some(width / height)
86        } else {
87            None
88        };
89
90        Self {
91            width: Some(Au::from_f32_px(width)),
92            height: Some(Au::from_f32_px(height)),
93            ratio,
94        }
95    }
96
97    pub(crate) fn from_natural_size_in_dots(natural_size_in_dots: PhysicalSize<f64>) -> Self {
98        // FIXME: should 'image-resolution' (when implemented) be used *instead* of
99        // `script::dom::htmlimageelement::ImageRequest::current_pixel_density`?
100        // https://drafts.csswg.org/css-images-4/#the-image-resolution
101        let dppx = 1.0;
102        let width = natural_size_in_dots.width as f32 / dppx;
103        let height = natural_size_in_dots.height as f32 / dppx;
104        Self::from_width_and_height(width, height)
105    }
106
107    pub(crate) fn empty() -> Self {
108        Self {
109            width: None,
110            height: None,
111            ratio: None,
112        }
113    }
114}
115
116#[derive(Debug, MallocSizeOf)]
117pub(crate) struct CanvasInfo {
118    pub source: Option<ImageKey>,
119}
120
121#[derive(Debug, MallocSizeOf)]
122pub(crate) struct IFrameInfo {
123    pub pipeline_id: PipelineId,
124    pub browsing_context_id: BrowsingContextId,
125}
126
127#[derive(Debug, MallocSizeOf)]
128pub(crate) struct ImageInfo {
129    pub image: Option<Image>,
130    pub showing_broken_image_icon: bool,
131    pub url: Option<ServoUrl>,
132}
133
134#[derive(Debug, MallocSizeOf)]
135pub(crate) struct VideoInfo {
136    pub image_key: Option<ImageKey>,
137}
138
139#[derive(Debug, MallocSizeOf)]
140pub(crate) enum ReplacedContentKind {
141    Image(ImageInfo),
142    IFrame(IFrameInfo),
143    Canvas(CanvasInfo),
144    Video(VideoInfo),
145    SVGElement(Option<VectorImage>),
146    Audio,
147}
148
149impl ReplacedContents {
150    pub fn for_element(
151        node: ServoThreadSafeLayoutNode<'_>,
152        context: &LayoutContext,
153    ) -> Option<Self> {
154        if let Some(ref data_attribute_string) = node.as_typeless_object_with_data_attribute() {
155            if let Some(url) = try_to_parse_image_data_url(data_attribute_string) {
156                return Self::from_image_url(
157                    node,
158                    context,
159                    &ComputedUrl::Valid(ServoArc::new(url)),
160                );
161            }
162        }
163
164        let (kind, natural_size) = {
165            if let Some((image_info, natural_size_in_dots)) = node.as_image() {
166                if let Some(content_image) = Self::from_content_property(node, context) {
167                    return Some(content_image);
168                }
169                (
170                    ReplacedContentKind::Image(image_info),
171                    NaturalSizes::from_natural_size_in_dots(natural_size_in_dots),
172                )
173            } else if let Some((canvas_info, natural_size_in_dots)) = node.as_canvas() {
174                (
175                    ReplacedContentKind::Canvas(canvas_info),
176                    NaturalSizes::from_natural_size_in_dots(natural_size_in_dots),
177                )
178            } else if let Some(iframe_info) = node.as_iframe() {
179                (
180                    ReplacedContentKind::IFrame(iframe_info),
181                    NaturalSizes::empty(),
182                )
183            } else if let Some((video_info, natural_size_in_dots)) = node.as_video() {
184                (
185                    ReplacedContentKind::Video(video_info),
186                    natural_size_in_dots
187                        .map_or_else(NaturalSizes::empty, NaturalSizes::from_natural_size_in_dots),
188                )
189            } else if let Some(svg_data) = node.as_svg() {
190                Self::svg_kind_size(svg_data, context, node)
191            } else if node
192                .as_html_element()
193                .is_some_and(|element| element.has_local_name(&local_name!("audio")))
194            {
195                let natural_size = NaturalSizes {
196                    width: None,
197                    // 40px is the height of the controls.
198                    // See /components/script/resources/media-controls.css
199                    height: Some(Au::from_px(40)),
200                    ratio: None,
201                };
202                (ReplacedContentKind::Audio, natural_size)
203            } else {
204                return Self::from_content_property(node, context);
205            }
206        };
207
208        if let ReplacedContentKind::Image(ImageInfo {
209            image: Some(Image::Raster(ref image)),
210            ..
211        }) = kind
212        {
213            context
214                .image_resolver
215                .handle_animated_image(node.opaque(), image.clone());
216        }
217
218        Some(Self {
219            kind,
220            natural_size,
221            base_fragment_info: node.into(),
222        })
223    }
224
225    fn svg_kind_size(
226        svg_data: SVGElementData,
227        context: &LayoutContext,
228        node: ServoThreadSafeLayoutNode<'_>,
229    ) -> (ReplacedContentKind, NaturalSizes) {
230        let rule_cache_conditions = &mut RuleCacheConditions::default();
231
232        let parent_style = node.style(&context.style_context);
233        let style_builder = StyleBuilder::new(
234            context.style_context.stylist.device(),
235            Some(context.style_context.stylist),
236            Some(&parent_style),
237            None,
238            None,
239            false,
240        );
241
242        let to_computed_context = Context::new(
243            style_builder,
244            context.style_context.quirks_mode(),
245            rule_cache_conditions,
246            ContainerSizeQuery::none(),
247        );
248
249        let attr_to_computed = |attr_val: &AttrValue| {
250            if let AttrValue::Length(_, length) = attr_val {
251                length.to_computed_value(&to_computed_context)
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                )
294                .ok()
295        });
296
297        let vector_image = cached_image.map(|image| match image {
298            Image::Vector(mut vector_image) => {
299                vector_image.svg_id = Some(svg_data.svg_id);
300                vector_image
301            },
302            _ => unreachable!("SVG element can't contain a raster image."),
303        });
304
305        (ReplacedContentKind::SVGElement(vector_image), natural_size)
306    }
307
308    fn from_content_property(
309        node: ServoThreadSafeLayoutNode<'_>,
310        context: &LayoutContext,
311    ) -> Option<Self> {
312        // If the `content` property is a single image URL, non-replaced boxes
313        // and images get replaced with the given image.
314        if let Content::Items(GenericContentItems { items, .. }) =
315            node.style(&context.style_context).clone_content()
316        {
317            if let [GenericContentItem::Image(image)] = items.as_slice() {
318                // Invalid images are treated as zero-sized.
319                return Some(
320                    Self::from_image(node, context, image)
321                        .unwrap_or_else(|| Self::zero_sized_invalid_image(node)),
322                );
323            }
324        }
325        None
326    }
327
328    pub fn from_image_url(
329        node: ServoThreadSafeLayoutNode<'_>,
330        context: &LayoutContext,
331        image_url: &ComputedUrl,
332    ) -> Option<Self> {
333        if let ComputedUrl::Valid(image_url) = image_url {
334            let (image, width, height) = match context.image_resolver.get_or_request_image_or_meta(
335                node.opaque(),
336                image_url.clone().into(),
337                LayoutImageDestination::BoxTreeConstruction,
338            ) {
339                LayoutImageCacheResult::DataAvailable(img_or_meta) => match img_or_meta {
340                    ImageOrMetadataAvailable::ImageAvailable { image, .. } => {
341                        if let Image::Raster(image) = &image {
342                            context
343                                .image_resolver
344                                .handle_animated_image(node.opaque(), image.clone());
345                        }
346                        let metadata = image.metadata();
347                        (
348                            Some(image.clone()),
349                            metadata.width as f32,
350                            metadata.height as f32,
351                        )
352                    },
353                    ImageOrMetadataAvailable::MetadataAvailable(metadata, _id) => {
354                        (None, metadata.width as f32, metadata.height as f32)
355                    },
356                },
357                LayoutImageCacheResult::Pending | LayoutImageCacheResult::LoadError => return None,
358            };
359            return Some(Self {
360                kind: ReplacedContentKind::Image(ImageInfo {
361                    image,
362                    showing_broken_image_icon: false,
363                    url: Some(image_url.clone().into()),
364                }),
365                natural_size: NaturalSizes::from_width_and_height(width, height),
366                base_fragment_info: node.into(),
367            });
368        }
369        None
370    }
371
372    pub fn from_image(
373        element: ServoThreadSafeLayoutNode<'_>,
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: ServoThreadSafeLayoutNode<'_>) -> 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 mut 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(ArcRefCell::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(ArcRefCell::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(ArcRefCell::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(ArcRefCell::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(vector_image) => {
575                let Some(vector_image) = vector_image else {
576                    return vec![];
577                };
578
579                // TODO: This is incorrect if the SVG has a viewBox.
580                base.rect = PhysicalSize::new(
581                    vector_image
582                        .metadata
583                        .width
584                        .try_into()
585                        .map_or(MAX_AU, Au::from_px),
586                    vector_image
587                        .metadata
588                        .height
589                        .try_into()
590                        .map_or(MAX_AU, Au::from_px),
591                )
592                .into();
593
594                let scale = layout_context.style_context.device_pixel_ratio();
595                let raster_size = Size2D::new(
596                    base.rect.size.width.scale_by(scale.0).to_px(),
597                    base.rect.size.height.scale_by(scale.0).to_px(),
598                );
599
600                let tag = self.base_fragment_info.tag.unwrap();
601                layout_context
602                    .image_resolver
603                    .rasterize_vector_image(
604                        vector_image.id,
605                        raster_size,
606                        tag.node,
607                        vector_image.svg_id.clone(),
608                    )
609                    .and_then(|image| image.id)
610                    .map(|image_key| {
611                        Fragment::Image(ArcRefCell::new(ImageFragment {
612                            base,
613                            clip,
614                            image_key: Some(image_key),
615                            showing_broken_image_icon: false,
616                            url: None,
617                        }))
618                    })
619                    .into_iter()
620                    .collect()
621            },
622            ReplacedContentKind::Audio => vec![],
623        }
624    }
625
626    pub(crate) fn preferred_aspect_ratio(
627        &self,
628        style: &ComputedValues,
629        padding_border_sums: &LogicalVec2<Au>,
630    ) -> Option<AspectRatio> {
631        if matches!(self.kind, ReplacedContentKind::Audio) {
632            // This isn't specified, but other browsers don't support `aspect-ratio` on `<audio>`.
633            // See <https://phabricator.services.mozilla.com/D118245>
634            return None;
635        }
636        if self.is_broken_image() {
637            // This isn't specified, but when an image is broken, we should prefer to the aspect
638            // ratio from the style, rather than the aspect ratio from the broken image icon.
639            // Note that the broken image icon *does* affect the content size of the image
640            // though as we want the image to be as big as the icon if the size was not specified
641            // in the style.
642            style.preferred_aspect_ratio(None, padding_border_sums)
643        } else {
644            style.preferred_aspect_ratio(self.natural_size.ratio, padding_border_sums)
645        }
646    }
647
648    /// The inline size that would result from combining the natural size
649    /// and the default object size, but disregarding the specified size.
650    /// <https://drafts.csswg.org/css-images-3/#natural-dimensions>
651    /// <https://drafts.csswg.org/css-images-3/#default-object-size>
652    /// <https://drafts.csswg.org/css-images-3/#specified-size>
653    pub(crate) fn fallback_inline_size(&self, writing_mode: WritingMode) -> Au {
654        if writing_mode.is_horizontal() {
655            self.natural_size.width.unwrap_or_else(|| Au::from_px(300))
656        } else {
657            self.natural_size.height.unwrap_or_else(|| Au::from_px(150))
658        }
659    }
660
661    /// The block size that would result from combining the natural size
662    /// and the default object size, but disregarding the specified size.
663    /// <https://drafts.csswg.org/css-images-3/#natural-dimensions>
664    /// <https://drafts.csswg.org/css-images-3/#default-object-size>
665    /// <https://drafts.csswg.org/css-images-3/#specified-size>
666    pub(crate) fn fallback_block_size(&self, writing_mode: WritingMode) -> Au {
667        if writing_mode.is_horizontal() {
668            self.natural_size.height.unwrap_or_else(|| Au::from_px(150))
669        } else {
670            self.natural_size.width.unwrap_or_else(|| Au::from_px(300))
671        }
672    }
673
674    pub(crate) fn logical_natural_sizes(
675        &self,
676        writing_mode: WritingMode,
677    ) -> LogicalVec2<Option<Au>> {
678        if writing_mode.is_horizontal() {
679            LogicalVec2 {
680                inline: self.natural_size.width,
681                block: self.natural_size.height,
682            }
683        } else {
684            LogicalVec2 {
685                inline: self.natural_size.height,
686                block: self.natural_size.width,
687            }
688        }
689    }
690
691    #[inline]
692    pub(crate) fn layout_style<'a>(&self, base: &'a LayoutBoxBase) -> LayoutStyle<'a> {
693        LayoutStyle::Default(&base.style)
694    }
695
696    pub(crate) fn layout(
697        &self,
698        layout_context: &LayoutContext,
699        containing_block_for_children: &ContainingBlock,
700        preferred_aspect_ratio: Option<AspectRatio>,
701        base: &LayoutBoxBase,
702        lazy_block_size: &LazySize,
703    ) -> CacheableLayoutResult {
704        let writing_mode = base.style.writing_mode;
705        let inline_size = containing_block_for_children.size.inline;
706        let content_block_size = self.content_size(
707            Direction::Block,
708            preferred_aspect_ratio,
709            &|| SizeConstraint::Definite(inline_size),
710            &|| self.fallback_block_size(writing_mode),
711        );
712        let size = LogicalVec2 {
713            inline: inline_size,
714            block: lazy_block_size.resolve(|| content_block_size),
715        }
716        .to_physical_size(writing_mode);
717        CacheableLayoutResult {
718            baselines: Default::default(),
719            collapsible_margins_in_children: CollapsedBlockMargins::zero(),
720            content_block_size,
721            content_inline_size_for_table: None,
722            // The result doesn't depend on `containing_block_for_children.size.block`,
723            // but it depends on `lazy_block_size`, which is probably tied to that.
724            depends_on_block_constraints: true,
725            fragments: self.make_fragments(layout_context, &base.style, size),
726            specific_layout_info: None,
727        }
728    }
729}
730
731impl ComputeInlineContentSizes for ReplacedContents {
732    fn compute_inline_content_sizes(
733        &self,
734        _: &LayoutContext,
735        constraint_space: &ConstraintSpace,
736    ) -> InlineContentSizesResult {
737        let inline_content_size = self.content_size(
738            Direction::Inline,
739            constraint_space.preferred_aspect_ratio,
740            &|| constraint_space.block_size,
741            &|| self.fallback_inline_size(constraint_space.style.writing_mode),
742        );
743        InlineContentSizesResult {
744            sizes: inline_content_size.into(),
745            depends_on_block_constraints: constraint_space.preferred_aspect_ratio.is_some(),
746        }
747    }
748}
749
750fn try_to_parse_image_data_url(string: &str) -> Option<Url> {
751    if !string.starts_with("data:") {
752        return None;
753    }
754    let data_url = DataUrl::process(string).ok()?;
755    let mime_type = data_url.mime_type();
756    if mime_type.type_ != "image" {
757        return None;
758    }
759
760    // TODO: Find a better way to test for supported image formats. Currently this type of check is
761    // repeated several places in Servo, but should be centralized somehow.
762    if !matches!(
763        mime_type.subtype.as_str(),
764        "png" | "jpeg" | "gif" | "webp" | "bmp" | "ico"
765    ) {
766        return None;
767    }
768
769    Url::parse(string).ok()
770}