Files
servo/components/script/dom/html/htmlimageelement.rs
Andrei Volykhin ef9f16027b html: Add missing 'width' and 'height' reflected IDL dimension attributes (#39606)
The HTML specification defines the 'width' and 'height' attributes as
dimension attributes for the embedded content and images (img, iframe,
embed, object, video, source, input with image type)
https://html.spec.whatwg.org/multipage/#dimension-attributes and for the
tables (table, col, colgroup, thead, tbody, tfoot, tr, td, th) even
these attributes are marked as obsolete.

And UA are expected to use these 'width' and 'height' attributes as
style presentational hints for the rendering.

The embedded content and images:
https://html.spec.whatwg.org/multipage/#dimRendering

The tables:
https://html.spec.whatwg.org/multipage/#tables-2:the-table-element-4

Added missing 'width' and/or 'height' reflected IDL attributes:
- conformant 'unsigned long' IDL attributes for the 'img, source, video'
(with new macro 'make_dimension_uint*)
- obsolete 'DOMString' IDL attributes for the 'td, th, col, colgroup'

Moved the `fn attribute_affects_presentational_hints()` from Element to
specific HTML and SVG elements with attributes which affects
presentational hints for rendering.

Testing: Improvements in the following tests
- html/dom/idlharness.https.html
- html/dom/reflection-embedded.html
- html/dom/reflection-tabular.html
-
html/rendering/replaced-elements/attributes-for-embedded-content-and-images/picture-aspect-ratio.html

Signed-off-by: Andrei Volykhin <andrei.volykhin@gmail.com>
2025-10-09 18:03:47 +00:00

2342 lines
90 KiB
Rust

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cell::Cell;
use std::default::Default;
use std::rc::Rc;
use std::sync::{Arc, LazyLock};
use std::{char, mem};
use app_units::Au;
use cssparser::{Parser, ParserInput};
use dom_struct::dom_struct;
use euclid::default::{Point2D, Size2D};
use html5ever::{LocalName, Prefix, QualName, local_name, ns};
use js::jsapi::JSAutoRealm;
use js::rust::HandleObject;
use mime::{self, Mime};
use net_traits::http_status::HttpStatus;
use net_traits::image_cache::{
Image, ImageCache, ImageCacheResult, ImageLoadListener, ImageOrMetadataAvailable,
ImageResponse, PendingImageId, UsePlaceholder,
};
use net_traits::request::{CorsSettings, Destination, Initiator, RequestId};
use net_traits::{
FetchMetadata, FetchResponseListener, FetchResponseMsg, NetworkError, ReferrerPolicy,
ResourceFetchTiming, ResourceTimingType,
};
use num_traits::ToPrimitive;
use pixels::{
CorsStatus, ImageMetadata, PixelFormat, Snapshot, SnapshotAlphaMode, SnapshotPixelFormat,
};
use regex::Regex;
use rustc_hash::FxHashSet;
use servo_url::ServoUrl;
use servo_url::origin::MutableOrigin;
use style::attr::{AttrValue, LengthOrPercentageOrAuto, parse_unsigned_integer};
use style::context::QuirksMode;
use style::parser::ParserContext;
use style::stylesheets::{CssRuleType, Origin};
use style::values::specified::source_size_list::SourceSizeList;
use style_traits::ParsingMode;
use url::Url;
use crate::document_loader::{LoadBlocker, LoadType};
use crate::dom::activation::Activatable;
use crate::dom::attr::Attr;
use crate::dom::bindings::cell::{DomRefCell, RefMut};
use crate::dom::bindings::codegen::Bindings::DOMRectBinding::DOMRect_Binding::DOMRectMethods;
use crate::dom::bindings::codegen::Bindings::ElementBinding::Element_Binding::ElementMethods;
use crate::dom::bindings::codegen::Bindings::HTMLImageElementBinding::HTMLImageElementMethods;
use crate::dom::bindings::codegen::Bindings::MouseEventBinding::MouseEventMethods;
use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMethods;
use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
use crate::dom::bindings::error::{Error, Fallible};
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::refcounted::{Trusted, TrustedPromise};
use crate::dom::bindings::reflector::DomGlobal;
use crate::dom::bindings::root::{DomRoot, LayoutDom, MutNullableDom};
use crate::dom::bindings::str::{DOMString, USVString};
use crate::dom::csp::{GlobalCspReporting, Violation};
use crate::dom::document::Document;
use crate::dom::element::{
AttributeMutation, CustomElementCreationMode, Element, ElementCreator, LayoutElementHelpers,
cors_setting_for_element, referrer_policy_for_element, reflect_cross_origin_attribute,
reflect_referrer_policy_attribute, set_cross_origin_attribute,
};
use crate::dom::event::Event;
use crate::dom::eventtarget::EventTarget;
use crate::dom::globalscope::GlobalScope;
use crate::dom::html::htmlareaelement::HTMLAreaElement;
use crate::dom::html::htmlelement::HTMLElement;
use crate::dom::html::htmlformelement::{FormControl, HTMLFormElement};
use crate::dom::html::htmlmapelement::HTMLMapElement;
use crate::dom::html::htmlpictureelement::HTMLPictureElement;
use crate::dom::html::htmlsourceelement::HTMLSourceElement;
use crate::dom::medialist::MediaList;
use crate::dom::mouseevent::MouseEvent;
use crate::dom::node::{BindContext, Node, NodeDamage, NodeTraits, ShadowIncluding, UnbindContext};
use crate::dom::performanceresourcetiming::InitiatorType;
use crate::dom::promise::Promise;
use crate::dom::virtualmethods::VirtualMethods;
use crate::dom::window::Window;
use crate::fetch::create_a_potential_cors_request;
use crate::microtask::{Microtask, MicrotaskRunnable};
use crate::network_listener::{self, PreInvoke, ResourceTimingListener};
use crate::realms::enter_realm;
use crate::script_runtime::CanGc;
use crate::script_thread::ScriptThread;
/// Supported image MIME types as defined by
/// <https://mimesniff.spec.whatwg.org/#image-mime-type>.
/// Keep this in sync with 'detect_image_format' from components/pixels/lib.rs
const SUPPORTED_IMAGE_MIME_TYPES: &[&str] = &[
"image/bmp",
"image/gif",
"image/jpeg",
"image/jpg",
"image/pjpeg",
"image/png",
"image/apng",
"image/x-png",
"image/svg+xml",
"image/vnd.microsoft.icon",
"image/x-icon",
"image/webp",
];
#[derive(Clone, Copy, Debug)]
enum ParseState {
InDescriptor,
InParens,
AfterDescriptor,
}
/// <https://html.spec.whatwg.org/multipage/#source-set>
#[derive(MallocSizeOf)]
pub(crate) struct SourceSet {
image_sources: Vec<ImageSource>,
source_size: SourceSizeList,
}
impl SourceSet {
fn new() -> SourceSet {
SourceSet {
image_sources: Vec::new(),
source_size: SourceSizeList::empty(),
}
}
}
#[derive(Clone, Debug, MallocSizeOf, PartialEq)]
pub struct ImageSource {
pub url: String,
pub descriptor: Descriptor,
}
#[derive(Clone, Debug, MallocSizeOf, PartialEq)]
pub struct Descriptor {
pub width: Option<u32>,
pub density: Option<f64>,
}
#[derive(Clone, Copy, JSTraceable, MallocSizeOf)]
#[allow(dead_code)]
enum State {
Unavailable,
PartiallyAvailable,
CompletelyAvailable,
Broken,
}
#[derive(Clone, Copy, JSTraceable, MallocSizeOf)]
enum ImageRequestPhase {
Pending,
Current,
}
#[derive(JSTraceable, MallocSizeOf)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
struct ImageRequest {
state: State,
#[no_trace]
parsed_url: Option<ServoUrl>,
source_url: Option<USVString>,
blocker: DomRefCell<Option<LoadBlocker>>,
#[no_trace]
image: Option<Image>,
#[no_trace]
metadata: Option<ImageMetadata>,
#[no_trace]
final_url: Option<ServoUrl>,
current_pixel_density: Option<f64>,
}
#[dom_struct]
pub(crate) struct HTMLImageElement {
htmlelement: HTMLElement,
image_request: Cell<ImageRequestPhase>,
current_request: DomRefCell<ImageRequest>,
pending_request: DomRefCell<ImageRequest>,
form_owner: MutNullableDom<HTMLFormElement>,
generation: Cell<u32>,
source_set: DomRefCell<SourceSet>,
/// <https://html.spec.whatwg.org/multipage/#concept-img-dimension-attribute-source>
/// Always non-null after construction.
dimension_attribute_source: MutNullableDom<Element>,
last_selected_source: DomRefCell<Option<USVString>>,
#[conditional_malloc_size_of]
image_decode_promises: DomRefCell<Vec<Rc<Promise>>>,
/// Line number this element was created on
line_number: u64,
}
impl HTMLImageElement {
// https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument
pub(crate) fn is_usable(&self) -> Fallible<bool> {
// If image has an intrinsic width or intrinsic height (or both) equal to zero, then return bad.
if let Some(image) = &self.current_request.borrow().image {
let intrinsic_size = image.metadata();
if intrinsic_size.width == 0 || intrinsic_size.height == 0 {
return Ok(false);
}
}
match self.current_request.borrow().state {
// If image's current request's state is broken, then throw an "InvalidStateError" DOMException.
State::Broken => Err(Error::InvalidState(None)),
State::CompletelyAvailable => Ok(true),
// If image is not fully decodable, then return bad.
State::PartiallyAvailable | State::Unavailable => Ok(false),
}
}
pub(crate) fn image_data(&self) -> Option<Image> {
self.current_request.borrow().image.clone()
}
/// Gets the copy of the raster image data.
pub(crate) fn get_raster_image_data(&self) -> Option<Snapshot> {
let Some(img) = self.image_data()?.as_raster_image() else {
warn!("Vector image is not supported as raster image source");
return None;
};
let size = Size2D::new(img.metadata.width, img.metadata.height);
let format = match img.format {
PixelFormat::BGRA8 => SnapshotPixelFormat::BGRA,
PixelFormat::RGBA8 => SnapshotPixelFormat::RGBA,
pixel_format => {
unimplemented!("unsupported pixel format ({:?})", pixel_format)
},
};
let alpha_mode = SnapshotAlphaMode::Transparent {
premultiplied: false,
};
let snapshot = Snapshot::from_vec(
size.cast(),
format,
alpha_mode,
img.first_frame().bytes.to_vec(),
);
Some(snapshot)
}
}
/// The context required for asynchronously loading an external image.
struct ImageContext {
/// Reference to the script thread image cache.
image_cache: Arc<dyn ImageCache>,
/// Indicates whether the request failed, and why
status: Result<(), NetworkError>,
/// The cache ID for this request.
id: PendingImageId,
/// Used to mark abort
aborted: bool,
/// The document associated with this request
doc: Trusted<Document>,
/// timing data for this resource
resource_timing: ResourceFetchTiming,
url: ServoUrl,
element: Trusted<HTMLImageElement>,
}
impl FetchResponseListener for ImageContext {
fn process_request_body(&mut self, _: RequestId) {}
fn process_request_eof(&mut self, _: RequestId) {}
fn process_response(
&mut self,
request_id: RequestId,
metadata: Result<FetchMetadata, NetworkError>,
) {
debug!("got {:?} for {:?}", metadata.as_ref().map(|_| ()), self.url);
self.image_cache.notify_pending_response(
self.id,
FetchResponseMsg::ProcessResponse(request_id, metadata.clone()),
);
let metadata = metadata.ok().map(|meta| match meta {
FetchMetadata::Unfiltered(m) => m,
FetchMetadata::Filtered { unsafe_, .. } => unsafe_,
});
// Step 14.5 of https://html.spec.whatwg.org/multipage/#img-environment-changes
if let Some(metadata) = metadata.as_ref() {
if let Some(ref content_type) = metadata.content_type {
let mime: Mime = content_type.clone().into_inner().into();
if mime.type_() == mime::MULTIPART && mime.subtype().as_str() == "x-mixed-replace" {
self.aborted = true;
}
}
}
let status = metadata
.as_ref()
.map(|m| m.status.clone())
.unwrap_or_else(HttpStatus::new_error);
self.status = {
if status.is_error() {
Err(NetworkError::Internal(
"No http status code received".to_owned(),
))
} else if status.is_success() {
Ok(())
} else {
Err(NetworkError::Internal(format!(
"HTTP error code {}",
status.code()
)))
}
};
}
fn process_response_chunk(&mut self, request_id: RequestId, payload: Vec<u8>) {
if self.status.is_ok() {
self.image_cache.notify_pending_response(
self.id,
FetchResponseMsg::ProcessResponseChunk(request_id, payload),
);
}
}
fn process_response_eof(
&mut self,
request_id: RequestId,
response: Result<ResourceFetchTiming, NetworkError>,
) {
self.image_cache.notify_pending_response(
self.id,
FetchResponseMsg::ProcessResponseEOF(request_id, response),
);
}
fn resource_timing_mut(&mut self) -> &mut ResourceFetchTiming {
&mut self.resource_timing
}
fn resource_timing(&self) -> &ResourceFetchTiming {
&self.resource_timing
}
fn submit_resource_timing(&mut self) {
network_listener::submit_timing(self, CanGc::note())
}
fn process_csp_violations(&mut self, _request_id: RequestId, violations: Vec<Violation>) {
let global = &self.resource_timing_global();
let elem = self.element.root();
let source_position = elem
.upcast::<Element>()
.compute_source_position(elem.line_number as u32);
global.report_csp_violations(violations, None, Some(source_position));
}
}
impl ResourceTimingListener for ImageContext {
fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
(
InitiatorType::LocalName("img".to_string()),
self.url.clone(),
)
}
fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
self.doc.root().global()
}
}
impl PreInvoke for ImageContext {
fn should_invoke(&self) -> bool {
!self.aborted
}
}
#[allow(non_snake_case)]
impl HTMLImageElement {
/// Update the current image with a valid URL.
fn fetch_image(&self, img_url: &ServoUrl, can_gc: CanGc) {
let window = self.owner_window();
let cache_result = window.image_cache().get_cached_image_status(
img_url.clone(),
window.origin().immutable().clone(),
cors_setting_for_element(self.upcast()),
UsePlaceholder::Yes,
);
match cache_result {
ImageCacheResult::Available(ImageOrMetadataAvailable::ImageAvailable {
image,
url,
is_placeholder,
}) => {
if is_placeholder {
if let Some(raster_image) = image.as_raster_image() {
self.process_image_response(
ImageResponse::PlaceholderLoaded(raster_image, url),
can_gc,
)
}
} else {
self.process_image_response(ImageResponse::Loaded(image, url), can_gc)
}
},
ImageCacheResult::Available(ImageOrMetadataAvailable::MetadataAvailable(
metadata,
id,
)) => {
self.process_image_response(ImageResponse::MetadataLoaded(metadata), can_gc);
self.register_image_cache_callback(id, ChangeType::Element);
},
ImageCacheResult::Pending(id) => {
self.register_image_cache_callback(id, ChangeType::Element);
},
ImageCacheResult::ReadyForRequest(id) => {
self.fetch_request(img_url, id);
self.register_image_cache_callback(id, ChangeType::Element);
},
ImageCacheResult::LoadError => self.process_image_response(ImageResponse::None, can_gc),
};
}
fn register_image_cache_callback(&self, id: PendingImageId, change_type: ChangeType) {
let trusted_node = Trusted::new(self);
let generation = self.generation_id();
let window = self.owner_window();
let sender = window.register_image_cache_listener(id, move |response| {
let trusted_node = trusted_node.clone();
let window = trusted_node.root().owner_window();
let callback_type = change_type.clone();
window
.as_global_scope()
.task_manager()
.networking_task_source()
.queue(task!(process_image_response: move || {
let element = trusted_node.root();
// Ignore any image response for a previous request that has been discarded.
if generation != element.generation_id() {
return;
}
match callback_type {
ChangeType::Element => {
element.process_image_response(response.response, CanGc::note());
}
ChangeType::Environment { selected_source, selected_pixel_density } => {
element.process_image_response_for_environment_change(
response.response, selected_source, generation, selected_pixel_density, CanGc::note()
);
}
}
}));
});
window
.image_cache()
.add_listener(ImageLoadListener::new(sender, window.pipeline_id(), id));
}
fn fetch_request(&self, img_url: &ServoUrl, id: PendingImageId) {
let document = self.owner_document();
let window = self.owner_window();
let context = ImageContext {
image_cache: window.image_cache(),
status: Ok(()),
id,
aborted: false,
doc: Trusted::new(&document),
element: Trusted::new(self),
resource_timing: ResourceFetchTiming::new(ResourceTimingType::Resource),
url: img_url.clone(),
};
// https://html.spec.whatwg.org/multipage/#update-the-image-data steps 17-20
// This function is also used to prefetch an image in `script::dom::servoparser::prefetch`.
let global = document.global();
let mut request = create_a_potential_cors_request(
Some(window.webview_id()),
img_url.clone(),
Destination::Image,
cors_setting_for_element(self.upcast()),
None,
global.get_referrer(),
document.insecure_requests_policy(),
document.has_trustworthy_ancestor_or_current_origin(),
global.policy_container(),
)
.origin(document.origin().immutable().clone())
.pipeline_id(Some(document.global().pipeline_id()))
.referrer_policy(referrer_policy_for_element(self.upcast()));
if Self::uses_srcset_or_picture(self.upcast()) {
request = request.initiator(Initiator::ImageSet);
}
// This is a background load because the load blocker already fulfills the
// purpose of delaying the document's load event.
document.fetch_background(request, context);
}
// Steps common to when an image has been loaded.
fn handle_loaded_image(&self, image: Image, url: ServoUrl, can_gc: CanGc) {
self.current_request.borrow_mut().metadata = Some(image.metadata());
self.current_request.borrow_mut().final_url = Some(url);
self.current_request.borrow_mut().image = Some(image);
self.current_request.borrow_mut().state = State::CompletelyAvailable;
LoadBlocker::terminate(&self.current_request.borrow().blocker, can_gc);
// Mark the node dirty
self.upcast::<Node>().dirty(NodeDamage::Other);
self.resolve_image_decode_promises();
}
/// Step 24 of <https://html.spec.whatwg.org/multipage/#update-the-image-data>
fn process_image_response(&self, image: ImageResponse, can_gc: CanGc) {
// TODO: Handle multipart/x-mixed-replace
let (trigger_image_load, trigger_image_error) = match (image, self.image_request.get()) {
(ImageResponse::Loaded(image, url), ImageRequestPhase::Current) => {
self.handle_loaded_image(image, url, can_gc);
(true, false)
},
(ImageResponse::PlaceholderLoaded(image, url), ImageRequestPhase::Current) => {
self.handle_loaded_image(Image::Raster(image), url, can_gc);
(false, true)
},
(ImageResponse::Loaded(image, url), ImageRequestPhase::Pending) => {
self.abort_request(State::Unavailable, ImageRequestPhase::Pending, can_gc);
self.image_request.set(ImageRequestPhase::Current);
self.handle_loaded_image(image, url, can_gc);
(true, false)
},
(ImageResponse::PlaceholderLoaded(image, url), ImageRequestPhase::Pending) => {
self.abort_request(State::Unavailable, ImageRequestPhase::Pending, can_gc);
self.image_request.set(ImageRequestPhase::Current);
self.handle_loaded_image(Image::Raster(image), url, can_gc);
(false, true)
},
(ImageResponse::MetadataLoaded(meta), ImageRequestPhase::Current) => {
self.current_request.borrow_mut().state = State::PartiallyAvailable;
self.current_request.borrow_mut().metadata = Some(meta);
(false, false)
},
(ImageResponse::MetadataLoaded(_), ImageRequestPhase::Pending) => {
self.pending_request.borrow_mut().state = State::PartiallyAvailable;
(false, false)
},
(ImageResponse::None, ImageRequestPhase::Current) => {
self.abort_request(State::Broken, ImageRequestPhase::Current, can_gc);
(false, true)
},
(ImageResponse::None, ImageRequestPhase::Pending) => {
self.abort_request(State::Broken, ImageRequestPhase::Current, can_gc);
self.abort_request(State::Broken, ImageRequestPhase::Pending, can_gc);
self.image_request.set(ImageRequestPhase::Current);
(false, true)
},
};
// Fire image.onload and loadend
if trigger_image_load {
// TODO: https://html.spec.whatwg.org/multipage/#fire-a-progress-event-or-event
self.upcast::<EventTarget>()
.fire_event(atom!("load"), can_gc);
self.upcast::<EventTarget>()
.fire_event(atom!("loadend"), can_gc);
}
// Fire image.onerror
if trigger_image_error {
self.upcast::<EventTarget>()
.fire_event(atom!("error"), can_gc);
self.upcast::<EventTarget>()
.fire_event(atom!("loadend"), can_gc);
}
}
fn process_image_response_for_environment_change(
&self,
image: ImageResponse,
src: USVString,
generation: u32,
selected_pixel_density: f64,
can_gc: CanGc,
) {
match image {
ImageResponse::Loaded(image, url) => {
self.pending_request.borrow_mut().metadata = Some(image.metadata());
self.pending_request.borrow_mut().final_url = Some(url);
self.pending_request.borrow_mut().image = Some(image);
self.finish_reacting_to_environment_change(src, generation, selected_pixel_density);
},
ImageResponse::PlaceholderLoaded(image, url) => {
let image = Image::Raster(image);
self.pending_request.borrow_mut().metadata = Some(image.metadata());
self.pending_request.borrow_mut().final_url = Some(url);
self.pending_request.borrow_mut().image = Some(image);
self.finish_reacting_to_environment_change(src, generation, selected_pixel_density);
},
ImageResponse::MetadataLoaded(meta) => {
self.pending_request.borrow_mut().metadata = Some(meta);
},
ImageResponse::None => {
self.abort_request(State::Unavailable, ImageRequestPhase::Pending, can_gc);
},
};
}
/// <https://html.spec.whatwg.org/multipage/#abort-the-image-request>
fn abort_request(&self, state: State, request: ImageRequestPhase, can_gc: CanGc) {
let mut request = match request {
ImageRequestPhase::Current => self.current_request.borrow_mut(),
ImageRequestPhase::Pending => self.pending_request.borrow_mut(),
};
LoadBlocker::terminate(&request.blocker, can_gc);
request.state = state;
request.image = None;
request.metadata = None;
if matches!(state, State::Broken) {
self.reject_image_decode_promises();
} else if matches!(state, State::CompletelyAvailable) {
self.resolve_image_decode_promises();
}
}
/// <https://html.spec.whatwg.org/multipage/#create-a-source-set>
fn create_source_set(&self) -> SourceSet {
let element = self.upcast::<Element>();
// Step 1. Let source set be an empty source set.
let mut source_set = SourceSet::new();
// Step 2. If srcset is not an empty string, then set source set to the result of parsing
// srcset.
if let Some(srcset) = element.get_attribute(&ns!(), &local_name!("srcset")) {
source_set.image_sources = parse_a_srcset_attribute(&srcset.value());
}
// Step 3. Set source set's source size to the result of parsing sizes with img.
if let Some(sizes) = element.get_attribute(&ns!(), &local_name!("sizes")) {
source_set.source_size = parse_a_sizes_attribute(&sizes.value());
}
// Step 4. If default source is not the empty string and source set does not contain an
// image source with a pixel density descriptor value of 1, and no image source with a width
// descriptor, append default source to source set.
let src_attribute = element.get_string_attribute(&local_name!("src"));
let is_src_empty = src_attribute.is_empty();
let no_density_source_of_1 = source_set
.image_sources
.iter()
.all(|source| source.descriptor.density != Some(1.));
let no_width_descriptor = source_set
.image_sources
.iter()
.all(|source| source.descriptor.width.is_none());
if !is_src_empty && no_density_source_of_1 && no_width_descriptor {
source_set.image_sources.push(ImageSource {
url: src_attribute.to_string(),
descriptor: Descriptor {
width: None,
density: None,
},
})
}
// Step 5. Normalize the source densities of source set.
self.normalise_source_densities(&mut source_set);
// Step 6. Return source set.
source_set
}
/// <https://html.spec.whatwg.org/multipage/#update-the-source-set>
fn update_source_set(&self) {
// Step 1. Set el's source set to an empty source set.
*self.source_set.borrow_mut() = SourceSet::new();
// Step 2. Let elements be « el ».
// Step 3. If el is an img element whose parent node is a picture element, then replace the
// contents of elements with el's parent node's child elements, retaining relative order.
// Step 4. Let img be el if el is an img element, otherwise null.
let elem = self.upcast::<Element>();
let parent = elem.upcast::<Node>().GetParentElement();
let elements = match parent.as_ref() {
Some(p) => {
if p.is::<HTMLPictureElement>() {
p.upcast::<Node>()
.children()
.filter_map(DomRoot::downcast::<Element>)
.map(|n| DomRoot::from_ref(&*n))
.collect()
} else {
vec![DomRoot::from_ref(elem)]
}
},
None => vec![DomRoot::from_ref(elem)],
};
// Step 5. For each child in elements:
for element in &elements {
// Step 5.1. If child is el:
if *element == DomRoot::from_ref(elem) {
// Step 5.1.10. Set el's source set to the result of creating a source set given
// default source, srcset, sizes, and img.
*self.source_set.borrow_mut() = self.create_source_set();
// Step 5.1.11. Return.
return;
}
// Step 5.2. If child is not a source element, then continue.
if !element.is::<HTMLSourceElement>() {
continue;
}
let mut source_set = SourceSet::new();
// Step 5.3. If child does not have a srcset attribute, continue to the next child.
// Step 5.4. Parse child's srcset attribute and let source set be the returned source
// set.
match element.get_attribute(&ns!(), &local_name!("srcset")) {
Some(srcset) => {
source_set.image_sources = parse_a_srcset_attribute(&srcset.value());
},
_ => continue,
}
// Step 5.5. If source set has zero image sources, continue to the next child.
if source_set.image_sources.is_empty() {
continue;
}
// Step 5.6. If child has a media attribute, and its value does not match the
// environment, continue to the next child.
if let Some(media) = element.get_attribute(&ns!(), &local_name!("media")) {
if !MediaList::matches_environment(&element.owner_document(), &media.value()) {
continue;
}
}
// Step 5.7. Parse child's sizes attribute with img, and let source set's source size be
// the returned value.
if let Some(sizes) = element.get_attribute(&ns!(), &local_name!("sizes")) {
source_set.source_size = parse_a_sizes_attribute(&sizes.value());
}
// Step 5.8. If child has a type attribute, and its value is an unknown or unsupported
// MIME type, continue to the next child.
if let Some(type_) = element.get_attribute(&ns!(), &local_name!("type")) {
if !is_supported_image_mime_type(&type_.value()) {
continue;
}
}
// Step 5.9. If child has width or height attributes, set el's dimension attribute
// source to child. Otherwise, set el's dimension attribute source to el.
if element
.get_attribute(&ns!(), &local_name!("width"))
.is_some() ||
element
.get_attribute(&ns!(), &local_name!("height"))
.is_some()
{
self.dimension_attribute_source.set(Some(element));
} else {
self.dimension_attribute_source.set(Some(elem));
}
// Step 5.10. Normalize the source densities of source set.
self.normalise_source_densities(&mut source_set);
// Step 5.11. Set el's source set to source set.
*self.source_set.borrow_mut() = source_set;
// Step 5.12. Return.
return;
}
}
fn evaluate_source_size_list(&self, source_size_list: &SourceSizeList) -> Au {
let document = self.owner_document();
let quirks_mode = document.quirks_mode();
source_size_list.evaluate(document.window().layout().device(), quirks_mode)
}
/// <https://html.spec.whatwg.org/multipage/#normalise-the-source-densities>
fn normalise_source_densities(&self, source_set: &mut SourceSet) {
// Step 1. Let source size be source set's source size.
let source_size = self.evaluate_source_size_list(&source_set.source_size);
// Step 2. For each image source in source set:
for image_source in &mut source_set.image_sources {
// Step 2.1. If the image source has a pixel density descriptor, continue to the next
// image source.
if image_source.descriptor.density.is_some() {
continue;
}
// Step 2.2. Otherwise, if the image source has a width descriptor, replace the width
// descriptor with a pixel density descriptor with a value of the width descriptor value
// divided by source size and a unit of x.
if image_source.descriptor.width.is_some() {
let width = image_source.descriptor.width.unwrap();
image_source.descriptor.density = Some(width as f64 / source_size.to_f64_px());
} else {
// Step 2.3. Otherwise, give the image source a pixel density descriptor of 1x.
image_source.descriptor.density = Some(1_f64);
}
}
}
/// <https://html.spec.whatwg.org/multipage/#select-an-image-source>
fn select_image_source(&self) -> Option<(USVString, f64)> {
// Step 1. Update the source set for el.
self.update_source_set();
// Step 2. If el's source set is empty, return null as the URL and undefined as the pixel
// density.
if self.source_set.borrow().image_sources.is_empty() {
return None;
}
// Step 3. Return the result of selecting an image from el's source set.
self.select_image_source_from_source_set()
}
/// <https://html.spec.whatwg.org/multipage/#select-an-image-source-from-a-source-set>
fn select_image_source_from_source_set(&self) -> Option<(USVString, f64)> {
// Step 1. If an entry b in sourceSet has the same associated pixel density descriptor as an
// earlier entry a in sourceSet, then remove entry b. Repeat this step until none of the
// entries in sourceSet have the same associated pixel density descriptor as an earlier
// entry.
let source_set = self.source_set.borrow();
let len = source_set.image_sources.len();
// Using FxHash is ok here as the indices are just 0..len
let mut repeat_indices = FxHashSet::default();
for outer_index in 0..len {
if repeat_indices.contains(&outer_index) {
continue;
}
let imgsource = &source_set.image_sources[outer_index];
let pixel_density = imgsource.descriptor.density.unwrap();
for inner_index in (outer_index + 1)..len {
let imgsource2 = &source_set.image_sources[inner_index];
if pixel_density == imgsource2.descriptor.density.unwrap() {
repeat_indices.insert(inner_index);
}
}
}
let mut max = (0f64, 0);
let img_sources = &mut vec![];
for (index, image_source) in source_set.image_sources.iter().enumerate() {
if repeat_indices.contains(&index) {
continue;
}
let den = image_source.descriptor.density.unwrap();
if max.0 < den {
max = (den, img_sources.len());
}
img_sources.push(image_source);
}
// Step 2. In an implementation-defined manner, choose one image source from sourceSet. Let
// selectedSource be this choice.
let mut best_candidate = max;
let device_pixel_ratio = self
.owner_document()
.window()
.viewport_details()
.hidpi_scale_factor
.get() as f64;
for (index, image_source) in img_sources.iter().enumerate() {
let current_den = image_source.descriptor.density.unwrap();
if current_den < best_candidate.0 && current_den >= device_pixel_ratio {
best_candidate = (current_den, index);
}
}
let selected_source = img_sources.remove(best_candidate.1).clone();
// Step 3. Return selectedSource and its associated pixel density.
Some((
USVString(selected_source.url),
selected_source.descriptor.density.unwrap(),
))
}
fn init_image_request(
&self,
request: &mut RefMut<'_, ImageRequest>,
url: &ServoUrl,
src: &USVString,
can_gc: CanGc,
) {
request.parsed_url = Some(url.clone());
request.source_url = Some(src.clone());
request.image = None;
request.metadata = None;
let document = self.owner_document();
LoadBlocker::terminate(&request.blocker, can_gc);
*request.blocker.borrow_mut() =
Some(LoadBlocker::new(&document, LoadType::Image(url.clone())));
}
/// Step 13-17 of html.spec.whatwg.org/multipage/#update-the-image-data
fn prepare_image_request(
&self,
url: &ServoUrl,
src: &USVString,
selected_pixel_density: f64,
can_gc: CanGc,
) {
match self.image_request.get() {
ImageRequestPhase::Pending => {
if let Some(pending_url) = self.pending_request.borrow().parsed_url.clone() {
// Step 13
if pending_url == *url {
return;
}
}
},
ImageRequestPhase::Current => {
let mut current_request = self.current_request.borrow_mut();
let mut pending_request = self.pending_request.borrow_mut();
// step 16, create a new "image_request"
match (current_request.parsed_url.clone(), current_request.state) {
(Some(parsed_url), State::PartiallyAvailable) => {
// Step 14
if parsed_url == *url {
// Step 15 abort pending request
pending_request.image = None;
pending_request.parsed_url = None;
LoadBlocker::terminate(&pending_request.blocker, can_gc);
// TODO: queue a task to restart animation, if restart-animation is set
return;
}
pending_request.current_pixel_density = Some(selected_pixel_density);
self.image_request.set(ImageRequestPhase::Pending);
self.init_image_request(&mut pending_request, url, src, can_gc);
},
(_, State::Broken) | (_, State::Unavailable) => {
// Step 17
current_request.current_pixel_density = Some(selected_pixel_density);
self.init_image_request(&mut current_request, url, src, can_gc);
self.reject_image_decode_promises();
},
(_, _) => {
// step 17
pending_request.current_pixel_density = Some(selected_pixel_density);
self.image_request.set(ImageRequestPhase::Pending);
self.init_image_request(&mut pending_request, url, src, can_gc);
},
}
},
}
self.fetch_image(url, can_gc);
}
/// Step 8-12 of html.spec.whatwg.org/multipage/#update-the-image-data
fn update_the_image_data_sync_steps(&self, can_gc: CanGc) {
let document = self.owner_document();
let global = self.owner_global();
let task_manager = global.task_manager();
let task_source = task_manager.dom_manipulation_task_source();
let this = Trusted::new(self);
let (src, pixel_density) = match self.select_image_source() {
// Step 8
Some(data) => data,
None => {
self.abort_request(State::Broken, ImageRequestPhase::Current, can_gc);
self.abort_request(State::Broken, ImageRequestPhase::Pending, can_gc);
// Step 9.
task_source.queue(task!(image_null_source_error: move || {
let this = this.root();
{
let mut current_request =
this.current_request.borrow_mut();
current_request.source_url = None;
current_request.parsed_url = None;
}
let elem = this.upcast::<Element>();
let src_present = elem.has_attribute(&local_name!("src"));
if src_present || Self::uses_srcset_or_picture(elem) {
this.upcast::<EventTarget>().fire_event(atom!("error"), CanGc::note());
}
}));
return;
},
};
// Step 11
let base_url = document.base_url();
let parsed_url = base_url.join(&src.0);
match parsed_url {
Ok(url) => {
// Step 13-17
self.prepare_image_request(&url, &src, pixel_density, can_gc);
},
Err(_) => {
self.abort_request(State::Broken, ImageRequestPhase::Current, can_gc);
self.abort_request(State::Broken, ImageRequestPhase::Pending, can_gc);
// Step 12.1-12.5.
let src = src.0;
task_source.queue(task!(image_selected_source_error: move || {
let this = this.root();
{
let mut current_request =
this.current_request.borrow_mut();
current_request.source_url = Some(USVString(src))
}
this.upcast::<EventTarget>().fire_event(atom!("error"), CanGc::note());
}));
},
}
}
/// <https://html.spec.whatwg.org/multipage/#update-the-image-data>
pub(crate) fn update_the_image_data(&self, can_gc: CanGc) {
let document = self.owner_document();
let window = document.window();
let elem = self.upcast::<Element>();
let src = elem.get_url_attribute(&local_name!("src"));
let base_url = document.base_url();
// https://html.spec.whatwg.org/multipage/#reacting-to-dom-mutations
// Always first set the current request to unavailable,
// ensuring img.complete is false.
{
let mut current_request = self.current_request.borrow_mut();
current_request.state = State::Unavailable;
}
if !document.is_active() {
// Step 1 (if the document is inactive)
// TODO: use GlobalScope::enqueue_microtask,
// to queue micro task to come back to this algorithm
}
// Step 2 abort if user-agent does not supports images
// NOTE: Servo only supports images, skipping this step
// Step 3, 4
let mut selected_source = None;
let mut pixel_density = None;
let src_set = elem.get_url_attribute(&local_name!("srcset"));
let is_parent_picture = elem
.upcast::<Node>()
.GetParentElement()
.is_some_and(|p| p.is::<HTMLPictureElement>());
if src_set.is_empty() && !is_parent_picture && !src.is_empty() {
selected_source = Some(src.clone());
pixel_density = Some(1_f64);
};
// Step 5
self.last_selected_source
.borrow_mut()
.clone_from(&selected_source);
// Step 6, check the list of available images
if let Some(src) = selected_source {
if let Ok(img_url) = base_url.join(&src) {
let image_cache = window.image_cache();
let response = image_cache.get_image(
img_url.clone(),
window.origin().immutable().clone(),
cors_setting_for_element(self.upcast()),
);
if let Some(image) = response {
// Cancel any outstanding tasks that were queued before the src was
// set on this element.
self.generation.set(self.generation.get() + 1);
// Step 6.3
let metadata = image.metadata();
// Step 6.3.2 abort requests
self.abort_request(
State::CompletelyAvailable,
ImageRequestPhase::Current,
can_gc,
);
self.abort_request(State::Unavailable, ImageRequestPhase::Pending, can_gc);
let mut current_request = self.current_request.borrow_mut();
current_request.final_url = Some(img_url.clone());
current_request.image = Some(image);
current_request.metadata = Some(metadata);
// Step 6.3.6
current_request.current_pixel_density = pixel_density;
let this = Trusted::new(self);
let src = src.0;
self.owner_global()
.task_manager()
.dom_manipulation_task_source()
.queue(task!(image_load_event: move || {
let this = this.root();
{
let mut current_request =
this.current_request.borrow_mut();
current_request.parsed_url = Some(img_url);
current_request.source_url = Some(USVString(src));
}
// TODO: restart animation, if set.
this.upcast::<EventTarget>().fire_event(atom!("load"), CanGc::note());
}));
return;
}
}
}
// step 7, await a stable state.
self.generation.set(self.generation.get() + 1);
let task = ImageElementMicrotask::StableStateUpdateImageData {
elem: DomRoot::from_ref(self),
generation: self.generation.get(),
};
ScriptThread::await_stable_state(Microtask::ImageElement(task));
}
/// <https://html.spec.whatwg.org/multipage/#img-environment-changes>
pub(crate) fn react_to_environment_changes(&self) {
// Step 1
let task = ImageElementMicrotask::EnvironmentChanges {
elem: DomRoot::from_ref(self),
generation: self.generation.get(),
};
ScriptThread::await_stable_state(Microtask::ImageElement(task));
}
/// Step 2-12 of <https://html.spec.whatwg.org/multipage/#img-environment-changes>
fn react_to_environment_changes_sync_steps(&self, generation: u32, can_gc: CanGc) {
let elem = self.upcast::<Element>();
let document = elem.owner_document();
let has_pending_request = matches!(self.image_request.get(), ImageRequestPhase::Pending);
// Step 2
if !document.is_active() || !Self::uses_srcset_or_picture(elem) || has_pending_request {
return;
}
// Steps 3-4
let (selected_source, selected_pixel_density) = match self.select_image_source() {
Some(selected) => selected,
None => return,
};
// Step 5
let same_source = match *self.last_selected_source.borrow() {
Some(ref last_src) => *last_src == selected_source,
_ => false,
};
let same_selected_pixel_density = match self.current_request.borrow().current_pixel_density
{
Some(den) => selected_pixel_density == den,
_ => false,
};
if same_source && same_selected_pixel_density {
return;
}
let base_url = document.base_url();
// Step 6
let img_url = match base_url.join(&selected_source.0) {
Ok(url) => url,
Err(_) => return,
};
// Step 12
self.image_request.set(ImageRequestPhase::Pending);
self.init_image_request(
&mut self.pending_request.borrow_mut(),
&img_url,
&selected_source,
can_gc,
);
// Step 14
let window = self.owner_window();
let cache_result = window.image_cache().get_cached_image_status(
img_url.clone(),
window.origin().immutable().clone(),
cors_setting_for_element(self.upcast()),
UsePlaceholder::No,
);
let change_type = ChangeType::Environment {
selected_source: selected_source.clone(),
selected_pixel_density,
};
match cache_result {
ImageCacheResult::Available(ImageOrMetadataAvailable::ImageAvailable { .. }) => {
// Step 15
self.finish_reacting_to_environment_change(
selected_source,
generation,
selected_pixel_density,
)
},
ImageCacheResult::Available(ImageOrMetadataAvailable::MetadataAvailable(m, id)) => {
self.process_image_response_for_environment_change(
ImageResponse::MetadataLoaded(m),
selected_source,
generation,
selected_pixel_density,
can_gc,
);
self.register_image_cache_callback(id, change_type);
},
ImageCacheResult::LoadError => {
self.process_image_response_for_environment_change(
ImageResponse::None,
selected_source,
generation,
selected_pixel_density,
can_gc,
);
},
ImageCacheResult::ReadyForRequest(id) => {
self.fetch_request(&img_url, id);
self.register_image_cache_callback(id, change_type);
},
ImageCacheResult::Pending(id) => {
self.register_image_cache_callback(id, change_type);
},
}
}
/// <https://html.spec.whatwg.org/multipage/#dom-img-decode>
fn react_to_decode_image_sync_steps(&self, promise: Rc<Promise>, can_gc: CanGc) {
// Step 2.2. If any of the following are true: this's node document is not fully active; or
// this's current request's state is broken, then reject promise with an "EncodingError"
// DOMException.
if !self.owner_document().is_fully_active() ||
matches!(self.current_request.borrow().state, State::Broken)
{
promise.reject_error(Error::Encoding, can_gc);
} else if matches!(
self.current_request.borrow().state,
State::CompletelyAvailable
) {
// this doesn't follow the spec, but it's been discussed in <https://github.com/whatwg/html/issues/4217>
promise.resolve_native(&(), can_gc);
} else if matches!(self.current_request.borrow().state, State::Unavailable) &&
self.current_request.borrow().source_url.is_none()
{
// Note: Despite being not explicitly stated in the specification but if current
// request's state is unavailable and current URL is empty string (<img> without "src"
// and "srcset" attributes) then reject promise with an "EncodingError" DOMException.
promise.reject_error(Error::Encoding, can_gc);
} else {
self.image_decode_promises.borrow_mut().push(promise);
}
}
/// <https://html.spec.whatwg.org/multipage/#dom-img-decode>
fn resolve_image_decode_promises(&self) {
if self.image_decode_promises.borrow().is_empty() {
return;
}
// Step 3. If the decoding process completes successfully, then queue a
// global task on the DOM manipulation task source with global to
// resolve promise with undefined.
let trusted_image_decode_promises: Vec<TrustedPromise> = self
.image_decode_promises
.borrow()
.iter()
.map(|promise| TrustedPromise::new(promise.clone()))
.collect();
self.image_decode_promises.borrow_mut().clear();
self.owner_global()
.task_manager()
.dom_manipulation_task_source()
.queue(task!(fulfill_image_decode_promises: move || {
for trusted_promise in trusted_image_decode_promises {
trusted_promise.root().resolve_native(&(), CanGc::note());
}
}));
}
/// <https://html.spec.whatwg.org/multipage/#dom-img-decode>
fn reject_image_decode_promises(&self) {
if self.image_decode_promises.borrow().is_empty() {
return;
}
// Step 3. Queue a global task on the DOM manipulation task source with
// global to reject promise with an "EncodingError" DOMException.
let trusted_image_decode_promises: Vec<TrustedPromise> = self
.image_decode_promises
.borrow()
.iter()
.map(|promise| TrustedPromise::new(promise.clone()))
.collect();
self.image_decode_promises.borrow_mut().clear();
self.owner_global()
.task_manager()
.dom_manipulation_task_source()
.queue(task!(reject_image_decode_promises: move || {
for trusted_promise in trusted_image_decode_promises {
trusted_promise.root().reject_error(Error::Encoding, CanGc::note());
}
}));
}
/// Step 15 for <https://html.spec.whatwg.org/multipage/#img-environment-changes>
fn finish_reacting_to_environment_change(
&self,
src: USVString,
generation: u32,
selected_pixel_density: f64,
) {
let this = Trusted::new(self);
let src = src.0;
self.owner_global().task_manager().dom_manipulation_task_source().queue(
task!(image_load_event: move || {
let this = this.root();
let relevant_mutation = this.generation.get() != generation;
// Step 15.1
if relevant_mutation {
this.abort_request(State::Unavailable, ImageRequestPhase::Pending, CanGc::note());
return;
}
// Step 15.2
*this.last_selected_source.borrow_mut() = Some(USVString(src));
{
let mut pending_request = this.pending_request.borrow_mut();
pending_request.current_pixel_density = Some(selected_pixel_density);
// Step 15.3
pending_request.state = State::CompletelyAvailable;
// Step 15.4
// Already a part of the list of available images due to Step 14
// Step 15.5
#[allow(clippy::swap_with_temporary)]
mem::swap(&mut this.current_request.borrow_mut(), &mut pending_request);
}
this.abort_request(State::Unavailable, ImageRequestPhase::Pending, CanGc::note());
// Step 15.6
this.upcast::<Node>().dirty(NodeDamage::Other);
// Step 15.7
this.upcast::<EventTarget>().fire_event(atom!("load"), CanGc::note());
})
);
}
fn uses_srcset_or_picture(elem: &Element) -> bool {
let has_src = elem.has_attribute(&local_name!("srcset"));
let is_parent_picture = elem
.upcast::<Node>()
.GetParentElement()
.is_some_and(|p| p.is::<HTMLPictureElement>());
has_src || is_parent_picture
}
fn new_inherited(
local_name: LocalName,
prefix: Option<Prefix>,
document: &Document,
creator: ElementCreator,
) -> HTMLImageElement {
HTMLImageElement {
htmlelement: HTMLElement::new_inherited(local_name, prefix, document),
image_request: Cell::new(ImageRequestPhase::Current),
current_request: DomRefCell::new(ImageRequest {
state: State::Unavailable,
parsed_url: None,
source_url: None,
image: None,
metadata: None,
blocker: DomRefCell::new(None),
final_url: None,
current_pixel_density: None,
}),
pending_request: DomRefCell::new(ImageRequest {
state: State::Unavailable,
parsed_url: None,
source_url: None,
image: None,
metadata: None,
blocker: DomRefCell::new(None),
final_url: None,
current_pixel_density: None,
}),
form_owner: Default::default(),
generation: Default::default(),
source_set: DomRefCell::new(SourceSet::new()),
dimension_attribute_source: Default::default(),
last_selected_source: DomRefCell::new(None),
image_decode_promises: DomRefCell::new(vec![]),
line_number: creator.return_line_number(),
}
}
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
pub(crate) fn new(
local_name: LocalName,
prefix: Option<Prefix>,
document: &Document,
proto: Option<HandleObject>,
creator: ElementCreator,
can_gc: CanGc,
) -> DomRoot<HTMLImageElement> {
let image_element = Node::reflect_node_with_proto(
Box::new(HTMLImageElement::new_inherited(
local_name, prefix, document, creator,
)),
document,
proto,
can_gc,
);
image_element
.dimension_attribute_source
.set(Some(image_element.upcast()));
image_element
}
pub(crate) fn areas(&self) -> Option<Vec<DomRoot<HTMLAreaElement>>> {
let elem = self.upcast::<Element>();
let usemap_attr = elem.get_attribute(&ns!(), &local_name!("usemap"))?;
let value = usemap_attr.value();
if value.is_empty() || !value.is_char_boundary(1) {
return None;
}
let (first, last) = value.split_at(1);
if first != "#" || last.is_empty() {
return None;
}
let useMapElements = self
.owner_document()
.upcast::<Node>()
.traverse_preorder(ShadowIncluding::No)
.filter_map(DomRoot::downcast::<HTMLMapElement>)
.find(|n| {
n.upcast::<Element>()
.get_name()
.is_some_and(|n| *n == *last)
});
useMapElements.map(|mapElem| mapElem.get_area_elements())
}
pub(crate) fn same_origin(&self, origin: &MutableOrigin) -> bool {
if let Some(ref image) = self.current_request.borrow().image {
return image.cors_status() == CorsStatus::Safe;
}
self.current_request
.borrow()
.final_url
.as_ref()
.is_some_and(|url| url.scheme() == "data" || url.origin().same_origin(origin))
}
fn generation_id(&self) -> u32 {
self.generation.get()
}
}
#[derive(JSTraceable, MallocSizeOf)]
pub(crate) enum ImageElementMicrotask {
StableStateUpdateImageData {
elem: DomRoot<HTMLImageElement>,
generation: u32,
},
EnvironmentChanges {
elem: DomRoot<HTMLImageElement>,
generation: u32,
},
Decode {
elem: DomRoot<HTMLImageElement>,
#[conditional_malloc_size_of]
promise: Rc<Promise>,
},
}
impl MicrotaskRunnable for ImageElementMicrotask {
fn handler(&self, can_gc: CanGc) {
match *self {
ImageElementMicrotask::StableStateUpdateImageData {
ref elem,
ref generation,
} => {
// Step 7 of https://html.spec.whatwg.org/multipage/#update-the-image-data,
// stop here if other instances of this algorithm have been scheduled
if elem.generation.get() == *generation {
elem.update_the_image_data_sync_steps(can_gc);
}
},
ImageElementMicrotask::EnvironmentChanges {
ref elem,
ref generation,
} => {
elem.react_to_environment_changes_sync_steps(*generation, can_gc);
},
ImageElementMicrotask::Decode {
ref elem,
ref promise,
} => {
elem.react_to_decode_image_sync_steps(promise.clone(), can_gc);
},
}
}
fn enter_realm(&self) -> JSAutoRealm {
match self {
&ImageElementMicrotask::StableStateUpdateImageData { ref elem, .. } |
&ImageElementMicrotask::EnvironmentChanges { ref elem, .. } |
&ImageElementMicrotask::Decode { ref elem, .. } => enter_realm(&**elem),
}
}
}
pub(crate) trait LayoutHTMLImageElementHelpers {
fn image_url(self) -> Option<ServoUrl>;
fn image_density(self) -> Option<f64>;
fn image_data(self) -> (Option<Image>, Option<ImageMetadata>);
fn get_width(self) -> LengthOrPercentageOrAuto;
fn get_height(self) -> LengthOrPercentageOrAuto;
}
impl<'dom> LayoutDom<'dom, HTMLImageElement> {
#[allow(unsafe_code)]
fn current_request(self) -> &'dom ImageRequest {
unsafe { self.unsafe_get().current_request.borrow_for_layout() }
}
#[allow(unsafe_code)]
fn dimension_attribute_source(self) -> LayoutDom<'dom, Element> {
unsafe {
self.unsafe_get()
.dimension_attribute_source
.get_inner_as_layout()
.expect("dimension attribute source should be always non-null")
}
}
}
impl LayoutHTMLImageElementHelpers for LayoutDom<'_, HTMLImageElement> {
fn image_url(self) -> Option<ServoUrl> {
self.current_request().parsed_url.clone()
}
fn image_data(self) -> (Option<Image>, Option<ImageMetadata>) {
let current_request = self.current_request();
(current_request.image.clone(), current_request.metadata)
}
fn image_density(self) -> Option<f64> {
self.current_request().current_pixel_density
}
fn get_width(self) -> LengthOrPercentageOrAuto {
self.dimension_attribute_source()
.get_attr_for_layout(&ns!(), &local_name!("width"))
.map(AttrValue::as_dimension)
.cloned()
.unwrap_or(LengthOrPercentageOrAuto::Auto)
}
fn get_height(self) -> LengthOrPercentageOrAuto {
self.dimension_attribute_source()
.get_attr_for_layout(&ns!(), &local_name!("height"))
.map(AttrValue::as_dimension)
.cloned()
.unwrap_or(LengthOrPercentageOrAuto::Auto)
}
}
/// <https://html.spec.whatwg.org/multipage/#parse-a-sizes-attribute>
fn parse_a_sizes_attribute(value: &str) -> SourceSizeList {
let mut input = ParserInput::new(value);
let mut parser = Parser::new(&mut input);
let url_data = Url::parse("about:blank").unwrap().into();
let context = ParserContext::new(
Origin::Author,
&url_data,
Some(CssRuleType::Style),
// FIXME(emilio): why ::empty() instead of ::DEFAULT? Also, what do
// browsers do regarding quirks-mode in a media list?
ParsingMode::empty(),
QuirksMode::NoQuirks,
/* namespaces = */ Default::default(),
None,
None,
);
SourceSizeList::parse(&context, &mut parser)
}
#[allow(non_snake_case)]
impl HTMLImageElementMethods<crate::DomTypeHolder> for HTMLImageElement {
/// <https://html.spec.whatwg.org/multipage/#dom-image>
fn Image(
window: &Window,
proto: Option<HandleObject>,
can_gc: CanGc,
width: Option<u32>,
height: Option<u32>,
) -> Fallible<DomRoot<HTMLImageElement>> {
// Step 1. Let document be the current global object's associated Document.
let document = window.Document();
// Step 2. Let img be the result of creating an element given document, "img", and the HTML
// namespace.
let element = Element::create(
QualName::new(None, ns!(html), local_name!("img")),
None,
&document,
ElementCreator::ScriptCreated,
CustomElementCreationMode::Synchronous,
proto,
can_gc,
);
let image = DomRoot::downcast::<HTMLImageElement>(element).unwrap();
// Step 3. If width is given, then set an attribute value for img using "width" and width.
if let Some(w) = width {
image.SetWidth(w);
}
// Step 4. If height is given, then set an attribute value for img using "height" and
// height.
if let Some(h) = height {
image.SetHeight(h);
}
// Step 5. Return img.
Ok(image)
}
// https://html.spec.whatwg.org/multipage/#dom-img-alt
make_getter!(Alt, "alt");
// https://html.spec.whatwg.org/multipage/#dom-img-alt
make_setter!(SetAlt, "alt");
// https://html.spec.whatwg.org/multipage/#dom-img-src
make_url_getter!(Src, "src");
// https://html.spec.whatwg.org/multipage/#dom-img-src
make_url_setter!(SetSrc, "src");
// https://html.spec.whatwg.org/multipage/#dom-img-srcset
make_url_getter!(Srcset, "srcset");
// https://html.spec.whatwg.org/multipage/#dom-img-src
make_url_setter!(SetSrcset, "srcset");
// <https://html.spec.whatwg.org/multipage/#dom-img-sizes>
make_getter!(Sizes, "sizes");
// <https://html.spec.whatwg.org/multipage/#dom-img-sizes>
make_setter!(SetSizes, "sizes");
// https://html.spec.whatwg.org/multipage/#dom-img-crossOrigin
fn GetCrossOrigin(&self) -> Option<DOMString> {
reflect_cross_origin_attribute(self.upcast::<Element>())
}
// https://html.spec.whatwg.org/multipage/#dom-img-crossOrigin
fn SetCrossOrigin(&self, value: Option<DOMString>, can_gc: CanGc) {
set_cross_origin_attribute(self.upcast::<Element>(), value, can_gc);
}
// https://html.spec.whatwg.org/multipage/#dom-img-usemap
make_getter!(UseMap, "usemap");
// https://html.spec.whatwg.org/multipage/#dom-img-usemap
make_setter!(SetUseMap, "usemap");
// https://html.spec.whatwg.org/multipage/#dom-img-ismap
make_bool_getter!(IsMap, "ismap");
// https://html.spec.whatwg.org/multipage/#dom-img-ismap
make_bool_setter!(SetIsMap, "ismap");
// <https://html.spec.whatwg.org/multipage/#dom-img-width>
fn Width(&self) -> u32 {
let node = self.upcast::<Node>();
node.content_box()
.map(|rect| rect.size.width.to_px() as u32)
.unwrap_or_else(|| self.NaturalWidth())
}
// <https://html.spec.whatwg.org/multipage/#dom-img-width>
make_dimension_uint_setter!(SetWidth, "width");
// <https://html.spec.whatwg.org/multipage/#dom-img-height>
fn Height(&self) -> u32 {
let node = self.upcast::<Node>();
node.content_box()
.map(|rect| rect.size.height.to_px() as u32)
.unwrap_or_else(|| self.NaturalHeight())
}
// <https://html.spec.whatwg.org/multipage/#dom-img-height>
make_dimension_uint_setter!(SetHeight, "height");
// https://html.spec.whatwg.org/multipage/#dom-img-naturalwidth
fn NaturalWidth(&self) -> u32 {
let request = self.current_request.borrow();
let pixel_density = request.current_pixel_density.unwrap_or(1f64);
match request.metadata {
Some(ref metadata) => (metadata.width as f64 / pixel_density) as u32,
None => 0,
}
}
// https://html.spec.whatwg.org/multipage/#dom-img-naturalheight
fn NaturalHeight(&self) -> u32 {
let request = self.current_request.borrow();
let pixel_density = request.current_pixel_density.unwrap_or(1f64);
match request.metadata {
Some(ref metadata) => (metadata.height as f64 / pixel_density) as u32,
None => 0,
}
}
// https://html.spec.whatwg.org/multipage/#dom-img-complete
fn Complete(&self) -> bool {
let elem = self.upcast::<Element>();
let srcset_absent = !elem.has_attribute(&local_name!("srcset"));
if !elem.has_attribute(&local_name!("src")) && srcset_absent {
return true;
}
let src = elem.get_string_attribute(&local_name!("src"));
if srcset_absent && src.is_empty() {
return true;
}
let request = self.current_request.borrow();
let request_state = request.state;
match request_state {
State::CompletelyAvailable | State::Broken => true,
State::PartiallyAvailable | State::Unavailable => false,
}
}
// https://html.spec.whatwg.org/multipage/#dom-img-currentsrc
fn CurrentSrc(&self) -> USVString {
let current_request = self.current_request.borrow();
let url = &current_request.parsed_url;
match *url {
Some(ref url) => USVString(url.clone().into_string()),
None => {
let unparsed_url = &current_request.source_url;
match *unparsed_url {
Some(ref url) => url.clone(),
None => USVString("".to_owned()),
}
},
}
}
/// <https://html.spec.whatwg.org/multipage/#dom-img-referrerpolicy>
fn ReferrerPolicy(&self) -> DOMString {
reflect_referrer_policy_attribute(self.upcast::<Element>())
}
// <https://html.spec.whatwg.org/multipage/#dom-img-referrerpolicy>
make_setter!(SetReferrerPolicy, "referrerpolicy");
/// <https://html.spec.whatwg.org/multipage/#dom-img-decode>
fn Decode(&self, can_gc: CanGc) -> Rc<Promise> {
// Step 1. Let promise be a new promise.
let promise = Promise::new(&self.global(), can_gc);
// Step 2. Queue a microtask to perform the following steps:
let task = ImageElementMicrotask::Decode {
elem: DomRoot::from_ref(self),
promise: promise.clone(),
};
ScriptThread::await_stable_state(Microtask::ImageElement(task));
// Step 3. Return promise.
promise
}
// https://html.spec.whatwg.org/multipage/#dom-img-name
make_getter!(Name, "name");
// https://html.spec.whatwg.org/multipage/#dom-img-name
make_atomic_setter!(SetName, "name");
// https://html.spec.whatwg.org/multipage/#dom-img-align
make_getter!(Align, "align");
// https://html.spec.whatwg.org/multipage/#dom-img-align
make_setter!(SetAlign, "align");
// https://html.spec.whatwg.org/multipage/#dom-img-hspace
make_uint_getter!(Hspace, "hspace");
// https://html.spec.whatwg.org/multipage/#dom-img-hspace
make_uint_setter!(SetHspace, "hspace");
// https://html.spec.whatwg.org/multipage/#dom-img-vspace
make_uint_getter!(Vspace, "vspace");
// https://html.spec.whatwg.org/multipage/#dom-img-vspace
make_uint_setter!(SetVspace, "vspace");
// https://html.spec.whatwg.org/multipage/#dom-img-longdesc
make_getter!(LongDesc, "longdesc");
// https://html.spec.whatwg.org/multipage/#dom-img-longdesc
make_setter!(SetLongDesc, "longdesc");
// https://html.spec.whatwg.org/multipage/#dom-img-border
make_getter!(Border, "border");
// https://html.spec.whatwg.org/multipage/#dom-img-border
make_setter!(SetBorder, "border");
}
impl VirtualMethods for HTMLImageElement {
fn super_type(&self) -> Option<&dyn VirtualMethods> {
Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
}
fn adopting_steps(&self, old_doc: &Document, can_gc: CanGc) {
self.super_type().unwrap().adopting_steps(old_doc, can_gc);
self.update_the_image_data(can_gc);
}
fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation, can_gc: CanGc) {
self.super_type()
.unwrap()
.attribute_mutated(attr, mutation, can_gc);
match attr.local_name() {
&local_name!("src") |
&local_name!("srcset") |
&local_name!("width") |
&local_name!("sizes") => {
// <https://html.spec.whatwg.org/multipage/#reacting-to-dom-mutations>
// The element's src, srcset, width, or sizes attributes are set, changed, or
// removed.
self.update_the_image_data(can_gc);
},
&local_name!("crossorigin") => {
// <https://html.spec.whatwg.org/multipage/#reacting-to-dom-mutations>
// The element's crossorigin attribute's state is changed.
let cross_origin_state_changed = match mutation {
AttributeMutation::Removed | AttributeMutation::Set(None) => true,
AttributeMutation::Set(Some(old_value)) => {
let new_cors_setting =
CorsSettings::from_enumerated_attribute(&attr.value());
let old_cors_setting = CorsSettings::from_enumerated_attribute(old_value);
new_cors_setting != old_cors_setting
},
};
if cross_origin_state_changed {
self.update_the_image_data(can_gc);
}
},
&local_name!("referrerpolicy") => {
// <https://html.spec.whatwg.org/multipage/#reacting-to-dom-mutations>
// The element's referrerpolicy attribute's state is changed.
let referrer_policy_state_changed = match mutation {
AttributeMutation::Removed | AttributeMutation::Set(None) => {
ReferrerPolicy::from(&**attr.value()) != ReferrerPolicy::EmptyString
},
AttributeMutation::Set(Some(old_value)) => {
ReferrerPolicy::from(&**attr.value()) != ReferrerPolicy::from(&**old_value)
},
};
if referrer_policy_state_changed {
self.update_the_image_data(can_gc);
}
},
_ => {},
}
}
fn attribute_affects_presentational_hints(&self, attr: &Attr) -> bool {
match attr.local_name() {
&local_name!("width") | &local_name!("height") => true,
_ => self
.super_type()
.unwrap()
.attribute_affects_presentational_hints(attr),
}
}
fn parse_plain_attribute(&self, name: &LocalName, value: DOMString) -> AttrValue {
match name {
&local_name!("width") | &local_name!("height") => {
AttrValue::from_dimension(value.into())
},
&local_name!("hspace") | &local_name!("vspace") => AttrValue::from_u32(value.into(), 0),
_ => self
.super_type()
.unwrap()
.parse_plain_attribute(name, value),
}
}
fn handle_event(&self, event: &Event, can_gc: CanGc) {
if event.type_() != atom!("click") {
return;
}
let area_elements = self.areas();
let elements = match area_elements {
Some(x) => x,
None => return,
};
// Fetch click coordinates
let mouse_event = match event.downcast::<MouseEvent>() {
Some(x) => x,
None => return,
};
let point = Point2D::new(
mouse_event.ClientX().to_f32().unwrap(),
mouse_event.ClientY().to_f32().unwrap(),
);
let bcr = self.upcast::<Element>().GetBoundingClientRect(can_gc);
let bcr_p = Point2D::new(bcr.X() as f32, bcr.Y() as f32);
// Walk HTMLAreaElements
for element in elements {
let shape = element.get_shape_from_coords();
let shp = match shape {
Some(x) => x.absolute_coords(bcr_p),
None => return,
};
if shp.hit_test(&point) {
element.activation_behavior(event, self.upcast(), can_gc);
return;
}
}
}
/// <https://html.spec.whatwg.org/multipage/#the-img-element:html-element-insertion-steps>
fn bind_to_tree(&self, context: &BindContext, can_gc: CanGc) {
if let Some(s) = self.super_type() {
s.bind_to_tree(context, can_gc);
}
let document = self.owner_document();
if context.tree_connected {
document.register_responsive_image(self);
}
let parent = self.upcast::<Node>().GetParentNode().unwrap();
// Step 1. If insertedNode's parent is a picture element, then, count this as a relevant
// mutation for insertedNode.
if parent.is::<HTMLPictureElement>() && std::ptr::eq(&*parent, context.parent) {
self.update_the_image_data(can_gc);
}
}
/// <https://html.spec.whatwg.org/multipage/#the-img-element:html-element-removing-steps>
fn unbind_from_tree(&self, context: &UnbindContext, can_gc: CanGc) {
self.super_type().unwrap().unbind_from_tree(context, can_gc);
let document = self.owner_document();
document.unregister_responsive_image(self);
// Step 1. If oldParent is a picture element, then, count this as a relevant mutation for
// removedNode.
if context.parent.is::<HTMLPictureElement>() && !self.upcast::<Node>().has_parent() {
self.update_the_image_data(can_gc);
}
}
}
impl FormControl for HTMLImageElement {
fn form_owner(&self) -> Option<DomRoot<HTMLFormElement>> {
self.form_owner.get()
}
fn set_form_owner(&self, form: Option<&HTMLFormElement>) {
self.form_owner.set(form);
}
fn to_element(&self) -> &Element {
self.upcast::<Element>()
}
fn is_listed(&self) -> bool {
false
}
}
/// Collect sequence of code points
/// <https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points>
pub(crate) fn collect_sequence_characters(
s: &str,
mut predicate: impl FnMut(&char) -> bool,
) -> (&str, &str) {
let i = s.find(|ch| !predicate(&ch)).unwrap_or(s.len());
(&s[0..i], &s[i..])
}
/// <https://html.spec.whatwg.org/multipage/#valid-non-negative-integer>
/// TODO(#39315): Use the validation rule from Stylo
fn is_valid_non_negative_integer_string(s: &str) -> bool {
s.chars().all(|c| c.is_ascii_digit())
}
/// <https://html.spec.whatwg.org/multipage/#valid-floating-point-number>
/// TODO(#39315): Use the validation rule from Stylo
fn is_valid_floating_point_number_string(s: &str) -> bool {
static RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^-?(?:\d+\.\d+|\d+|\.\d+)(?:(e|E)(\+|\-)?\d+)?$").unwrap());
RE.is_match(s)
}
/// Parse an `srcset` attribute:
/// <https://html.spec.whatwg.org/multipage/#parsing-a-srcset-attribute>.
pub fn parse_a_srcset_attribute(input: &str) -> Vec<ImageSource> {
// > 1. Let input be the value passed to this algorithm.
// > 2. Let position be a pointer into input, initially pointing at the start of the string.
let mut current_index = 0;
// > 3. Let candidates be an initially empty source set.
let mut candidates = vec![];
while current_index < input.len() {
let remaining_string = &input[current_index..];
// > 4. Splitting loop: Collect a sequence of code points that are ASCII whitespace or
// > U+002C COMMA characters from input given position. If any U+002C COMMA
// > characters were collected, that is a parse error.
// NOTE: A parse error indicating a non-fatal mismatch between the input and the
// requirements will be silently ignored to match the behavior of other browsers.
// <https://html.spec.whatwg.org/multipage/#concept-microsyntax-parse-error>
let (collected_characters, string_after_whitespace) =
collect_sequence_characters(remaining_string, |character| {
*character == ',' || character.is_ascii_whitespace()
});
// Add the length of collected whitespace, to find the start of the URL we are going
// to parse.
current_index += collected_characters.len();
// > 5. If position is past the end of input, return candidates.
if string_after_whitespace.is_empty() {
return candidates;
}
// 6. Collect a sequence of code points that are not ASCII whitespace from input
// given position, and let that be url.
let (url, _) =
collect_sequence_characters(string_after_whitespace, |c| !char::is_ascii_whitespace(c));
// Add the length of `url` that we will parse to advance the index of the next part
// of the string to prase.
current_index += url.len();
// 7. Let descriptors be a new empty list.
let mut descriptors = Vec::new();
// > 8. If url ends with U+002C (,), then:
// > 1. Remove all trailing U+002C COMMA characters from url. If this removed
// > more than one character, that is a parse error.
if url.ends_with(',') {
let image_source = ImageSource {
url: url.trim_end_matches(',').into(),
descriptor: Descriptor {
width: None,
density: None,
},
};
candidates.push(image_source);
continue;
}
// Otherwise:
// > 8.1. Descriptor tokenizer: Skip ASCII whitespace within input given position.
let descriptors_string = &input[current_index..];
let (spaces, descriptors_string) =
collect_sequence_characters(descriptors_string, |character| {
character.is_ascii_whitespace()
});
current_index += spaces.len();
// > 8.2. Let current descriptor be the empty string.
let mut current_descriptor = String::new();
// > 8.3. Let state be "in descriptor".
let mut state = ParseState::InDescriptor;
// > 8.4. Let c be the character at position. Do the following depending on the value of
// > state. For the purpose of this step, "EOF" is a special character representing
// > that position is past the end of input.
let mut characters = descriptors_string.chars();
let mut character = characters.next();
if let Some(character) = character {
current_index += character.len_utf8();
}
loop {
match (state, character) {
(ParseState::InDescriptor, Some(character)) if character.is_ascii_whitespace() => {
// > If current descriptor is not empty, append current descriptor to
// > descriptors and let current descriptor be the empty string. Set
// > state to after descriptor.
if !current_descriptor.is_empty() {
descriptors.push(current_descriptor);
current_descriptor = String::new();
state = ParseState::AfterDescriptor;
}
},
(ParseState::InDescriptor, Some(',')) => {
// > Advance position to the next character in input. If current descriptor
// > is not empty, append current descriptor to descriptors. Jump to the
// > step labeled descriptor parser.
if !current_descriptor.is_empty() {
descriptors.push(current_descriptor);
}
break;
},
(ParseState::InDescriptor, Some('(')) => {
// > Append c to current descriptor. Set state to in parens.
current_descriptor.push('(');
state = ParseState::InParens;
},
(ParseState::InDescriptor, Some(character)) => {
// > Append c to current descriptor.
current_descriptor.push(character);
},
(ParseState::InDescriptor, None) => {
// > If current descriptor is not empty, append current descriptor to
// > descriptors. Jump to the step labeled descriptor parser.
if !current_descriptor.is_empty() {
descriptors.push(current_descriptor);
}
break;
},
(ParseState::InParens, Some(')')) => {
// > Append c to current descriptor. Set state to in descriptor.
current_descriptor.push(')');
state = ParseState::InDescriptor;
},
(ParseState::InParens, Some(character)) => {
// Append c to current descriptor.
current_descriptor.push(character);
},
(ParseState::InParens, None) => {
// > Append current descriptor to descriptors. Jump to the step
// > labeled descriptor parser.
descriptors.push(current_descriptor);
break;
},
(ParseState::AfterDescriptor, Some(character))
if character.is_ascii_whitespace() =>
{
// > Stay in this state.
},
(ParseState::AfterDescriptor, Some(_)) => {
// > Set state to in descriptor. Set position to the previous
// > character in input.
state = ParseState::InDescriptor;
continue;
},
(ParseState::AfterDescriptor, None) => {
// > Jump to the step labeled descriptor parser.
break;
},
}
character = characters.next();
if let Some(character) = character {
current_index += character.len_utf8();
}
}
// > 9. Descriptor parser: Let error be no.
let mut error = false;
// > 10. Let width be absent.
let mut width: Option<u32> = None;
// > 11. Let density be absent.
let mut density: Option<f64> = None;
// > 12. Let future-compat-h be absent.
let mut future_compat_h: Option<u32> = None;
// > 13. For each descriptor in descriptors, run the appropriate set of steps from
// > the following list:
for descriptor in descriptors.into_iter() {
let Some(last_character) = descriptor.chars().last() else {
break;
};
let first_part_of_string = &descriptor[0..descriptor.len() - last_character.len_utf8()];
match last_character {
// > If the descriptor consists of a valid non-negative integer followed by a
// > U+0077 LATIN SMALL LETTER W character
// > 1. If the user agent does not support the sizes attribute, let error be yes.
// > 2. If width and density are not both absent, then let error be yes.
// > 3. Apply the rules for parsing non-negative integers to the descriptor.
// > If the result is 0, let error be yes. Otherwise, let width be the result.
'w' if is_valid_non_negative_integer_string(first_part_of_string) &&
density.is_none() &&
width.is_none() =>
{
match parse_unsigned_integer(first_part_of_string.chars()) {
Ok(number) if number > 0 => {
width = Some(number);
continue;
},
_ => error = true,
}
},
// > If the descriptor consists of a valid floating-point number followed by a
// > U+0078 LATIN SMALL LETTER X character
// > 1. If width, density and future-compat-h are not all absent, then let
// > error be yes.
// > 2. Apply the rules for parsing floating-point number values to the
// > descriptor. If the result is less than 0, let error be yes. Otherwise, let
// > density be the result.
//
// The HTML specification has a procedure for parsing floats that is different enough from
// the one that stylo uses, that it's better to use Rust's float parser here. This is
// what Gecko does, but it also checks to see if the number is a valid HTML-spec compliant
// number first. Not doing that means that we might be parsing numbers that otherwise
// wouldn't parse.
'x' if is_valid_floating_point_number_string(first_part_of_string) &&
width.is_none() &&
density.is_none() &&
future_compat_h.is_none() =>
{
match first_part_of_string.parse::<f64>() {
Ok(number) if number.is_finite() && number >= 0. => {
density = Some(number);
continue;
},
_ => error = true,
}
},
// > If the descriptor consists of a valid non-negative integer followed by a
// > U+0068 LATIN SMALL LETTER H character
// > This is a parse error.
// > 1. If future-compat-h and density are not both absent, then let error be
// > yes.
// > 2. Apply the rules for parsing non-negative integers to the descriptor.
// > If the result is 0, let error be yes. Otherwise, let future-compat-h be the
// > result.
'h' if is_valid_non_negative_integer_string(first_part_of_string) &&
future_compat_h.is_none() &&
density.is_none() =>
{
match parse_unsigned_integer(first_part_of_string.chars()) {
Ok(number) if number > 0 => {
future_compat_h = Some(number);
continue;
},
_ => error = true,
}
},
// > Anything else
// > Let error be yes.
_ => error = true,
}
if error {
break;
}
}
// > 14. If future-compat-h is not absent and width is absent, let error be yes.
if future_compat_h.is_some() && width.is_none() {
error = true;
}
// Step 15. If error is still no, then append a new image source to candidates whose URL is
// url, associated with a width width if not absent and a pixel density density if not
// absent. Otherwise, there is a parse error.
if !error {
let image_source = ImageSource {
url: url.into(),
descriptor: Descriptor { width, density },
};
candidates.push(image_source);
}
// Step 16. Return to the step labeled splitting loop.
}
candidates
}
#[derive(Clone)]
enum ChangeType {
Environment {
selected_source: USVString,
selected_pixel_density: f64,
},
Element,
}
/// Returns true if the given image MIME type is supported.
fn is_supported_image_mime_type(input: &str) -> bool {
// Remove any leading and trailing HTTP whitespace from input.
let mime_type = input.trim();
// <https://mimesniff.spec.whatwg.org/#mime-type-essence>
let mime_type_essence = match mime_type.find(';') {
Some(semi) => &mime_type[..semi],
_ => mime_type,
};
// The HTML specification says the type attribute may be present and if present, the value
// must be a valid MIME type string. However an empty type attribute is implicitly supported
// to match the behavior of other browsers.
// <https://html.spec.whatwg.org/multipage/#attr-source-type>
if mime_type_essence.is_empty() {
return true;
}
SUPPORTED_IMAGE_MIME_TYPES.contains(&mime_type_essence)
}