diff options
Diffstat (limited to 'testing/marionette/capture.js')
-rw-r--r-- | testing/marionette/capture.js | 205 |
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(""); +} |