1use 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::logical_geometry::{Direction, WritingMode};
23use style::properties::{ComputedValues, StyleBuilder};
24use style::rule_cache::RuleCacheConditions;
25use style::rule_tree::RuleCascadeFlags;
26use style::servo::url::ComputedUrl;
27use style::stylesheets::container_rule::ContainerSizeQuery;
28use style::values::CSSFloat;
29use style::values::computed::image::Image as ComputedImage;
30use style::values::computed::{Content, Context, ToComputedValue};
31use style::values::generics::counters::{GenericContentItem, GenericContentItems};
32use url::Url;
33use web_atoms::local_name;
34use webrender_api::ImageKey;
35
36use crate::context::{LayoutContext, LayoutImageCacheResult};
37use crate::dom::NodeExt;
38use crate::fragment_tree::{
39 BaseFragment, BaseFragmentInfo, CollapsedBlockMargins, Fragment, IFrameFragment, ImageFragment,
40};
41use crate::geom::{LogicalVec2, PhysicalPoint, PhysicalRect, PhysicalSize};
42use crate::layout_box_base::{IndependentFormattingContextLayoutResult, LayoutBoxBase};
43use crate::sizing::{
44 ComputeInlineContentSizes, InlineContentSizesResult, LazySize, SizeConstraint,
45};
46use crate::style_ext::{AspectRatio, Clamp, ComputedValuesExt, LayoutStyle};
47use crate::{ConstraintSpace, ContainingBlock};
48
49#[derive(Debug, MallocSizeOf)]
50pub(crate) struct ReplacedContents {
51 pub kind: ReplacedContentKind,
52 natural_size: NaturalSizes,
53 base_fragment_info: BaseFragmentInfo,
54}
55
56#[derive(Debug, MallocSizeOf)]
74pub(crate) struct NaturalSizes {
75 pub width: Option<Au>,
76 pub height: Option<Au>,
77 pub ratio: Option<CSSFloat>,
78}
79
80impl NaturalSizes {
81 pub(crate) fn from_width_and_height(width: f32, height: f32) -> Self {
82 let ratio = if width.is_normal() && height.is_normal() {
86 Some(width / height)
87 } else {
88 None
89 };
90
91 Self {
92 width: Some(Au::from_f32_px(width)),
93 height: Some(Au::from_f32_px(height)),
94 ratio,
95 }
96 }
97
98 pub(crate) fn from_natural_size_in_dots(natural_size_in_dots: PhysicalSize<f64>) -> Self {
99 let dppx = 1.0;
103 let width = natural_size_in_dots.width as f32 / dppx;
104 let height = natural_size_in_dots.height as f32 / dppx;
105 Self::from_width_and_height(width, height)
106 }
107
108 pub(crate) fn empty() -> Self {
109 Self {
110 width: None,
111 height: None,
112 ratio: None,
113 }
114 }
115}
116
117#[derive(Debug, MallocSizeOf)]
118pub(crate) struct CanvasInfo {
119 pub source: Option<ImageKey>,
120}
121
122#[derive(Debug, MallocSizeOf)]
123pub(crate) struct IFrameInfo {
124 pub pipeline_id: PipelineId,
125 pub browsing_context_id: BrowsingContextId,
126}
127
128#[derive(Debug, MallocSizeOf)]
129pub(crate) struct ImageInfo {
130 pub image: Option<Image>,
131 pub showing_broken_image_icon: bool,
132 pub url: Option<ServoUrl>,
133}
134
135#[derive(Debug, MallocSizeOf)]
136pub(crate) struct VideoInfo {
137 pub image_key: Option<ImageKey>,
138}
139
140#[derive(Debug, MallocSizeOf)]
141pub(crate) enum ReplacedContentKind {
142 Image(ImageInfo),
143 IFrame(IFrameInfo),
144 Canvas(CanvasInfo),
145 Video(VideoInfo),
146 SVGElement {
147 vector_image: Option<VectorImage>,
148 has_viewbox: bool,
149 },
150 Audio,
151}
152
153impl ReplacedContents {
154 pub fn for_element(node: ServoLayoutNode<'_>, context: &LayoutContext) -> Option<Self> {
155 if let Some(ref data_attribute_string) = node.as_typeless_object_with_data_attribute() &&
156 let Some(url) = try_to_parse_image_data_url(data_attribute_string)
157 {
158 return Self::from_image_url(node, context, &ComputedUrl::Valid(ServoArc::new(url)));
159 }
160
161 let (kind, natural_size) = {
162 if let Some((image_info, natural_size_in_dots)) = node.as_image() {
163 if let Some(content_image) = Self::from_content_property(node, context) {
164 return Some(content_image);
165 }
166 (
167 ReplacedContentKind::Image(image_info),
168 NaturalSizes::from_natural_size_in_dots(natural_size_in_dots),
169 )
170 } else if let Some((canvas_info, natural_size_in_dots)) = node.as_canvas() {
171 (
172 ReplacedContentKind::Canvas(canvas_info),
173 NaturalSizes::from_natural_size_in_dots(natural_size_in_dots),
174 )
175 } else if let Some(iframe_info) = node.as_iframe() {
176 (
177 ReplacedContentKind::IFrame(iframe_info),
178 NaturalSizes::empty(),
179 )
180 } else if let Some((video_info, natural_size_in_dots)) = node.as_video() {
181 (
182 ReplacedContentKind::Video(video_info),
183 natural_size_in_dots
184 .map_or_else(NaturalSizes::empty, NaturalSizes::from_natural_size_in_dots),
185 )
186 } else if let Some(svg_data) = node.as_svg() {
187 Self::svg_kind_size(svg_data, context, node)
188 } else if node
189 .as_html_element()
190 .is_some_and(|element| element.local_name() == &local_name!("audio"))
191 {
192 let natural_size = NaturalSizes {
193 width: None,
194 height: Some(Au::from_px(40)),
197 ratio: None,
198 };
199 (ReplacedContentKind::Audio, natural_size)
200 } else {
201 return Self::from_content_property(node, context);
202 }
203 };
204
205 if let ReplacedContentKind::Image(ImageInfo {
206 image: Some(Image::Raster(ref image)),
207 ..
208 }) = kind
209 {
210 context
211 .image_resolver
212 .handle_animated_image(node.opaque(), image.clone());
213 }
214
215 Some(Self {
216 kind,
217 natural_size,
218 base_fragment_info: node.into(),
219 })
220 }
221
222 fn svg_kind_size(
223 svg_data: SVGElementData,
224 context: &LayoutContext,
225 node: ServoLayoutNode<'_>,
226 ) -> (ReplacedContentKind, NaturalSizes) {
227 let rule_cache_conditions = &mut RuleCacheConditions::default();
228
229 let parent_style = node.style(&context.style_context);
230 let style_builder = StyleBuilder::new(
231 context.style_context.stylist.device(),
232 Some(context.style_context.stylist),
233 Some(&parent_style),
234 None,
235 None,
236 false,
237 );
238
239 let to_computed_context = Context::new(
240 style_builder,
241 context.style_context.quirks_mode(),
242 rule_cache_conditions,
243 ContainerSizeQuery::none(),
244 RuleCascadeFlags::empty(),
245 );
246
247 let attr_to_computed = |attr_val: &AttrValue| {
248 if let AttrValue::LengthPercentage(_, length_percentage) = attr_val {
249 length_percentage
250 .to_computed_value(&to_computed_context)?
251 .to_length()
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 context
277 .image_resolver
278 .queue_svg_element_for_serialization(node);
279 None
280 },
281 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 InternalRequest::Yes,
294 )
295 .ok()
296 });
297
298 let vector_image = cached_image.map(|image| match image {
299 Image::Vector(mut vector_image) => {
300 vector_image.svg_id = Some(svg_data.svg_id);
301 vector_image
302 },
303 _ => unreachable!("SVG element can't contain a raster image."),
304 });
305
306 (
307 ReplacedContentKind::SVGElement {
308 vector_image,
309 has_viewbox: svg_data.view_box.is_some(),
310 },
311 natural_size,
312 )
313 }
314
315 fn from_content_property(node: ServoLayoutNode<'_>, context: &LayoutContext) -> Option<Self> {
316 if let Content::Items(GenericContentItems { items, .. }) =
319 node.style(&context.style_context).clone_content() &&
320 let [GenericContentItem::Image(image)] = items.as_slice()
321 {
322 return Some(
324 Self::from_image(node, context, image)
325 .unwrap_or_else(|| Self::zero_sized_invalid_image(node)),
326 );
327 }
328 None
329 }
330
331 pub fn from_image_url(
332 node: ServoLayoutNode<'_>,
333 context: &LayoutContext,
334 image_url: &ComputedUrl,
335 ) -> Option<Self> {
336 let ComputedUrl::Valid(image_url) = image_url else {
337 return None;
338 };
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 InternalRequest::No,
344 ) {
345 LayoutImageCacheResult::DataAvailable(img_or_meta) => match img_or_meta {
346 ImageOrMetadataAvailable::ImageAvailable { image, .. } => {
347 if let Image::Raster(image) = &image {
348 context
349 .image_resolver
350 .handle_animated_image(node.opaque(), image.clone());
351 }
352 let metadata = image.metadata();
353 (Some(image), metadata.width as f32, metadata.height as f32)
354 },
355 ImageOrMetadataAvailable::MetadataAvailable(metadata, _id) => {
356 (None, metadata.width as f32, metadata.height as f32)
357 },
358 },
359 LayoutImageCacheResult::Pending | LayoutImageCacheResult::LoadError => return None,
360 };
361 Some(Self {
362 kind: ReplacedContentKind::Image(ImageInfo {
363 image,
364 showing_broken_image_icon: false,
365 url: Some(image_url.clone().into()),
366 }),
367 natural_size: NaturalSizes::from_width_and_height(width, height),
368 base_fragment_info: node.into(),
369 })
370 }
371
372 pub fn from_image(
373 element: ServoLayoutNode<'_>,
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, }
381 }
382
383 pub(crate) fn zero_sized_invalid_image(node: ServoLayoutNode<'_>) -> 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 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(Arc::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(Arc::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(Arc::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(Arc::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 {
575 vector_image,
576 has_viewbox,
577 } => {
578 let Some(vector_image) = vector_image else {
579 return vec![];
580 };
581
582 if !has_viewbox {
583 base.set_rect(
584 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
600 let scale = layout_context.style_context.device_pixel_ratio();
601 let content_size = base.rect().size;
602 let raster_size = Size2D::new(
603 content_size.width.scale_by(scale.0).to_px(),
604 content_size.height.scale_by(scale.0).to_px(),
605 );
606
607 let tag = self.base_fragment_info.tag.unwrap();
608 layout_context
609 .image_resolver
610 .rasterize_vector_image(
611 vector_image.id,
612 raster_size,
613 tag.node,
614 vector_image.svg_id.clone(),
615 )
616 .and_then(|image| image.id)
617 .map(|image_key| {
618 Fragment::Image(Arc::new(ImageFragment {
619 base,
620 clip,
621 image_key: Some(image_key),
622 showing_broken_image_icon: false,
623 url: None,
624 }))
625 })
626 .into_iter()
627 .collect()
628 },
629 ReplacedContentKind::Audio => vec![],
630 }
631 }
632
633 pub(crate) fn preferred_aspect_ratio(
634 &self,
635 style: &ComputedValues,
636 padding_border_sums: &LogicalVec2<Au>,
637 ) -> Option<AspectRatio> {
638 if matches!(self.kind, ReplacedContentKind::Audio) {
639 return None;
642 }
643 if self.is_broken_image() {
644 style.preferred_aspect_ratio(None, padding_border_sums)
650 } else {
651 style.preferred_aspect_ratio(self.natural_size.ratio, padding_border_sums)
652 }
653 }
654
655 pub(crate) fn fallback_inline_size(&self, writing_mode: WritingMode) -> Au {
661 if writing_mode.is_horizontal() {
662 self.natural_size.width.unwrap_or_else(|| Au::from_px(300))
663 } else {
664 self.natural_size.height.unwrap_or_else(|| Au::from_px(150))
665 }
666 }
667
668 pub(crate) fn fallback_block_size(&self, writing_mode: WritingMode) -> Au {
674 if writing_mode.is_horizontal() {
675 self.natural_size.height.unwrap_or_else(|| Au::from_px(150))
676 } else {
677 self.natural_size.width.unwrap_or_else(|| Au::from_px(300))
678 }
679 }
680
681 pub(crate) fn logical_natural_sizes(
682 &self,
683 writing_mode: WritingMode,
684 ) -> LogicalVec2<Option<Au>> {
685 if writing_mode.is_horizontal() {
686 LogicalVec2 {
687 inline: self.natural_size.width,
688 block: self.natural_size.height,
689 }
690 } else {
691 LogicalVec2 {
692 inline: self.natural_size.height,
693 block: self.natural_size.width,
694 }
695 }
696 }
697
698 #[inline]
699 pub(crate) fn layout_style<'a>(&self, base: &'a LayoutBoxBase) -> LayoutStyle<'a> {
700 LayoutStyle::Default(&base.style)
701 }
702
703 pub(crate) fn layout(
704 &self,
705 layout_context: &LayoutContext,
706 containing_block_for_children: &ContainingBlock,
707 preferred_aspect_ratio: Option<AspectRatio>,
708 base: &LayoutBoxBase,
709 lazy_block_size: &LazySize,
710 ) -> IndependentFormattingContextLayoutResult {
711 let writing_mode = base.style.writing_mode;
712 let inline_size = containing_block_for_children.size.inline;
713 let content_block_size = self.content_size(
714 Direction::Block,
715 preferred_aspect_ratio,
716 &|| SizeConstraint::Definite(inline_size),
717 &|| self.fallback_block_size(writing_mode),
718 );
719 let size = LogicalVec2 {
720 inline: inline_size,
721 block: lazy_block_size.resolve(|| content_block_size),
722 }
723 .to_physical_size(writing_mode);
724 IndependentFormattingContextLayoutResult {
725 baselines: Default::default(),
726 collapsible_margins_in_children: CollapsedBlockMargins::zero(),
727 content_block_size,
728 content_inline_size_for_table: None,
729 depends_on_block_constraints: true,
732 fragments: self.make_fragments(layout_context, &base.style, size),
733 specific_layout_info: None,
734 }
735 }
736}
737
738impl ComputeInlineContentSizes for ReplacedContents {
739 fn compute_inline_content_sizes(
740 &self,
741 _: &LayoutContext,
742 constraint_space: &ConstraintSpace,
743 ) -> InlineContentSizesResult {
744 let inline_content_size = self.content_size(
745 Direction::Inline,
746 constraint_space.preferred_aspect_ratio,
747 &|| constraint_space.block_size,
748 &|| self.fallback_inline_size(constraint_space.style.writing_mode),
749 );
750 InlineContentSizesResult {
751 sizes: inline_content_size.into(),
752 depends_on_block_constraints: constraint_space.preferred_aspect_ratio.is_some(),
753 }
754 }
755}
756
757fn try_to_parse_image_data_url(string: &str) -> Option<Url> {
758 if !string.starts_with("data:") {
759 return None;
760 }
761 let data_url = DataUrl::process(string).ok()?;
762 let mime_type = data_url.mime_type();
763 if mime_type.type_ != "image" {
764 return None;
765 }
766
767 if !matches!(
770 mime_type.subtype.as_str(),
771 "png" | "jpeg" | "gif" | "webp" | "bmp" | "ico"
772 ) {
773 return None;
774 }
775
776 Url::parse(string).ok()
777}