/* 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 { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); const CONTAINER_FLASHING_DURATION = 500; const STRINGS_URI = "devtools/shared/locales/screenshot.properties"; const L10N = new LocalizationHelper(STRINGS_URI); // These values are used to truncate the resulting image if the captured area is bigger. // This is to avoid failing to produce a screenshot at all. // It is recommended to keep these values in sync with the corresponding screenshots addon // values in /browser/extensions/screenshots/selector/uicontrol.js const MAX_IMAGE_WIDTH = 10000; const MAX_IMAGE_HEIGHT = 10000; /** * This function is called to simulate camera effects * @param {BrowsingContext} browsingContext: The browsing context associated with the * browser element we want to animate. */ function simulateCameraFlash(browsingContext) { // If there's no topFrameElement (it can happen if the screenshot is taken from the // browser toolbox), use the top chrome window document element. const node = browsingContext.topFrameElement || browsingContext.topChromeWindow.document.documentElement; if (!node) { console.error( "Can't find an element to play the camera flash animation on for the following browsing context:", browsingContext ); return; } // Don't take a screenshot if the user prefers reduced motion. if (node.ownerGlobal.matchMedia("(prefers-reduced-motion)").matches) { return; } node.animate([{ opacity: 0 }, { opacity: 1 }], { duration: CONTAINER_FLASHING_DURATION, }); } /** * Take a screenshot of a browser element given its browsingContext. * * @param {Object} args * @param {Number} args.delay: Number of seconds to wait before taking the screenshot * @param {Object|null} args.rect: Object with left, top, width and height properties * representing the rect **inside the browser element** that should * be rendered. If null, the current viewport of the element will be rendered. * @param {Boolean} args.fullpage: Should the screenshot be the height of the whole page * @param {String} args.filename: Expected filename for the screenshot * @param {Number} args.snapshotScale: Scale that will be used by `drawSnapshot` to take the screenshot. * ⚠️ Note that the scale might be decreased if the resulting image would * be too big to draw safely. A warning message will be returned if that's * the case. * @param {Number} args.fileScale: Scale of the exported file. Defaults to args.snapshotScale. * @param {Boolean} args.disableFlash: Set to true to disable the flash animation when the * screenshot is taken. * @param {BrowsingContext} browsingContext * @returns {Object} object with the following properties: * - data {String}: The dataURL representing the screenshot * - height {Number}: Height of the resulting screenshot * - width {Number}: Width of the resulting screenshot * - filename {String}: Filename of the resulting screenshot * - messages {Array}: An array of object representing the * different messages and their level that should be displayed to the user. */ async function captureScreenshot(args, browsingContext) { const messages = []; let filename = getFilename(args.filename); if (args.fullpage) { filename = filename.replace(".png", "-fullpage.png"); } let { left, top, width, height } = args.rect || {}; // Truncate the width and height if necessary. if (width && (width > MAX_IMAGE_WIDTH || height > MAX_IMAGE_HEIGHT)) { width = Math.min(width, MAX_IMAGE_WIDTH); height = Math.min(height, MAX_IMAGE_HEIGHT); messages.push({ level: "warn", text: L10N.getFormatStr("screenshotTruncationWarning", width, height), }); } let rect = null; if (args.rect) { rect = new globalThis.DOMRect( Math.round(left), Math.round(top), Math.round(width), Math.round(height) ); } const document = browsingContext.topChromeWindow.document; const canvas = document.createElementNS( "http://www.w3.org/1999/xhtml", "canvas" ); const drawToCanvas = async actualRatio => { // Even after decreasing width, height and ratio, there may still be cases where the // hardware fails at creating the image. Let's catch this so we can at least show an // error message to the user. try { const snapshot = await browsingContext.currentWindowGlobal.drawSnapshot( rect, actualRatio, "rgb(255,255,255)", args.fullpage ); const fileScale = args.fileScale || actualRatio; const renderingWidth = (snapshot.width / actualRatio) * fileScale; const renderingHeight = (snapshot.height / actualRatio) * fileScale; canvas.width = renderingWidth; canvas.height = renderingHeight; width = renderingWidth; height = renderingHeight; const ctx = canvas.getContext("2d"); ctx.drawImage(snapshot, 0, 0, renderingWidth, renderingHeight); // 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.toDataURL("image/png", ""); } catch (e) { return null; } }; const ratio = args.snapshotScale; let data = await drawToCanvas(ratio); if (!data && ratio > 1.0) { // If the user provided DPR or the window.devicePixelRatio was higher than 1, // try again with a reduced ratio. messages.push({ level: "warn", text: L10N.getStr("screenshotDPRDecreasedWarning"), }); data = await drawToCanvas(1.0); } if (!data) { messages.push({ level: "error", text: L10N.getStr("screenshotRenderingError"), }); } if (data && args.disableFlash !== true) { simulateCameraFlash(browsingContext); } return { data, height, width, filename, messages, }; } exports.captureScreenshot = captureScreenshot; /** * We may have a filename specified in args, or we might have to generate * one. */ function getFilename(defaultName) { // Create a name for the file if not present if (defaultName) { return defaultName; } const date = new Date(); const monthString = (date.getMonth() + 1).toString().padStart(2, "0"); const dayString = date.getDate().toString().padStart(2, "0"); const dateString = `${date.getFullYear()}-${monthString}-${dayString}`; const timeString = date.toTimeString().replace(/:/g, ".").split(" ")[0]; return ( L10N.getFormatStr("screenshotGeneratedFilename", dateString, timeString) + ".png" ); }