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