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::context::TreeCountingCaches;
23use style::dom::DummyElementContext;
24use style::logical_geometry::{Direction, WritingMode};
25use style::properties::{ComputedValues, StyleBuilder};
26use style::rule_cache::RuleCacheConditions;
27use style::rule_tree::RuleCascadeFlags;
28use style::stylesheets::container_rule::ContainerSizeQuery;
29use style::url::ComputedUrl;
30use style::values::CSSFloat;
31use style::values::computed::image::Image as ComputedImage;
32use style::values::computed::{Content, Context, ToComputedValue};
33use style::values::generics::counters::{GenericContentItem, GenericContentItems};
34use url::Url;
35use web_atoms::local_name;
36use webrender_api::ImageKey;
37
38use crate::context::{LayoutContext, LayoutImageCacheResult};
39use crate::dom::NodeExt;
40use crate::fragment_tree::{
41 BaseFragment, BaseFragmentInfo, CollapsedBlockMargins, Fragment, IFrameFragment, ImageFragment,
42};
43use crate::geom::{LogicalVec2, PhysicalPoint, PhysicalRect, PhysicalSize};
44use crate::layout_box_base::{IndependentFormattingContextLayoutResult, LayoutBoxBase};
45use crate::sizing::{
46 ComputeInlineContentSizes, InlineContentSizesResult, LazySize, SizeConstraint,
47};
48use crate::style_ext::{AspectRatio, Clamp, ComputedValuesExt, LayoutStyle};
49use crate::{ConstraintSpace, ContainingBlock};
50
51#[derive(Debug, MallocSizeOf)]
52pub(crate) struct ReplacedContents {
53 pub kind: ReplacedContentKind,
54 pub is_content_replacement: bool,
57 natural_size: NaturalSizes,
58 base_fragment_info: BaseFragmentInfo,
59}
60
61#[derive(Debug, MallocSizeOf)]
79pub(crate) struct NaturalSizes {
80 pub width: Option<Au>,
81 pub height: Option<Au>,
82 pub ratio: Option<CSSFloat>,
83}
84
85impl NaturalSizes {
86 pub(crate) fn from_width_and_height(width: f32, height: f32) -> Self {
87 let ratio = if width.is_normal() && height.is_normal() {
91 Some(width / height)
92 } else {
93 None
94 };
95
96 Self {
97 width: Some(Au::from_f32_px(width)),
98 height: Some(Au::from_f32_px(height)),
99 ratio,
100 }
101 }
102
103 pub(crate) fn from_natural_size_in_dots(natural_size_in_dots: PhysicalSize<f64>) -> Self {
104 let dppx = 1.0;
108 let width = natural_size_in_dots.width as f32 / dppx;
109 let height = natural_size_in_dots.height as f32 / dppx;
110 Self::from_width_and_height(width, height)
111 }
112
113 pub(crate) fn empty() -> Self {
114 Self {
115 width: None,
116 height: None,
117 ratio: None,
118 }
119 }
120}
121
122#[derive(Debug, MallocSizeOf)]
123pub(crate) struct CanvasInfo {
124 pub source: Option<ImageKey>,
125}
126
127#[derive(Debug, MallocSizeOf)]
128pub(crate) struct IFrameInfo {
129 pub pipeline_id: PipelineId,
130 pub browsing_context_id: BrowsingContextId,
131}
132
133#[derive(Debug, MallocSizeOf)]
134pub(crate) struct ImageInfo {
135 pub image: Option<Image>,
136 pub showing_broken_image_icon: bool,
137 pub url: Option<ServoUrl>,
138}
139
140#[derive(Debug, MallocSizeOf)]
141pub(crate) struct VideoInfo {
142 pub image_key: Option<ImageKey>,
143}
144
145#[derive(Debug, MallocSizeOf)]
146pub(crate) enum ReplacedContentKind {
147 Image(ImageInfo),
148 IFrame(IFrameInfo),
149 Canvas(CanvasInfo),
150 Video(VideoInfo),
151 SVGElement {
152 vector_image: Option<VectorImage>,
153 has_viewbox: bool,
154 },
155 Audio,
156}
157
158impl ReplacedContents {
159 pub fn for_element(node: ServoLayoutNode<'_>, context: &LayoutContext) -> Option<Self> {
160 if let Some(ref data_attribute_string) = node.as_typeless_object_with_data_attribute() &&
161 let Some(url) = try_to_parse_image_data_url(data_attribute_string)
162 {
163 return Self::from_image_url(node, context, &ComputedUrl::Valid(ServoArc::new(url)));
164 }
165
166 let (kind, natural_size) = {
167 if let Some((image_info, natural_size_in_dots)) = node.as_image() {
168 if let Some(content_image) = Self::from_content_property(node, context) {
169 return Some(content_image);
170 }
171 (
172 ReplacedContentKind::Image(image_info),
173 NaturalSizes::from_natural_size_in_dots(natural_size_in_dots),
174 )
175 } else if let Some((canvas_info, natural_size_in_dots)) = node.as_canvas() {
176 (
177 ReplacedContentKind::Canvas(canvas_info),
178 NaturalSizes::from_natural_size_in_dots(natural_size_in_dots),
179 )
180 } else if let Some(iframe_info) = node.as_iframe() {
181 (
182 ReplacedContentKind::IFrame(iframe_info),
183 NaturalSizes::empty(),
184 )
185 } else if let Some((video_info, natural_size_in_dots)) = node.as_video() {
186 (
187 ReplacedContentKind::Video(video_info),
188 natural_size_in_dots
189 .map_or_else(NaturalSizes::empty, NaturalSizes::from_natural_size_in_dots),
190 )
191 } else if let Some(svg_data) = node.as_svg() {
192 Self::svg_kind_size(svg_data, context, node)
193 } else if node
194 .as_html_element()
195 .is_some_and(|element| element.local_name() == &local_name!("audio"))
196 {
197 let natural_size = NaturalSizes {
198 width: None,
199 height: Some(Au::from_px(40)),
202 ratio: None,
203 };
204 (ReplacedContentKind::Audio, natural_size)
205 } else {
206 return Self::from_content_property(node, context);
207 }
208 };
209
210 if let ReplacedContentKind::Image(ImageInfo {
211 image: Some(Image::Raster(ref image)),
212 ..
213 }) = kind
214 {
215 context
216 .image_resolver
217 .handle_animated_image(node.opaque(), image.clone());
218 }
219
220 Some(Self {
221 kind,
222 is_content_replacement: false,
223 natural_size,
224 base_fragment_info: node.into(),
225 })
226 }
227
228 fn svg_kind_size(
229 svg_data: SVGElementData,
230 context: &LayoutContext,
231 node: ServoLayoutNode<'_>,
232 ) -> (ReplacedContentKind, NaturalSizes) {
233 let rule_cache_conditions = &mut RuleCacheConditions::default();
234 let mut tree_counting_caches = TreeCountingCaches::default();
235
236 let parent_style = node.style(&context.style_context);
237 let style_builder = StyleBuilder::new(
238 context.style_context.stylist.device(),
239 Some(context.style_context.stylist),
240 Some(&parent_style),
241 None,
242 None,
243 false,
244 );
245
246 let element_context = &DummyElementContext;
250
251 let to_computed_context = Context::new(
252 style_builder,
253 context.style_context.quirks_mode(),
254 rule_cache_conditions,
255 ContainerSizeQuery::none(),
256 RuleCascadeFlags::empty(),
257 element_context,
258 &mut tree_counting_caches,
259 );
260
261 let attr_to_computed = |attr_val: &AttrValue| {
262 if let AttrValue::LengthPercentage(_, length_percentage) = attr_val {
263 length_percentage
264 .to_computed_value(&to_computed_context)?
265 .to_length()
266 } else {
267 None
268 }
269 };
270 let width = svg_data.width.and_then(attr_to_computed);
271 let height = svg_data.height.and_then(attr_to_computed);
272
273 let ratio = match (width, height) {
274 (Some(width), Some(height)) if !width.is_zero() && !height.is_zero() => {
275 Some(width.px() / height.px())
276 },
277 _ => svg_data.ratio_from_view_box(),
278 };
279
280 let natural_size = NaturalSizes {
281 width: width.map(|w| Au::from_f32_px(w.px())),
282 height: height.map(|h| Au::from_f32_px(h.px())),
283 ratio,
284 };
285
286 let svg_source = match svg_data.source {
287 None => {
288 context
291 .image_resolver
292 .queue_svg_element_for_serialization(node);
293 None
294 },
295 Some(svg_source_result) => svg_source_result.ok(),
298 };
299
300 let cached_image = svg_source.and_then(|svg_source| {
301 context
302 .image_resolver
303 .get_cached_image_for_url(
304 node.opaque(),
305 svg_source,
306 LayoutImageDestination::BoxTreeConstruction,
307 InternalRequest::Yes,
308 )
309 .ok()
310 });
311
312 let vector_image = cached_image.map(|image| match image {
313 Image::Vector(mut vector_image) => {
314 vector_image.svg_id = Some(svg_data.svg_id);
315 vector_image
316 },
317 _ => unreachable!("SVG element can't contain a raster image."),
318 });
319
320 (
321 ReplacedContentKind::SVGElement {
322 vector_image,
323 has_viewbox: svg_data.view_box.is_some(),
324 },
325 natural_size,
326 )
327 }
328
329 fn from_content_property(node: ServoLayoutNode<'_>, context: &LayoutContext) -> Option<Self> {
330 if let Content::Items(GenericContentItems { items, .. }) =
333 node.style(&context.style_context).clone_content() &&
334 let [GenericContentItem::Image(image)] = items.as_slice()
335 {
336 let mut replaced_contents = Self::from_image(node, context, image)
338 .unwrap_or_else(|| Self::zero_sized_invalid_image(node));
339
340 replaced_contents.is_content_replacement = true;
341 node.clear_fragments_and_dirty_fragment_caches_of_descendants();
342 return Some(replaced_contents);
343 }
344 None
345 }
346
347 pub fn from_image_url(
348 node: ServoLayoutNode<'_>,
349 context: &LayoutContext,
350 image_url: &ComputedUrl,
351 ) -> Option<Self> {
352 let ComputedUrl::Valid(image_url) = image_url else {
353 return None;
354 };
355 let (image, width, height) = match context.image_resolver.get_or_request_image_or_meta(
356 node.opaque(),
357 image_url.clone().into(),
358 LayoutImageDestination::BoxTreeConstruction,
359 InternalRequest::No,
360 ) {
361 LayoutImageCacheResult::DataAvailable(img_or_meta) => match img_or_meta {
362 ImageOrMetadataAvailable::ImageAvailable { image, .. } => {
363 if let Image::Raster(image) = &image {
364 context
365 .image_resolver
366 .handle_animated_image(node.opaque(), image.clone());
367 }
368 let metadata = image.metadata();
369 (Some(image), metadata.width as f32, metadata.height as f32)
370 },
371 ImageOrMetadataAvailable::MetadataAvailable(metadata, _id) => {
372 (None, metadata.width as f32, metadata.height as f32)
373 },
374 },
375 LayoutImageCacheResult::Pending | LayoutImageCacheResult::LoadError => return None,
376 };
377 Some(Self {
378 kind: ReplacedContentKind::Image(ImageInfo {
379 image,
380 showing_broken_image_icon: false,
381 url: Some(image_url.clone().into()),
382 }),
383 is_content_replacement: false,
384 natural_size: NaturalSizes::from_width_and_height(width, height),
385 base_fragment_info: node.into(),
386 })
387 }
388
389 pub fn from_image(
390 element: ServoLayoutNode<'_>,
391 context: &LayoutContext,
392 image: &ComputedImage,
393 ) -> Option<Self> {
394 match image {
395 ComputedImage::Url(image_url) => Self::from_image_url(element, context, image_url),
396 _ => None, }
398 }
399
400 pub(crate) fn zero_sized_invalid_image(node: ServoLayoutNode<'_>) -> Self {
401 Self {
402 kind: ReplacedContentKind::Image(ImageInfo {
403 image: None,
404 showing_broken_image_icon: false,
405 url: None,
406 }),
407 is_content_replacement: false,
408 natural_size: NaturalSizes::from_width_and_height(0., 0.),
409 base_fragment_info: node.into(),
410 }
411 }
412
413 #[inline]
414 fn is_broken_image(&self) -> bool {
415 matches!(&self.kind, ReplacedContentKind::Image(image_info) if image_info.showing_broken_image_icon)
416 }
417
418 #[inline]
419 fn content_size(
420 &self,
421 axis: Direction,
422 preferred_aspect_ratio: Option<AspectRatio>,
423 get_size_in_opposite_axis: &dyn Fn() -> SizeConstraint,
424 get_fallback_size: &dyn Fn() -> Au,
425 ) -> Au {
426 let Some(ratio) = preferred_aspect_ratio else {
427 return get_fallback_size();
428 };
429 let transfer = |size| ratio.compute_dependent_size(axis, size);
430 match get_size_in_opposite_axis() {
431 SizeConstraint::Definite(size) => transfer(size),
432 SizeConstraint::MinMax(min_size, max_size) => get_fallback_size()
433 .clamp_between_extremums(transfer(min_size), max_size.map(transfer)),
434 }
435 }
436
437 fn calculate_fragment_rect(
438 &self,
439 style: &ServoArc<ComputedValues>,
440 size: PhysicalSize<Au>,
441 ) -> (PhysicalSize<Au>, PhysicalRect<Au>) {
442 if let ReplacedContentKind::Image(ImageInfo {
443 image: Some(Image::Raster(image)),
444 showing_broken_image_icon: true,
445 url: _,
446 }) = &self.kind
447 {
448 let size = Size2D::new(
449 Au::from_f32_px(image.metadata.width as f32),
450 Au::from_f32_px(image.metadata.height as f32),
451 )
452 .min(size);
453 return (PhysicalSize::zero(), size.into());
454 }
455
456 let natural_size = PhysicalSize::new(
457 self.natural_size.width.unwrap_or(size.width),
458 self.natural_size.height.unwrap_or(size.height),
459 );
460
461 let object_fit_size = self.natural_size.ratio.map_or(size, |width_over_height| {
462 let preserve_aspect_ratio_with_comparison =
463 |size: PhysicalSize<Au>, comparison: fn(&Au, &Au) -> bool| {
464 let candidate_width = size.height.scale_by(width_over_height);
465 if comparison(&candidate_width, &size.width) {
466 return PhysicalSize::new(candidate_width, size.height);
467 }
468
469 let candidate_height = size.width.scale_by(1. / width_over_height);
470 debug_assert!(comparison(&candidate_height, &size.height));
471 PhysicalSize::new(size.width, candidate_height)
472 };
473
474 match style.clone_object_fit() {
475 ObjectFit::Fill => size,
476 ObjectFit::Contain => preserve_aspect_ratio_with_comparison(size, PartialOrd::le),
477 ObjectFit::Cover => preserve_aspect_ratio_with_comparison(size, PartialOrd::ge),
478 ObjectFit::None => natural_size,
479 ObjectFit::ScaleDown => {
480 preserve_aspect_ratio_with_comparison(size.min(natural_size), PartialOrd::le)
481 },
482 }
483 });
484
485 let object_position = style.clone_object_position();
486 let horizontal_position = object_position
487 .horizontal
488 .to_used_value(size.width - object_fit_size.width);
489 let vertical_position = object_position
490 .vertical
491 .to_used_value(size.height - object_fit_size.height);
492
493 let object_position = PhysicalPoint::new(horizontal_position, vertical_position);
494 (
495 object_fit_size,
496 PhysicalRect::new(object_position, object_fit_size),
497 )
498 }
499
500 pub fn make_fragments(
501 &self,
502 layout_context: &LayoutContext,
503 style: &ServoArc<ComputedValues>,
504 size: PhysicalSize<Au>,
505 ) -> Vec<Fragment> {
506 let (object_fit_size, rect) = self.calculate_fragment_rect(style, size);
507 let clip = PhysicalRect::new(PhysicalPoint::origin(), size);
508
509 let base = BaseFragment::new(self.base_fragment_info, style.clone().into(), rect);
510 match &self.kind {
511 ReplacedContentKind::Image(image_info) => image_info
512 .image
513 .as_ref()
514 .and_then(|image| match image {
515 Image::Raster(raster_image) => raster_image.id,
516 Image::Vector(vector_image) => {
517 let scale = layout_context.style_context.device_pixel_ratio();
518 let width = object_fit_size.width.scale_by(scale.0).to_px();
519 let height = object_fit_size.height.scale_by(scale.0).to_px();
520 let size = Size2D::new(width, height);
521 let tag = self.base_fragment_info.tag?;
522 layout_context
523 .image_resolver
524 .rasterize_vector_image(
525 vector_image.id,
526 size,
527 tag.node,
528 vector_image.svg_id.clone(),
529 )
530 .and_then(|i| i.id)
531 },
532 })
533 .map(|image_key| {
534 Fragment::Image(Arc::new(ImageFragment {
535 base,
536 clip,
537 image_key: Some(image_key),
538 showing_broken_image_icon: image_info.showing_broken_image_icon,
539 url: image_info.url.clone(),
540 }))
541 })
542 .into_iter()
543 .collect(),
544 ReplacedContentKind::Video(video_info) => {
545 vec![Fragment::Image(Arc::new(ImageFragment {
546 base,
547 clip,
548 image_key: video_info.image_key,
549 showing_broken_image_icon: false,
550 url: None,
551 }))]
552 },
553 ReplacedContentKind::IFrame(iframe) => {
554 let size = Size2D::new(rect.size.width.to_f32_px(), rect.size.height.to_f32_px());
555 let hidpi_scale_factor = layout_context.style_context.device_pixel_ratio();
556
557 layout_context.iframe_sizes.lock().insert(
558 iframe.browsing_context_id,
559 IFrameSize {
560 browsing_context_id: iframe.browsing_context_id,
561 pipeline_id: iframe.pipeline_id,
562 viewport_details: ViewportDetails {
563 size,
564 hidpi_scale_factor: Scale::new(hidpi_scale_factor.0),
565 device_size: layout_context.device_size.cast_unit(),
566 },
567 },
568 );
569 vec![Fragment::IFrame(Arc::new(IFrameFragment {
570 base,
571 pipeline_id: iframe.pipeline_id,
572 }))]
573 },
574 ReplacedContentKind::Canvas(canvas_info) => {
575 if self.natural_size.width == Some(Au::zero()) ||
576 self.natural_size.height == Some(Au::zero())
577 {
578 return vec![];
579 }
580
581 let Some(image_key) = canvas_info.source else {
582 return vec![];
583 };
584
585 vec![Fragment::Image(Arc::new(ImageFragment {
586 base,
587 clip,
588 image_key: Some(image_key),
589 showing_broken_image_icon: false,
590 url: None,
591 }))]
592 },
593 ReplacedContentKind::SVGElement {
594 vector_image,
595 has_viewbox,
596 } => {
597 let Some(vector_image) = vector_image else {
598 return vec![];
599 };
600
601 if !has_viewbox {
602 base.set_rect(
603 PhysicalSize::new(
604 vector_image
605 .metadata
606 .width
607 .try_into()
608 .map_or(MAX_AU, Au::from_px),
609 vector_image
610 .metadata
611 .height
612 .try_into()
613 .map_or(MAX_AU, Au::from_px),
614 )
615 .into(),
616 );
617 }
618
619 let scale = layout_context.style_context.device_pixel_ratio();
620 let content_size = base.rect().size;
621 let raster_size = Size2D::new(
622 content_size.width.scale_by(scale.0).to_px(),
623 content_size.height.scale_by(scale.0).to_px(),
624 );
625
626 let tag = self.base_fragment_info.tag.unwrap();
627 layout_context
628 .image_resolver
629 .rasterize_vector_image(
630 vector_image.id,
631 raster_size,
632 tag.node,
633 vector_image.svg_id.clone(),
634 )
635 .and_then(|image| image.id)
636 .map(|image_key| {
637 Fragment::Image(Arc::new(ImageFragment {
638 base,
639 clip,
640 image_key: Some(image_key),
641 showing_broken_image_icon: false,
642 url: None,
643 }))
644 })
645 .into_iter()
646 .collect()
647 },
648 ReplacedContentKind::Audio => vec![],
649 }
650 }
651
652 pub(crate) fn preferred_aspect_ratio(
653 &self,
654 style: &ComputedValues,
655 padding_border_sums: &LogicalVec2<Au>,
656 ) -> Option<AspectRatio> {
657 if matches!(self.kind, ReplacedContentKind::Audio) {
658 return None;
661 }
662 if self.is_broken_image() {
663 style.preferred_aspect_ratio(None, padding_border_sums)
669 } else {
670 style.preferred_aspect_ratio(self.natural_size.ratio, padding_border_sums)
671 }
672 }
673
674 pub(crate) fn fallback_inline_size(&self, writing_mode: WritingMode) -> Au {
680 if writing_mode.is_horizontal() {
681 self.natural_size.width.unwrap_or_else(|| Au::from_px(300))
682 } else {
683 self.natural_size.height.unwrap_or_else(|| Au::from_px(150))
684 }
685 }
686
687 pub(crate) fn fallback_block_size(&self, writing_mode: WritingMode) -> Au {
693 if writing_mode.is_horizontal() {
694 self.natural_size.height.unwrap_or_else(|| Au::from_px(150))
695 } else {
696 self.natural_size.width.unwrap_or_else(|| Au::from_px(300))
697 }
698 }
699
700 pub(crate) fn logical_natural_sizes(
701 &self,
702 writing_mode: WritingMode,
703 ) -> LogicalVec2<Option<Au>> {
704 if writing_mode.is_horizontal() {
705 LogicalVec2 {
706 inline: self.natural_size.width,
707 block: self.natural_size.height,
708 }
709 } else {
710 LogicalVec2 {
711 inline: self.natural_size.height,
712 block: self.natural_size.width,
713 }
714 }
715 }
716
717 #[inline]
718 pub(crate) fn layout_style<'a>(&self, base: &'a LayoutBoxBase) -> LayoutStyle<'a> {
719 LayoutStyle::Default(&base.style)
720 }
721
722 pub(crate) fn layout(
723 &self,
724 layout_context: &LayoutContext,
725 containing_block_for_children: &ContainingBlock,
726 preferred_aspect_ratio: Option<AspectRatio>,
727 base: &LayoutBoxBase,
728 lazy_block_size: &LazySize,
729 ) -> IndependentFormattingContextLayoutResult {
730 let writing_mode = base.style.writing_mode;
731 let inline_size = containing_block_for_children.size.inline;
732 let content_block_size = self.content_size(
733 Direction::Block,
734 preferred_aspect_ratio,
735 &|| SizeConstraint::Definite(inline_size),
736 &|| self.fallback_block_size(writing_mode),
737 );
738 let size = LogicalVec2 {
739 inline: inline_size,
740 block: lazy_block_size.resolve(|| content_block_size),
741 }
742 .to_physical_size(writing_mode);
743 IndependentFormattingContextLayoutResult {
744 baselines: Default::default(),
745 collapsible_margins_in_children: CollapsedBlockMargins::zero(),
746 content_block_size,
747 content_inline_size_for_table: None,
748 depends_on_block_constraints: true,
751 fragments: self.make_fragments(layout_context, &base.style, size),
752 specific_layout_info: None,
753 }
754 }
755}
756
757impl ComputeInlineContentSizes for ReplacedContents {
758 fn compute_inline_content_sizes(
759 &self,
760 _: &LayoutContext,
761 constraint_space: &ConstraintSpace,
762 ) -> InlineContentSizesResult {
763 let inline_content_size = self.content_size(
764 Direction::Inline,
765 constraint_space.preferred_aspect_ratio,
766 &|| constraint_space.block_size,
767 &|| self.fallback_inline_size(constraint_space.style.writing_mode),
768 );
769 InlineContentSizesResult {
770 sizes: inline_content_size.into(),
771 depends_on_block_constraints: constraint_space.preferred_aspect_ratio.is_some(),
772 }
773 }
774}
775
776fn try_to_parse_image_data_url(string: &str) -> Option<Url> {
777 if !string.starts_with("data:") {
778 return None;
779 }
780 let data_url = DataUrl::process(string).ok()?;
781 let mime_type = data_url.mime_type();
782 if mime_type.type_ != "image" {
783 return None;
784 }
785
786 if !matches!(
789 mime_type.subtype.as_str(),
790 "png" | "jpeg" | "gif" | "webp" | "bmp" | "ico"
791 ) {
792 return None;
793 }
794
795 Url::parse(string).ok()
796}