mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
When the computed restyle damage is empty, do not do a layout. Instead, just rebuild the display list. In the future, even that can be omitted, but that requires changes to the compositor. These kind of relayouts commonly happen when the cursor is moving around the page and no style rules cause changes to :hover. Testing: This is covered existing WPT tests and should only have performance impacts. Unfortunately there are currently no performance tests. Signed-off-by: Martin Robinson <mrobinson@igalia.com> Co-authored-by: Oriol Brufau <obrufau@igalia.com>
748 lines
27 KiB
Rust
748 lines
27 KiB
Rust
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
|
|
use std::cell::{Cell, RefCell};
|
|
use std::collections::HashMap;
|
|
use std::rc::Rc;
|
|
|
|
use canvas_traits::webgl::{GLContextAttributes, WebGLVersion};
|
|
use constellation_traits::BlobImpl;
|
|
#[cfg(feature = "webgpu")]
|
|
use constellation_traits::ScriptToConstellationMessage;
|
|
use dom_struct::dom_struct;
|
|
use euclid::default::Size2D;
|
|
use html5ever::{LocalName, Prefix, local_name, ns};
|
|
use image::codecs::jpeg::JpegEncoder;
|
|
use image::codecs::png::PngEncoder;
|
|
use image::codecs::webp::WebPEncoder;
|
|
use image::{ColorType, ImageEncoder};
|
|
#[cfg(feature = "webgpu")]
|
|
use ipc_channel::ipc::{self as ipcchan};
|
|
use js::error::throw_type_error;
|
|
use js::rust::{HandleObject, HandleValue};
|
|
use script_layout_interface::HTMLCanvasData;
|
|
use servo_media::streams::MediaStreamType;
|
|
use servo_media::streams::registry::MediaStreamId;
|
|
use snapshot::Snapshot;
|
|
use style::attr::AttrValue;
|
|
|
|
use super::node::NodeDamage;
|
|
pub(crate) use crate::canvas_context::*;
|
|
use crate::conversions::Convert;
|
|
use crate::dom::attr::Attr;
|
|
use crate::dom::bindings::callback::ExceptionHandling;
|
|
use crate::dom::bindings::cell::{DomRefCell, Ref};
|
|
use crate::dom::bindings::codegen::Bindings::HTMLCanvasElementBinding::{
|
|
BlobCallback, HTMLCanvasElementMethods, RenderingContext as RootedRenderingContext,
|
|
};
|
|
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::inheritance::Castable;
|
|
use crate::dom::bindings::num::Finite;
|
|
use crate::dom::bindings::refcounted::Trusted;
|
|
use crate::dom::bindings::reflector::{DomGlobal, DomObject};
|
|
use crate::dom::bindings::root::{Dom, DomRoot, LayoutDom, ToLayout};
|
|
use crate::dom::bindings::str::{DOMString, USVString};
|
|
use crate::dom::blob::Blob;
|
|
use crate::dom::canvasrenderingcontext2d::CanvasRenderingContext2D;
|
|
use crate::dom::document::Document;
|
|
use crate::dom::element::{AttributeMutation, Element, LayoutElementHelpers};
|
|
#[cfg(not(feature = "webgpu"))]
|
|
use crate::dom::gpucanvascontext::GPUCanvasContext;
|
|
use crate::dom::htmlelement::HTMLElement;
|
|
use crate::dom::mediastream::MediaStream;
|
|
use crate::dom::mediastreamtrack::MediaStreamTrack;
|
|
use crate::dom::node::{Node, NodeTraits};
|
|
use crate::dom::offscreencanvas::OffscreenCanvas;
|
|
use crate::dom::values::UNSIGNED_LONG_MAX;
|
|
use crate::dom::virtualmethods::VirtualMethods;
|
|
use crate::dom::webgl2renderingcontext::WebGL2RenderingContext;
|
|
use crate::dom::webglrenderingcontext::WebGLRenderingContext;
|
|
#[cfg(feature = "webgpu")]
|
|
use crate::dom::webgpu::gpucanvascontext::GPUCanvasContext;
|
|
use crate::script_runtime::{CanGc, JSContext};
|
|
|
|
const DEFAULT_WIDTH: u32 = 300;
|
|
const DEFAULT_HEIGHT: u32 = 150;
|
|
|
|
#[derive(PartialEq)]
|
|
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()
|
|
}
|
|
}
|
|
|
|
/// <https://html.spec.whatwg.org/multipage/#htmlcanvaselement>
|
|
#[dom_struct]
|
|
pub(crate) struct HTMLCanvasElement {
|
|
htmlelement: HTMLElement,
|
|
|
|
/// <https://html.spec.whatwg.org/multipage/#concept-canvas-context-mode>
|
|
context_mode: DomRefCell<Option<RenderingContext>>,
|
|
|
|
// 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 {
|
|
fn new_inherited(
|
|
local_name: LocalName,
|
|
prefix: Option<Prefix>,
|
|
document: &Document,
|
|
) -> HTMLCanvasElement {
|
|
HTMLCanvasElement {
|
|
htmlelement: HTMLElement::new_inherited(local_name, prefix, document),
|
|
context_mode: DomRefCell::new(None),
|
|
callback_id: Cell::new(0),
|
|
blob_callbacks: RefCell::new(HashMap::new()),
|
|
}
|
|
}
|
|
|
|
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
|
|
pub(crate) fn new(
|
|
local_name: LocalName,
|
|
prefix: Option<Prefix>,
|
|
document: &Document,
|
|
proto: Option<HandleObject>,
|
|
can_gc: CanGc,
|
|
) -> DomRoot<HTMLCanvasElement> {
|
|
Node::reflect_node_with_proto(
|
|
Box::new(HTMLCanvasElement::new_inherited(
|
|
local_name, prefix, document,
|
|
)),
|
|
document,
|
|
proto,
|
|
can_gc,
|
|
)
|
|
}
|
|
|
|
fn recreate_contexts_after_resize(&self) {
|
|
if let Some(ref context) = *self.context_mode.borrow() {
|
|
context.resize()
|
|
}
|
|
}
|
|
|
|
pub(crate) fn get_size(&self) -> Size2D<u32> {
|
|
Size2D::new(self.Width(), self.Height())
|
|
}
|
|
|
|
pub(crate) fn origin_is_clean(&self) -> bool {
|
|
match *self.context_mode.borrow() {
|
|
Some(ref context) => context.origin_is_clean(),
|
|
_ => true,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn mark_as_dirty(&self) {
|
|
if let Some(ref context) = *self.context_mode.borrow() {
|
|
context.mark_as_dirty()
|
|
}
|
|
}
|
|
|
|
pub(crate) fn set_natural_width(&self, value: u32, can_gc: CanGc) {
|
|
let value = if value > UNSIGNED_LONG_MAX {
|
|
DEFAULT_WIDTH
|
|
} else {
|
|
value
|
|
};
|
|
let element = self.upcast::<Element>();
|
|
element.set_uint_attribute(&html5ever::local_name!("width"), value, can_gc);
|
|
}
|
|
|
|
pub(crate) fn set_natural_height(&self, value: u32, can_gc: CanGc) {
|
|
let value = if value > UNSIGNED_LONG_MAX {
|
|
DEFAULT_HEIGHT
|
|
} else {
|
|
value
|
|
};
|
|
let element = self.upcast::<Element>();
|
|
element.set_uint_attribute(&html5ever::local_name!("height"), value, can_gc);
|
|
}
|
|
}
|
|
|
|
impl LayoutHTMLCanvasElementHelpers for LayoutDom<'_, HTMLCanvasElement> {
|
|
#[allow(unsafe_code)]
|
|
fn data(self) -> HTMLCanvasData {
|
|
let source = unsafe {
|
|
match self.unsafe_get().context_mode.borrow_for_layout().as_ref() {
|
|
Some(RenderingContext::Context2d(context)) => {
|
|
context.to_layout().canvas_data_source()
|
|
},
|
|
Some(RenderingContext::WebGL(context)) => context.to_layout().canvas_data_source(),
|
|
Some(RenderingContext::WebGL2(context)) => context.to_layout().canvas_data_source(),
|
|
#[cfg(feature = "webgpu")]
|
|
Some(RenderingContext::WebGPU(context)) => context.to_layout().canvas_data_source(),
|
|
Some(RenderingContext::Placeholder(_)) | None => None,
|
|
}
|
|
};
|
|
|
|
let width_attr = self
|
|
.upcast::<Element>()
|
|
.get_attr_for_layout(&ns!(), &local_name!("width"));
|
|
let height_attr = self
|
|
.upcast::<Element>()
|
|
.get_attr_for_layout(&ns!(), &local_name!("height"));
|
|
HTMLCanvasData {
|
|
source,
|
|
width: width_attr.map_or(DEFAULT_WIDTH, |val| val.as_uint()),
|
|
height: height_attr.map_or(DEFAULT_HEIGHT, |val| val.as_uint()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl HTMLCanvasElement {
|
|
pub(crate) fn context(&self) -> Option<Ref<RenderingContext>> {
|
|
Ref::filter_map(self.context_mode.borrow(), |ctx| ctx.as_ref()).ok()
|
|
}
|
|
|
|
fn get_or_init_2d_context(&self, can_gc: CanGc) -> Option<DomRoot<CanvasRenderingContext2D>> {
|
|
if let Some(ctx) = self.context() {
|
|
return match *ctx {
|
|
RenderingContext::Context2d(ref ctx) => Some(DomRoot::from_ref(ctx)),
|
|
_ => None,
|
|
};
|
|
}
|
|
|
|
let window = self.owner_window();
|
|
let size = self.get_size();
|
|
let context = CanvasRenderingContext2D::new(window.as_global_scope(), self, size, can_gc);
|
|
*self.context_mode.borrow_mut() =
|
|
Some(RenderingContext::Context2d(Dom::from_ref(&*context)));
|
|
Some(context)
|
|
}
|
|
|
|
fn get_or_init_webgl_context(
|
|
&self,
|
|
cx: JSContext,
|
|
options: HandleValue,
|
|
can_gc: CanGc,
|
|
) -> Option<DomRoot<WebGLRenderingContext>> {
|
|
if let Some(ctx) = self.context() {
|
|
return match *ctx {
|
|
RenderingContext::WebGL(ref ctx) => Some(DomRoot::from_ref(ctx)),
|
|
_ => None,
|
|
};
|
|
}
|
|
let window = self.owner_window();
|
|
let size = self.get_size();
|
|
let attrs = Self::get_gl_attributes(cx, options)?;
|
|
let canvas = HTMLCanvasElementOrOffscreenCanvas::HTMLCanvasElement(DomRoot::from_ref(self));
|
|
let context = WebGLRenderingContext::new(
|
|
&window,
|
|
&canvas,
|
|
WebGLVersion::WebGL1,
|
|
size,
|
|
attrs,
|
|
can_gc,
|
|
)?;
|
|
*self.context_mode.borrow_mut() = Some(RenderingContext::WebGL(Dom::from_ref(&*context)));
|
|
Some(context)
|
|
}
|
|
|
|
fn get_or_init_webgl2_context(
|
|
&self,
|
|
cx: JSContext,
|
|
options: HandleValue,
|
|
can_gc: CanGc,
|
|
) -> Option<DomRoot<WebGL2RenderingContext>> {
|
|
if !WebGL2RenderingContext::is_webgl2_enabled(cx, self.global().reflector().get_jsobject())
|
|
{
|
|
return None;
|
|
}
|
|
if let Some(ctx) = self.context() {
|
|
return match *ctx {
|
|
RenderingContext::WebGL2(ref ctx) => Some(DomRoot::from_ref(ctx)),
|
|
_ => None,
|
|
};
|
|
}
|
|
let window = self.owner_window();
|
|
let size = self.get_size();
|
|
let attrs = Self::get_gl_attributes(cx, options)?;
|
|
let canvas = HTMLCanvasElementOrOffscreenCanvas::HTMLCanvasElement(DomRoot::from_ref(self));
|
|
let context = WebGL2RenderingContext::new(&window, &canvas, size, attrs, can_gc)?;
|
|
*self.context_mode.borrow_mut() = Some(RenderingContext::WebGL2(Dom::from_ref(&*context)));
|
|
Some(context)
|
|
}
|
|
|
|
#[cfg(not(feature = "webgpu"))]
|
|
fn get_or_init_webgpu_context(&self) -> Option<DomRoot<GPUCanvasContext>> {
|
|
None
|
|
}
|
|
|
|
#[cfg(feature = "webgpu")]
|
|
fn get_or_init_webgpu_context(&self, can_gc: CanGc) -> Option<DomRoot<GPUCanvasContext>> {
|
|
if let Some(ctx) = self.context() {
|
|
return match *ctx {
|
|
RenderingContext::WebGPU(ref ctx) => Some(DomRoot::from_ref(ctx)),
|
|
_ => None,
|
|
};
|
|
}
|
|
let (sender, receiver) = ipcchan::channel().unwrap();
|
|
let global_scope = self.owner_global();
|
|
let _ = global_scope
|
|
.script_to_constellation_chan()
|
|
.send(ScriptToConstellationMessage::GetWebGPUChan(sender));
|
|
receiver
|
|
.recv()
|
|
.expect("Failed to get WebGPU channel")
|
|
.map(|channel| {
|
|
let context = GPUCanvasContext::new(&global_scope, self, channel, can_gc);
|
|
*self.context_mode.borrow_mut() =
|
|
Some(RenderingContext::WebGPU(Dom::from_ref(&*context)));
|
|
context
|
|
})
|
|
}
|
|
|
|
/// Gets the base WebGLRenderingContext for WebGL or WebGL 2, if exists.
|
|
pub(crate) fn get_base_webgl_context(&self) -> Option<DomRoot<WebGLRenderingContext>> {
|
|
match *self.context_mode.borrow() {
|
|
Some(RenderingContext::WebGL(ref context)) => Some(DomRoot::from_ref(context)),
|
|
Some(RenderingContext::WebGL2(ref context)) => Some(context.base_context()),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
#[allow(unsafe_code)]
|
|
fn get_gl_attributes(cx: JSContext, options: HandleValue) -> Option<GLContextAttributes> {
|
|
unsafe {
|
|
match WebGLContextAttributes::new(cx, options) {
|
|
Ok(ConversionResult::Success(attrs)) => Some(attrs.convert()),
|
|
Ok(ConversionResult::Failure(error)) => {
|
|
throw_type_error(*cx, &error);
|
|
None
|
|
},
|
|
_ => {
|
|
debug!("Unexpected error on conversion of WebGLContextAttributes");
|
|
None
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn is_valid(&self) -> bool {
|
|
self.Height() != 0 && self.Width() != 0
|
|
}
|
|
|
|
pub(crate) fn get_image_data(&self) -> Option<Snapshot> {
|
|
match self.context_mode.borrow().as_ref() {
|
|
Some(context) => context.get_image_data(),
|
|
None => {
|
|
let size = self.get_size();
|
|
if size.width == 0 || size.height == 0 {
|
|
None
|
|
} else {
|
|
Some(Snapshot::cleared(size.cast()))
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
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>,
|
|
snapshot: &Snapshot,
|
|
encoder: &mut W,
|
|
) {
|
|
// We can't use self.Width() or self.Height() here, since the size of the canvas
|
|
// may have changed since the snapshot was created. Truncating the dimensions to a
|
|
// u32 can't panic, since the data comes from a canvas which is always smaller than
|
|
// u32::MAX.
|
|
let canvas_data = snapshot.data();
|
|
let width = snapshot.size().width as u32;
|
|
let height = snapshot.size().height as u32;
|
|
|
|
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(canvas_data, width, 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(canvas_data, width, 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(canvas_data, width, height, ColorType::Rgba8)
|
|
.unwrap();
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
impl HTMLCanvasElementMethods<crate::DomTypeHolder> for HTMLCanvasElement {
|
|
// https://html.spec.whatwg.org/multipage/#dom-canvas-width
|
|
make_uint_getter!(Width, "width", DEFAULT_WIDTH);
|
|
|
|
/// <https://html.spec.whatwg.org/multipage/#dom-canvas-width>
|
|
fn SetWidth(&self, value: u32, can_gc: CanGc) -> Fallible<()> {
|
|
// > When setting the value of the width or height attribute, if the context mode of the canvas element
|
|
// > is set to placeholder, the user agent must throw an "InvalidStateError" DOMException and leave the
|
|
// > attribute's value unchanged.
|
|
if let Some(RenderingContext::Placeholder(_)) = *self.context_mode.borrow() {
|
|
return Err(Error::InvalidState);
|
|
}
|
|
|
|
let value = if value > UNSIGNED_LONG_MAX {
|
|
DEFAULT_WIDTH
|
|
} else {
|
|
value
|
|
};
|
|
let element = self.upcast::<Element>();
|
|
element.set_uint_attribute(&html5ever::local_name!("width"), value, can_gc);
|
|
Ok(())
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-canvas-height
|
|
make_uint_getter!(Height, "height", DEFAULT_HEIGHT);
|
|
|
|
/// <https://html.spec.whatwg.org/multipage/#dom-canvas-height>
|
|
fn SetHeight(&self, value: u32, can_gc: CanGc) -> Fallible<()> {
|
|
// > When setting the value of the width or height attribute, if the context mode of the canvas element
|
|
// > is set to placeholder, the user agent must throw an "InvalidStateError" DOMException and leave the
|
|
// > attribute's value unchanged.
|
|
if let Some(RenderingContext::Placeholder(_)) = *self.context_mode.borrow() {
|
|
return Err(Error::InvalidState);
|
|
}
|
|
|
|
let value = if value > UNSIGNED_LONG_MAX {
|
|
DEFAULT_HEIGHT
|
|
} else {
|
|
value
|
|
};
|
|
let element = self.upcast::<Element>();
|
|
element.set_uint_attribute(&html5ever::local_name!("height"), value, can_gc);
|
|
Ok(())
|
|
}
|
|
|
|
/// <https://html.spec.whatwg.org/multipage/#dom-canvas-getcontext>
|
|
fn GetContext(
|
|
&self,
|
|
cx: JSContext,
|
|
id: DOMString,
|
|
options: HandleValue,
|
|
can_gc: CanGc,
|
|
) -> Fallible<Option<RootedRenderingContext>> {
|
|
// Always throw an InvalidState exception when the canvas is in Placeholder mode (See table in the spec).
|
|
if let Some(RenderingContext::Placeholder(_)) = *self.context_mode.borrow() {
|
|
return Err(Error::InvalidState);
|
|
}
|
|
|
|
Ok(match &*id {
|
|
"2d" => self
|
|
.get_or_init_2d_context(can_gc)
|
|
.map(RootedRenderingContext::CanvasRenderingContext2D),
|
|
"webgl" | "experimental-webgl" => self
|
|
.get_or_init_webgl_context(cx, options, can_gc)
|
|
.map(RootedRenderingContext::WebGLRenderingContext),
|
|
"webgl2" | "experimental-webgl2" => self
|
|
.get_or_init_webgl2_context(cx, options, can_gc)
|
|
.map(RootedRenderingContext::WebGL2RenderingContext),
|
|
#[cfg(feature = "webgpu")]
|
|
"webgpu" => self
|
|
.get_or_init_webgpu_context(can_gc)
|
|
.map(RootedRenderingContext::GPUCanvasContext),
|
|
_ => None,
|
|
})
|
|
}
|
|
|
|
/// <https://html.spec.whatwg.org/multipage/#dom-canvas-todataurl>
|
|
fn ToDataURL(
|
|
&self,
|
|
_context: JSContext,
|
|
mime_type: DOMString,
|
|
quality: HandleValue,
|
|
) -> Fallible<USVString> {
|
|
// Step 1.
|
|
if !self.origin_is_clean() {
|
|
return Err(Error::Security);
|
|
}
|
|
|
|
// Step 2.
|
|
if self.Width() == 0 || self.Height() == 0 {
|
|
return Ok(USVString("data:,".into()));
|
|
}
|
|
|
|
// Step 3.
|
|
let Some(mut snapshot) = self.get_image_data() else {
|
|
return Ok(USVString("data:,".into()));
|
|
};
|
|
|
|
let image_type = EncodedImageType::from(mime_type);
|
|
snapshot.transform(
|
|
if image_type == EncodedImageType::Jpeg {
|
|
snapshot::AlphaMode::AsOpaque {
|
|
premultiplied: true,
|
|
}
|
|
} else {
|
|
snapshot::AlphaMode::Transparent {
|
|
premultiplied: false,
|
|
}
|
|
},
|
|
snapshot::PixelFormat::RGBA,
|
|
);
|
|
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,
|
|
);
|
|
|
|
self.encode_for_mime_type(
|
|
&image_type,
|
|
Self::maybe_quality(quality),
|
|
&snapshot,
|
|
&mut encoder,
|
|
);
|
|
encoder.into_inner();
|
|
Ok(USVString(url))
|
|
}
|
|
|
|
/// <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);
|
|
}
|
|
|
|
// Step 2. Let result be null.
|
|
// Step 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_image_data()
|
|
};
|
|
|
|
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(mut snapshot) = result {
|
|
snapshot.transform(
|
|
snapshot::AlphaMode::Transparent{ premultiplied: false },
|
|
snapshot::PixelFormat::RGBA
|
|
);
|
|
// 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, &snapshot, &mut encoded);
|
|
let blob_impl = BlobImpl::new_from_bytes(encoded, image_type.as_mime_type());
|
|
// Step 4.2.1 Set result to a new Blob object, created in the relevant realm of this canvas element
|
|
let blob = Blob::new(&this.global(), blob_impl, CanGc::note());
|
|
|
|
// Step 4.2.2 Invoke callback with « result » and "report".
|
|
let _ = callback.Call__(Some(&blob), ExceptionHandling::Report, CanGc::note());
|
|
} else {
|
|
let _ = callback.Call__(None, ExceptionHandling::Report, CanGc::note());
|
|
}
|
|
}));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// <https://html.spec.whatwg.org/multipage/#dom-canvas-transfercontroltooffscreen>
|
|
fn TransferControlToOffscreen(&self, can_gc: CanGc) -> Fallible<DomRoot<OffscreenCanvas>> {
|
|
if self.context_mode.borrow().is_some() {
|
|
// Step 1.
|
|
// If this canvas element's context mode is not set to none, throw an "InvalidStateError" DOMException.
|
|
return Err(Error::InvalidState);
|
|
};
|
|
|
|
// Step 2.
|
|
// Let offscreenCanvas be a new OffscreenCanvas object with its width and height equal to the values of
|
|
// the width and height content attributes of this canvas element.
|
|
// Step 3.
|
|
// Set the placeholder canvas element of offscreenCanvas to a weak reference to this canvas element.
|
|
let offscreen_canvas = OffscreenCanvas::new(
|
|
&self.global(),
|
|
None,
|
|
self.Width().into(),
|
|
self.Height().into(),
|
|
Some(&Dom::from_ref(self)),
|
|
can_gc,
|
|
);
|
|
|
|
// Step 4. Set this canvas element's context mode to placeholder.
|
|
*self.context_mode.borrow_mut() =
|
|
Some(RenderingContext::Placeholder(offscreen_canvas.as_traced()));
|
|
|
|
// Step 5. Return offscreenCanvas.
|
|
Ok(offscreen_canvas)
|
|
}
|
|
|
|
/// <https://w3c.github.io/mediacapture-fromelement/#dom-htmlcanvaselement-capturestream>
|
|
fn CaptureStream(
|
|
&self,
|
|
_frame_request_rate: Option<Finite<f64>>,
|
|
can_gc: CanGc,
|
|
) -> DomRoot<MediaStream> {
|
|
let global = self.global();
|
|
let stream = MediaStream::new(&global, can_gc);
|
|
let track = MediaStreamTrack::new(
|
|
&global,
|
|
MediaStreamId::new(),
|
|
MediaStreamType::Video,
|
|
can_gc,
|
|
);
|
|
stream.AddTrack(&track);
|
|
stream
|
|
}
|
|
}
|
|
|
|
impl VirtualMethods for HTMLCanvasElement {
|
|
fn super_type(&self) -> Option<&dyn VirtualMethods> {
|
|
Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
|
|
}
|
|
|
|
fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation, can_gc: CanGc) {
|
|
self.super_type()
|
|
.unwrap()
|
|
.attribute_mutated(attr, mutation, can_gc);
|
|
match attr.local_name() {
|
|
&local_name!("width") | &local_name!("height") => {
|
|
self.recreate_contexts_after_resize();
|
|
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
|
|
},
|
|
_ => {},
|
|
};
|
|
}
|
|
|
|
fn parse_plain_attribute(&self, name: &LocalName, value: DOMString) -> AttrValue {
|
|
match *name {
|
|
local_name!("width") => AttrValue::from_u32(value.into(), DEFAULT_WIDTH),
|
|
local_name!("height") => AttrValue::from_u32(value.into(), DEFAULT_HEIGHT),
|
|
_ => self
|
|
.super_type()
|
|
.unwrap()
|
|
.parse_plain_attribute(name, value),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Convert<GLContextAttributes> for WebGLContextAttributes {
|
|
fn convert(self) -> GLContextAttributes {
|
|
GLContextAttributes {
|
|
alpha: self.alpha,
|
|
depth: self.depth,
|
|
stencil: self.stencil,
|
|
antialias: self.antialias,
|
|
premultiplied_alpha: self.premultipliedAlpha,
|
|
preserve_drawing_buffer: self.preserveDrawingBuffer,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) mod utils {
|
|
use net_traits::image_cache::ImageResponse;
|
|
use net_traits::request::CorsSettings;
|
|
use servo_url::ServoUrl;
|
|
|
|
use crate::dom::window::Window;
|
|
|
|
pub(crate) fn request_image_from_cache(
|
|
window: &Window,
|
|
url: ServoUrl,
|
|
cors_setting: Option<CorsSettings>,
|
|
) -> ImageResponse {
|
|
let image_cache = window.image_cache();
|
|
let result = image_cache.get_image(
|
|
url.clone(),
|
|
window.origin().immutable().clone(),
|
|
cors_setting,
|
|
);
|
|
|
|
match result {
|
|
Some(image) => ImageResponse::Loaded(image, url),
|
|
None => ImageResponse::None,
|
|
}
|
|
}
|
|
}
|