2020-03-19 19:07:56 +01:00
/*
* Copyright ( c ) 2018 - 2020 , Andreas Kling < kling @ serenityos . org >
*
2021-04-22 01:24:48 -07:00
* SPDX - License - Identifier : BSD - 2 - Clause
2020-03-19 19:07:56 +01:00
*/
2021-04-19 23:47:29 +02:00
# include <AK/Base64.h>
2020-04-15 16:55:36 +02:00
# include <AK/Checked.h>
2023-08-03 10:26:13 +02:00
# include <AK/MemoryStream.h>
2020-03-19 19:07:56 +01:00
# include <LibGfx/Bitmap.h>
2023-08-03 10:26:13 +02:00
# include <LibGfx/ImageFormats/JPEGWriter.h>
2023-03-21 14:58:06 -04:00
# include <LibGfx/ImageFormats/PNGWriter.h>
2023-08-03 01:29:31 +02:00
# include <LibWeb/Bindings/ExceptionOrUtils.h>
2024-04-27 12:09:58 +12:00
# include <LibWeb/Bindings/HTMLCanvasElementPrototype.h>
2021-09-24 13:49:57 +02:00
# include <LibWeb/CSS/StyleComputer.h>
2024-08-14 11:46:56 +01:00
# include <LibWeb/CSS/StyleValues/CSSKeywordValue.h>
2023-08-31 19:22:17 +02:00
# include <LibWeb/CSS/StyleValues/RatioStyleValue.h>
# include <LibWeb/CSS/StyleValues/StyleValueList.h>
2020-03-19 19:07:56 +01:00
# include <LibWeb/DOM/Document.h>
2020-07-26 15:08:16 +02:00
# include <LibWeb/HTML/CanvasRenderingContext2D.h>
# include <LibWeb/HTML/HTMLCanvasElement.h>
2023-08-31 22:12:14 +02:00
# include <LibWeb/HTML/Numbers.h>
2023-08-03 01:29:31 +02:00
# include <LibWeb/HTML/Scripting/ExceptionReporter.h>
2020-11-22 15:53:01 +01:00
# include <LibWeb/Layout/CanvasBox.h>
2023-08-03 01:29:31 +02:00
# include <LibWeb/Platform/EventLoopPlugin.h>
# include <LibWeb/WebIDL/AbstractOperations.h>
2020-03-19 19:07:56 +01:00
2020-07-28 18:20:36 +02:00
namespace Web : : HTML {
2020-03-19 19:07:56 +01:00
2023-11-19 19:47:52 +01:00
JS_DEFINE_ALLOCATOR ( HTMLCanvasElement ) ;
2020-04-15 12:12:19 +02:00
static constexpr auto max_canvas_area = 16384 * 16384 ;
2022-02-18 21:00:52 +01:00
HTMLCanvasElement : : HTMLCanvasElement ( DOM : : Document & document , DOM : : QualifiedName qualified_name )
2021-02-07 11:20:15 +01:00
: HTMLElement ( document , move ( qualified_name ) )
2020-03-19 19:07:56 +01:00
{
}
2022-03-14 13:21:51 -06:00
HTMLCanvasElement : : ~ HTMLCanvasElement ( ) = default ;
2020-03-19 19:07:56 +01:00
2023-08-07 08:41:28 +02:00
void HTMLCanvasElement : : initialize ( JS : : Realm & realm )
2023-01-10 06:28:20 -05:00
{
2023-08-07 08:41:28 +02:00
Base : : initialize ( realm ) ;
2024-03-16 13:13:08 +01:00
WEB_SET_PROTOTYPE_FOR_INTERFACE ( HTMLCanvasElement ) ;
2023-01-10 06:28:20 -05:00
}
2022-09-02 15:53:02 +02:00
void HTMLCanvasElement : : visit_edges ( Cell : : Visitor & visitor )
{
Base : : visit_edges ( visitor ) ;
m_context . visit (
[ & ] ( JS : : NonnullGCPtr < CanvasRenderingContext2D > & context ) {
2023-11-19 16:18:00 +13:00
visitor . visit ( context ) ;
2022-09-02 15:53:02 +02:00
} ,
[ & ] ( JS : : NonnullGCPtr < WebGL : : WebGLRenderingContext > & context ) {
2023-11-19 16:18:00 +13:00
visitor . visit ( context ) ;
2022-09-02 15:53:02 +02:00
} ,
[ ] ( Empty ) {
} ) ;
}
2023-08-31 19:22:17 +02:00
void HTMLCanvasElement : : apply_presentational_hints ( CSS : : StyleProperties & style ) const
{
// https://html.spec.whatwg.org/multipage/rendering.html#attributes-for-embedded-content-and-images
// The width and height attributes map to the aspect-ratio property on canvas elements.
// FIXME: Multiple elements have aspect-ratio presentational hints, make this into a helper function
// https://html.spec.whatwg.org/multipage/rendering.html#map-to-the-aspect-ratio-property
// if element has both attributes w and h, and parsing those attributes' values using the rules for parsing non-negative integers doesn't generate an error for either
2024-01-16 19:04:45 +01:00
auto w = parse_non_negative_integer ( get_attribute_value ( HTML : : AttributeNames : : width ) ) ;
auto h = parse_non_negative_integer ( get_attribute_value ( HTML : : AttributeNames : : height ) ) ;
2023-08-31 19:22:17 +02:00
if ( w . has_value ( ) & & h . has_value ( ) )
// then the user agent is expected to use the parsed integers as a presentational hint for the 'aspect-ratio' property of the form auto w / h.
style . set_property ( CSS : : PropertyID : : AspectRatio ,
CSS : : StyleValueList : : create ( CSS : : StyleValueVector {
2024-08-14 14:06:03 +01:00
CSS : : CSSKeywordValue : : create ( CSS : : Keyword : : Auto ) ,
2023-08-31 19:22:17 +02:00
CSS : : RatioStyleValue : : create ( CSS : : Ratio { static_cast < double > ( w . value ( ) ) , static_cast < double > ( h . value ( ) ) } ) } ,
CSS : : StyleValueList : : Separator : : Space ) ) ;
}
2020-06-21 15:37:13 +02:00
unsigned HTMLCanvasElement : : width ( ) const
2020-03-19 19:07:56 +01:00
{
2023-08-31 22:12:14 +02:00
// https://html.spec.whatwg.org/multipage/canvas.html#obtain-numeric-values
// The rules for parsing non-negative integers must be used to obtain their numeric values.
// If an attribute is missing, or if parsing its value returns an error, then the default value must be used instead.
// The width attribute defaults to 300
2024-01-16 19:04:45 +01:00
return parse_non_negative_integer ( get_attribute_value ( HTML : : AttributeNames : : width ) ) . value_or ( 300 ) ;
2020-03-19 19:07:56 +01:00
}
2020-06-21 15:37:13 +02:00
unsigned HTMLCanvasElement : : height ( ) const
2020-03-19 19:07:56 +01:00
{
2023-08-31 22:12:14 +02:00
// https://html.spec.whatwg.org/multipage/canvas.html#obtain-numeric-values
// The rules for parsing non-negative integers must be used to obtain their numeric values.
// If an attribute is missing, or if parsing its value returns an error, then the default value must be used instead.
// the height attribute defaults to 150
2024-01-16 19:04:45 +01:00
return parse_non_negative_integer ( get_attribute_value ( HTML : : AttributeNames : : height ) ) . value_or ( 150 ) ;
2020-03-19 19:07:56 +01:00
}
2022-06-04 04:22:42 +01:00
void HTMLCanvasElement : : reset_context_to_default_state ( )
{
m_context . visit (
2022-09-02 15:53:02 +02:00
[ ] ( JS : : NonnullGCPtr < CanvasRenderingContext2D > & context ) {
2022-06-04 04:22:42 +01:00
context - > reset_to_default_state ( ) ;
} ,
2022-09-02 15:53:02 +02:00
[ ] ( JS : : NonnullGCPtr < WebGL : : WebGLRenderingContext > & ) {
2022-06-04 04:22:42 +01:00
TODO ( ) ;
} ,
[ ] ( Empty ) {
// Do nothing.
} ) ;
}
2023-05-25 20:37:57 +01:00
WebIDL : : ExceptionOr < void > HTMLCanvasElement : : set_width ( unsigned value )
2021-11-13 00:54:21 +01:00
{
2023-10-08 11:42:00 +13:00
TRY ( set_attribute ( HTML : : AttributeNames : : width , MUST ( String : : number ( value ) ) ) ) ;
2022-04-11 03:05:19 +02:00
m_bitmap = nullptr ;
2022-06-04 04:22:42 +01:00
reset_context_to_default_state ( ) ;
2023-05-25 20:37:57 +01:00
return { } ;
2021-11-13 00:54:21 +01:00
}
2023-05-25 20:37:57 +01:00
WebIDL : : ExceptionOr < void > HTMLCanvasElement : : set_height ( unsigned value )
2021-11-13 00:54:21 +01:00
{
2023-10-08 11:42:00 +13:00
TRY ( set_attribute ( HTML : : AttributeNames : : height , MUST ( String : : number ( value ) ) ) ) ;
2022-04-11 03:05:19 +02:00
m_bitmap = nullptr ;
2022-06-04 04:22:42 +01:00
reset_context_to_default_state ( ) ;
2023-05-25 20:37:57 +01:00
return { } ;
2021-11-13 00:54:21 +01:00
}
2022-10-17 14:41:50 +02:00
JS : : GCPtr < Layout : : Node > HTMLCanvasElement : : create_layout_node ( NonnullRefPtr < CSS : : StyleProperties > style )
2020-03-19 19:07:56 +01:00
{
2022-10-17 14:41:50 +02:00
return heap ( ) . allocate_without_realm < Layout : : CanvasBox > ( document ( ) , * this , move ( style ) ) ;
2020-03-19 19:07:56 +01:00
}
2022-06-04 04:22:42 +01:00
HTMLCanvasElement : : HasOrCreatedContext HTMLCanvasElement : : create_2d_context ( )
{
if ( ! m_context . has < Empty > ( ) )
2022-09-02 15:53:02 +02:00
return m_context . has < JS : : NonnullGCPtr < CanvasRenderingContext2D > > ( ) ? HasOrCreatedContext : : Yes : HasOrCreatedContext : : No ;
2022-06-04 04:22:42 +01:00
2023-08-13 13:05:26 +02:00
m_context = CanvasRenderingContext2D : : create ( realm ( ) , * this ) ;
2022-06-04 04:22:42 +01:00
return HasOrCreatedContext : : Yes ;
}
JS : : ThrowCompletionOr < HTMLCanvasElement : : HasOrCreatedContext > HTMLCanvasElement : : create_webgl_context ( JS : : Value options )
2020-03-19 19:07:56 +01:00
{
2022-06-04 04:22:42 +01:00
if ( ! m_context . has < Empty > ( ) )
2022-09-02 15:53:02 +02:00
return m_context . has < JS : : NonnullGCPtr < WebGL : : WebGLRenderingContext > > ( ) ? HasOrCreatedContext : : Yes : HasOrCreatedContext : : No ;
2022-06-04 04:22:42 +01:00
2022-09-25 18:12:50 -06:00
auto maybe_context = TRY ( WebGL : : WebGLRenderingContext : : create ( realm ( ) , * this , options ) ) ;
2022-06-04 04:22:42 +01:00
if ( ! maybe_context )
return HasOrCreatedContext : : No ;
2022-09-02 15:53:02 +02:00
m_context = JS : : NonnullGCPtr < WebGL : : WebGLRenderingContext > ( * maybe_context ) ;
2022-06-04 04:22:42 +01:00
return HasOrCreatedContext : : Yes ;
2020-03-19 19:07:56 +01:00
}
2022-06-04 04:22:42 +01:00
// https://html.spec.whatwg.org/multipage/canvas.html#dom-canvas-getcontext
2023-09-03 15:58:55 +12:00
JS : : ThrowCompletionOr < HTMLCanvasElement : : RenderingContext > HTMLCanvasElement : : get_context ( String const & type , JS : : Value options )
2020-04-15 12:12:19 +02:00
{
2022-06-04 04:22:42 +01:00
// 1. If options is not an object, then set options to null.
if ( ! options . is_object ( ) )
options = JS : : js_null ( ) ;
// 2. Set options to the result of converting options to a JavaScript value.
// NOTE: No-op.
// 3. Run the steps in the cell of the following table whose column header matches this canvas element's canvas context mode and whose row header matches contextId:
// NOTE: See the spec for the full table.
if ( type = = " 2d " sv ) {
if ( create_2d_context ( ) = = HasOrCreatedContext : : Yes )
2022-09-02 15:53:02 +02:00
return JS : : make_handle ( * m_context . get < JS : : NonnullGCPtr < HTML : : CanvasRenderingContext2D > > ( ) ) ;
2022-06-04 04:22:42 +01:00
return Empty { } ;
}
// NOTE: The WebGL spec says "experimental-webgl" is also acceptable and must be equivalent to "webgl". Other engines accept this, so we do too.
if ( type . is_one_of ( " webgl " sv , " experimental-webgl " sv ) ) {
if ( TRY ( create_webgl_context ( options ) ) = = HasOrCreatedContext : : Yes )
2022-09-02 15:53:02 +02:00
return JS : : make_handle ( * m_context . get < JS : : NonnullGCPtr < WebGL : : WebGLRenderingContext > > ( ) ) ;
2022-06-04 04:22:42 +01:00
return Empty { } ;
}
return Empty { } ;
}
static Gfx : : IntSize bitmap_size_for_canvas ( HTMLCanvasElement const & canvas , size_t minimum_width , size_t minimum_height )
{
auto width = max ( canvas . width ( ) , minimum_width ) ;
auto height = max ( canvas . height ( ) , minimum_height ) ;
2020-04-15 16:55:36 +02:00
Checked < size_t > area = width ;
area * = height ;
if ( area . has_overflow ( ) ) {
2021-01-09 14:02:45 +01:00
dbgln ( " Refusing to create {}x{} canvas (overflow) " , width , height ) ;
2020-04-15 12:12:19 +02:00
return { } ;
}
2020-04-15 16:55:36 +02:00
if ( area . value ( ) > max_canvas_area ) {
2021-01-09 14:02:45 +01:00
dbgln ( " Refusing to create {}x{} canvas (exceeds maximum size) " , width , height ) ;
2020-04-15 12:12:19 +02:00
return { } ;
}
2020-06-21 15:37:13 +02:00
return Gfx : : IntSize ( width , height ) ;
2020-04-15 12:12:19 +02:00
}
2022-06-04 04:22:42 +01:00
bool HTMLCanvasElement : : create_bitmap ( size_t minimum_width , size_t minimum_height )
2020-03-19 19:07:56 +01:00
{
2022-06-04 04:22:42 +01:00
auto size = bitmap_size_for_canvas ( * this , minimum_width , minimum_height ) ;
2020-04-15 12:12:19 +02:00
if ( size . is_empty ( ) ) {
m_bitmap = nullptr ;
return false ;
2020-03-19 19:07:56 +01:00
}
2021-11-06 19:30:59 +01:00
if ( ! m_bitmap | | m_bitmap - > size ( ) ! = size ) {
2024-08-02 13:15:45 +02:00
auto bitmap_or_error = Gfx : : Bitmap : : create ( Gfx : : BitmapFormat : : BGRA8888 , Gfx : : AlphaType : : Premultiplied , size ) ;
2021-11-06 19:30:59 +01:00
if ( bitmap_or_error . is_error ( ) )
return false ;
m_bitmap = bitmap_or_error . release_value_but_fixme_should_propagate_errors ( ) ;
}
2020-04-15 16:55:36 +02:00
return m_bitmap ;
2020-03-19 19:07:56 +01:00
}
2023-08-03 10:12:11 +02:00
struct SerializeBitmapResult {
ByteBuffer buffer ;
StringView mime_type ;
} ;
// https://html.spec.whatwg.org/multipage/canvas.html#a-serialisation-of-the-bitmap-as-a-file
2023-08-03 10:26:13 +02:00
static ErrorOr < SerializeBitmapResult > serialize_bitmap ( Gfx : : Bitmap const & bitmap , StringView type , Optional < double > quality )
2023-08-03 10:12:11 +02:00
{
2023-08-03 10:26:13 +02:00
// If type is an image format that supports variable quality (such as "image/jpeg"), quality is given, and type is not "image/png", then,
// if Type(quality) is Number, and quality is in the range 0.0 to 1.0 inclusive, the user agent must treat quality as the desired quality level.
// Otherwise, the user agent must use its default quality value, as if the quality argument had not been given.
if ( quality . has_value ( ) & & ! ( * quality > = 0.0 & & * quality < = 1.0 ) )
quality = OptionalNone { } ;
if ( type . equals_ignoring_ascii_case ( " image/jpeg " sv ) ) {
AllocatingMemoryStream file ;
Gfx : : JPEGWriter : : Options jpeg_options ;
if ( quality . has_value ( ) )
jpeg_options . quality = static_cast < int > ( quality . value ( ) * 100 ) ;
TRY ( Gfx : : JPEGWriter : : encode ( file , bitmap , jpeg_options ) ) ;
return SerializeBitmapResult { TRY ( file . read_until_eof ( ) ) , " image/jpeg " sv } ;
}
2023-08-03 10:12:11 +02:00
// 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. [PNG]
return SerializeBitmapResult { TRY ( Gfx : : PNGWriter : : encode ( bitmap ) ) , " image/png " sv } ;
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-canvas-todataurl
2023-09-16 22:00:37 +02:00
String HTMLCanvasElement : : to_data_url ( StringView type , Optional < double > quality )
2021-04-19 23:47:29 +02:00
{
2023-09-16 22:00:37 +02:00
// It is possible the the canvas doesn't have a associated bitmap so create one
if ( ! bitmap ( ) )
create_bitmap ( ) ;
2023-08-03 10:12:11 +02:00
// FIXME: 1. If this canvas element's bitmap's origin-clean flag is set to false, then throw a "SecurityError" DOMException.
// 2. If this canvas element's bitmap has no pixels (i.e. either its horizontal dimension or its vertical dimension is zero)
// then return the string "data:,". (This is the shortest data: URL; it represents the empty string in a text/plain resource.)
2021-04-19 23:47:29 +02:00
if ( ! m_bitmap )
2023-09-03 15:58:55 +12:00
return " data:, " _string ;
2023-08-03 10:12:11 +02:00
// 3. Let file be a serialization of this canvas element's bitmap as a file, passing type and quality if given.
auto file = serialize_bitmap ( * m_bitmap , type , move ( quality ) ) ;
// 4. If file is null then return "data:,".
if ( file . is_error ( ) ) {
dbgln ( " HTMLCanvasElement: Failed to encode canvas bitmap to {}: {} " , type , file . error ( ) ) ;
2023-09-03 15:58:55 +12:00
return " data:, " _string ;
2022-12-05 20:34:27 +01:00
}
2023-08-03 10:12:11 +02:00
// 5. Return a data: URL representing file. [RFC2397]
auto base64_encoded_or_error = encode_base64 ( file . value ( ) . buffer ) ;
2022-12-19 00:23:47 +01:00
if ( base64_encoded_or_error . is_error ( ) ) {
2023-09-03 15:58:55 +12:00
return " data:, " _string ;
2022-12-19 00:23:47 +01:00
}
2024-02-11 20:15:39 +13:00
return MUST ( URL : : create_with_data ( file . value ( ) . mime_type , base64_encoded_or_error . release_value ( ) , true ) . to_string ( ) ) ;
2021-04-19 23:47:29 +02:00
}
2023-08-03 01:29:31 +02:00
// https://html.spec.whatwg.org/multipage/canvas.html#dom-canvas-toblob
2023-09-03 15:58:55 +12:00
WebIDL : : ExceptionOr < void > HTMLCanvasElement : : to_blob ( JS : : NonnullGCPtr < WebIDL : : CallbackType > callback , StringView type , Optional < double > quality )
2023-08-03 01:29:31 +02:00
{
2023-09-16 22:00:37 +02:00
// It is possible the the canvas doesn't have a associated bitmap so create one
if ( ! bitmap ( ) )
create_bitmap ( ) ;
2023-08-03 01:29:31 +02:00
// FIXME: 1. If this canvas element's bitmap's origin-clean flag is set to false, then throw a "SecurityError" DOMException.
// 2. Let result be null.
RefPtr < Gfx : : Bitmap > bitmap_result ;
// 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.
if ( m_bitmap )
bitmap_result = TRY_OR_THROW_OOM ( vm ( ) , m_bitmap - > clone ( ) ) ;
// 4. Run these steps in parallel:
Platform : : EventLoopPlugin : : the ( ) . deferred_invoke ( [ this , callback , bitmap_result , type , quality ] {
// 1. If result is non-null, then set result to a serialization of result as a file with type and quality if given.
Optional < SerializeBitmapResult > file_result ;
if ( bitmap_result ) {
if ( auto result = serialize_bitmap ( * bitmap_result , type , move ( quality ) ) ; ! result . is_error ( ) )
file_result = result . release_value ( ) ;
}
// 2. Queue an element task on the canvas blob serialization task source given the canvas element to run these steps:
queue_an_element_task ( Task : : Source : : CanvasBlobSerializationTask , [ this , callback , file_result = move ( file_result ) ] {
auto maybe_error = Bindings : : throw_dom_exception_if_needed ( vm ( ) , [ & ] ( ) - > WebIDL : : ExceptionOr < void > {
// 1. If result is non-null, then set result to a new Blob object, created in the relevant realm of this canvas element, representing result. [FILEAPI]
JS : : GCPtr < FileAPI : : Blob > blob_result ;
if ( file_result . has_value ( ) )
2023-08-13 13:05:26 +02:00
blob_result = FileAPI : : Blob : : create ( realm ( ) , file_result - > buffer , TRY_OR_THROW_OOM ( vm ( ) , String : : from_utf8 ( file_result - > mime_type ) ) ) ;
2023-08-03 01:29:31 +02:00
// 2. Invoke callback with « result ».
TRY ( WebIDL : : invoke_callback ( * callback , { } , move ( blob_result ) ) ) ;
return { } ;
} ) ;
if ( maybe_error . is_throw_completion ( ) )
report_exception ( maybe_error . throw_completion ( ) , realm ( ) ) ;
} ) ;
} ) ;
return { } ;
}
2022-06-04 04:22:42 +01:00
void HTMLCanvasElement : : present ( )
{
m_context . visit (
2022-09-02 15:53:02 +02:00
[ ] ( JS : : NonnullGCPtr < CanvasRenderingContext2D > & ) {
2022-06-04 04:22:42 +01:00
// Do nothing, CRC2D writes directly to the canvas bitmap.
} ,
2022-09-02 15:53:02 +02:00
[ ] ( JS : : NonnullGCPtr < WebGL : : WebGLRenderingContext > & context ) {
2022-06-04 04:22:42 +01:00
context - > present ( ) ;
} ,
[ ] ( Empty ) {
// Do nothing.
} ) ;
}
2020-03-19 19:07:56 +01:00
}