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::Operation => DOMErrorName::OperationError,
Error::NotAllowed => DOMErrorName::NotAllowedError,
Error::Encoding => DOMErrorName::EncodingError,
Error::Type(message) => unsafe {
assert!(!JS_IsExceptionPending(*cx));
throw_type_error(*cx, &message);

View file

@ -43,8 +43,6 @@ use style::values::specified::source_size_list::SourceSizeList;
use style_traits::ParsingMode;
use url::Url;
use super::domexception::DOMErrorName;
use super::types::DOMException;
use crate::document_loader::{LoadBlocker, LoadType};
use crate::dom::activation::Activatable;
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::error::{Error, Fallible};
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::root::{DomRoot, LayoutDom, MutNullableDom};
use crate::dom::bindings::str::{DOMString, USVString};
@ -486,7 +484,7 @@ impl HTMLImageElement {
LoadBlocker::terminate(&self.current_request.borrow().blocker, can_gc);
// Mark the node dirty
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>
@ -597,9 +595,9 @@ impl HTMLImageElement {
request.metadata = None;
if matches!(state, State::Broken) {
self.reject_image_decode_promises(can_gc);
self.reject_image_decode_promises();
} 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
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
@ -1186,10 +1185,7 @@ impl HTMLImageElement {
if !document.is_fully_active() ||
matches!(self.current_request.borrow().state, State::Broken)
{
promise.reject_native(
&DOMException::new(&document.global(), DOMErrorName::EncodingError, can_gc),
can_gc,
);
promise.reject_error(Error::Encoding, can_gc);
} else if matches!(
self.current_request.borrow().state,
State::CompletelyAvailable
@ -1203,22 +1199,59 @@ impl HTMLImageElement {
}
}
fn resolve_image_decode_promises(&self, can_gc: CanGc) {
for promise in self.image_decode_promises.borrow().iter() {
promise.resolve_native(&(), can_gc);
/// <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());
}
}));
}
fn reject_image_decode_promises(&self, can_gc: CanGc) {
let document = self.owner_document();
for promise in self.image_decode_promises.borrow().iter() {
promise.reject_native(
&DOMException::new(&document.global(), DOMErrorName::EncodingError, can_gc),
can_gc,
);
/// <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>

View file

@ -9,6 +9,7 @@ use crate::codegen::PrototypeList::proto_id_to_name;
use crate::script_runtime::JSContext as SafeJSContext;
/// DOM exceptions that can be thrown by a native DOM method.
/// <https://webidl.spec.whatwg.org/#dfn-error-names-table>
#[derive(Clone, Debug, MallocSizeOf)]
pub enum Error {
/// IndexSizeError DOMException
@ -67,6 +68,8 @@ pub enum Error {
Operation,
/// NotAllowedError DOMException
NotAllowed,
/// EncodingError DOMException
Encoding,
/// TypeError JavaScript Error
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]
[HTMLImageElement.prototype.decode(), attach to DOM before promise resolves.: src in empty picture not 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]
[HTMLImageElement.prototype.decode(), attach to DOM before promise resolves.: src in empty picture cached]
expected: FAIL