diff options
author | webbeef <me@webbeef.org> | 2025-01-10 11:04:42 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-01-10 19:04:42 +0000 |
commit | 44d1e2ae0d6263e941341f2228f9b043cbf7c18c (patch) | |
tree | 97f19d7daff9abfae622ba9535cb3a2e2c70a2d9 /components/script | |
parent | 9aaa6934b7b616285acbffced9c0ba266c1a7d74 (diff) | |
download | servo-44d1e2ae0d6263e941341f2228f9b043cbf7c18c.tar.gz servo-44d1e2ae0d6263e941341f2228f9b043cbf7c18c.zip |
Implement HTMLCanvasElement.toBlob (#34938)
This refactors some of the code that is shared with toDataURL, and
updates the webidl definition to match the current spec (using a default
value for the mime type).
Signed-off-by: webbeef <me@webbeef.org>
Diffstat (limited to 'components/script')
-rw-r--r-- | components/script/dom/htmlcanvaselement.rs | 284 | ||||
-rw-r--r-- | components/script/dom/webidls/HTMLCanvasElement.webidl | 8 | ||||
-rw-r--r-- | components/script/task_manager.rs | 1 | ||||
-rw-r--r-- | components/script/task_source.rs | 3 |
4 files changed, 205 insertions, 91 deletions
diff --git a/components/script/dom/htmlcanvaselement.rs b/components/script/dom/htmlcanvaselement.rs index 5e30743aeba..4fb7a12e1f2 100644 --- a/components/script/dom/htmlcanvaselement.rs +++ b/components/script/dom/htmlcanvaselement.rs @@ -2,6 +2,10 @@ * 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, RefCell}; +use std::collections::HashMap; +use std::rc::Rc; + use canvas_traits::canvas::{CanvasId, CanvasMsg, FromScriptMsg}; use canvas_traits::webgl::{GLContextAttributes, WebGLVersion}; use dom_struct::dom_struct; @@ -18,6 +22,7 @@ use js::error::throw_type_error; use js::rust::{HandleObject, HandleValue}; use profile_traits::ipc; use script_layout_interface::{HTMLCanvasData, HTMLCanvasDataSource}; +use script_traits::serializable::BlobImpl; #[cfg(feature = "webgpu")] use script_traits::ScriptMsg; use servo_media::streams::registry::MediaStreamId; @@ -27,18 +32,21 @@ use style::attr::AttrValue; use crate::dom::attr::Attr; use crate::dom::bindings::cell::{ref_filter_map, DomRefCell, Ref}; use crate::dom::bindings::codegen::Bindings::HTMLCanvasElementBinding::{ - HTMLCanvasElementMethods, RenderingContext, + BlobCallback, HTMLCanvasElementMethods, RenderingContext, }; use crate::dom::bindings::codegen::Bindings::MediaStreamBinding::MediaStreamMethods; use crate::dom::bindings::codegen::Bindings::WebGLRenderingContextBinding::WebGLContextAttributes; use crate::dom::bindings::codegen::UnionTypes::HTMLCanvasElementOrOffscreenCanvas; use crate::dom::bindings::conversions::ConversionResult; use crate::dom::bindings::error::{Error, Fallible}; +use crate::dom::bindings::import::module::ExceptionHandling; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::num::Finite; +use crate::dom::bindings::refcounted::Trusted; use crate::dom::bindings::reflector::DomObject; use crate::dom::bindings::root::{Dom, DomRoot, LayoutDom}; use crate::dom::bindings::str::{DOMString, USVString}; +use crate::dom::blob::Blob; use crate::dom::canvasrenderingcontext2d::{ CanvasRenderingContext2D, LayoutCanvasRenderingContext2DHelpers, }; @@ -60,6 +68,40 @@ use crate::script_runtime::{CanGc, JSContext}; const DEFAULT_WIDTH: u32 = 300; const DEFAULT_HEIGHT: u32 = 150; +enum EncodedImageType { + Png, + Jpeg, + Webp, +} + +impl From<DOMString> for EncodedImageType { + // From: https://html.spec.whatwg.org/multipage/#serialising-bitmaps-to-a-file + // User agents must support PNG ("image/png"). User agents may support other types. + // If the user agent does not support the requested type, then it must create the file using the PNG format. + // Anything different than image/jpeg or image/webp is thus treated as PNG. + fn from(mime_type: DOMString) -> Self { + let mime = mime_type.to_string().to_lowercase(); + if mime == "image/jpeg" { + Self::Jpeg + } else if mime == "image/webp" { + Self::Webp + } else { + Self::Png + } + } +} + +impl EncodedImageType { + fn as_mime_type(&self) -> String { + match self { + Self::Png => "image/png", + Self::Jpeg => "image/jpeg", + Self::Webp => "image/webp", + } + .to_owned() + } +} + #[crown::unrooted_must_root_lint::must_root] #[derive(Clone, JSTraceable, MallocSizeOf)] pub(crate) enum CanvasContext { @@ -74,6 +116,10 @@ pub(crate) enum CanvasContext { pub(crate) struct HTMLCanvasElement { htmlelement: HTMLElement, context: DomRefCell<Option<CanvasContext>>, + // This id and hashmap are used to keep track of ongoing toBlob() calls. + callback_id: Cell<u32>, + #[ignore_malloc_size_of = "not implemented for webidl callbacks"] + blob_callbacks: RefCell<HashMap<u32, Rc<BlobCallback>>>, } impl HTMLCanvasElement { @@ -85,6 +131,8 @@ impl HTMLCanvasElement { HTMLCanvasElement { htmlelement: HTMLElement::new_inherited(local_name, prefix, document), context: DomRefCell::new(None), + callback_id: Cell::new(0), + blob_callbacks: RefCell::new(HashMap::new()), } } @@ -354,6 +402,78 @@ impl HTMLCanvasElement { Some((data, size)) } + + fn get_content(&self) -> Option<Vec<u8>> { + match *self.context.borrow() { + Some(CanvasContext::Context2d(ref context)) => { + Some(context.get_rect(Rect::from_size(self.get_size()))) + }, + Some(CanvasContext::WebGL(ref context)) => context.get_image_data(self.get_size()), + Some(CanvasContext::WebGL2(ref context)) => { + context.base_context().get_image_data(self.get_size()) + }, + //TODO: Add method get_image_data to GPUCanvasContext + #[cfg(feature = "webgpu")] + Some(CanvasContext::WebGPU(_)) => None, + None => { + // Each pixel is fully-transparent black. + Some(vec![0; (self.Width() * self.Height() * 4) as usize]) + }, + } + } + + fn maybe_quality(quality: HandleValue) -> Option<f64> { + if quality.is_number() { + Some(quality.to_number()) + } else { + None + } + } + + fn encode_for_mime_type<W: std::io::Write>( + &self, + image_type: &EncodedImageType, + quality: Option<f64>, + bytes: &[u8], + encoder: &mut W, + ) { + match image_type { + EncodedImageType::Png => { + // FIXME(nox): https://github.com/image-rs/image-png/issues/86 + // FIXME(nox): https://github.com/image-rs/image-png/issues/87 + PngEncoder::new(encoder) + .write_image(bytes, self.Width(), self.Height(), ColorType::Rgba8) + .unwrap(); + }, + EncodedImageType::Jpeg => { + let jpeg_encoder = if let Some(quality) = quality { + // The specification allows quality to be in [0.0..1.0] but the JPEG encoder + // expects it to be in [1..100] + if (0.0..=1.0).contains(&quality) { + JpegEncoder::new_with_quality( + encoder, + (quality * 100.0).round().clamp(1.0, 100.0) as u8, + ) + } else { + JpegEncoder::new(encoder) + } + } else { + JpegEncoder::new(encoder) + }; + + jpeg_encoder + .write_image(bytes, self.Width(), self.Height(), ColorType::Rgba8) + .unwrap(); + }, + + EncodedImageType::Webp => { + // No quality support because of https://github.com/image-rs/image/issues/1984 + WebPEncoder::new_lossless(encoder) + .write_image(bytes, self.Width(), self.Height(), ColorType::Rgba8) + .unwrap(); + }, + } + } } impl HTMLCanvasElementMethods<crate::DomTypeHolder> for HTMLCanvasElement { @@ -395,11 +515,11 @@ impl HTMLCanvasElementMethods<crate::DomTypeHolder> for HTMLCanvasElement { } } - // https://html.spec.whatwg.org/multipage/#dom-canvas-todataurl + /// <https://html.spec.whatwg.org/multipage/#dom-canvas-todataurl> fn ToDataURL( &self, _context: JSContext, - mime_type: Option<DOMString>, + mime_type: DOMString, quality: HandleValue, ) -> Fallible<USVString> { // Step 1. @@ -413,102 +533,90 @@ impl HTMLCanvasElementMethods<crate::DomTypeHolder> for HTMLCanvasElement { } // Step 3. - let file = match *self.context.borrow() { - Some(CanvasContext::Context2d(ref context)) => { - context.get_rect(Rect::from_size(self.get_size())) - }, - Some(CanvasContext::WebGL(ref context)) => { - match context.get_image_data(self.get_size()) { - Some(data) => data, - None => return Ok(USVString("data:,".into())), - } - }, - Some(CanvasContext::WebGL2(ref context)) => { - match context.base_context().get_image_data(self.get_size()) { - Some(data) => data, - None => return Ok(USVString("data:,".into())), - } - }, - //TODO: Add method get_image_data to GPUCanvasContext - #[cfg(feature = "webgpu")] - Some(CanvasContext::WebGPU(_)) => return Ok(USVString("data:,".into())), - None => { - // Each pixel is fully-transparent black. - vec![0; (self.Width() * self.Height() * 4) as usize] - }, - }; - - enum ImageType { - Png, - Jpeg, - Webp, - } - - // From: https://html.spec.whatwg.org/multipage/#serialising-bitmaps-to-a-file - // User agents must support PNG ("image/png"). User agents may support other types. - // If the user agent does not support the requested type, then it must create the file using the PNG format. - // Anything different than image/jpeg is thus treated as PNG. - let (image_type, url) = match mime_type { - Some(mime) => { - let mime = mime.to_string().to_lowercase(); - if mime == "image/jpeg" { - (ImageType::Jpeg, "data:image/jpeg;base64,") - } else if mime == "image/webp" { - (ImageType::Webp, "data:image/webp;base64,") - } else { - (ImageType::Png, "data:image/png;base64,") - } - }, - _ => (ImageType::Png, "data:image/png;base64,"), + let Some(file) = self.get_content() else { + return Ok(USVString("data:,".into())); }; - let mut url = url.to_owned(); + let image_type = EncodedImageType::from(mime_type); + let mut url = format!("data:{};base64,", image_type.as_mime_type()); let mut encoder = base64::write::EncoderStringWriter::from_consumer( &mut url, &base64::engine::general_purpose::STANDARD, ); - match image_type { - ImageType::Png => { - // FIXME(nox): https://github.com/image-rs/image-png/issues/86 - // FIXME(nox): https://github.com/image-rs/image-png/issues/87 - PngEncoder::new(&mut encoder) - .write_image(&file, self.Width(), self.Height(), ColorType::Rgba8) - .unwrap(); - }, - ImageType::Jpeg => { - let jpeg_encoder = if quality.is_number() { - let quality = quality.to_number(); - // The specification allows quality to be in [0.0..1.0] but the JPEG encoder - // expects it to be in [1..100] - if (0.0..=1.0).contains(&quality) { - JpegEncoder::new_with_quality( - &mut encoder, - (quality * 100.0).round().clamp(1.0, 100.0) as u8, - ) - } else { - JpegEncoder::new(&mut encoder) - } - } else { - JpegEncoder::new(&mut encoder) - }; - - jpeg_encoder - .write_image(&file, self.Width(), self.Height(), ColorType::Rgba8) - .unwrap(); - }, + self.encode_for_mime_type( + &image_type, + Self::maybe_quality(quality), + &file, + &mut encoder, + ); + encoder.into_inner(); + Ok(USVString(url)) + } - ImageType::Webp => { - // No quality support because of https://github.com/image-rs/image/issues/1984 - WebPEncoder::new_lossless(&mut encoder) - .write_image(&file, self.Width(), self.Height(), ColorType::Rgba8) - .unwrap(); - }, + /// <https://html.spec.whatwg.org/multipage/#dom-canvas-toblob> + fn ToBlob( + &self, + _cx: JSContext, + callback: Rc<BlobCallback>, + mime_type: DOMString, + quality: HandleValue, + ) -> Fallible<()> { + // Step 1. + // If this canvas element's bitmap's origin-clean flag is set to false, then throw a + // "SecurityError" DOMException. + if !self.origin_is_clean() { + return Err(Error::Security); } - encoder.into_inner(); - Ok(USVString(url)) + // Step 2. and 3. + // If this canvas element's bitmap has pixels (i.e., neither its horizontal dimension + // nor its vertical dimension is zero), + // then set result to a copy of this canvas element's bitmap. + let result = if self.Width() == 0 || self.Height() == 0 { + None + } else { + self.get_content() + }; + + let this = Trusted::new(self); + let callback_id = self.callback_id.get().wrapping_add(1); + self.callback_id.set(callback_id); + + self.blob_callbacks + .borrow_mut() + .insert(callback_id, callback); + let quality = Self::maybe_quality(quality); + let image_type = EncodedImageType::from(mime_type); + self.global() + .task_manager() + .canvas_blob_task_source() + .queue(task!(to_blob: move || { + let this = this.root(); + let Some(callback) = &this.blob_callbacks.borrow_mut().remove(&callback_id) else { + return error!("Expected blob callback, but found none!"); + }; + + if let Some(bytes) = result { + // Step 4.1 + // If result is non-null, then set result to a serialization of result as a file with + // type and quality if given. + let mut encoded: Vec<u8> = vec![]; + + this.encode_for_mime_type(&image_type, quality, &bytes, &mut encoded); + let blob_impl = BlobImpl::new_from_bytes(encoded, image_type.as_mime_type()); + // Step 4.2.1 & 4.2.2 + // Set result to a new Blob object, created in the relevant realm of this canvas element + // Invoke callback with « result » and "report". + let blob = Blob::new(&this.global(), blob_impl, CanGc::note()); + let _ = callback.Call__(Some(&blob), ExceptionHandling::Report); + } else { + let _ = callback.Call__(None, ExceptionHandling::Report); + } + })); + + Ok(()) } /// <https://w3c.github.io/mediacapture-fromelement/#dom-htmlcanvaselement-capturestream> diff --git a/components/script/dom/webidls/HTMLCanvasElement.webidl b/components/script/dom/webidls/HTMLCanvasElement.webidl index 5c33aa3532e..84bfab7d587 100644 --- a/components/script/dom/webidls/HTMLCanvasElement.webidl +++ b/components/script/dom/webidls/HTMLCanvasElement.webidl @@ -18,8 +18,10 @@ interface HTMLCanvasElement : HTMLElement { RenderingContext? getContext(DOMString contextId, optional any options = null); [Throws] - USVString toDataURL(optional DOMString type, optional any quality); - //void toBlob(BlobCallback _callback, optional DOMString type, optional any quality); + USVString toDataURL(optional DOMString type = "image/png", optional any quality); + + [Throws] + undefined toBlob(BlobCallback callback, optional DOMString type = "image/png", optional any quality); //OffscreenCanvas transferControlToOffscreen(); }; @@ -28,4 +30,4 @@ partial interface HTMLCanvasElement { MediaStream captureStream (optional double frameRequestRate); }; -//callback BlobCallback = void (Blob? blob); +callback BlobCallback = undefined(Blob? blob); diff --git a/components/script/task_manager.rs b/components/script/task_manager.rs index 3a5498b9a5f..eeae042b6ee 100644 --- a/components/script/task_manager.rs +++ b/components/script/task_manager.rs @@ -131,6 +131,7 @@ impl TaskManager { .cancel_pending_tasks_for_source(task_source_name); } + task_source_functions!(self, canvas_blob_task_source, Canvas); task_source_functions!(self, dom_manipulation_task_source, DOMManipulation); task_source_functions!(self, file_reading_task_source, FileReading); task_source_functions!(self, gamepad_task_source, Gamepad); diff --git a/components/script/task_source.rs b/components/script/task_source.rs index 418dcf5501c..0030dcb8c30 100644 --- a/components/script/task_source.rs +++ b/components/script/task_source.rs @@ -24,6 +24,7 @@ use crate::task_manager::TaskManager; /// [`TaskSourceName::all`]. #[derive(Clone, Copy, Debug, Eq, Hash, JSTraceable, MallocSizeOf, PartialEq)] pub(crate) enum TaskSourceName { + Canvas, DOMManipulation, FileReading, HistoryTraversal, @@ -44,6 +45,7 @@ pub(crate) enum TaskSourceName { impl From<TaskSourceName> for ScriptThreadEventCategory { fn from(value: TaskSourceName) -> Self { match value { + TaskSourceName::Canvas => ScriptThreadEventCategory::ScriptEvent, TaskSourceName::DOMManipulation => ScriptThreadEventCategory::ScriptEvent, TaskSourceName::FileReading => ScriptThreadEventCategory::FileRead, TaskSourceName::HistoryTraversal => ScriptThreadEventCategory::HistoryEvent, @@ -66,6 +68,7 @@ impl From<TaskSourceName> for ScriptThreadEventCategory { impl TaskSourceName { pub(crate) fn all() -> &'static [TaskSourceName] { &[ + TaskSourceName::Canvas, TaskSourceName::DOMManipulation, TaskSourceName::FileReading, TaskSourceName::HistoryTraversal, |