2015-10-13 15:52:26 +00:00
|
|
|
/* 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";
|
|
|
|
|
2019-01-17 18:18:31 +00:00
|
|
|
const { XPCOMUtils } = ChromeUtils.import(
|
|
|
|
"resource://gre/modules/XPCOMUtils.jsm"
|
|
|
|
);
|
2018-06-08 12:16:29 +00:00
|
|
|
|
2019-01-17 18:18:31 +00:00
|
|
|
const { InvalidArgumentError } = ChromeUtils.import(
|
|
|
|
"chrome://marionette/content/error.js"
|
|
|
|
);
|
|
|
|
const { Log } = ChromeUtils.import("chrome://marionette/content/log.js");
|
2018-02-26 17:42:56 +00:00
|
|
|
|
2018-08-28 11:40:11 +00:00
|
|
|
XPCOMUtils.defineLazyGetter(this, "logger", Log.get);
|
2018-06-08 12:16:29 +00:00
|
|
|
XPCOMUtils.defineLazyGlobalGetters(this, ["crypto"]);
|
2016-04-18 01:37:14 +00:00
|
|
|
|
2015-10-13 15:52:26 +00:00
|
|
|
this.EXPORTED_SYMBOLS = ["capture"];
|
|
|
|
|
|
|
|
const CONTEXT_2D = "2d";
|
|
|
|
const BG_COLOUR = "rgb(255,255,255)";
|
2018-08-28 11:40:11 +00:00
|
|
|
const MAX_SKIA_DIMENSIONS = 32767;
|
2015-10-13 15:52:26 +00:00
|
|
|
const PNG_MIME = "image/png";
|
|
|
|
const XHTML_NS = "http://www.w3.org/1999/xhtml";
|
|
|
|
|
2017-07-26 12:11:53 +00:00
|
|
|
/**
|
|
|
|
* Provides primitives to capture screenshots.
|
|
|
|
*
|
|
|
|
* @namespace
|
|
|
|
*/
|
2015-10-13 15:52:26 +00:00
|
|
|
this.capture = {};
|
|
|
|
|
2016-12-20 14:30:48 +00:00
|
|
|
capture.Format = {
|
|
|
|
Base64: 0,
|
|
|
|
Hash: 1,
|
|
|
|
};
|
|
|
|
|
2015-10-13 15:52:26 +00:00
|
|
|
/**
|
|
|
|
* Take a screenshot of a single element.
|
|
|
|
*
|
|
|
|
* @param {Node} node
|
|
|
|
* The node to take a screenshot of.
|
|
|
|
* @param {Array.<Node>=} highlights
|
|
|
|
* Optional array of nodes, around which a border will be marked to
|
|
|
|
* highlight them in the screenshot.
|
|
|
|
*
|
|
|
|
* @return {HTMLCanvasElement}
|
|
|
|
* The canvas element where the element has been painted on.
|
|
|
|
*/
|
2017-06-29 23:40:24 +00:00
|
|
|
capture.element = function(node, highlights = []) {
|
2017-01-27 09:51:03 +00:00
|
|
|
let win = node.ownerGlobal;
|
2015-10-13 15:52:26 +00:00
|
|
|
let rect = node.getBoundingClientRect();
|
|
|
|
|
|
|
|
return capture.canvas(win, rect.left, rect.top, rect.width, rect.height, {
|
2017-05-09 16:23:47 +00:00
|
|
|
highlights,
|
|
|
|
});
|
2015-10-13 15:52:26 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2016-12-05 17:27:15 +00:00
|
|
|
* Take a screenshot of the window's viewport by taking into account
|
|
|
|
* the current offsets.
|
2015-10-13 15:52:26 +00:00
|
|
|
*
|
2016-12-05 17:27:15 +00:00
|
|
|
* @param {DOMWindow} win
|
|
|
|
* The DOM window providing the document element to capture,
|
|
|
|
* and the offsets for the viewport.
|
2015-10-13 15:52:26 +00:00
|
|
|
* @param {Array.<Node>=} highlights
|
|
|
|
* Optional array of nodes, around which a border will be marked to
|
|
|
|
* highlight them in the screenshot.
|
|
|
|
*
|
|
|
|
* @return {HTMLCanvasElement}
|
|
|
|
* The canvas element where the viewport has been painted on.
|
|
|
|
*/
|
2017-06-29 23:40:24 +00:00
|
|
|
capture.viewport = function(win, highlights = []) {
|
2015-10-13 15:52:26 +00:00
|
|
|
return capture.canvas(
|
2016-12-05 17:27:15 +00:00
|
|
|
win,
|
2015-10-13 15:52:26 +00:00
|
|
|
win.pageXOffset,
|
|
|
|
win.pageYOffset,
|
2019-01-09 14:47:04 +00:00
|
|
|
win.innerWidth,
|
|
|
|
win.innerHeight,
|
2017-05-09 16:23:47 +00:00
|
|
|
{ highlights }
|
|
|
|
);
|
2015-10-13 15:52:26 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Low-level interface to draw a rectangle off the framebuffer.
|
|
|
|
*
|
2016-12-05 17:27:15 +00:00
|
|
|
* @param {DOMWindow} win
|
|
|
|
* The DOM window used for the framebuffer, and providing the interfaces
|
|
|
|
* for creating an HTMLCanvasElement.
|
2015-10-13 15:52:26 +00:00
|
|
|
* @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 {Array.<Node>=} highlights
|
|
|
|
* Optional array of nodes, around which a border will be marked to
|
|
|
|
* highlight them in the screenshot.
|
2017-05-09 16:23:47 +00:00
|
|
|
* @param {HTMLCanvasElement=} canvas
|
|
|
|
* Optional canvas to reuse for the screenshot.
|
2017-05-09 16:26:08 +00:00
|
|
|
* @param {number=} flags
|
|
|
|
* Optional integer representing flags to pass to drawWindow; these
|
|
|
|
* are defined on CanvasRenderingContext2D.
|
2015-10-13 15:52:26 +00:00
|
|
|
*
|
|
|
|
* @return {HTMLCanvasElement}
|
|
|
|
* The canvas on which the selection from the window's framebuffer
|
|
|
|
* has been painted on.
|
|
|
|
*/
|
2017-06-29 23:40:24 +00:00
|
|
|
capture.canvas = function(
|
|
|
|
win,
|
|
|
|
left,
|
|
|
|
top,
|
|
|
|
width,
|
|
|
|
height,
|
2017-05-09 16:26:08 +00:00
|
|
|
{ highlights = [], canvas = null, flags = null } = {}
|
|
|
|
) {
|
2017-06-29 23:40:24 +00:00
|
|
|
const scale = win.devicePixelRatio;
|
2017-06-23 21:13:27 +00:00
|
|
|
|
2017-05-09 16:23:47 +00:00
|
|
|
if (canvas === null) {
|
2018-08-28 11:40:11 +00:00
|
|
|
let canvasWidth = width * scale;
|
|
|
|
let canvasHeight = height * scale;
|
|
|
|
|
|
|
|
if (canvasWidth > MAX_SKIA_DIMENSIONS) {
|
|
|
|
logger.warn(
|
|
|
|
"Reducing screenshot width because it exceeds " +
|
|
|
|
MAX_SKIA_DIMENSIONS +
|
|
|
|
" pixels"
|
|
|
|
);
|
|
|
|
canvasWidth = MAX_SKIA_DIMENSIONS;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (canvasHeight > MAX_SKIA_DIMENSIONS) {
|
|
|
|
logger.warn(
|
|
|
|
"Reducing screenshot height because it exceeds " +
|
|
|
|
MAX_SKIA_DIMENSIONS +
|
|
|
|
" pixels"
|
|
|
|
);
|
|
|
|
canvasHeight = MAX_SKIA_DIMENSIONS;
|
|
|
|
}
|
|
|
|
|
2017-05-09 16:23:47 +00:00
|
|
|
canvas = win.document.createElementNS(XHTML_NS, "canvas");
|
2018-08-28 11:40:11 +00:00
|
|
|
canvas.width = canvasWidth;
|
|
|
|
canvas.height = canvasHeight;
|
2017-05-09 16:23:47 +00:00
|
|
|
}
|
2015-10-13 15:52:26 +00:00
|
|
|
|
|
|
|
let ctx = canvas.getContext(CONTEXT_2D);
|
2017-05-09 16:26:08 +00:00
|
|
|
if (flags === null) {
|
2019-08-02 11:09:02 +00:00
|
|
|
flags =
|
|
|
|
ctx.DRAWWINDOW_DRAW_CARET |
|
|
|
|
ctx.DRAWWINDOW_DRAW_VIEW |
|
|
|
|
ctx.DRAWWINDOW_USE_WIDGET_LAYERS;
|
2017-06-29 23:40:24 +00:00
|
|
|
}
|
2016-12-28 13:13:31 +00:00
|
|
|
|
|
|
|
ctx.scale(scale, scale);
|
|
|
|
ctx.drawWindow(win, left, top, width, height, BG_COLOUR, flags);
|
2017-05-09 16:26:45 +00:00
|
|
|
if (highlights.length) {
|
|
|
|
ctx = capture.highlight_(ctx, highlights, top, left);
|
|
|
|
}
|
2015-10-13 15:52:26 +00:00
|
|
|
|
|
|
|
return canvas;
|
|
|
|
};
|
|
|
|
|
2017-06-29 23:40:24 +00:00
|
|
|
capture.highlight_ = function(context, highlights, top = 0, left = 0) {
|
2018-02-26 17:42:56 +00:00
|
|
|
if (typeof highlights == "undefined") {
|
|
|
|
throw new InvalidArgumentError("Missing highlights");
|
2015-10-13 15:52:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
context.lineWidth = "2";
|
|
|
|
context.strokeStyle = "red";
|
|
|
|
context.save();
|
|
|
|
|
|
|
|
for (let el of highlights) {
|
|
|
|
let rect = el.getBoundingClientRect();
|
|
|
|
let oy = -top;
|
|
|
|
let ox = -left;
|
|
|
|
|
|
|
|
context.strokeRect(rect.left + ox, rect.top + oy, rect.width, rect.height);
|
|
|
|
}
|
|
|
|
|
|
|
|
return context;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Encode the contents of an HTMLCanvasElement to a Base64 encoded string.
|
|
|
|
*
|
|
|
|
* @param {HTMLCanvasElement} canvas
|
|
|
|
* The canvas to encode.
|
|
|
|
*
|
|
|
|
* @return {string}
|
|
|
|
* A Base64 encoded string.
|
|
|
|
*/
|
2017-06-29 23:40:24 +00:00
|
|
|
capture.toBase64 = function(canvas) {
|
2015-10-13 15:52:26 +00:00
|
|
|
let u = canvas.toDataURL(PNG_MIME);
|
|
|
|
return u.substring(u.indexOf(",") + 1);
|
|
|
|
};
|
2016-04-18 01:37:14 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2017-06-29 23:40:24 +00:00
|
|
|
capture.toHash = function(canvas) {
|
2016-04-18 01:37:14 +00:00
|
|
|
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);
|
2017-06-29 23:40:24 +00:00
|
|
|
let padding = "00000000";
|
2016-04-18 01:37:14 +00:00
|
|
|
let paddedValue = (padding + stringValue).slice(-padding.length);
|
|
|
|
hexCodes.push(paddedValue);
|
|
|
|
}
|
|
|
|
return hexCodes.join("");
|
2017-06-29 23:40:24 +00:00
|
|
|
}
|