You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
604 lines
15 KiB
604 lines
15 KiB
/* 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_NONE);
|
|
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_NONE);
|
|
|
|
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_NONE);
|
|
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;
|
|
}
|
|
|
|
typedef struct {
|
|
uint8_t *ptr;
|
|
size_t stride;
|
|
ssize_t width;
|
|
ssize_t height;
|
|
} raw_bitmap;
|
|
|
|
typedef struct {
|
|
raw_bitmap src;
|
|
raw_bitmap dst;
|
|
/* These are relative to the destination top/left */
|
|
ssize_t dst_x;
|
|
ssize_t dst_y;
|
|
/* These are relative to the source top/left */
|
|
ssize_t x1;
|
|
ssize_t y1;
|
|
/* And these are +1, so a 1x1 copy will have x2==x1+1 etc */
|
|
ssize_t x2;
|
|
ssize_t y2;
|
|
} copy_operation;
|
|
|
|
/**
|
|
* Copy from src to dst
|
|
*
|
|
* Note, this is destructive to its copy_operation input
|
|
*
|
|
* \param op The copy operation to perform
|
|
* \return Whether the destination bitmap was altered
|
|
*/
|
|
static bool
|
|
canvas2d__copy_bitmap_to_bitmap(copy_operation *op)
|
|
{
|
|
/* Constrain src rectangle to src bitmap size */
|
|
if (op->x1 < 0) op->x1 = 0;
|
|
if (op->y1 < 0) op->y1 = 0;
|
|
if (op->x2 > op->src.width) op->x2 = op->src.width;
|
|
if (op->y2 > op->src.height) op->y2 = op->src.height;
|
|
/* Offset the rectangle into dst coordinates */
|
|
op->x1 += op->dst_x;
|
|
op->x2 += op->dst_x;
|
|
op->y1 += op->dst_y;
|
|
op->y2 += op->dst_y;
|
|
/* Constrain dst rectangle to dst bitmap */
|
|
if (op->x1 < 0) op->x1 = 0;
|
|
if (op->y1 < 0) op->y1 = 0;
|
|
if (op->x2 > op->dst.width) op->x2 = op->dst.width;
|
|
if (op->y2 > op->dst.height) op->y2 = op->dst.height;
|
|
/* If we have nothing to copy, stop now */
|
|
if ((op->x2 - op->x1) < 1 ||
|
|
(op->y2 - op->y1) < 1)
|
|
return false;
|
|
/* Okay, stuff to copy, so let's begin */
|
|
op->src.ptr +=
|
|
(op->src.stride * (op->y1 - op->dst_y)) + /* move down y1 rows */
|
|
(op->x1 - op->dst_x) * 4; /* and across x1 pixels */
|
|
op->dst.ptr +=
|
|
(op->dst.stride * op->y1) + /* down down y1 rows */
|
|
(op->x1 * 4); /* and across x1 pixels */
|
|
for (ssize_t rowctr = op->y2 - op->y1; rowctr > 0; --rowctr) {
|
|
memcpy(op->dst.ptr, op->src.ptr, (op->x2 - op->x1) * 4);
|
|
op->src.ptr += op->src.stride;
|
|
op->dst.ptr += op->dst.stride;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* 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;
|
|
copy_operation copyop;
|
|
|
|
if (priv->bitmap == NULL)
|
|
return duk_generic_error(ctx, "Canvas in bad state, sorry");
|
|
|
|
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
|
|
*/
|
|
copyop.src.ptr = guit->bitmap->get_buffer(priv->bitmap);
|
|
copyop.src.stride = priv->stride;
|
|
copyop.src.width = priv->width;
|
|
copyop.src.height = priv->height;
|
|
|
|
copyop.dst.ptr = idpriv->data;
|
|
copyop.dst.stride = idpriv->width * 4;
|
|
copyop.dst.width = idpriv->width;
|
|
copyop.dst.height = idpriv->height;
|
|
|
|
/* Copying to top/left of our new bitmap */
|
|
copyop.dst_x = 0;
|
|
copyop.dst_y = 0;
|
|
|
|
/* Copying from x,y for width,height */
|
|
copyop.x1 = x;
|
|
copyop.x2 = x + width;
|
|
copyop.y1 = y;
|
|
copyop.y2 = y + height;
|
|
|
|
/* We don't care if the copy operation wrote or not because
|
|
* we don't need to invalidate ImageData objects
|
|
*/
|
|
(void)canvas2d__copy_bitmap_to_bitmap(©op);
|
|
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;
|
|
copy_operation copyop;
|
|
|
|
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);
|
|
|
|
/* Copying from the input ImageData object */
|
|
copyop.src.ptr = idpriv->data;
|
|
copyop.src.stride = idpriv->width * 4;
|
|
copyop.src.width = idpriv->width;
|
|
copyop.src.height = idpriv->height;
|
|
|
|
/* Copying to ourselves */
|
|
copyop.dst.ptr = guit->bitmap->get_buffer(priv->bitmap);
|
|
copyop.dst.stride = priv->stride;
|
|
copyop.dst.width = priv->width;
|
|
copyop.dst.height = priv->height;
|
|
|
|
/* X Y target coordinates */
|
|
copyop.dst_x = duk_to_int(ctx, 1);
|
|
copyop.dst_y = duk_to_int(ctx, 2);
|
|
|
|
if (duk_get_top(ctx) < 7) {
|
|
/* Clipping data not provided */
|
|
copyop.x1 = 0;
|
|
copyop.y1 = 0;
|
|
copyop.x2 = idpriv->width;
|
|
copyop.y2 = idpriv->height;
|
|
} else {
|
|
copyop.x1 = duk_to_int(ctx, 3);
|
|
copyop.y1 = duk_to_int(ctx, 4);
|
|
copyop.x2 = copyop.x1 + duk_to_int(ctx, 5);
|
|
copyop.y2 = copyop.y1 + duk_to_int(ctx, 6);
|
|
}
|
|
|
|
if (canvas2d__copy_bitmap_to_bitmap(©op)) {
|
|
guit->bitmap->modified(priv->bitmap);
|
|
redraw_node((dom_node *)(priv->canvas));
|
|
}
|
|
|
|
return 0;
|
|
%}
|