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, UsePlaceholder, 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>),
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),
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 UsePlaceholder::No,
196 LayoutImageDestination::BoxTreeConstruction,
197 )
198 .ok();
199
200 let vector_image = result.map(|result| match result {
201 Image::Vector(vector_image) => vector_image,
202 _ => unreachable!("SVG element can't contain a raster image."),
203 });
204 let natural_size = NaturalSizes {
205 width: svg_data.width.map(Au::from_px),
206 height: svg_data.height.map(Au::from_px),
207 ratio: svg_data.ratio,
208 };
209 (ReplacedContentKind::SVGElement(vector_image), natural_size)
210 } else {
211 return None;
212 }
213 };
214
215 if let ReplacedContentKind::Image(Some(Image::Raster(ref image))) = kind {
216 context
217 .image_resolver
218 .handle_animated_image(node.opaque(), image.clone());
219 }
220
221 Some(Self {
222 kind,
223 natural_size,
224 base_fragment_info: node.into(),
225 })
226 }
227
228 pub fn from_image_url(
229 node: ServoThreadSafeLayoutNode<'_>,
230 context: &LayoutContext,
231 image_url: &ComputedUrl,
232 ) -> Option<Self> {
233 if let ComputedUrl::Valid(image_url) = image_url {
234 let (image, width, height) = match context.image_resolver.get_or_request_image_or_meta(
235 node.opaque(),
236 image_url.clone().into(),
237 UsePlaceholder::No,
238 LayoutImageDestination::BoxTreeConstruction,
239 ) {
240 LayoutImageCacheResult::DataAvailable(img_or_meta) => match img_or_meta {
241 ImageOrMetadataAvailable::ImageAvailable { image, .. } => {
242 let metadata = image.metadata();
243 (
244 Some(image.clone()),
245 metadata.width as f32,
246 metadata.height as f32,
247 )
248 },
249 ImageOrMetadataAvailable::MetadataAvailable(metadata, _id) => {
250 (None, metadata.width as f32, metadata.height as f32)
251 },
252 },
253 LayoutImageCacheResult::Pending | LayoutImageCacheResult::LoadError => return None,
254 };
255
256 return Some(Self {
257 kind: ReplacedContentKind::Image(image),
258 natural_size: NaturalSizes::from_width_and_height(width, height),
259 base_fragment_info: node.into(),
260 });
261 }
262 None
263 }
264
265 pub fn from_image(
266 element: ServoThreadSafeLayoutNode<'_>,
267 context: &LayoutContext,
268 image: &ComputedImage,
269 ) -> Option<Self> {
270 match image {
271 ComputedImage::Url(image_url) => Self::from_image_url(element, context, image_url),
272 _ => None, }
274 }
275
276 #[inline]
277 fn content_size(
278 &self,
279 axis: Direction,
280 preferred_aspect_ratio: Option<AspectRatio>,
281 get_size_in_opposite_axis: &dyn Fn() -> SizeConstraint,
282 get_fallback_size: &dyn Fn() -> Au,
283 ) -> Au {
284 let Some(ratio) = preferred_aspect_ratio else {
285 return get_fallback_size();
286 };
287 let transfer = |size| ratio.compute_dependent_size(axis, size);
288 match get_size_in_opposite_axis() {
289 SizeConstraint::Definite(size) => transfer(size),
290 SizeConstraint::MinMax(min_size, max_size) => get_fallback_size()
291 .clamp_between_extremums(transfer(min_size), max_size.map(transfer)),
292 }
293 }
294
295 pub fn make_fragments(
296 &self,
297 layout_context: &LayoutContext,
298 style: &ServoArc<ComputedValues>,
299 size: PhysicalSize<Au>,
300 ) -> Vec<Fragment> {
301 let natural_size = PhysicalSize::new(
302 self.natural_size.width.unwrap_or(size.width),
303 self.natural_size.height.unwrap_or(size.height),
304 );
305
306 let object_fit_size = self.natural_size.ratio.map_or(size, |width_over_height| {
307 let preserve_aspect_ratio_with_comparison =
308 |size: PhysicalSize<Au>, comparison: fn(&Au, &Au) -> bool| {
309 let candidate_width = size.height.scale_by(width_over_height);
310 if comparison(&candidate_width, &size.width) {
311 return PhysicalSize::new(candidate_width, size.height);
312 }
313
314 let candidate_height = size.width.scale_by(1. / width_over_height);
315 debug_assert!(comparison(&candidate_height, &size.height));
316 PhysicalSize::new(size.width, candidate_height)
317 };
318
319 match style.clone_object_fit() {
320 ObjectFit::Fill => size,
321 ObjectFit::Contain => preserve_aspect_ratio_with_comparison(size, PartialOrd::le),
322 ObjectFit::Cover => preserve_aspect_ratio_with_comparison(size, PartialOrd::ge),
323 ObjectFit::None => natural_size,
324 ObjectFit::ScaleDown => {
325 preserve_aspect_ratio_with_comparison(size.min(natural_size), PartialOrd::le)
326 },
327 }
328 });
329
330 let object_position = style.clone_object_position();
331 let horizontal_position = object_position
332 .horizontal
333 .to_used_value(size.width - object_fit_size.width);
334 let vertical_position = object_position
335 .vertical
336 .to_used_value(size.height - object_fit_size.height);
337
338 let rect = PhysicalRect::new(
339 PhysicalPoint::new(horizontal_position, vertical_position),
340 object_fit_size,
341 );
342 let clip = PhysicalRect::new(PhysicalPoint::origin(), size);
343
344 match &self.kind {
345 ReplacedContentKind::Image(image) => image
346 .as_ref()
347 .and_then(|image| match image {
348 Image::Raster(raster_image) => raster_image.id,
349 Image::Vector(vector_image) => {
350 let scale = layout_context.style_context.device_pixel_ratio();
351 let width = object_fit_size.width.scale_by(scale.0).to_px();
352 let height = object_fit_size.height.scale_by(scale.0).to_px();
353 let size = Size2D::new(width, height);
354 let tag = self.base_fragment_info.tag?;
355 layout_context
356 .image_resolver
357 .rasterize_vector_image(vector_image.id, size, tag.node)
358 .and_then(|i| i.id)
359 },
360 })
361 .map(|image_key| {
362 Fragment::Image(ArcRefCell::new(ImageFragment {
363 base: self.base_fragment_info.into(),
364 style: style.clone(),
365 rect,
366 clip,
367 image_key: Some(image_key),
368 }))
369 })
370 .into_iter()
371 .collect(),
372 ReplacedContentKind::Video(video) => {
373 vec![Fragment::Image(ArcRefCell::new(ImageFragment {
374 base: self.base_fragment_info.into(),
375 style: style.clone(),
376 rect,
377 clip,
378 image_key: video.as_ref().map(|video| video.image_key),
379 }))]
380 },
381 ReplacedContentKind::IFrame(iframe) => {
382 let size = Size2D::new(rect.size.width.to_f32_px(), rect.size.height.to_f32_px());
383 let hidpi_scale_factor = layout_context.style_context.device_pixel_ratio();
384
385 layout_context.iframe_sizes.lock().insert(
386 iframe.browsing_context_id,
387 IFrameSize {
388 browsing_context_id: iframe.browsing_context_id,
389 pipeline_id: iframe.pipeline_id,
390 viewport_details: ViewportDetails {
391 size,
392 hidpi_scale_factor: Scale::new(hidpi_scale_factor.0),
393 },
394 },
395 );
396 vec![Fragment::IFrame(ArcRefCell::new(IFrameFragment {
397 base: self.base_fragment_info.into(),
398 style: style.clone(),
399 pipeline_id: iframe.pipeline_id,
400 rect,
401 }))]
402 },
403 ReplacedContentKind::Canvas(canvas_info) => {
404 if self.natural_size.width == Some(Au::zero()) ||
405 self.natural_size.height == Some(Au::zero())
406 {
407 return vec![];
408 }
409
410 let Some(image_key) = canvas_info.source else {
411 return vec![];
412 };
413
414 vec![Fragment::Image(ArcRefCell::new(ImageFragment {
415 base: self.base_fragment_info.into(),
416 style: style.clone(),
417 rect,
418 clip,
419 image_key: Some(image_key),
420 }))]
421 },
422 ReplacedContentKind::SVGElement(vector_image) => {
423 let Some(vector_image) = vector_image else {
424 return vec![];
425 };
426 let scale = layout_context.style_context.device_pixel_ratio();
427 let size = PhysicalSize::new(
429 vector_image
430 .metadata
431 .width
432 .try_into()
433 .map_or(MAX_AU, Au::from_px),
434 vector_image
435 .metadata
436 .height
437 .try_into()
438 .map_or(MAX_AU, Au::from_px),
439 );
440 let rect = PhysicalRect::from_size(size);
441 let raster_size = Size2D::new(
442 size.width.scale_by(scale.0).to_px(),
443 size.height.scale_by(scale.0).to_px(),
444 );
445 let tag = self.base_fragment_info.tag.unwrap();
446 layout_context
447 .image_resolver
448 .rasterize_vector_image(vector_image.id, raster_size, tag.node)
449 .and_then(|image| image.id)
450 .map(|image_key| {
451 Fragment::Image(ArcRefCell::new(ImageFragment {
452 base: self.base_fragment_info.into(),
453 style: style.clone(),
454 rect,
455 clip,
456 image_key: Some(image_key),
457 }))
458 })
459 .into_iter()
460 .collect()
461 },
462 }
463 }
464
465 pub(crate) fn preferred_aspect_ratio(
466 &self,
467 style: &ComputedValues,
468 padding_border_sums: &LogicalVec2<Au>,
469 ) -> Option<AspectRatio> {
470 style.preferred_aspect_ratio(self.natural_size.ratio, padding_border_sums)
471 }
472
473 pub(crate) fn fallback_inline_size(&self, writing_mode: WritingMode) -> Au {
479 if writing_mode.is_horizontal() {
480 self.natural_size.width.unwrap_or_else(|| Au::from_px(300))
481 } else {
482 self.natural_size.height.unwrap_or_else(|| Au::from_px(150))
483 }
484 }
485
486 pub(crate) fn fallback_block_size(&self, writing_mode: WritingMode) -> Au {
492 if writing_mode.is_horizontal() {
493 self.natural_size.height.unwrap_or_else(|| Au::from_px(150))
494 } else {
495 self.natural_size.width.unwrap_or_else(|| Au::from_px(300))
496 }
497 }
498
499 #[inline]
500 pub(crate) fn layout_style<'a>(&self, base: &'a LayoutBoxBase) -> LayoutStyle<'a> {
501 LayoutStyle::Default(&base.style)
502 }
503
504 pub(crate) fn layout(
505 &self,
506 layout_context: &LayoutContext,
507 containing_block_for_children: &ContainingBlock,
508 preferred_aspect_ratio: Option<AspectRatio>,
509 base: &LayoutBoxBase,
510 lazy_block_size: &LazySize,
511 ) -> CacheableLayoutResult {
512 let writing_mode = base.style.writing_mode;
513 let inline_size = containing_block_for_children.size.inline;
514 let content_block_size = self.content_size(
515 Direction::Block,
516 preferred_aspect_ratio,
517 &|| SizeConstraint::Definite(inline_size),
518 &|| self.fallback_block_size(writing_mode),
519 );
520 let size = LogicalVec2 {
521 inline: inline_size,
522 block: lazy_block_size.resolve(|| content_block_size),
523 }
524 .to_physical_size(writing_mode);
525 CacheableLayoutResult {
526 baselines: Default::default(),
527 collapsible_margins_in_children: CollapsedBlockMargins::zero(),
528 content_block_size,
529 content_inline_size_for_table: None,
530 depends_on_block_constraints: true,
533 fragments: self.make_fragments(layout_context, &base.style, size),
534 specific_layout_info: None,
535 }
536 }
537}
538
539impl ComputeInlineContentSizes for ReplacedContents {
540 fn compute_inline_content_sizes(
541 &self,
542 _: &LayoutContext,
543 constraint_space: &ConstraintSpace,
544 ) -> InlineContentSizesResult {
545 let inline_content_size = self.content_size(
546 Direction::Inline,
547 constraint_space.preferred_aspect_ratio,
548 &|| constraint_space.block_size,
549 &|| self.fallback_inline_size(constraint_space.style.writing_mode),
550 );
551 InlineContentSizesResult {
552 sizes: inline_content_size.into(),
553 depends_on_block_constraints: constraint_space.preferred_aspect_ratio.is_some(),
554 }
555 }
556}
557
558fn try_to_parse_image_data_url(string: &str) -> Option<Url> {
559 if !string.starts_with("data:") {
560 return None;
561 }
562 let data_url = DataUrl::process(string).ok()?;
563 let mime_type = data_url.mime_type();
564 if mime_type.type_ != "image" {
565 return None;
566 }
567
568 if !matches!(
571 mime_type.subtype.as_str(),
572 "png" | "jpeg" | "gif" | "webp" | "bmp" | "ico"
573 ) {
574 return None;
575 }
576
577 Url::parse(string).ok()
578}