1use app_units::{Au, MAX_AU};
6use base::id::{BrowsingContextId, PipelineId};
7use data_url::DataUrl;
8use embedder_traits::ViewportDetails;
9use euclid::{Scale, Size2D};
10use layout_api::wrapper_traits::ThreadSafeLayoutNode;
11use layout_api::{IFrameSize, LayoutImageDestination};
12use malloc_size_of_derive::MallocSizeOf;
13use net_traits::image_cache::{Image, ImageOrMetadataAvailable, VectorImage};
14use script::layout_dom::ServoThreadSafeLayoutNode;
15use servo_arc::Arc as ServoArc;
16use style::Zero;
17use style::computed_values::object_fit::T as ObjectFit;
18use style::logical_geometry::{Direction, WritingMode};
19use style::properties::ComputedValues;
20use style::servo::url::ComputedUrl;
21use style::values::CSSFloat;
22use style::values::computed::image::Image as ComputedImage;
23use url::Url;
24use webrender_api::ImageKey;
25
26use crate::cell::ArcRefCell;
27use crate::context::{LayoutContext, LayoutImageCacheResult};
28use crate::dom::NodeExt;
29use crate::fragment_tree::{
30 BaseFragmentInfo, CollapsedBlockMargins, Fragment, IFrameFragment, ImageFragment,
31};
32use crate::geom::{LogicalVec2, PhysicalPoint, PhysicalRect, PhysicalSize};
33use crate::layout_box_base::{CacheableLayoutResult, LayoutBoxBase};
34use crate::sizing::{
35 ComputeInlineContentSizes, InlineContentSizesResult, LazySize, SizeConstraint,
36};
37use crate::style_ext::{AspectRatio, Clamp, ComputedValuesExt, LayoutStyle};
38use crate::{ConstraintSpace, ContainingBlock};
39
40#[derive(Debug, MallocSizeOf)]
41pub(crate) struct ReplacedContents {
42 pub kind: ReplacedContentKind,
43 natural_size: NaturalSizes,
44 base_fragment_info: BaseFragmentInfo,
45}
46
47#[derive(Debug, MallocSizeOf)]
65pub(crate) struct NaturalSizes {
66 pub width: Option<Au>,
67 pub height: Option<Au>,
68 pub ratio: Option<CSSFloat>,
69}
70
71impl NaturalSizes {
72 pub(crate) fn from_width_and_height(width: f32, height: f32) -> Self {
73 let ratio = if width.is_normal() && height.is_normal() {
77 Some(width / height)
78 } else {
79 None
80 };
81
82 Self {
83 width: Some(Au::from_f32_px(width)),
84 height: Some(Au::from_f32_px(height)),
85 ratio,
86 }
87 }
88
89 pub(crate) fn from_natural_size_in_dots(natural_size_in_dots: PhysicalSize<f64>) -> Self {
90 let dppx = 1.0;
94 let width = natural_size_in_dots.width as f32 / dppx;
95 let height = natural_size_in_dots.height as f32 / dppx;
96 Self::from_width_and_height(width, height)
97 }
98
99 pub(crate) fn empty() -> Self {
100 Self {
101 width: None,
102 height: None,
103 ratio: None,
104 }
105 }
106}
107
108#[derive(Debug, MallocSizeOf)]
109pub(crate) struct CanvasInfo {
110 pub source: Option<ImageKey>,
111}
112
113#[derive(Debug, MallocSizeOf)]
114pub(crate) struct IFrameInfo {
115 pub pipeline_id: PipelineId,
116 pub browsing_context_id: BrowsingContextId,
117}
118
119#[derive(Debug, MallocSizeOf)]
120pub(crate) struct VideoInfo {
121 pub image_key: webrender_api::ImageKey,
122}
123
124#[derive(Debug, MallocSizeOf)]
125pub(crate) enum ReplacedContentKind {
126 Image(Option<Image>, bool ),
127 IFrame(IFrameInfo),
128 Canvas(CanvasInfo),
129 Video(Option<VideoInfo>),
130 SVGElement(Option<VectorImage>),
131}
132
133impl ReplacedContents {
134 pub fn for_element(
135 node: ServoThreadSafeLayoutNode<'_>,
136 context: &LayoutContext,
137 ) -> Option<Self> {
138 if let Some(ref data_attribute_string) = node.as_typeless_object_with_data_attribute() {
139 if let Some(url) = try_to_parse_image_data_url(data_attribute_string) {
140 return Self::from_image_url(
141 node,
142 context,
143 &ComputedUrl::Valid(ServoArc::new(url)),
144 );
145 }
146 }
147
148 let (kind, natural_size) = {
149 if let Some((image, natural_size_in_dots)) = node.as_image() {
150 (
151 ReplacedContentKind::Image(image, node.showing_broken_image_icon()),
152 NaturalSizes::from_natural_size_in_dots(natural_size_in_dots),
153 )
154 } else if let Some((canvas_info, natural_size_in_dots)) = node.as_canvas() {
155 (
156 ReplacedContentKind::Canvas(canvas_info),
157 NaturalSizes::from_natural_size_in_dots(natural_size_in_dots),
158 )
159 } else if let Some((pipeline_id, browsing_context_id)) = node.as_iframe() {
160 (
161 ReplacedContentKind::IFrame(IFrameInfo {
162 pipeline_id,
163 browsing_context_id,
164 }),
165 NaturalSizes::empty(),
166 )
167 } else if let Some((image_key, natural_size_in_dots)) = node.as_video() {
168 (
169 ReplacedContentKind::Video(image_key.map(|key| VideoInfo { image_key: key })),
170 natural_size_in_dots
171 .map_or_else(NaturalSizes::empty, NaturalSizes::from_natural_size_in_dots),
172 )
173 } else if let Some(svg_data) = node.as_svg() {
174 let svg_source = match svg_data.source {
175 None => {
176 context
179 .image_resolver
180 .queue_svg_element_for_serialization(node);
181 return None;
182 },
183 Some(Err(_)) => {
184 return None;
186 },
187 Some(Ok(svg_source)) => svg_source,
188 };
189
190 let result = context
191 .image_resolver
192 .get_cached_image_for_url(
193 node.opaque(),
194 svg_source,
195 LayoutImageDestination::BoxTreeConstruction,
196 )
197 .ok();
198
199 let vector_image = result.map(|result| match result {
200 Image::Vector(vector_image) => vector_image,
201 _ => unreachable!("SVG element can't contain a raster image."),
202 });
203 let natural_size = NaturalSizes {
204 width: svg_data.width.map(Au::from_px),
205 height: svg_data.height.map(Au::from_px),
206 ratio: svg_data.ratio,
207 };
208 (ReplacedContentKind::SVGElement(vector_image), natural_size)
209 } else {
210 return None;
211 }
212 };
213
214 if let ReplacedContentKind::Image(Some(Image::Raster(ref image)), _) = kind {
215 context
216 .image_resolver
217 .handle_animated_image(node.opaque(), image.clone());
218 }
219
220 Some(Self {
221 kind,
222 natural_size,
223 base_fragment_info: node.into(),
224 })
225 }
226
227 pub fn from_image_url(
228 node: ServoThreadSafeLayoutNode<'_>,
229 context: &LayoutContext,
230 image_url: &ComputedUrl,
231 ) -> Option<Self> {
232 if let ComputedUrl::Valid(image_url) = image_url {
233 let (image, width, height) = match context.image_resolver.get_or_request_image_or_meta(
234 node.opaque(),
235 image_url.clone().into(),
236 LayoutImageDestination::BoxTreeConstruction,
237 ) {
238 LayoutImageCacheResult::DataAvailable(img_or_meta) => match img_or_meta {
239 ImageOrMetadataAvailable::ImageAvailable { image, .. } => {
240 let metadata = image.metadata();
241 (
242 Some(image.clone()),
243 metadata.width as f32,
244 metadata.height as f32,
245 )
246 },
247 ImageOrMetadataAvailable::MetadataAvailable(metadata, _id) => {
248 (None, metadata.width as f32, metadata.height as f32)
249 },
250 },
251 LayoutImageCacheResult::Pending | LayoutImageCacheResult::LoadError => return None,
252 };
253
254 return Some(Self {
255 kind: ReplacedContentKind::Image(image, false ),
256 natural_size: NaturalSizes::from_width_and_height(width, height),
257 base_fragment_info: node.into(),
258 });
259 }
260 None
261 }
262
263 pub fn from_image(
264 element: ServoThreadSafeLayoutNode<'_>,
265 context: &LayoutContext,
266 image: &ComputedImage,
267 ) -> Option<Self> {
268 match image {
269 ComputedImage::Url(image_url) => Self::from_image_url(element, context, image_url),
270 _ => None, }
272 }
273
274 #[inline]
275 fn is_broken_image(&self) -> bool {
276 matches!(self.kind, ReplacedContentKind::Image(_, true))
277 }
278
279 #[inline]
280 fn content_size(
281 &self,
282 axis: Direction,
283 preferred_aspect_ratio: Option<AspectRatio>,
284 get_size_in_opposite_axis: &dyn Fn() -> SizeConstraint,
285 get_fallback_size: &dyn Fn() -> Au,
286 ) -> Au {
287 let Some(ratio) = preferred_aspect_ratio else {
288 return get_fallback_size();
289 };
290 let transfer = |size| ratio.compute_dependent_size(axis, size);
291 match get_size_in_opposite_axis() {
292 SizeConstraint::Definite(size) => transfer(size),
293 SizeConstraint::MinMax(min_size, max_size) => get_fallback_size()
294 .clamp_between_extremums(transfer(min_size), max_size.map(transfer)),
295 }
296 }
297
298 fn calculate_fragment_rect(
299 &self,
300 style: &ServoArc<ComputedValues>,
301 size: PhysicalSize<Au>,
302 ) -> (PhysicalSize<Au>, PhysicalRect<Au>) {
303 if let ReplacedContentKind::Image(Some(Image::Raster(image)), true) = &self.kind {
304 let size = Size2D::new(
305 Au::from_f32_px(image.metadata.width as f32),
306 Au::from_f32_px(image.metadata.height as f32),
307 )
308 .min(size);
309 return (PhysicalSize::zero(), size.into());
310 }
311
312 let natural_size = PhysicalSize::new(
313 self.natural_size.width.unwrap_or(size.width),
314 self.natural_size.height.unwrap_or(size.height),
315 );
316
317 let object_fit_size = self.natural_size.ratio.map_or(size, |width_over_height| {
318 let preserve_aspect_ratio_with_comparison =
319 |size: PhysicalSize<Au>, comparison: fn(&Au, &Au) -> bool| {
320 let candidate_width = size.height.scale_by(width_over_height);
321 if comparison(&candidate_width, &size.width) {
322 return PhysicalSize::new(candidate_width, size.height);
323 }
324
325 let candidate_height = size.width.scale_by(1. / width_over_height);
326 debug_assert!(comparison(&candidate_height, &size.height));
327 PhysicalSize::new(size.width, candidate_height)
328 };
329
330 match style.clone_object_fit() {
331 ObjectFit::Fill => size,
332 ObjectFit::Contain => preserve_aspect_ratio_with_comparison(size, PartialOrd::le),
333 ObjectFit::Cover => preserve_aspect_ratio_with_comparison(size, PartialOrd::ge),
334 ObjectFit::None => natural_size,
335 ObjectFit::ScaleDown => {
336 preserve_aspect_ratio_with_comparison(size.min(natural_size), PartialOrd::le)
337 },
338 }
339 });
340
341 let object_position = style.clone_object_position();
342 let horizontal_position = object_position
343 .horizontal
344 .to_used_value(size.width - object_fit_size.width);
345 let vertical_position = object_position
346 .vertical
347 .to_used_value(size.height - object_fit_size.height);
348
349 let object_position = PhysicalPoint::new(horizontal_position, vertical_position);
350 (
351 object_fit_size,
352 PhysicalRect::new(object_position, object_fit_size),
353 )
354 }
355
356 pub fn make_fragments(
357 &self,
358 layout_context: &LayoutContext,
359 style: &ServoArc<ComputedValues>,
360 size: PhysicalSize<Au>,
361 ) -> Vec<Fragment> {
362 let (object_fit_size, rect) = self.calculate_fragment_rect(style, size);
363 let clip = PhysicalRect::new(PhysicalPoint::origin(), size);
364
365 match &self.kind {
366 ReplacedContentKind::Image(image, showing_broken_image_icon) => image
367 .as_ref()
368 .and_then(|image| match image {
369 Image::Raster(raster_image) => raster_image.id,
370 Image::Vector(vector_image) => {
371 let scale = layout_context.style_context.device_pixel_ratio();
372 let width = object_fit_size.width.scale_by(scale.0).to_px();
373 let height = object_fit_size.height.scale_by(scale.0).to_px();
374 let size = Size2D::new(width, height);
375 let tag = self.base_fragment_info.tag?;
376 layout_context
377 .image_resolver
378 .rasterize_vector_image(vector_image.id, size, tag.node)
379 .and_then(|i| i.id)
380 },
381 })
382 .map(|image_key| {
383 Fragment::Image(ArcRefCell::new(ImageFragment {
384 base: self.base_fragment_info.into(),
385 style: style.clone(),
386 rect,
387 clip,
388 image_key: Some(image_key),
389 showing_broken_image_icon: *showing_broken_image_icon,
390 }))
391 })
392 .into_iter()
393 .collect(),
394 ReplacedContentKind::Video(video) => {
395 vec![Fragment::Image(ArcRefCell::new(ImageFragment {
396 base: self.base_fragment_info.into(),
397 style: style.clone(),
398 rect,
399 clip,
400 image_key: video.as_ref().map(|video| video.image_key),
401 showing_broken_image_icon: false,
402 }))]
403 },
404 ReplacedContentKind::IFrame(iframe) => {
405 let size = Size2D::new(rect.size.width.to_f32_px(), rect.size.height.to_f32_px());
406 let hidpi_scale_factor = layout_context.style_context.device_pixel_ratio();
407
408 layout_context.iframe_sizes.lock().insert(
409 iframe.browsing_context_id,
410 IFrameSize {
411 browsing_context_id: iframe.browsing_context_id,
412 pipeline_id: iframe.pipeline_id,
413 viewport_details: ViewportDetails {
414 size,
415 hidpi_scale_factor: Scale::new(hidpi_scale_factor.0),
416 },
417 },
418 );
419 vec![Fragment::IFrame(ArcRefCell::new(IFrameFragment {
420 base: self.base_fragment_info.into(),
421 style: style.clone(),
422 pipeline_id: iframe.pipeline_id,
423 rect,
424 }))]
425 },
426 ReplacedContentKind::Canvas(canvas_info) => {
427 if self.natural_size.width == Some(Au::zero()) ||
428 self.natural_size.height == Some(Au::zero())
429 {
430 return vec![];
431 }
432
433 let Some(image_key) = canvas_info.source else {
434 return vec![];
435 };
436
437 vec![Fragment::Image(ArcRefCell::new(ImageFragment {
438 base: self.base_fragment_info.into(),
439 style: style.clone(),
440 rect,
441 clip,
442 image_key: Some(image_key),
443 showing_broken_image_icon: false,
444 }))]
445 },
446 ReplacedContentKind::SVGElement(vector_image) => {
447 let Some(vector_image) = vector_image else {
448 return vec![];
449 };
450 let scale = layout_context.style_context.device_pixel_ratio();
451 let size = PhysicalSize::new(
453 vector_image
454 .metadata
455 .width
456 .try_into()
457 .map_or(MAX_AU, Au::from_px),
458 vector_image
459 .metadata
460 .height
461 .try_into()
462 .map_or(MAX_AU, Au::from_px),
463 );
464 let rect = PhysicalRect::from_size(size);
465 let raster_size = Size2D::new(
466 size.width.scale_by(scale.0).to_px(),
467 size.height.scale_by(scale.0).to_px(),
468 );
469 let tag = self.base_fragment_info.tag.unwrap();
470 layout_context
471 .image_resolver
472 .rasterize_vector_image(vector_image.id, raster_size, tag.node)
473 .and_then(|image| image.id)
474 .map(|image_key| {
475 Fragment::Image(ArcRefCell::new(ImageFragment {
476 base: self.base_fragment_info.into(),
477 style: style.clone(),
478 rect,
479 clip,
480 image_key: Some(image_key),
481 showing_broken_image_icon: false,
482 }))
483 })
484 .into_iter()
485 .collect()
486 },
487 }
488 }
489
490 pub(crate) fn preferred_aspect_ratio(
491 &self,
492 style: &ComputedValues,
493 padding_border_sums: &LogicalVec2<Au>,
494 ) -> Option<AspectRatio> {
495 if self.is_broken_image() {
496 style.preferred_aspect_ratio(None, padding_border_sums)
502 } else {
503 style.preferred_aspect_ratio(self.natural_size.ratio, padding_border_sums)
504 }
505 }
506
507 pub(crate) fn fallback_inline_size(&self, writing_mode: WritingMode) -> Au {
513 if writing_mode.is_horizontal() {
514 self.natural_size.width.unwrap_or_else(|| Au::from_px(300))
515 } else {
516 self.natural_size.height.unwrap_or_else(|| Au::from_px(150))
517 }
518 }
519
520 pub(crate) fn fallback_block_size(&self, writing_mode: WritingMode) -> Au {
526 if writing_mode.is_horizontal() {
527 self.natural_size.height.unwrap_or_else(|| Au::from_px(150))
528 } else {
529 self.natural_size.width.unwrap_or_else(|| Au::from_px(300))
530 }
531 }
532
533 #[inline]
534 pub(crate) fn layout_style<'a>(&self, base: &'a LayoutBoxBase) -> LayoutStyle<'a> {
535 LayoutStyle::Default(&base.style)
536 }
537
538 pub(crate) fn layout(
539 &self,
540 layout_context: &LayoutContext,
541 containing_block_for_children: &ContainingBlock,
542 preferred_aspect_ratio: Option<AspectRatio>,
543 base: &LayoutBoxBase,
544 lazy_block_size: &LazySize,
545 ) -> CacheableLayoutResult {
546 let writing_mode = base.style.writing_mode;
547 let inline_size = containing_block_for_children.size.inline;
548 let content_block_size = self.content_size(
549 Direction::Block,
550 preferred_aspect_ratio,
551 &|| SizeConstraint::Definite(inline_size),
552 &|| self.fallback_block_size(writing_mode),
553 );
554 let size = LogicalVec2 {
555 inline: inline_size,
556 block: lazy_block_size.resolve(|| content_block_size),
557 }
558 .to_physical_size(writing_mode);
559 CacheableLayoutResult {
560 baselines: Default::default(),
561 collapsible_margins_in_children: CollapsedBlockMargins::zero(),
562 content_block_size,
563 content_inline_size_for_table: None,
564 depends_on_block_constraints: true,
567 fragments: self.make_fragments(layout_context, &base.style, size),
568 specific_layout_info: None,
569 }
570 }
571}
572
573impl ComputeInlineContentSizes for ReplacedContents {
574 fn compute_inline_content_sizes(
575 &self,
576 _: &LayoutContext,
577 constraint_space: &ConstraintSpace,
578 ) -> InlineContentSizesResult {
579 let inline_content_size = self.content_size(
580 Direction::Inline,
581 constraint_space.preferred_aspect_ratio,
582 &|| constraint_space.block_size,
583 &|| self.fallback_inline_size(constraint_space.style.writing_mode),
584 );
585 InlineContentSizesResult {
586 sizes: inline_content_size.into(),
587 depends_on_block_constraints: constraint_space.preferred_aspect_ratio.is_some(),
588 }
589 }
590}
591
592fn try_to_parse_image_data_url(string: &str) -> Option<Url> {
593 if !string.starts_with("data:") {
594 return None;
595 }
596 let data_url = DataUrl::process(string).ok()?;
597 let mime_type = data_url.mime_type();
598 if mime_type.type_ != "image" {
599 return None;
600 }
601
602 if !matches!(
605 mime_type.subtype.as_str(),
606 "png" | "jpeg" | "gif" | "webp" | "bmp" | "ico"
607 ) {
608 return None;
609 }
610
611 Url::parse(string).ok()
612}