/* HTML canvas element rendering context binding using duktape and libdom * * Copyright 2020 Daniel Silverstone * * 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 ) * \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; %}