/* 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/. */ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", }); XPCOMUtils.defineLazyPreferenceGetter( lazy, "canvasMaxArea", "gfx.canvas.max-area" ); XPCOMUtils.defineLazyPreferenceGetter( lazy, "canvasMaxSize", "gfx.canvas.max-size" ); XPCOMUtils.defineLazyPreferenceGetter( lazy, "compositorReadback", "remote.screenshot.use_readback", false ); const CONTEXT_2D = "2d"; const BG_COLOUR = "rgb(255,255,255)"; const XHTML_NS = "http://www.w3.org/1999/xhtml"; /** * Provides primitives to capture screenshots. * * @namespace */ export const 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 {BrowsingContext} browsingContext * The BrowsingContext from which the snapshot should be taken. * @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 {object=} options * @param {HTMLCanvasElement=} options.canvas * Optional canvas to reuse for the screenshot. * @param {number=} options.flags * Optional integer representing flags to pass to drawWindow; these * are defined on CanvasRenderingContext2D. * @param {number=} options.dX * Horizontal offset between the browser window and content area. Defaults to 0. * @param {number=} options.dY * Vertical offset between the browser window and content area. Defaults to 0. * @param {boolean=} options.readback * If true, read back a snapshot of the pixel data currently in the * compositor/window. Defaults to false, unless the * `remote.screenshot.use_readback` preference is set. * @param {boolean=} options.drawView * If true, the rectangle is relative to the visible viewport rather than to * the page, and view level rendering such as the root scrollbars is * included. Ignored when reading back. Defaults to false. * * @returns {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, drawView = false, } = {} ) { if (lazy.compositorReadback && !readback) { // Readback can only return the content area composited on screen, and its // coordinates are relative to the chrome window rather than the document, // so any requested region degrades to the whole content area. const browser = browsingContext.top.embedderElement; if (browser) { readback = true; ({ left, top, width, height } = browser.getBoundingClientRect()); } } // FIXME(bug 1761032): This looks a bit sketchy, overrideDPPX doesn't // influence rendering... const scale = browsingContext.overrideDPPX || win.devicePixelRatio; const canvasHeight = height * scale; const canvasWidth = width * scale; const canvasArea = canvasWidth * canvasHeight; if (canvasWidth > lazy.canvasMaxSize) { throw new lazy.error.UnsupportedOperationError( `${canvasWidth} exceeds the maximum allowed screenshot width of ${lazy.canvasMaxSize} pixels` ); } if (canvasHeight > lazy.canvasMaxSize) { throw new lazy.error.UnsupportedOperationError( `${canvasHeight} exceeds the maximum allowed screenshot height of ${lazy.canvasMaxSize} pixels` ); } if (canvasArea > lazy.canvasMaxArea) { throw new lazy.error.UnsupportedOperationError( `${canvasArea} exceeds the maximum allowed screenshot area of ${lazy.canvasMaxArea} square pixels` ); } try { 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, { drawView } ); 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(); } } catch (error) { throw new lazy.error.UnknownError( `Unable to capture screenshot: ${error.message}` ); } return canvas; }; /** * Encode the contents of an HTMLCanvasElement to a Base64 encoded string. * * @param {HTMLCanvasElement} canvas * The canvas to encode. * @param {string} type * The image format to output such as `image/png`. * @param {number} encoderOptions * A number between 0 and 1 representing the image quality. Defaults to `1.0`. * * @returns {string} * A Base64 encoded string. */ capture.toBase64 = function (canvas, type, encoderOptions = 1.0) { let u = canvas.toDataURL(type, encoderOptions); 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. * * @returns {string} * A hex digest of the SHA-256 hash of the base64 encoded string. */ capture.toHash = function (canvas) { let u = capture.toBase64(canvas, "image/png"); let buffer = new TextEncoder().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. * * @returns {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(""); }