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