Skip to main content

layout/
replaced.rs

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