summaryrefslogtreecommitdiffstats
path: root/testing/marionette/capture.js
diff options
context:
space:
mode:
Diffstat (limited to 'testing/marionette/capture.js')
-rw-r--r--testing/marionette/capture.js205
1 files changed, 205 insertions, 0 deletions
diff --git a/testing/marionette/capture.js b/testing/marionette/capture.js
new file mode 100644
index 0000000000..6746baa56c
--- /dev/null
+++ b/testing/marionette/capture.js
@@ -0,0 +1,205 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const EXPORTED_SYMBOLS = ["capture"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ Log: "chrome://marionette/content/log.js",
+});
+
+XPCOMUtils.defineLazyGetter(this, "logger", () => Log.get());
+XPCOMUtils.defineLazyGlobalGetters(this, ["crypto"]);
+
+const CONTEXT_2D = "2d";
+const BG_COLOUR = "rgb(255,255,255)";
+const MAX_CANVAS_DIMENSION = 32767;
+const MAX_CANVAS_AREA = 472907776;
+const PNG_MIME = "image/png";
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+/**
+ * Provides primitives to capture screenshots.
+ *
+ * @namespace
+ */
+this.capture = {};
+
+capture.Format = {
+ Base64: 0,
+ Hash: 1,
+};
+
+/**
+ * Draw a rectangle off the framebuffer.
+ *
+ * @param {DOMWindow} win
+ * The DOM window used for the framebuffer, and providing the interfaces
+ * for creating an HTMLCanvasElement.
+ * @param {number} left
+ * The left, X axis offset of the rectangle.
+ * @param {number} top
+ * The top, Y axis offset of the rectangle.
+ * @param {number} width
+ * The width dimension of the rectangle to paint.
+ * @param {number} height
+ * The height dimension of the rectangle to paint.
+ * @param {HTMLCanvasElement=} canvas
+ * Optional canvas to reuse for the screenshot.
+ * @param {number=} flags
+ * Optional integer representing flags to pass to drawWindow; these
+ * are defined on CanvasRenderingContext2D.
+ * @param {number=} dX
+ * Horizontal offset between the browser window and content area. Defaults to 0.
+ * @param {number=} dY
+ * Vertical offset between the browser window and content area. Defaults to 0.
+ * @param {boolean=} readback
+ * If true, read back a snapshot of the pixel data currently in the
+ * compositor/window. Defaults to false.
+ *
+ * @return {HTMLCanvasElement}
+ * The canvas on which the selection from the window's framebuffer
+ * has been painted on.
+ */
+capture.canvas = async function(
+ win,
+ browsingContext,
+ left,
+ top,
+ width,
+ height,
+ { canvas = null, flags = null, dX = 0, dY = 0, readback = false } = {}
+) {
+ const scale = win.devicePixelRatio;
+
+ let canvasHeight = height * scale;
+ let canvasWidth = width * scale;
+
+ // Cap the screenshot size for width and height at 2^16 pixels,
+ // which is the maximum allowed canvas size. Higher dimensions will
+ // trigger exceptions in Gecko.
+ if (canvasWidth > MAX_CANVAS_DIMENSION) {
+ logger.warn(
+ "Limiting screen capture width to maximum allowed " +
+ MAX_CANVAS_DIMENSION +
+ " pixels"
+ );
+ width = Math.floor(MAX_CANVAS_DIMENSION / scale);
+ canvasWidth = width * scale;
+ }
+
+ if (canvasHeight > MAX_CANVAS_DIMENSION) {
+ logger.warn(
+ "Limiting screen capture height to maximum allowed " +
+ MAX_CANVAS_DIMENSION +
+ " pixels"
+ );
+ height = Math.floor(MAX_CANVAS_DIMENSION / scale);
+ canvasHeight = height * scale;
+ }
+
+ // If the area is larger, reduce the height to keep the full width.
+ if (canvasWidth * canvasHeight > MAX_CANVAS_AREA) {
+ logger.warn(
+ "Limiting screen capture area to maximum allowed " +
+ MAX_CANVAS_AREA +
+ " pixels"
+ );
+ height = Math.floor(MAX_CANVAS_AREA / (canvasWidth * scale));
+ canvasHeight = height * scale;
+ }
+
+ if (canvas === null) {
+ canvas = win.document.createElementNS(XHTML_NS, "canvas");
+ canvas.width = canvasWidth;
+ canvas.height = canvasHeight;
+ }
+
+ const ctx = canvas.getContext(CONTEXT_2D);
+
+ if (readback) {
+ if (flags === null) {
+ flags =
+ ctx.DRAWWINDOW_DRAW_CARET |
+ ctx.DRAWWINDOW_DRAW_VIEW |
+ ctx.DRAWWINDOW_USE_WIDGET_LAYERS;
+ }
+
+ // drawWindow doesn't take scaling into account.
+ ctx.scale(scale, scale);
+ ctx.drawWindow(win, left + dX, top + dY, width, height, BG_COLOUR, flags);
+ } else {
+ let rect = new DOMRect(left, top, width, height);
+ let snapshot = await browsingContext.currentWindowGlobal.drawSnapshot(
+ rect,
+ scale,
+ BG_COLOUR
+ );
+
+ ctx.drawImage(snapshot, 0, 0);
+
+ // Bug 1574935 - Huge dimensions can trigger an OOM because multiple copies
+ // of the bitmap will exist in memory. Force the removal of the snapshot
+ // because it is no longer needed.
+ snapshot.close();
+ }
+
+ return canvas;
+};
+
+/**
+ * Encode the contents of an HTMLCanvasElement to a Base64 encoded string.
+ *
+ * @param {HTMLCanvasElement} canvas
+ * The canvas to encode.
+ *
+ * @return {string}
+ * A Base64 encoded string.
+ */
+capture.toBase64 = function(canvas) {
+ let u = canvas.toDataURL(PNG_MIME);
+ return u.substring(u.indexOf(",") + 1);
+};
+
+/**
+ * Hash the contents of an HTMLCanvasElement to a SHA-256 hex digest.
+ *
+ * @param {HTMLCanvasElement} canvas
+ * The canvas to encode.
+ *
+ * @return {string}
+ * A hex digest of the SHA-256 hash of the base64 encoded string.
+ */
+capture.toHash = function(canvas) {
+ let u = capture.toBase64(canvas);
+ let buffer = new TextEncoder("utf-8").encode(u);
+ return crypto.subtle.digest("SHA-256", buffer).then(hash => hex(hash));
+};
+
+/**
+ * Convert buffer into to hex.
+ *
+ * @param {ArrayBuffer} buffer
+ * The buffer containing the data to convert to hex.
+ *
+ * @return {string}
+ * A hex digest of the input buffer.
+ */
+function hex(buffer) {
+ let hexCodes = [];
+ let view = new DataView(buffer);
+ for (let i = 0; i < view.byteLength; i += 4) {
+ let value = view.getUint32(i);
+ let stringValue = value.toString(16);
+ let padding = "00000000";
+ let paddedValue = (padding + stringValue).slice(-padding.length);
+ hexCodes.push(paddedValue);
+ }
+ return hexCodes.join("");
+}