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