diff options
Diffstat (limited to 'devtools/server/actors/utils/capture-screenshot.js')
-rw-r--r-- | devtools/server/actors/utils/capture-screenshot.js | 200 |
1 files changed, 200 insertions, 0 deletions
diff --git a/devtools/server/actors/utils/capture-screenshot.js b/devtools/server/actors/utils/capture-screenshot.js new file mode 100644 index 0000000000..e7b46620b2 --- /dev/null +++ b/devtools/server/actors/utils/capture-screenshot.js @@ -0,0 +1,200 @@ +/* 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<Object{text, level}>}: 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" + ); +} |