htmlimageelement: Reject decode promises on the current request mutation (#37828)

Follow the HTML specification and reject the pending image decode
promises
on any current request mutation.
https://html.spec.whatwg.org/multipage/#updating-the-image-data (step
18)
https://html.spec.whatwg.org/multipage/#dom-img-decode (step 3)

Fulfill and reject image decode promises by queueing
a global tasks on the DOM manipulation task source.

Testing: Improvements in the following tests
- html/semantics/embedded-content/the-img-element/decode/*

Signed-off-by: Andrei Volykhin <andrei.volykhin@gmail.com>
This commit is contained in:
Andrei Volykhin 2025-07-02 12:29:47 +03:00 committed by GitHub
parent 45fb0bac82
commit 4cbadde144
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 58 additions and 48 deletions

View file

@ -97,6 +97,7 @@ pub(crate) fn throw_dom_exception(
Error::NotReadable => DOMErrorName::NotReadableError, Error::NotReadable => DOMErrorName::NotReadableError,
Error::Operation => DOMErrorName::OperationError, Error::Operation => DOMErrorName::OperationError,
Error::NotAllowed => DOMErrorName::NotAllowedError, Error::NotAllowed => DOMErrorName::NotAllowedError,
Error::Encoding => DOMErrorName::EncodingError,
Error::Type(message) => unsafe { Error::Type(message) => unsafe {
assert!(!JS_IsExceptionPending(*cx)); assert!(!JS_IsExceptionPending(*cx));
throw_type_error(*cx, &message); throw_type_error(*cx, &message);

View file

@ -43,8 +43,6 @@ use style::values::specified::source_size_list::SourceSizeList;
use style_traits::ParsingMode; use style_traits::ParsingMode;
use url::Url; use url::Url;
use super::domexception::DOMErrorName;
use super::types::DOMException;
use crate::document_loader::{LoadBlocker, LoadType}; use crate::document_loader::{LoadBlocker, LoadType};
use crate::dom::activation::Activatable; use crate::dom::activation::Activatable;
use crate::dom::attr::Attr; use crate::dom::attr::Attr;
@ -57,7 +55,7 @@ use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMeth
use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods; use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
use crate::dom::bindings::error::{Error, Fallible}; use crate::dom::bindings::error::{Error, Fallible};
use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::refcounted::Trusted; use crate::dom::bindings::refcounted::{Trusted, TrustedPromise};
use crate::dom::bindings::reflector::DomGlobal; use crate::dom::bindings::reflector::DomGlobal;
use crate::dom::bindings::root::{DomRoot, LayoutDom, MutNullableDom}; use crate::dom::bindings::root::{DomRoot, LayoutDom, MutNullableDom};
use crate::dom::bindings::str::{DOMString, USVString}; use crate::dom::bindings::str::{DOMString, USVString};
@ -486,7 +484,7 @@ impl HTMLImageElement {
LoadBlocker::terminate(&self.current_request.borrow().blocker, can_gc); LoadBlocker::terminate(&self.current_request.borrow().blocker, can_gc);
// Mark the node dirty // Mark the node dirty
self.upcast::<Node>().dirty(NodeDamage::Other); self.upcast::<Node>().dirty(NodeDamage::Other);
self.resolve_image_decode_promises(can_gc); self.resolve_image_decode_promises();
} }
/// Step 24 of <https://html.spec.whatwg.org/multipage/#update-the-image-data> /// Step 24 of <https://html.spec.whatwg.org/multipage/#update-the-image-data>
@ -597,9 +595,9 @@ impl HTMLImageElement {
request.metadata = None; request.metadata = None;
if matches!(state, State::Broken) { if matches!(state, State::Broken) {
self.reject_image_decode_promises(can_gc); self.reject_image_decode_promises();
} else if matches!(state, State::CompletelyAvailable) { } else if matches!(state, State::CompletelyAvailable) {
self.resolve_image_decode_promises(can_gc); self.resolve_image_decode_promises();
} }
} }
@ -894,6 +892,7 @@ impl HTMLImageElement {
// Step 17 // Step 17
current_request.current_pixel_density = Some(selected_pixel_density); current_request.current_pixel_density = Some(selected_pixel_density);
self.init_image_request(&mut current_request, url, src, can_gc); self.init_image_request(&mut current_request, url, src, can_gc);
self.reject_image_decode_promises();
}, },
(_, _) => { (_, _) => {
// step 17 // step 17
@ -1186,10 +1185,7 @@ impl HTMLImageElement {
if !document.is_fully_active() || if !document.is_fully_active() ||
matches!(self.current_request.borrow().state, State::Broken) matches!(self.current_request.borrow().state, State::Broken)
{ {
promise.reject_native( promise.reject_error(Error::Encoding, can_gc);
&DOMException::new(&document.global(), DOMErrorName::EncodingError, can_gc),
can_gc,
);
} else if matches!( } else if matches!(
self.current_request.borrow().state, self.current_request.borrow().state,
State::CompletelyAvailable State::CompletelyAvailable
@ -1203,22 +1199,59 @@ impl HTMLImageElement {
} }
} }
fn resolve_image_decode_promises(&self, can_gc: CanGc) { /// <https://html.spec.whatwg.org/multipage/#dom-img-decode>
for promise in self.image_decode_promises.borrow().iter() { fn resolve_image_decode_promises(&self) {
promise.resolve_native(&(), can_gc); if self.image_decode_promises.borrow().is_empty() {
} return;
self.image_decode_promises.borrow_mut().clear();
} }
fn reject_image_decode_promises(&self, can_gc: CanGc) { // Step 3. If the decoding process completes successfully, then queue a
let document = self.owner_document(); // global task on the DOM manipulation task source with global to
for promise in self.image_decode_promises.borrow().iter() { // resolve promise with undefined.
promise.reject_native( let trusted_image_decode_promises: Vec<TrustedPromise> = self
&DOMException::new(&document.global(), DOMErrorName::EncodingError, can_gc), .image_decode_promises
can_gc, .borrow()
); .iter()
} .map(|promise| TrustedPromise::new(promise.clone()))
.collect();
self.image_decode_promises.borrow_mut().clear(); 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> /// Step 15 for <https://html.spec.whatwg.org/multipage/#img-environment-changes>

View file

@ -9,6 +9,7 @@ use crate::codegen::PrototypeList::proto_id_to_name;
use crate::script_runtime::JSContext as SafeJSContext; use crate::script_runtime::JSContext as SafeJSContext;
/// DOM exceptions that can be thrown by a native DOM method. /// DOM exceptions that can be thrown by a native DOM method.
/// <https://webidl.spec.whatwg.org/#dfn-error-names-table>
#[derive(Clone, Debug, MallocSizeOf)] #[derive(Clone, Debug, MallocSizeOf)]
pub enum Error { pub enum Error {
/// IndexSizeError DOMException /// IndexSizeError DOMException
@ -67,6 +68,8 @@ pub enum Error {
Operation, Operation,
/// NotAllowedError DOMException /// NotAllowedError DOMException
NotAllowed, NotAllowed,
/// EncodingError DOMException
Encoding,
/// TypeError JavaScript Error /// TypeError JavaScript Error
Type(String), Type(String),

View file

@ -1,15 +0,0 @@
[image-decode-path-changes.html]
[HTMLImageElement.prototype.decode(), src/srcset mutation tests. src changes fail decode; following good png decode succeeds.]
expected: FAIL
[HTMLImageElement.prototype.decode(), src/srcset mutation tests. src changes fail decode; following good svg decode succeeds.]
expected: FAIL
[HTMLImageElement.prototype.decode(), src/srcset mutation tests. srcset changes fail decode; following good decode succeeds.]
expected: FAIL
[HTMLImageElement.prototype.decode(), src/srcset mutation tests. src changes fail decode.]
expected: FAIL
[HTMLImageElement.prototype.decode(), src/srcset mutation tests. srcset changes fail decode.]
expected: FAIL

View file

@ -1,15 +1,3 @@
[image-decode-with-quick-attach.html] [image-decode-with-quick-attach.html]
[HTMLImageElement.prototype.decode(), attach to DOM before promise resolves.: src in empty picture not cached] [HTMLImageElement.prototype.decode(), attach to DOM before promise resolves.: src in empty picture cached]
expected: FAIL
[HTMLImageElement.prototype.decode(), attach to DOM before promise resolves.: srcset in empty picture]
expected: FAIL
[HTMLImageElement.prototype.decode(), attach to DOM before promise resolves.: src in picture with source not cached]
expected: FAIL
[HTMLImageElement.prototype.decode(), attach to DOM before promise resolves.: src in picture with source cached]
expected: FAIL
[HTMLImageElement.prototype.decode(), attach to DOM before promise resolves.: srcset in picture with source]
expected: FAIL expected: FAIL