Skip to main content

script/dom/
srcset.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::sync::LazyLock;
6
7use app_units::Au;
8use cssparser::{Parser, ParserInput};
9use regex::Regex;
10use rustc_hash::FxHashSet;
11use script_bindings::codegen::GenericBindings::NodeBinding::NodeMethods;
12use script_bindings::inheritance::Castable;
13use script_bindings::root::DomRoot;
14use script_bindings::str::USVString;
15use style::attr::parse_unsigned_integer;
16use style::stylesheets::CssRuleType;
17use style::values::specified::source_size_list::SourceSizeList;
18use style_traits::ParsingMode;
19use url::Url;
20use xml5ever::local_name;
21
22use crate::css::parser_context_for_anonymous_content;
23use crate::dom::htmlimageelement::HTMLImageElement;
24use crate::dom::htmllinkelement::HTMLLinkElement;
25use crate::dom::htmlpictureelement::HTMLPictureElement;
26use crate::dom::htmlsourceelement::HTMLSourceElement;
27use crate::dom::medialist::MediaList;
28use crate::dom::node::NodeTraits;
29use crate::dom::{Element, Node};
30
31/// Supported image MIME types as defined by
32/// <https://mimesniff.spec.whatwg.org/#image-mime-type>.
33/// Keep this in sync with 'detect_image_format' from components/pixels/lib.rs
34const SUPPORTED_IMAGE_MIME_TYPES: &[&str] = &[
35    "image/bmp",
36    "image/gif",
37    "image/jpeg",
38    "image/jpg",
39    "image/pjpeg",
40    "image/png",
41    "image/apng",
42    "image/x-png",
43    "image/svg+xml",
44    "image/vnd.microsoft.icon",
45    "image/x-icon",
46    "image/webp",
47];
48
49/// <https://html.spec.whatwg.org/multipage/#source-set>
50#[derive(MallocSizeOf)]
51pub(crate) struct SourceSet {
52    pub image_sources: Vec<ImageSource>,
53    pub source_size: SourceSizeList,
54}
55
56/// <https://html.spec.whatwg.org/multipage/#image-source>
57#[derive(Clone, Debug, MallocSizeOf, PartialEq)]
58pub struct ImageSource {
59    pub url: String,
60    pub descriptor: Descriptor,
61}
62
63/// <https://html.spec.whatwg.org/multipage/#width-descriptor>
64/// <https://html.spec.whatwg.org/multipage/#pixel-density-descriptor>
65#[derive(Clone, Debug, MallocSizeOf, PartialEq)]
66pub struct Descriptor {
67    pub width: Option<u32>,
68    pub density: Option<f64>,
69}
70
71#[derive(Clone, Copy, Debug)]
72enum ParseState {
73    InDescriptor,
74    InParens,
75    AfterDescriptor,
76}
77
78impl SourceSet {
79    pub fn new() -> SourceSet {
80        SourceSet {
81            image_sources: Vec::new(),
82            source_size: SourceSizeList::empty(),
83        }
84    }
85
86    /// <https://html.spec.whatwg.org/multipage/#create-a-source-set>
87    pub fn create_source_set(element: &Element) -> SourceSet {
88        // Step 1. Let source set be an empty source set.
89        let mut source_set = SourceSet::new();
90
91        // Step 2. If srcset is not an empty string, then set source set to the result of parsing
92        // srcset.
93        if let Some(srcset) = element.get_attribute_string_value(&local_name!("srcset")) {
94            source_set.image_sources = parse_a_srcset_attribute(&srcset);
95        }
96
97        // Step 3. Set source set's source size to the result of parsing sizes with img.
98        if let Some(sizes) = element.get_attribute_string_value(&local_name!("sizes")) {
99            source_set.source_size = parse_a_sizes_attribute(&sizes);
100        }
101
102        // Step 4. If default source is not the empty string and source set does not contain an
103        // image source with a pixel density descriptor value of 1, and no image source with a width
104        // descriptor, append default source to source set.
105        let src = element.get_string_attribute(&local_name!("src"));
106        let no_density_source_of_1 = source_set
107            .image_sources
108            .iter()
109            .all(|source| source.descriptor.density != Some(1.));
110        let no_width_descriptor = source_set
111            .image_sources
112            .iter()
113            .all(|source| source.descriptor.width.is_none());
114        if !src.is_empty() && no_density_source_of_1 && no_width_descriptor {
115            source_set.image_sources.push(ImageSource {
116                url: String::from(src),
117                descriptor: Descriptor {
118                    width: None,
119                    density: None,
120                },
121            })
122        }
123
124        // Step 5. Normalize the source densities of source set.
125        source_set.normalise_source_densities(element);
126
127        // Step 6. Return source set.
128        source_set
129    }
130
131    /// <https://html.spec.whatwg.org/multipage/#update-the-source-set>
132    pub fn update_source_set(&mut self, el: &Element) {
133        // Step 1. Set el's source set to an empty source set.
134        *self = SourceSet::new();
135
136        // Step 2. Let elements be « el ».
137        // Step 3. If el is an img element whose parent node is a picture element, then replace the
138        // contents of elements with el's parent node's child elements, retaining relative order.
139        // Step 4. Let img be el if el is an img element, otherwise null.
140        let img = el.downcast::<HTMLImageElement>();
141        let parent = el.upcast::<Node>().GetParentElement();
142        let elements = match parent.as_ref() {
143            Some(p) => {
144                if p.is::<HTMLPictureElement>() {
145                    p.upcast::<Node>()
146                        .children()
147                        .filter_map(DomRoot::downcast::<Element>)
148                        .map(|n| DomRoot::from_ref(&*n))
149                        .collect()
150                } else {
151                    vec![DomRoot::from_ref(el)]
152                }
153            },
154            None => vec![DomRoot::from_ref(el)],
155        };
156
157        // Step 5. For each child in elements:
158        for child in &elements {
159            // Step 5.1. If child is el:
160            if *child == DomRoot::from_ref(el) {
161                // Step 5.1.10. Set el's source set to the result of creating a source set given
162                // default source, srcset, sizes, and img.
163                *self = SourceSet::create_source_set(el);
164
165                // Step 5.1.11. Return.
166                return;
167            }
168            // Spec note: If el is a link element, then elements contains only el, so this step
169            // will be reached immediately and the rest of the algorithm will not run.
170            debug_assert!(!el.is::<HTMLLinkElement>());
171            // Step 5.2. If child is not a source element, then continue.
172            if !child.is::<HTMLSourceElement>() {
173                continue;
174            }
175
176            let mut source_set = SourceSet::new();
177
178            // Step 5.3. If child does not have a srcset attribute, continue to the next child.
179            // Step 5.4. Parse child's srcset attribute and let source set be the returned source
180            // set.
181            match child.get_attribute_string_value(&local_name!("srcset")) {
182                Some(srcset) => {
183                    source_set.image_sources = parse_a_srcset_attribute(&srcset);
184                },
185                _ => continue,
186            }
187
188            // Step 5.5. If source set has zero image sources, continue to the next child.
189            if source_set.image_sources.is_empty() {
190                continue;
191            }
192
193            // Step 5.6. If child has a media attribute, and its value does not match the
194            // environment, continue to the next child.
195            if let Some(media) = child.get_attribute_string_value(&local_name!("media")) &&
196                !MediaList::matches_environment(&child.owner_document(), &media)
197            {
198                continue;
199            }
200
201            // Step 5.7. Parse child's sizes attribute with img, and let source set's source size be
202            // the returned value.
203            if let Some(sizes) = child.get_attribute_string_value(&local_name!("sizes")) {
204                source_set.source_size = parse_a_sizes_attribute(&sizes);
205            }
206
207            // Step 5.8. If child has a type attribute, and its value is an unknown or unsupported
208            // MIME type, continue to the next child.
209            if let Some(type_) = child.get_attribute_string_value(&local_name!("type")) &&
210                !is_supported_image_mime_type(&type_)
211            {
212                continue;
213            }
214
215            // Step 5.9. If child has width or height attributes, set el's dimension attribute
216            // source to child. Otherwise, set el's dimension attribute source to el.
217            if let Some(image) = img {
218                if child.has_attribute(&local_name!("width")) ||
219                    child.has_attribute(&local_name!("height"))
220                {
221                    image.set_dimension_attribute_source(Some(child));
222                } else {
223                    image.set_dimension_attribute_source(Some(el));
224                }
225            }
226
227            // Step 5.10. Normalize the source densities of source set.
228            source_set.normalise_source_densities(el);
229
230            // Step 5.11. Set el's source set to source set.
231            *self = source_set;
232
233            // Step 5.12. Return.
234            return;
235        }
236    }
237
238    pub fn evaluate_source_size_list(&self, element: &Element) -> Au {
239        let document = element.owner_document();
240        let quirks_mode = document.quirks_mode();
241        self.source_size
242            .evaluate(document.window().layout().device(), quirks_mode)
243    }
244
245    /// <https://html.spec.whatwg.org/multipage/#normalise-the-source-densities>
246    pub fn normalise_source_densities(&mut self, element: &Element) {
247        // Step 1. Let source size be source set's source size.
248        let source_size = self.evaluate_source_size_list(element);
249
250        // Step 2. For each image source in source set:
251        for image_source in self.image_sources.iter_mut() {
252            // Step 2.1. If the image source has a pixel density descriptor, continue to the next
253            // image source.
254            if image_source.descriptor.density.is_some() {
255                continue;
256            }
257
258            // Step 2.2. Otherwise, if the image source has a width descriptor, replace the width
259            // descriptor with a pixel density descriptor with a value of the width descriptor value
260            // divided by source size and a unit of x.
261            if let Some(width) = image_source.descriptor.width {
262                image_source.descriptor.density = Some(width as f64 / source_size.to_f64_px());
263            } else {
264                // Step 2.3. Otherwise, give the image source a pixel density descriptor of 1x.
265                image_source.descriptor.density = Some(1_f64);
266            }
267        }
268    }
269
270    /// <https://html.spec.whatwg.org/multipage/#select-an-image-source>
271    pub(crate) fn select_image_source(&mut self, element: &Element) -> Option<(USVString, f64)> {
272        // Step 1. Update the source set for el.
273        self.update_source_set(element);
274
275        // Step 2. If el's source set is empty, return null as the URL and undefined as the pixel
276        // density.
277        if self.image_sources.is_empty() {
278            return None;
279        }
280
281        // Step 3. Return the result of selecting an image from el's source set.
282        self.select_image_source_from_source_set(element)
283    }
284
285    /// <https://html.spec.whatwg.org/multipage/#select-an-image-source-from-a-source-set>
286    fn select_image_source_from_source_set(&self, element: &Element) -> Option<(USVString, f64)> {
287        // Step 1. If an entry b in sourceSet has the same associated pixel density descriptor as an
288        // earlier entry a in sourceSet, then remove entry b. Repeat this step until none of the
289        // entries in sourceSet have the same associated pixel density descriptor as an earlier
290        // entry.
291        let len = self.image_sources.len();
292
293        // Using FxHash is ok here as the indices are just 0..len
294        let mut repeat_indices = FxHashSet::default();
295        for outer_index in 0..len {
296            if repeat_indices.contains(&outer_index) {
297                continue;
298            }
299            let imgsource = &self.image_sources[outer_index];
300            let pixel_density = imgsource.descriptor.density.unwrap();
301            for inner_index in (outer_index + 1)..len {
302                let imgsource2 = &self.image_sources[inner_index];
303                if pixel_density == imgsource2.descriptor.density.unwrap() {
304                    repeat_indices.insert(inner_index);
305                }
306            }
307        }
308
309        let mut max = (0f64, 0);
310        let img_sources = &mut vec![];
311        for (index, image_source) in self.image_sources.iter().enumerate() {
312            if repeat_indices.contains(&index) {
313                continue;
314            }
315            let den = image_source.descriptor.density.unwrap();
316            if max.0 < den {
317                max = (den, img_sources.len());
318            }
319            img_sources.push(image_source);
320        }
321
322        // Step 2. In an implementation-defined manner, choose one image source from sourceSet. Let
323        // selectedSource be this choice.
324        let mut best_candidate = max;
325        let device_pixel_ratio = element
326            .owner_document()
327            .window()
328            .viewport_details()
329            .hidpi_scale_factor
330            .get() as f64;
331        for (index, image_source) in img_sources.iter().enumerate() {
332            let current_den = image_source.descriptor.density.unwrap();
333            if current_den < best_candidate.0 && current_den >= device_pixel_ratio {
334                best_candidate = (current_den, index);
335            }
336        }
337        let selected_source = img_sources.remove(best_candidate.1).clone();
338
339        // Step 3. Return selectedSource and its associated pixel density.
340        Some((
341            USVString(selected_source.url),
342            selected_source.descriptor.density.unwrap(),
343        ))
344    }
345}
346
347/// <https://html.spec.whatwg.org/multipage/#parse-a-sizes-attribute>
348pub fn parse_a_sizes_attribute(value: &str) -> SourceSizeList {
349    let mut input = ParserInput::new(value);
350    let mut parser = Parser::new(&mut input);
351    let url_data = Url::parse("about:blank").unwrap().into();
352    // FIXME(emilio): why ::empty() instead of ::DEFAULT? Also, what do
353    // browsers do regarding quirks-mode in a media list?
354    let context =
355        parser_context_for_anonymous_content(CssRuleType::Style, ParsingMode::empty(), &url_data);
356    SourceSizeList::parse(&context, &mut parser)
357}
358
359/// Collect sequence of code points
360/// <https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points>
361pub(crate) fn collect_sequence_characters(
362    s: &str,
363    mut predicate: impl FnMut(&char) -> bool,
364) -> (&str, &str) {
365    let i = s.find(|ch| !predicate(&ch)).unwrap_or(s.len());
366    (&s[0..i], &s[i..])
367}
368
369/// <https://html.spec.whatwg.org/multipage/#valid-non-negative-integer>
370/// TODO(#39315): Use the validation rule from Stylo
371fn is_valid_non_negative_integer_string(s: &str) -> bool {
372    s.chars().all(|c| c.is_ascii_digit())
373}
374
375/// <https://html.spec.whatwg.org/multipage/#valid-floating-point-number>
376/// TODO(#39315): Use the validation rule from Stylo
377fn is_valid_floating_point_number_string(s: &str) -> bool {
378    static RE: LazyLock<Regex> =
379        LazyLock::new(|| Regex::new(r"^-?(?:\d+\.\d+|\d+|\.\d+)(?:(e|E)(\+|\-)?\d+)?$").unwrap());
380
381    RE.is_match(s)
382}
383
384/// Parse an `srcset` attribute:
385/// <https://html.spec.whatwg.org/multipage/#parsing-a-srcset-attribute>.
386pub fn parse_a_srcset_attribute(input: &str) -> Vec<ImageSource> {
387    // > 1. Let input be the value passed to this algorithm.
388    // > 2. Let position be a pointer into input, initially pointing at the start of the string.
389    let mut current_index = 0;
390
391    // > 3. Let candidates be an initially empty source set.
392    let mut candidates = vec![];
393    while current_index < input.len() {
394        let remaining_string = &input[current_index..];
395
396        // > 4. Splitting loop: Collect a sequence of code points that are ASCII whitespace or
397        // > U+002C COMMA characters from input given position. If any U+002C COMMA
398        // > characters were collected, that is a parse error.
399        // NOTE: A parse error indicating a non-fatal mismatch between the input and the
400        // requirements will be silently ignored to match the behavior of other browsers.
401        // <https://html.spec.whatwg.org/multipage/#concept-microsyntax-parse-error>
402        let (collected_characters, string_after_whitespace) =
403            collect_sequence_characters(remaining_string, |character| {
404                *character == ',' || character.is_ascii_whitespace()
405            });
406
407        // Add the length of collected whitespace, to find the start of the URL we are going
408        // to parse.
409        current_index += collected_characters.len();
410
411        // > 5. If position is past the end of input, return candidates.
412        if string_after_whitespace.is_empty() {
413            return candidates;
414        }
415
416        // 6. Collect a sequence of code points that are not ASCII whitespace from input
417        // given position, and let that be url.
418        let (url, _) =
419            collect_sequence_characters(string_after_whitespace, |c| !char::is_ascii_whitespace(c));
420
421        // Add the length of `url` that we will parse to advance the index of the next part
422        // of the string to prase.
423        current_index += url.len();
424
425        // 7. Let descriptors be a new empty list.
426        let mut descriptors = Vec::new();
427
428        // > 8. If url ends with U+002C (,), then:
429        // >    1. Remove all trailing U+002C COMMA characters from url. If this removed
430        // >       more than one character, that is a parse error.
431        if url.ends_with(',') {
432            let image_source = ImageSource {
433                url: url.trim_end_matches(',').into(),
434                descriptor: Descriptor {
435                    width: None,
436                    density: None,
437                },
438            };
439            candidates.push(image_source);
440            continue;
441        }
442
443        // Otherwise:
444        // > 8.1. Descriptor tokenizer: Skip ASCII whitespace within input given position.
445        let descriptors_string = &input[current_index..];
446        let (spaces, descriptors_string) =
447            collect_sequence_characters(descriptors_string, |character| {
448                character.is_ascii_whitespace()
449            });
450        current_index += spaces.len();
451
452        // > 8.2. Let current descriptor be the empty string.
453        let mut current_descriptor = String::new();
454
455        // > 8.3. Let state be "in descriptor".
456        let mut state = ParseState::InDescriptor;
457
458        // > 8.4. Let c be the character at position. Do the following depending on the value of
459        // > state. For the purpose of this step, "EOF" is a special character representing
460        // > that position is past the end of input.
461        let mut characters = descriptors_string.chars();
462        let mut character = characters.next();
463        if let Some(character) = character {
464            current_index += character.len_utf8();
465        }
466
467        loop {
468            match (state, character) {
469                (ParseState::InDescriptor, Some(character)) if character.is_ascii_whitespace() => {
470                    // > If current descriptor is not empty, append current descriptor to
471                    // > descriptors and let current descriptor be the empty string. Set
472                    // > state to after descriptor.
473                    if !current_descriptor.is_empty() {
474                        descriptors.push(current_descriptor);
475                        current_descriptor = String::new();
476                        state = ParseState::AfterDescriptor;
477                    }
478                },
479                (ParseState::InDescriptor, Some(',')) => {
480                    // > Advance position to the next character in input. If current descriptor
481                    // > is not empty, append current descriptor to descriptors. Jump to the
482                    // > step labeled descriptor parser.
483                    if !current_descriptor.is_empty() {
484                        descriptors.push(current_descriptor);
485                    }
486                    break;
487                },
488                (ParseState::InDescriptor, Some('(')) => {
489                    // > Append c to current descriptor. Set state to in parens.
490                    current_descriptor.push('(');
491                    state = ParseState::InParens;
492                },
493                (ParseState::InDescriptor, Some(character)) => {
494                    // > Append c to current descriptor.
495                    current_descriptor.push(character);
496                },
497                (ParseState::InDescriptor, None) => {
498                    // > If current descriptor is not empty, append current descriptor to
499                    // > descriptors. Jump to the step labeled descriptor parser.
500                    if !current_descriptor.is_empty() {
501                        descriptors.push(current_descriptor);
502                    }
503                    break;
504                },
505                (ParseState::InParens, Some(')')) => {
506                    // > Append c to current descriptor. Set state to in descriptor.
507                    current_descriptor.push(')');
508                    state = ParseState::InDescriptor;
509                },
510                (ParseState::InParens, Some(character)) => {
511                    // Append c to current descriptor.
512                    current_descriptor.push(character);
513                },
514                (ParseState::InParens, None) => {
515                    // > Append current descriptor to descriptors. Jump to the step
516                    // > labeled descriptor parser.
517                    descriptors.push(current_descriptor);
518                    break;
519                },
520                (ParseState::AfterDescriptor, Some(character))
521                    if character.is_ascii_whitespace() =>
522                {
523                    // > Stay in this state.
524                },
525                (ParseState::AfterDescriptor, Some(_)) => {
526                    // > Set state to in descriptor. Set position to the previous
527                    // > character in input.
528                    state = ParseState::InDescriptor;
529                    continue;
530                },
531                (ParseState::AfterDescriptor, None) => {
532                    // > Jump to the step labeled descriptor parser.
533                    break;
534                },
535            }
536
537            character = characters.next();
538            if let Some(character) = character {
539                current_index += character.len_utf8();
540            }
541        }
542
543        // > 9. Descriptor parser: Let error be no.
544        let mut error = false;
545        // > 10. Let width be absent.
546        let mut width: Option<u32> = None;
547        // > 11. Let density be absent.
548        let mut density: Option<f64> = None;
549        // > 12. Let future-compat-h be absent.
550        let mut future_compat_h: Option<u32> = None;
551
552        // > 13. For each descriptor in descriptors, run the appropriate set of steps from
553        // > the following list:
554        for descriptor in descriptors.into_iter() {
555            let Some(last_character) = descriptor.chars().last() else {
556                break;
557            };
558
559            let first_part_of_string = &descriptor[0..descriptor.len() - last_character.len_utf8()];
560            match last_character {
561                // > If the descriptor consists of a valid non-negative integer followed by a
562                // > U+0077 LATIN SMALL LETTER W character
563                // > 1. If the user agent does not support the sizes attribute, let error be yes.
564                // > 2. If width and density are not both absent, then let error be yes.
565                // > 3. Apply the rules for parsing non-negative integers to the descriptor.
566                // >    If the result is 0, let error be yes. Otherwise, let width be the result.
567                'w' if is_valid_non_negative_integer_string(first_part_of_string) &&
568                    density.is_none() &&
569                    width.is_none() =>
570                {
571                    match parse_unsigned_integer(first_part_of_string.chars()) {
572                        Ok(number) if number > 0 => {
573                            width = Some(number);
574                            continue;
575                        },
576                        _ => error = true,
577                    }
578                },
579
580                // > If the descriptor consists of a valid floating-point number followed by a
581                // > U+0078 LATIN SMALL LETTER X character
582                // > 1. If width, density and future-compat-h are not all absent, then let
583                // >    error be yes.
584                // > 2. Apply the rules for parsing floating-point number values to the
585                // >    descriptor. If the result is less than 0, let error be yes. Otherwise, let
586                // >    density be the result.
587                //
588                // The HTML specification has a procedure for parsing floats that is different enough from
589                // the one that stylo uses, that it's better to use Rust's float parser here. This is
590                // what Gecko does, but it also checks to see if the number is a valid HTML-spec compliant
591                // number first. Not doing that means that we might be parsing numbers that otherwise
592                // wouldn't parse.
593                'x' if is_valid_floating_point_number_string(first_part_of_string) &&
594                    width.is_none() &&
595                    density.is_none() &&
596                    future_compat_h.is_none() =>
597                {
598                    match first_part_of_string.parse::<f64>() {
599                        Ok(number) if number.is_finite() && number >= 0. => {
600                            density = Some(number);
601                            continue;
602                        },
603                        _ => error = true,
604                    }
605                },
606
607                // > If the descriptor consists of a valid non-negative integer followed by a
608                // > U+0068 LATIN SMALL LETTER H character
609                // >   This is a parse error.
610                // > 1. If future-compat-h and density are not both absent, then let error be
611                // >    yes.
612                // > 2. Apply the rules for parsing non-negative integers to the descriptor.
613                // >    If the result is 0, let error be yes. Otherwise, let future-compat-h be the
614                // >    result.
615                'h' if is_valid_non_negative_integer_string(first_part_of_string) &&
616                    future_compat_h.is_none() &&
617                    density.is_none() =>
618                {
619                    match parse_unsigned_integer(first_part_of_string.chars()) {
620                        Ok(number) if number > 0 => {
621                            future_compat_h = Some(number);
622                            continue;
623                        },
624                        _ => error = true,
625                    }
626                },
627
628                // > Anything else
629                // >  Let error be yes.
630                _ => error = true,
631            }
632
633            if error {
634                break;
635            }
636        }
637
638        // > 14. If future-compat-h is not absent and width is absent, let error be yes.
639        if future_compat_h.is_some() && width.is_none() {
640            error = true;
641        }
642
643        // Step 15. If error is still no, then append a new image source to candidates whose URL is
644        // url, associated with a width width if not absent and a pixel density density if not
645        // absent. Otherwise, there is a parse error.
646        if !error {
647            let image_source = ImageSource {
648                url: url.into(),
649                descriptor: Descriptor { width, density },
650            };
651            candidates.push(image_source);
652        }
653
654        // Step 16. Return to the step labeled splitting loop.
655    }
656    candidates
657}
658
659/// Returns true if the given image MIME type is supported.
660fn is_supported_image_mime_type(input: &str) -> bool {
661    // Remove any leading and trailing HTTP whitespace from input.
662    let mime_type = input.trim();
663
664    // <https://mimesniff.spec.whatwg.org/#mime-type-essence>
665    let mime_type_essence = match mime_type.find(';') {
666        Some(semi) => &mime_type[..semi],
667        _ => mime_type,
668    };
669
670    // The HTML specification says the type attribute may be present and if present, the value
671    // must be a valid MIME type string. However an empty type attribute is implicitly supported
672    // to match the behavior of other browsers.
673    // <https://html.spec.whatwg.org/multipage/#attr-source-type>
674    if mime_type_essence.is_empty() {
675        return true;
676    }
677
678    SUPPORTED_IMAGE_MIME_TYPES.contains(&mime_type_essence)
679}