netsurf/content/handlers/javascript/duktape/CanvasRenderingContext2D.bnd
Daniel Silverstone 74b8767ee2
Canvas: Do not overrun destination bitmap during creation
Signed-off-by: Daniel Silverstone <dsilvers@digital-scurf.org>
2020-05-24 16:21:36 +01:00

532 lines
14 KiB
Plaintext

/* HTML canvas element rendering context binding using duktape and libdom
*
* Copyright 2020 Daniel Silverstone <dsilvers@netsurf-browser.org>
*
* This file is part of NetSurf, http://www.netsurf-browser.org/
*
* Released under the terms of the MIT License,
* http://www.opensource.org/licenses/mit-license
*/
class CanvasRenderingContext2D {
private struct dom_html_canvas_element *canvas;
private struct bitmap *bitmap;
private int width;
private int height;
private size_t stride;
private dom_event_listener *listener;
prologue %{
/* prologue */
#include "desktop/gui_internal.h"
#include "desktop/gui_table.h"
#include "netsurf/bitmap.h"
#include "utils/corestrings.h"
/* It's a smidge naughty of us to read
* this particular header, but we're needing
* to redraw the node we represent
*/
#include "content/handlers/html/private.h"
static void redraw_node(dom_node *node)
{
struct box *box = NULL;
html_content *htmlc = NULL;
dom_exception exc;
dom_document *doc;
exc = dom_node_get_user_data(node,
corestring_dom___ns_key_box_node_data,
&box);
if (exc != DOM_NO_ERR || box == NULL) {
return;
}
exc = dom_node_get_owner_document(node, &doc);
if (exc != DOM_NO_ERR || doc == NULL) {
return;
}
exc = dom_node_get_user_data(doc,
corestring_dom___ns_key_html_content_data,
&htmlc);
if (exc != DOM_NO_ERR || htmlc == NULL) {
dom_node_unref(doc);
return;
}
html__redraw_a_box(htmlc, box);
dom_node_unref(doc);
}
/**
* deal with events from the DOM for canvas node user data
*
* \param operation The DOM operation happening
* \param key The user data key
* \param data The user data (our bitmap)
* \param src The DOM node emitting the event (our <canvas>)
* \param dst The target DOM node if applicable
*/
static void
canvas2d_user_data_handler(dom_node_operation operation,
dom_string *key,
void *data,
struct dom_node *src,
struct dom_node *dst)
{
struct bitmap *newbitmap, *bitmap = (struct bitmap*)data, *oldbitmap = NULL;
int width, height;
size_t stride;
if (dom_string_isequal(key,corestring_dom___ns_key_canvas_node_data) == false || data == NULL) {
/* Not for us */
return;
}
switch (operation) {
case DOM_NODE_CLONED:
width = guit->bitmap->get_width(bitmap);
height = guit->bitmap->get_height(bitmap);
stride = guit->bitmap->get_rowstride(bitmap);
newbitmap = guit->bitmap->create(width, height,
BITMAP_NEW);
if (newbitmap != NULL) {
if (guit->bitmap->get_rowstride(newbitmap) == stride) {
// Compatible bitmap, bung the data over
memcpy(guit->bitmap->get_buffer(newbitmap),
guit->bitmap->get_buffer(bitmap),
stride * height);
guit->bitmap->modified(newbitmap);
}
}
if (dom_node_set_user_data(dst,
corestring_dom___ns_key_canvas_node_data,
newbitmap, canvas2d_user_data_handler,
&oldbitmap) == DOM_NO_ERR) {
if (oldbitmap != NULL)
guit->bitmap->destroy(oldbitmap);
}
break;
case DOM_NODE_RENAMED:
case DOM_NODE_IMPORTED:
case DOM_NODE_ADOPTED:
break;
case DOM_NODE_DELETED:
guit->bitmap->destroy(bitmap);
break;
default:
NSLOG(netsurf, INFO, "User data operation not handled.");
assert(0);
}
}
/**
* Give the canvas element an appropriately sized bitmap
*
* \param node The DOM node being inserted
* \param[out] bitmap_out The bitmap created
* \return NSERROR_OK on success else appropriate error code
*/
static nserror canvas2d_create_bitmap(dom_node *node, struct bitmap **bitmap_out)
{
dom_exception exc;
dom_string *width_s = NULL, *height_s = NULL;
unsigned long width = 300, height = 150;
struct bitmap *bitmap, *oldbitmap = NULL;
exc = dom_element_get_attribute(node,
corestring_dom_width,
&width_s);
if (exc == DOM_NO_ERR && width_s != NULL) {
const char *ptr = (const char *)dom_string_data(width_s);
const char *endptr = ptr + dom_string_length(width_s);
char * ended;
unsigned long width_n = strtoul(ptr, &ended, 10);
if (ended == endptr || strcasecmp(ended, "px") == 0) {
/* parsed it all */
width = width_n;
}
dom_string_unref(width_s);
}
exc = dom_element_get_attribute(node,
corestring_dom_height,
&height_s);
if (exc == DOM_NO_ERR && height_s != NULL) {
const char *ptr = (const char *)dom_string_data(height_s);
const char *endptr = ptr + dom_string_length(height_s);
char * ended;
unsigned long height_n = strtoul(ptr, &ended, 10);
if (ended == endptr || strcasecmp(ended, "px") == 0) {
/* parsed it all */
height = height_n;
}
dom_string_unref(height_s);
}
bitmap = guit->bitmap->create(
(int)width, (int)height,
BITMAP_NEW);
if (bitmap == NULL) {
return NSERROR_NOMEM;
}
memset(guit->bitmap->get_buffer(bitmap),
0, /* Transparent black */
height * guit->bitmap->get_rowstride(bitmap));
guit->bitmap->modified(bitmap);
exc = dom_node_set_user_data(node,
corestring_dom___ns_key_canvas_node_data,
bitmap,
canvas2d_user_data_handler,
&oldbitmap);
if (exc != DOM_NO_ERR) {
guit->bitmap->destroy(bitmap);
return NSERROR_DOM;
}
assert(oldbitmap == NULL);
if (bitmap_out != NULL)
*bitmap_out = bitmap;
return NSERROR_OK;
}
/**
* Handle subtree modified events for our canvas node
*
* If width or height has changed relative to our priv, then
* we need to recreate the bitmap and reset our cached width
* and height values in order to be safe. Plus redraw ourselves.
*
* \param evt The event which occurred
* \param pw The private pointer for our canvas object
*/
static void
canvas2d__handle_dom_event(dom_event *evt, void *pw)
{
canvas_rendering_context2d_private_t *priv = pw;
dom_ulong width;
dom_ulong height;
dom_exception exc;
struct bitmap *newbitmap, *oldbitmap = NULL;
size_t stride;
dom_event_flow_phase phase;
exc = dom_event_get_event_phase(evt, &phase);
assert(exc == DOM_NO_ERR);
/* If we're not being hit right now, we're not up for it */
if (phase != DOM_AT_TARGET) return;
/* Rather than being complex about things, let's just work out
* what the width and height are and hope nothing else matters
*/
exc = dom_html_canvas_element_get_width(priv->canvas, &width);
if (exc != DOM_NO_ERR) return;
exc = dom_html_canvas_element_get_height(priv->canvas, &height);
if (exc != DOM_NO_ERR) return;
if ((int)height == priv->height && (int)width == priv->width) return;
/* Okay, we need to reallocate our bitmap and re-cache values */
newbitmap = guit->bitmap->create(width, height, BITMAP_NEW);
stride = guit->bitmap->get_rowstride(newbitmap);
if (newbitmap != NULL) {
memset(guit->bitmap->get_buffer(newbitmap),
0,
stride * height);
guit->bitmap->modified(newbitmap);
}
if (dom_node_set_user_data(priv->canvas,
corestring_dom___ns_key_canvas_node_data,
newbitmap, canvas2d_user_data_handler,
&oldbitmap) == DOM_NO_ERR) {
if (oldbitmap != NULL)
guit->bitmap->destroy(oldbitmap);
} else {
guit->bitmap->destroy(newbitmap);
/* We'll stick with the old, odd though that might be */
return;
}
/* Cache the new values */
priv->width = (int)width;
priv->height = (int)height;
priv->stride = stride;
priv->bitmap = newbitmap;
}
/* prologue ends */
%};
};
init CanvasRenderingContext2D(struct dom_html_canvas_element *canvas)
%{
struct bitmap *bitmap;
dom_exception exc;
assert(canvas != NULL);
priv->canvas = canvas;
dom_node_ref(canvas);
exc = dom_event_listener_create(canvas2d__handle_dom_event,
priv,
&priv->listener);
assert(exc == DOM_NO_ERR);
exc = dom_event_target_add_event_listener(
canvas,
corestring_dom_DOMSubtreeModified,
priv->listener,
false);
assert(exc == DOM_NO_ERR);
exc = dom_node_get_user_data(canvas,
corestring_dom___ns_key_canvas_node_data,
&bitmap);
assert(exc == DOM_NO_ERR);
if (bitmap == NULL) {
if (canvas2d_create_bitmap((dom_node *)canvas,
&bitmap) != NSERROR_OK) {
priv->bitmap = NULL;
priv->width = -1;
priv->height = -1;
priv->stride = 0;
return;
}
}
assert(bitmap != NULL);
priv->bitmap = bitmap;
priv->width = guit->bitmap->get_width(bitmap);
priv->height = guit->bitmap->get_height(bitmap);
priv->stride = guit->bitmap->get_rowstride(bitmap);
%}
fini CanvasRenderingContext2D()
%{
dom_exception exc;
exc = dom_event_target_remove_event_listener(
priv->canvas,
corestring_dom_DOMSubtreeModified,
priv->listener,
false);
assert(exc == DOM_NO_ERR);
dom_event_listener_unref(priv->listener);
dom_node_unref(priv->canvas);
%}
getter CanvasRenderingContext2D::canvas()
%{
dukky_push_node(ctx, (dom_node *)priv->canvas);
return 1;
%}
getter CanvasRenderingContext2D::width()
%{
dom_exception exc;
dom_ulong width;
exc = dom_html_canvas_element_get_width(priv->canvas, &width);
if (exc != DOM_NO_ERR) return 0;
duk_push_number(ctx, (duk_double_t)width);
return 1;
%}
setter CanvasRenderingContext2D::width()
%{
dom_exception exc;
dom_ulong width = duk_get_uint(ctx, 0);
exc = dom_html_canvas_element_set_width(priv->canvas, width);
if (exc != DOM_NO_ERR) return 0;
return 1;
%}
getter CanvasRenderingContext2D::height()
%{
dom_exception exc;
dom_ulong height;
exc = dom_html_canvas_element_get_height(priv->canvas, &height);
if (exc != DOM_NO_ERR) return 0;
duk_push_number(ctx, (duk_double_t)height);
return 1;
%}
setter CanvasRenderingContext2D::height()
%{
dom_exception exc;
dom_ulong height = duk_get_uint(ctx, 0);
exc = dom_html_canvas_element_set_height(priv->canvas, height);
if (exc != DOM_NO_ERR) return 0;
return 1;
%}
method CanvasRenderingContext2D::createImageData()
%{
/* Can be called either with width and height, or with a reference
* imagedata object
*/
image_data_private_t *idpriv;
int width, height;
if (duk_get_top(ctx) == 2) {
width = duk_to_int(ctx, 0);
height = duk_to_int(ctx, 1);
} else if (dukky_instanceof(ctx, 0, PROTO_NAME(IMAGEDATA))) {
duk_get_prop_string(ctx, 0, dukky_magic_string_private);
idpriv = duk_get_pointer(ctx, -1);
width = idpriv->width;
height = idpriv->height;
duk_pop(ctx);
} else {
duk_push_null(ctx);
return 1;
}
duk_push_int(ctx, width);
duk_push_int(ctx, height);
if (dukky_create_object(ctx,
PROTO_NAME(IMAGEDATA),
2) != DUK_EXEC_SUCCESS) {
return duk_error(ctx,
DUK_ERR_ERROR,
"Unable to create ImageData");
}
return 1;
%}
method CanvasRenderingContext2D::getImageData()
%{
/* called with x, y, width, height */
int x = duk_get_int(ctx, 0);
int y = duk_get_int(ctx, 1);
int width = duk_get_int(ctx, 2);
int height = duk_get_int(ctx, 3);
image_data_private_t *idpriv;
uint8_t *src_base, *dst_base;
if (priv->bitmap == NULL)
return duk_generic_error(ctx, "Canvas in bad state, sorry");
if (width < 1 || height < 1 ||
(x + width) > priv->width || (y + height) > priv->height) {
return duk_error(ctx, DUK_ERR_RANGE_ERROR, "invalid (%d,%d) (%dx%d)", x, y, width, height);
}
duk_push_int(ctx, width);
duk_push_int(ctx, height);
if (dukky_create_object(ctx,
PROTO_NAME(IMAGEDATA),
2) != DUK_EXEC_SUCCESS) {
return duk_error(ctx,
DUK_ERR_ERROR,
"Unable to create ImageData");
}
/* ... imgdata */
duk_get_prop_string(ctx, -1, dukky_magic_string_private);
idpriv = duk_get_pointer(ctx, -1);
duk_pop(ctx);
/* We now have access to the imagedata private, so we need to copy
* the pixel range out of ourselves
*/
src_base = guit->bitmap->get_buffer(priv->bitmap);
dst_base = idpriv->data;
for (int yy = y; yy < (y+height); ++yy) {
memcpy(dst_base, src_base + (x * 4), width * 4);
src_base += priv->stride;
dst_base += (width * 4);
}
return 1;
%}
method CanvasRenderingContext2D::putImageData()
%{
/* imgdata, x, y[, clipx, clipy, clipw, cliph] */
/* If provided, the clip coordinates are within the input image data */
/* We pretend the image is placed at x,y within ourselves, and then we
* copy the clip rectangle (defaults to whole image)
*/
image_data_private_t *idpriv;
int x = duk_to_int(ctx, 1);
int y = duk_to_int(ctx, 2);
int clipx = 0;
int clipy = 0;
int clipw = 0;
int cliph = 0;
uint8_t *bitmap_base;
if (!dukky_instanceof(ctx, 0, PROTO_NAME(IMAGEDATA))) {
return duk_generic_error(ctx, "Expected ImageData as first argument");
}
if (priv->bitmap == NULL)
return duk_generic_error(ctx, "Canvas in bad state, sorry");
duk_get_prop_string(ctx, 0, dukky_magic_string_private);
idpriv = duk_get_pointer(ctx, -1);
duk_pop(ctx);
if (duk_get_top(ctx) < 7) {
/* Clipping data not provided */
clipw = idpriv->width;
cliph = idpriv->height;
} else {
clipx = duk_to_int(ctx, 3);
clipy = duk_to_int(ctx, 4);
clipw = duk_to_int(ctx, 5);
cliph = duk_to_int(ctx, 6);
}
if (x < 0 || y < 0 || /* Not positioning negative */
(x + clipx + clipw) > priv->width || /* RHS not beyond bounds */
(y + clipy + cliph) > priv->height || /* bottom not beyond bounds */
clipx < 0 || clipy < 0 || /* Input in range */
(clipx + clipw) > idpriv->width || /* Input in range */
(clipy + cliph) > idpriv->height) { /* Input in range */
return duk_error(ctx, DUK_ERR_RANGE_ERROR, "invalid inputs: (%d,%d) (%d,%d) (%d,%d) (Me: %d,%d) (Img: %d,%d)",
x,y,clipx,clipy,clipw,cliph, priv->width, priv->height, idpriv->width, idpriv->height);
}
bitmap_base = guit->bitmap->get_buffer(priv->bitmap);
for (int yy = clipy; yy < (clipy + cliph); yy++) {
uint8_t *dst_row = bitmap_base + ((y + yy) * priv->stride);
uint8_t *src_row = idpriv->data + (yy * idpriv->width * 4);
memcpy(dst_row + ((x + clipx) * 4),
src_row + (clipx * 4),
clipw * 4);
}
guit->bitmap->modified(priv->bitmap);
redraw_node((dom_node *)(priv->canvas));
return 0;
%}