/* 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; use html5ever::{LocalName, Prefix, QualName, local_name, ns}; use js::context::JSContext; use js::realm::AutoRealm; 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, }; use net_traits::request::{CorsSettings, Destination, Initiator, RequestId}; use net_traits::{ FetchMetadata, FetchResponseMsg, NetworkError, ReferrerPolicy, ResourceFetchTiming, }; use num_traits::ToPrimitive; use pixels::{CorsStatus, ImageMetadata, Snapshot}; use regex::Regex; use rustc_hash::FxHashSet; use script_bindings::script_runtime::temp_cx; use servo_url::ServoUrl; use servo_url::origin::MutableOrigin; use style::attr::{AttrValue, LengthOrPercentageOrAuto, parse_unsigned_integer}; use style::stylesheets::CssRuleType; use style::values::specified::source_size_list::SourceSizeList; use style_traits::ParsingMode; use url::Url; use crate::css::parser_context_for_anonymous_content; 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, 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, MoveContext, Node, NodeDamage, NodeTraits, ShadowIncluding, UnbindContext, }; use crate::dom::performance::performanceresourcetiming::InitiatorType; use crate::dom::promise::Promise; use crate::dom::virtualmethods::VirtualMethods; use crate::dom::window::Window; use crate::fetch::{RequestWithGlobalScope, create_a_potential_cors_request}; use crate::microtask::{Microtask, MicrotaskRunnable}; use crate::network_listener::{self, FetchResponseListener, ResourceTimingListener}; use crate::realms::enter_auto_realm; use crate::script_runtime::CanGc; use crate::script_thread::ScriptThread; /// Supported image MIME types as defined by /// . /// 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, } /// #[derive(MallocSizeOf)] pub(crate) struct SourceSet { image_sources: Vec, 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, pub density: Option, } /// #[derive(Clone, Copy, JSTraceable, MallocSizeOf)] 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, source_url: Option, blocker: DomRefCell>, #[no_trace] image: Option, #[no_trace] metadata: Option, #[no_trace] final_url: Option, current_pixel_density: Option, } #[dom_struct] pub(crate) struct HTMLImageElement { htmlelement: HTMLElement, image_request: Cell, current_request: DomRefCell, pending_request: DomRefCell, form_owner: MutNullableDom, generation: Cell, source_set: DomRefCell, /// /// Always non-null after construction. dimension_attribute_source: MutNullableDom, /// last_selected_source: DomRefCell>, #[conditional_malloc_size_of] image_decode_promises: DomRefCell>>, /// 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 { // 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 { self.current_request.borrow().image.clone() } /// Gets the copy of the raster image data. pub(crate) fn get_raster_image_data(&self) -> Option { let Some(raster_image) = self.image_data()?.as_raster_image() else { warn!("Vector image is not supported as raster image source"); return None; }; Some(raster_image.as_snapshot()) } } /// The context required for asynchronously loading an external image. struct ImageContext { /// Reference to the script thread image cache. image_cache: Arc, /// 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, url: ServoUrl, element: Trusted, } impl FetchResponseListener for ImageContext { fn should_invoke(&self) -> bool { !self.aborted } fn process_request_body(&mut self, _: RequestId) {} fn process_response( &mut self, _: &mut js::context::JSContext, request_id: RequestId, metadata: Result, ) { 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::ResourceLoadError( "No http status code received".to_owned(), )) } else if status.is_success() { Ok(()) } else { Err(NetworkError::ResourceLoadError(format!( "HTTP error code {}", status.code() ))) } }; } fn process_response_chunk( &mut self, _: &mut js::context::JSContext, request_id: RequestId, payload: Vec, ) { if self.status.is_ok() { self.image_cache.notify_pending_response( self.id, FetchResponseMsg::ProcessResponseChunk(request_id, payload.into()), ); } } fn process_response_eof( self, cx: &mut js::context::JSContext, request_id: RequestId, response: Result<(), NetworkError>, timing: ResourceFetchTiming, ) { self.image_cache.notify_pending_response( self.id, FetchResponseMsg::ProcessResponseEOF(request_id, response.clone(), timing.clone()), ); network_listener::submit_timing(cx, &self, &response, &timing); } fn process_csp_violations(&mut self, _request_id: RequestId, violations: Vec) { let global = &self.resource_timing_global(); let elem = self.element.root(); let source_position = elem .upcast::() .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 { self.doc.root().global() } } #[expect(non_snake_case)] impl HTMLImageElement { /// Update the current image with a valid URL. fn fetch_image(&self, img_url: &ServoUrl, cx: &mut js::context::JSContext) { 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()), ); match cache_result { ImageCacheResult::Available(ImageOrMetadataAvailable::ImageAvailable { image, url, }) => self.process_image_response(ImageResponse::Loaded(image, url), cx), ImageCacheResult::Available(ImageOrMetadataAvailable::MetadataAvailable( metadata, id, )) => { self.process_image_response(ImageResponse::MetadataLoaded(metadata), cx); 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::FailedToLoadOrDecode => { self.process_image_response(ImageResponse::FailedToLoadOrDecode, cx) }, }; } 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 callback = 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 |cx| { 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, cx); } ChangeType::Environment { selected_source, selected_pixel_density } => { element.process_image_response_for_environment_change( response.response, selected_source, generation, selected_pixel_density, cx ); } } })); }); window.image_cache().add_listener(ImageLoadListener::new( callback, 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), 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(), ) .with_global_scope(&global) .referrer_policy(referrer_policy_for_element(self.upcast())); if self.uses_srcset_or_picture() { 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, cx: &mut js::context::JSContext) { 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, cx); // Mark the node dirty self.upcast::().dirty(NodeDamage::Other); self.resolve_image_decode_promises(); } /// fn process_image_response(&self, image: ImageResponse, cx: &mut js::context::JSContext) { // Step 27. As soon as possible, jump to the first applicable entry from the following list: // TODO => "If the resource type is multipart/x-mixed-replace" // => "If the resource type and data corresponds to a supported image format ..."" 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, cx); (true, false) }, (ImageResponse::Loaded(image, url), ImageRequestPhase::Pending) => { self.abort_request(State::Unavailable, ImageRequestPhase::Pending, cx); self.image_request.set(ImageRequestPhase::Current); self.handle_loaded_image(image, url, cx); (true, false) }, (ImageResponse::MetadataLoaded(meta), ImageRequestPhase::Current) => { // Otherwise, if the user agent is able to determine image request's image's width // and height, and image request is the current request, prepare image request for // presentation given the img element and set image request's state to partially // available. self.current_request.borrow_mut().state = State::PartiallyAvailable; self.current_request.borrow_mut().metadata = Some(meta); (false, false) }, (ImageResponse::MetadataLoaded(_), ImageRequestPhase::Pending) => { // If the user agent is able to determine image request's image's width and height, // and image request is the pending request, set image request's state to partially // available. self.pending_request.borrow_mut().state = State::PartiallyAvailable; (false, false) }, (ImageResponse::FailedToLoadOrDecode, ImageRequestPhase::Current) => { // Otherwise, if the user agent is able to determine that image request's image is // corrupted in some fatal way such that the image dimensions cannot be obtained, // and image request is the current request: // Step 1. Abort the image request for image request. self.abort_request(State::Broken, ImageRequestPhase::Current, cx); self.load_broken_image_icon(); // Step 2. If maybe omit events is not set or previousURL is not equal to urlString, // then fire an event named error at the img element. // TODO: Add missing `maybe omit events` flag and previousURL. (false, true) }, (ImageResponse::FailedToLoadOrDecode, ImageRequestPhase::Pending) => { // Otherwise, if the user agent is able to determine that image request's image is // corrupted in some fatal way such that the image dimensions cannot be obtained, // and image request is the pending request: // Step 1. Abort the image request for the current request and the pending request. self.abort_request(State::Broken, ImageRequestPhase::Current, cx); self.abort_request(State::Unavailable, ImageRequestPhase::Pending, cx); // Step 2. Upgrade the pending request to the current request. mem::swap( &mut *self.current_request.borrow_mut(), &mut *self.pending_request.borrow_mut(), ); self.image_request.set(ImageRequestPhase::Current); // Step 3. Set the current request's state to broken. self.current_request.borrow_mut().state = State::Broken; self.load_broken_image_icon(); // Step 4. Fire an event named error at the img element. (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::() .fire_event(atom!("load"), CanGc::from_cx(cx)); self.upcast::() .fire_event(atom!("loadend"), CanGc::from_cx(cx)); } // Fire image.onerror if trigger_image_error { self.upcast::() .fire_event(atom!("error"), CanGc::from_cx(cx)); self.upcast::() .fire_event(atom!("loadend"), CanGc::from_cx(cx)); } } /// The response part of /// . fn process_image_response_for_environment_change( &self, image: ImageResponse, selected_source: USVString, generation: u32, selected_pixel_density: f64, cx: &mut js::context::JSContext, ) { 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( selected_source, generation, selected_pixel_density, ); }, ImageResponse::FailedToLoadOrDecode => { // > Step 15.6: If response's unsafe response is a network error or if the // > image format is unsupported (as determined by applying the image // > sniffing rules, again as mentioned earlier), or if the user agent is // > able to determine that image request's image is corrupted in some fatal // > way such that the image dimensions cannot be obtained, or if the // > resource type is multipart/x-mixed-replace, then set the pending // > request to null and abort these steps. self.abort_request(State::Unavailable, ImageRequestPhase::Pending, cx); }, ImageResponse::MetadataLoaded(meta) => { self.pending_request.borrow_mut().metadata = Some(meta); }, }; } /// fn abort_request( &self, state: State, request: ImageRequestPhase, cx: &mut js::context::JSContext, ) { let mut request = match request { ImageRequestPhase::Current => self.current_request.borrow_mut(), ImageRequestPhase::Pending => self.pending_request.borrow_mut(), }; LoadBlocker::terminate(&request.blocker, cx); request.state = state; request.image = None; request.metadata = None; request.current_pixel_density = None; if matches!(state, State::Broken) { self.reject_image_decode_promises(); } else if matches!(state, State::CompletelyAvailable) { self.resolve_image_decode_promises(); } } /// fn create_source_set(&self) -> SourceSet { let element = self.upcast::(); // 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(&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(&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 = element.get_string_attribute(&local_name!("src")); 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 !src.is_empty() && no_density_source_of_1 && no_width_descriptor { source_set.image_sources.push(ImageSource { url: src.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 } /// 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::(); let parent = elem.upcast::().GetParentElement(); let elements = match parent.as_ref() { Some(p) => { if p.is::() { p.upcast::() .children() .filter_map(DomRoot::downcast::) .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::() { 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(&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(&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(&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(&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(&local_name!("width")).is_some() || element.get_attribute(&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) } /// 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); } } } /// 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() } /// 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, cx: &mut js::context::JSContext, ) { 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, cx); *request.blocker.borrow_mut() = Some(LoadBlocker::new(&document, LoadType::Image(url.clone()))); } /// fn prepare_image_request( &self, selected_source: &USVString, selected_pixel_density: f64, image_url: &ServoUrl, cx: &mut js::context::JSContext, ) { match self.image_request.get() { ImageRequestPhase::Pending => { // Step 14. If the pending request is not null and urlString is the same as the // pending request's current URL, then return. if self .pending_request .borrow() .parsed_url .as_ref() .is_some_and(|parsed_url| *parsed_url == *image_url) { return; } }, ImageRequestPhase::Current => { // Step 16. Abort the image request for the pending request. self.abort_request(State::Unavailable, ImageRequestPhase::Pending, cx); // Step 17. Set image request to a new image request whose current URL is urlString. let mut current_request = self.current_request.borrow_mut(); let mut pending_request = self.pending_request.borrow_mut(); match (current_request.parsed_url.as_ref(), current_request.state) { (Some(parsed_url), State::PartiallyAvailable) => { // Step 15. If urlString is the same as the current request's current URL // and the current request's state is partially available, then abort the // image request for the pending request, queue an element task on the DOM // manipulation task source given the img element to restart the animation // if restart animation is set, and return. if *parsed_url == *image_url { // TODO: queue a task to restart animation, if restart-animation is set return; } // Step 18. If the current request's state is unavailable or broken, then // set the current request to image request. Otherwise, set the pending // request to image request. self.image_request.set(ImageRequestPhase::Pending); self.init_image_request( &mut pending_request, image_url, selected_source, cx, ); pending_request.current_pixel_density = Some(selected_pixel_density); }, (_, State::Broken) | (_, State::Unavailable) => { // Step 18. If the current request's state is unavailable or broken, then // set the current request to image request. Otherwise, set the pending // request to image request. self.init_image_request( &mut current_request, image_url, selected_source, cx, ); current_request.current_pixel_density = Some(selected_pixel_density); self.reject_image_decode_promises(); }, (_, _) => { // Step 18. If the current request's state is unavailable or broken, then // set the current request to image request. Otherwise, set the pending // request to image request. self.image_request.set(ImageRequestPhase::Pending); self.init_image_request( &mut pending_request, image_url, selected_source, cx, ); pending_request.current_pixel_density = Some(selected_pixel_density); }, } }, } self.fetch_image(image_url, cx); } /// fn update_the_image_data_sync_steps(&self, cx: &mut js::context::JSContext) { // Step 10. Let selected source and selected pixel density be the URL and pixel density that // results from selecting an image source, respectively. let Some((selected_source, selected_pixel_density)) = self.select_image_source() else { // Step 11. If selected source is null, then: // Step 11.1. Set the current request's state to broken, abort the image request for the // current request and the pending request, and set the pending request to null. self.abort_request(State::Broken, ImageRequestPhase::Current, cx); self.abort_request(State::Unavailable, ImageRequestPhase::Pending, cx); self.image_request.set(ImageRequestPhase::Current); // Step 11.2. Queue an element task on the DOM manipulation task source given the img // element and the following steps: let this = Trusted::new(self); self.owner_global().task_manager().dom_manipulation_task_source().queue( task!(image_null_source_error: move |cx| { let this = this.root(); // Step 11.2.1. Change the current request's current URL to the empty string. { let mut current_request = this.current_request.borrow_mut(); current_request.source_url = None; current_request.parsed_url = None; } // Step 11.2.2. If all of the following are true: // the element has a src attribute or it uses srcset or picture; and // maybe omit events is not set or previousURL is not the empty string, // then fire an event named error at the img element. // TODO: Add missing `maybe omit events` flag and previousURL. let has_src_attribute = this.upcast::().has_attribute(&local_name!("src")); if has_src_attribute || this.uses_srcset_or_picture() { this.upcast::().fire_event(atom!("error"), CanGc::from_cx(cx)); } })); // Step 11.2.3. Return. return; }; // Step 12. Let urlString be the result of encoding-parsing-and-serializing a URL given // selected source, relative to the element's node document. let Ok(image_url) = self.owner_document().base_url().join(&selected_source) else { // Step 13. If urlString is failure, then: // Step 13.1. Abort the image request for the current request and the pending request. // Step 13.2. Set the current request's state to broken. self.abort_request(State::Broken, ImageRequestPhase::Current, cx); self.abort_request(State::Unavailable, ImageRequestPhase::Pending, cx); // Step 13.3. Set the pending request to null. self.image_request.set(ImageRequestPhase::Current); // Step 13.4. Queue an element task on the DOM manipulation task source given the img // element and the following steps: let this = Trusted::new(self); self.owner_global() .task_manager() .dom_manipulation_task_source() .queue(task!(image_selected_source_error: move |cx| { let this = this.root(); // Step 13.4.1. Change the current request's current URL to selected source. { let mut current_request = this.current_request.borrow_mut(); current_request.source_url = Some(selected_source); current_request.parsed_url = None; } // Step 13.4.2. If maybe omit events is not set or previousURL is not equal to // selected source, then fire an event named error at the img element. // TODO: Add missing `maybe omit events` flag and previousURL. this.upcast::().fire_event(atom!("error"), CanGc::from_cx(cx)); })); // Step 13.5. Return. return; }; self.prepare_image_request(&selected_source, selected_pixel_density, &image_url, cx); } /// pub(crate) fn update_the_image_data(&self, cx: &mut js::context::JSContext) { // Cancel any outstanding tasks that were queued before. self.generation.set(self.generation.get() + 1); // Step 1. If the element's node document is not fully active, then: if !self.owner_document().is_active() { // TODO Step 1.1. Continue running this algorithm in parallel. // TODO Step 1.2. Wait until the element's node document is fully active. // TODO Step 1.3. If another instance of this algorithm for this img element was started after // this instance (even if it aborted and is no longer running), then return. // TODO Step 1.4. Queue a microtask to continue this algorithm. } // Step 2. If the user agent cannot support images, or its support for images has been // disabled, then abort the image request for the current request and the pending request, // set the current request's state to unavailable, set the pending request to null, and // return. // Nothing specific to be done here since the user agent supports image processing. // Always first set the current request to unavailable, ensuring img.complete is false. // self.current_request.borrow_mut().state = State::Unavailable; // TODO Step 3. Let previousURL be the current request's current URL. // Step 4. Let selected source be null and selected pixel density be undefined. let mut selected_source = None; let mut selected_pixel_density = None; // Step 5. If the element does not use srcset or picture and it has a src attribute // specified whose value is not the empty string, then set selected source to the value of // the element's src attribute and set selected pixel density to 1.0. let src = self .upcast::() .get_string_attribute(&local_name!("src")); if !self.uses_srcset_or_picture() && !src.is_empty() { selected_source = Some(USVString(src.to_string())); selected_pixel_density = Some(1_f64); }; // Step 6. Set the element's last selected source to selected source. self.last_selected_source .borrow_mut() .clone_from(&selected_source); // Step 7. If selected source is not null, then: if let Some(selected_source) = selected_source { // Step 7.1. Let urlString be the result of encoding-parsing-and-serializing a URL given // selected source, relative to the element's node document. // Step 7.2. If urlString is failure, then abort this inner set of steps. if let Ok(image_url) = self.owner_document().base_url().join(&selected_source) { // Step 7.3. Let key be a tuple consisting of urlString, the img element's // crossorigin attribute's mode, and, if that mode is not No CORS, the node // document's origin. let window = self.owner_window(); let response = window.image_cache().get_image( image_url.clone(), window.origin().immutable().clone(), cors_setting_for_element(self.upcast()), ); // Step 7.4. If the list of available images contains an entry for key, then: if let Some(image) = response { // TODO Step 7.4.1. Set the ignore higher-layer caching flag for that entry. // Step 7.4.2. Abort the image request for the current request and the pending // request. self.abort_request(State::CompletelyAvailable, ImageRequestPhase::Current, cx); self.abort_request(State::Unavailable, ImageRequestPhase::Pending, cx); // Step 7.4.3. Set the pending request to null. self.image_request.set(ImageRequestPhase::Current); // Step 7.4.4. Set the current request to a new image request whose image data // is that of the entry and whose state is completely available. let mut current_request = self.current_request.borrow_mut(); current_request.metadata = Some(image.metadata()); current_request.image = Some(image); current_request.final_url = Some(image_url.clone()); // TODO Step 7.4.5. Prepare the current request for presentation given the img // element. self.upcast::().dirty(NodeDamage::Other); // Step 7.4.6. Set the current request's current pixel density to selected pixel // density. current_request.current_pixel_density = selected_pixel_density; // Step 7.4.7. Queue an element task on the DOM manipulation task source given // the img element and the following steps: let this = Trusted::new(self); self.owner_global() .task_manager() .dom_manipulation_task_source() .queue(task!(image_load_event: move |cx| { let this = this.root(); // TODO Step 7.4.7.1. If restart animation is set, then restart the // animation. // Step 7.4.7.2. Set the current request's current URL to urlString. { let mut current_request = this.current_request.borrow_mut(); current_request.source_url = Some(selected_source); current_request.parsed_url = Some(image_url); } // Step 7.4.7.3. If maybe omit events is not set or previousURL is not // equal to urlString, then fire an event named load at the img element. // TODO: Add missing `maybe omit events` flag and previousURL. this.upcast::().fire_event(atom!("load"), CanGc::from_cx(cx)); })); // Step 7.4.8. Abort the update the image data algorithm. return; } } } // Step 8. Queue a microtask to perform the rest of this algorithm, allowing the task that // invoked this algorithm to continue. let task = ImageElementMicrotask::UpdateImageData { elem: DomRoot::from_ref(self), generation: self.generation.get(), }; ScriptThread::await_stable_state(Microtask::ImageElement(task)); } /// pub(crate) fn react_to_environment_changes(&self) { // Step 1. Await a stable state. let task = ImageElementMicrotask::EnvironmentChanges { elem: DomRoot::from_ref(self), generation: self.generation.get(), }; ScriptThread::await_stable_state(Microtask::ImageElement(task)); } /// fn react_to_environment_changes_sync_steps( &self, generation: u32, cx: &mut js::context::JSContext, ) { let document = self.owner_document(); let has_pending_request = matches!(self.image_request.get(), ImageRequestPhase::Pending); // Step 2. If the img element does not use srcset or picture, its node document is not fully // active, it has image data whose resource type is multipart/x-mixed-replace, or its // pending request is not null, then return. if !document.is_active() || !self.uses_srcset_or_picture() || has_pending_request { return; } // Step 3. Let selected source and selected pixel density be the URL and pixel density that // results from selecting an image source, respectively. let Some((selected_source, selected_pixel_density)) = self.select_image_source() else { // Step 4. If selected source is null, then return. return; }; // Step 5. If selected source and selected pixel density are the same as the element's last // selected source and current pixel density, then return. let mut same_selected_source = self .last_selected_source .borrow() .as_ref() .is_some_and(|source| *source == selected_source); // There are missing steps for the element's last selected source in specification so let's // check the current request's current URL as well. // same_selected_source = same_selected_source || self.current_request .borrow() .source_url .as_ref() .is_some_and(|source| *source == selected_source); let same_selected_pixel_density = self .current_request .borrow() .current_pixel_density .is_some_and(|pixel_density| pixel_density == selected_pixel_density); if same_selected_source && same_selected_pixel_density { return; } // Step 6. Let urlString be the result of encoding-parsing-and-serializing a URL given // selected source, relative to the element's node document. // Step 7. If urlString is failure, then return. let Ok(image_url) = document.base_url().join(&selected_source) else { return; }; // Step 13. Set the element's pending request to image request. self.image_request.set(ImageRequestPhase::Pending); self.init_image_request( &mut self.pending_request.borrow_mut(), &image_url, &selected_source, cx, ); // Step 15. If the list of available images contains an entry for key, then set image // request's image data to that of the entry. Continue to the next step. let window = self.owner_window(); let cache_result = window.image_cache().get_cached_image_status( image_url.clone(), window.origin().immutable().clone(), cors_setting_for_element(self.upcast()), ); let change_type = ChangeType::Environment { selected_source: selected_source.clone(), selected_pixel_density, }; match cache_result { ImageCacheResult::Available(ImageOrMetadataAvailable::ImageAvailable { .. }) => { 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, cx, ); self.register_image_cache_callback(id, change_type); }, ImageCacheResult::FailedToLoadOrDecode => { self.process_image_response_for_environment_change( ImageResponse::FailedToLoadOrDecode, selected_source, generation, selected_pixel_density, cx, ); }, ImageCacheResult::ReadyForRequest(id) => { self.fetch_request(&image_url, id); self.register_image_cache_callback(id, change_type); }, ImageCacheResult::Pending(id) => { self.register_image_cache_callback(id, change_type); }, } } /// fn react_to_decode_image_sync_steps(&self, promise: Rc, 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(None), can_gc); } else if matches!( self.current_request.borrow().state, State::CompletelyAvailable ) { // this doesn't follow the spec, but it's been discussed in 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 ( without "src" // and "srcset" attributes) then reject promise with an "EncodingError" DOMException. // promise.reject_error(Error::Encoding(None), can_gc); } else { self.image_decode_promises.borrow_mut().push(promise); } } /// fn resolve_image_decode_promises(&self) { if self.image_decode_promises.borrow().is_empty() { return; } // Step 2.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 = 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::deprecated_note()); } })); } /// fn reject_image_decode_promises(&self) { if self.image_decode_promises.borrow().is_empty() { return; } // Step 2.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 = 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(None), CanGc::deprecated_note()); } })); } /// fn finish_reacting_to_environment_change( &self, selected_source: USVString, generation: u32, selected_pixel_density: f64, ) { // Step 16. Queue an element task on the DOM manipulation task source given the img element // and the following steps: let this = Trusted::new(self); self.owner_global() .task_manager() .dom_manipulation_task_source() .queue(task!(image_load_event: move |cx| { let this = this.root(); // Step 16.1. If the img element has experienced relevant mutations since this // algorithm started, then set the pending request to null and abort these steps. if this.generation.get() != generation { this.abort_request(State::Unavailable, ImageRequestPhase::Pending, cx); this.image_request.set(ImageRequestPhase::Current); return; } // Step 16.2. Set the img element's last selected source to selected source and the // img element's current pixel density to selected pixel density. *this.last_selected_source.borrow_mut() = Some(selected_source); { let mut pending_request = this.pending_request.borrow_mut(); // Step 16.3. Set the image request's state to completely available. pending_request.state = State::CompletelyAvailable; pending_request.current_pixel_density = Some(selected_pixel_density); // Step 16.4. Add the image to the list of available images using the key key, // with the ignore higher-layer caching flag set. // Already a part of the list of available images due to Step 15. // Step 16.5. Upgrade the pending request to the current request. mem::swap(&mut *this.current_request.borrow_mut(), &mut *pending_request); } this.abort_request(State::Unavailable, ImageRequestPhase::Pending, cx); this.image_request.set(ImageRequestPhase::Current); // TODO Step 16.6. Prepare image request for presentation given the img element. this.upcast::().dirty(NodeDamage::Other); // Step 16.7. Fire an event named load at the img element. this.upcast::().fire_event(atom!("load"), CanGc::from_cx(cx)); })); } /// fn uses_srcset_or_picture(&self) -> bool { let element = self.upcast::(); let has_srcset_attribute = element.has_attribute(&local_name!("srcset")); let has_parent_picture = element .upcast::() .GetParentElement() .is_some_and(|parent| parent.is::()); has_srcset_attribute || has_parent_picture } fn new_inherited( local_name: LocalName, prefix: Option, 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(), } } pub(crate) fn new( cx: &mut js::context::JSContext, local_name: LocalName, prefix: Option, document: &Document, proto: Option, creator: ElementCreator, ) -> DomRoot { let image_element = Node::reflect_node_with_proto( cx, Box::new(HTMLImageElement::new_inherited( local_name, prefix, document, creator, )), document, proto, ); image_element .dimension_attribute_source .set(Some(image_element.upcast())); image_element } pub(crate) fn areas(&self) -> Option>> { let elem = self.upcast::(); let usemap_attr = elem.get_attribute(&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::() .traverse_preorder(ShadowIncluding::No) .filter_map(DomRoot::downcast::) .find(|n| { n.upcast::() .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() } fn load_broken_image_icon(&self) { let window = self.owner_window(); let Some(broken_image_icon) = window.image_cache().get_broken_image_icon() else { return; }; self.current_request.borrow_mut().metadata = Some(broken_image_icon.metadata); self.current_request.borrow_mut().image = Some(Image::Raster(broken_image_icon)); self.upcast::().dirty(NodeDamage::Other); } /// Get the full URL of the current image of this `` element, returning `None` if the URL /// could not be joined with the `Document` URL. pub(crate) fn full_image_url_for_user_interface(&self) -> Option { self.owner_document() .base_url() .join(&self.CurrentSrc()) .ok() } } #[derive(JSTraceable, MallocSizeOf)] pub(crate) enum ImageElementMicrotask { UpdateImageData { elem: DomRoot, generation: u32, }, EnvironmentChanges { elem: DomRoot, generation: u32, }, Decode { elem: DomRoot, #[conditional_malloc_size_of] promise: Rc, }, } impl MicrotaskRunnable for ImageElementMicrotask { fn handler(&self, cx: &mut js::context::JSContext) { match *self { ImageElementMicrotask::UpdateImageData { ref elem, ref generation, } => { // // Step 9. If another instance of this algorithm for this img element was started // after this instance (even if it aborted and is no longer running), then return. if elem.generation.get() == *generation { elem.update_the_image_data_sync_steps(cx); } }, ImageElementMicrotask::EnvironmentChanges { ref elem, ref generation, } => { elem.react_to_environment_changes_sync_steps(*generation, cx); }, ImageElementMicrotask::Decode { ref elem, ref promise, } => { elem.react_to_decode_image_sync_steps(promise.clone(), CanGc::from_cx(cx)); }, } } fn enter_realm<'cx>(&self, cx: &'cx mut js::context::JSContext) -> AutoRealm<'cx> { match self { &ImageElementMicrotask::UpdateImageData { ref elem, .. } | &ImageElementMicrotask::EnvironmentChanges { ref elem, .. } | &ImageElementMicrotask::Decode { ref elem, .. } => enter_auto_realm(cx, &**elem), } } } impl<'dom> LayoutDom<'dom, HTMLImageElement> { #[expect(unsafe_code)] fn current_request(self) -> &'dom ImageRequest { unsafe { self.unsafe_get().current_request.borrow_for_layout() } } #[expect(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") } } pub(crate) fn image_url(self) -> Option { self.current_request().parsed_url.clone() } pub(crate) fn image_data(self) -> (Option, Option) { let current_request = self.current_request(); (current_request.image.clone(), current_request.metadata) } pub(crate) fn image_density(self) -> Option { self.current_request().current_pixel_density } pub(crate) fn showing_broken_image_icon(self) -> bool { matches!(self.current_request().state, State::Broken) } pub(crate) 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) } pub(crate) 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) } } /// 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(); // FIXME(emilio): why ::empty() instead of ::DEFAULT? Also, what do // browsers do regarding quirks-mode in a media list? let context = parser_context_for_anonymous_content(CssRuleType::Style, ParsingMode::empty(), &url_data); SourceSizeList::parse(&context, &mut parser) } impl HTMLImageElementMethods for HTMLImageElement { /// fn Image( cx: &mut JSContext, window: &Window, proto: Option, width: Option, height: Option, ) -> Fallible> { // 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( cx, QualName::new(None, ns!(html), local_name!("img")), None, &document, ElementCreator::ScriptCreated, CustomElementCreationMode::Synchronous, proto, ); let image = DomRoot::downcast::(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"); // make_getter!(Sizes, "sizes"); // make_setter!(SetSizes, "sizes"); /// fn GetCrossOrigin(&self) -> Option { reflect_cross_origin_attribute(self.upcast::()) } /// fn SetCrossOrigin(&self, cx: &mut JSContext, value: Option) { set_cross_origin_attribute(cx, self.upcast::(), value); } // 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"); // fn Width(&self) -> u32 { let node = self.upcast::(); node.content_box() .map(|rect| rect.size.width.to_px() as u32) .unwrap_or_else(|| self.NaturalWidth()) } // make_dimension_uint_setter!(SetWidth, "width"); // fn Height(&self) -> u32 { let node = self.upcast::(); node.content_box() .map(|rect| rect.size.height.to_px() as u32) .unwrap_or_else(|| self.NaturalHeight()) } // make_dimension_uint_setter!(SetHeight, "height"); /// fn NaturalWidth(&self) -> u32 { let request = self.current_request.borrow(); if matches!(request.state, State::Broken) { return 0; } 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, } } /// fn NaturalHeight(&self) -> u32 { let request = self.current_request.borrow(); if matches!(request.state, State::Broken) { return 0; } 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, } } /// fn Complete(&self) -> bool { let element = self.upcast::(); // Step 1. If any of the following are true: // both the src attribute and the srcset attribute are omitted; let has_srcset_attribute = element.has_attribute(&local_name!("srcset")); if !element.has_attribute(&local_name!("src")) && !has_srcset_attribute { return true; } // the srcset attribute is omitted and the src attribute's value is the empty string; let src = element.get_string_attribute(&local_name!("src")); if !has_srcset_attribute && src.is_empty() { return true; } // the img element's current request's state is completely available and its pending request // is null; or the img element's current request's state is broken and its pending request // is null, then return true. if matches!(self.image_request.get(), ImageRequestPhase::Current) && matches!( self.current_request.borrow().state, State::CompletelyAvailable | State::Broken ) { return true; } // Step 2. Return false. false } /// fn CurrentSrc(&self) -> USVString { let current_request = self.current_request.borrow(); let url = ¤t_request.parsed_url; match *url { Some(ref url) => USVString(url.clone().into_string()), None => { let unparsed_url = ¤t_request.source_url; match *unparsed_url { Some(ref url) => url.clone(), None => USVString("".to_owned()), } }, } } /// fn ReferrerPolicy(&self) -> DOMString { reflect_referrer_policy_attribute(self.upcast::()) } // make_setter!(SetReferrerPolicy, "referrerpolicy"); /// fn Decode(&self, cx: &mut JSContext) -> Rc { // Step 1. Let promise be a new promise. let promise = Promise::new2(cx, &self.global()); // 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_url_getter!(LongDesc, "longdesc"); // https://html.spec.whatwg.org/multipage/#dom-img-longdesc make_url_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::() as &dyn VirtualMethods) } fn adopting_steps(&self, cx: &mut JSContext, old_doc: &Document) { self.super_type().unwrap().adopting_steps(cx, old_doc); self.update_the_image_data(cx); } fn attribute_mutated( &self, cx: &mut js::context::JSContext, attr: &Attr, mutation: AttributeMutation, ) { self.super_type() .unwrap() .attribute_mutated(cx, attr, mutation); match attr.local_name() { &local_name!("src") | &local_name!("srcset") | &local_name!("width") | &local_name!("sizes") => { // // The element's src, srcset, width, or sizes attributes are set, changed, or // removed. self.update_the_image_data(cx); }, &local_name!("crossorigin") => { // // 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(cx); } }, &local_name!("referrerpolicy") => { // // 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(cx); } }, _ => {}, } } 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::() { 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::().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; } } } /// fn bind_to_tree(&self, cx: &mut JSContext, context: &BindContext) { if let Some(s) = self.super_type() { s.bind_to_tree(cx, context); } let document = self.owner_document(); if context.tree_connected { document.register_responsive_image(self); } let parent = self.upcast::().GetParentNode().unwrap(); // Step 1. If insertedNode's parent is a picture element, then, count this as a relevant // mutation for insertedNode. if parent.is::() && std::ptr::eq(&*parent, context.parent) { self.update_the_image_data(cx); } } /// fn unbind_from_tree(&self, cx: &mut js::context::JSContext, context: &UnbindContext) { self.super_type().unwrap().unbind_from_tree(cx, context); 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::() && !self.upcast::().has_parent() { self.update_the_image_data(cx); } } /// #[expect(unsafe_code)] fn moving_steps(&self, context: &MoveContext, can_gc: CanGc) { if let Some(super_type) = self.super_type() { super_type.moving_steps(context, can_gc); } // TODO: https://github.com/servo/servo/issues/43044 let mut cx = unsafe { temp_cx() }; let cx = &mut cx; // Step 1. If oldParent is a picture element, then, count this as a relevant mutation for movedNode. if let Some(old_parent) = context.old_parent { if old_parent.is::() { self.update_the_image_data(cx); } } } } impl FormControl for HTMLImageElement { fn form_owner(&self) -> Option> { self.form_owner.get() } fn set_form_owner(&self, form: Option<&HTMLFormElement>) { self.form_owner.set(form); } fn to_element(&self) -> &Element { self.upcast::() } fn is_listed(&self) -> bool { false } } /// Collect 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..]) } /// /// 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()) } /// /// TODO(#39315): Use the validation rule from Stylo fn is_valid_floating_point_number_string(s: &str) -> bool { static RE: LazyLock = LazyLock::new(|| Regex::new(r"^-?(?:\d+\.\d+|\d+|\.\d+)(?:(e|E)(\+|\-)?\d+)?$").unwrap()); RE.is_match(s) } /// Parse an `srcset` attribute: /// . pub fn parse_a_srcset_attribute(input: &str) -> Vec { // > 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. // 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 = None; // > 11. Let density be absent. let mut density: Option = None; // > 12. Let future-compat-h be absent. let mut future_compat_h: Option = 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::() { 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(); // 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. // if mime_type_essence.is_empty() { return true; } SUPPORTED_IMAGE_MIME_TYPES.contains(&mime_type_essence) }