capture-screenshot.js (7800B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); 8 9 const CONTAINER_FLASHING_DURATION = 500; 10 const STRINGS_URI = "devtools/shared/locales/screenshot.properties"; 11 const L10N = new LocalizationHelper(STRINGS_URI); 12 13 // These values are used to truncate the resulting image if the captured area is bigger. 14 // This is to avoid failing to produce a screenshot at all. 15 // It is recommended to keep these values in sync with the corresponding screenshots features 16 // values in browser/components/screenshots/ScreenshotsUtils.sys.mjs. 17 // 18 // TODO(Bug 1942439): Change the consts and related truncation warning logic to align it to the new consts 19 // used by ScreenshotsUtils.sys.mjs, which does not use the same approach nor the MAX_IMAGE_WIDTH 20 // and MAX_IMAGE_HEIGHT consts that the screenshots addon was originally using. 21 const MAX_IMAGE_WIDTH = 10000; 22 const MAX_IMAGE_HEIGHT = 10000; 23 24 /** 25 * This function is called to simulate camera effects 26 * 27 * @param {BrowsingContext} browsingContext: The browsing context associated with the 28 * browser element we want to animate. 29 */ 30 function simulateCameraFlash(browsingContext) { 31 // If there's no topFrameElement (it can happen if the screenshot is taken from the 32 // browser toolbox), use the top chrome window document element. 33 const node = 34 browsingContext.topFrameElement || 35 browsingContext.topChromeWindow.document.documentElement; 36 37 if (!node) { 38 console.error( 39 "Can't find an element to play the camera flash animation on for the following browsing context:", 40 browsingContext 41 ); 42 return; 43 } 44 45 // Don't take a screenshot if the user prefers reduced motion. 46 if (node.ownerGlobal.matchMedia("(prefers-reduced-motion)").matches) { 47 return; 48 } 49 50 node.animate([{ opacity: 0 }, { opacity: 1 }], { 51 duration: CONTAINER_FLASHING_DURATION, 52 }); 53 } 54 55 /** 56 * Take a screenshot of a browser element given its browsingContext. 57 * 58 * @param {object} args 59 * @param {number} args.delay: Number of seconds to wait before taking the screenshot 60 * @param {object | null} args.rect: Object with left, top, width and height properties 61 * representing the rect **inside the browser element** that should 62 * be rendered. If null, the current viewport of the element will be rendered. 63 * @param {boolean} args.fullpage: Should the screenshot be the height of the whole page 64 * @param {string} args.filename: Expected filename for the screenshot 65 * @param {number} args.snapshotScale: Scale that will be used by `drawSnapshot` to take the screenshot. 66 * ⚠️ Note that the scale might be decreased if the resulting image would 67 * be too big to draw safely. A warning message will be returned if that's 68 * the case. 69 * @param {number} args.fileScale: Scale of the exported file. Defaults to args.snapshotScale. 70 * @param {boolean} args.disableFlash: Set to true to disable the flash animation when the 71 * screenshot is taken. 72 * @param {BrowsingContext} browsingContext 73 * @returns {object} object with the following properties: 74 * - data {String}: The dataURL representing the screenshot 75 * - height {Number}: Height of the resulting screenshot 76 * - width {Number}: Width of the resulting screenshot 77 * - filename {String}: Filename of the resulting screenshot 78 * - messages {Array<Object{text, level}>}: An array of object representing the 79 * different messages and their level that should be displayed to the user. 80 */ 81 async function captureScreenshot(args, browsingContext) { 82 const messages = []; 83 84 let filename = getFilename(args.filename); 85 86 if (args.fullpage) { 87 filename = filename.replace(".png", "-fullpage.png"); 88 } 89 90 let { left, top, width, height } = args.rect || {}; 91 let _showScreenshotTruncationWarning = false; 92 93 // Truncate the width and height if necessary. 94 if (width && (width > MAX_IMAGE_WIDTH || height > MAX_IMAGE_HEIGHT)) { 95 _showScreenshotTruncationWarning = true; 96 width = Math.min(width, MAX_IMAGE_WIDTH); 97 height = Math.min(height, MAX_IMAGE_HEIGHT); 98 } 99 100 let rect = null; 101 if (args.rect) { 102 rect = new globalThis.DOMRect( 103 Math.round(left), 104 Math.round(top), 105 Math.round(width), 106 Math.round(height) 107 ); 108 } 109 110 const document = browsingContext.topChromeWindow.document; 111 const canvas = document.createElementNS( 112 "http://www.w3.org/1999/xhtml", 113 "canvas" 114 ); 115 116 const drawToCanvas = async actualRatio => { 117 // Even after decreasing width, height and ratio, there may still be cases where the 118 // hardware fails at creating the image. Let's catch this so we can at least show an 119 // error message to the user. 120 try { 121 const snapshot = await browsingContext.currentWindowGlobal.drawSnapshot( 122 rect, 123 actualRatio, 124 "rgb(255,255,255)", 125 args.fullpage 126 ); 127 128 const fileScale = args.fileScale || actualRatio; 129 const renderingWidth = (snapshot.width / actualRatio) * fileScale; 130 const renderingHeight = (snapshot.height / actualRatio) * fileScale; 131 canvas.width = renderingWidth; 132 canvas.height = renderingHeight; 133 width = renderingWidth; 134 height = renderingHeight; 135 const ctx = canvas.getContext("2d"); 136 ctx.drawImage(snapshot, 0, 0, renderingWidth, renderingHeight); 137 138 // Bug 1574935 - Huge dimensions can trigger an OOM because multiple copies 139 // of the bitmap will exist in memory. Force the removal of the snapshot 140 // because it is no longer needed. 141 snapshot.close(); 142 143 return canvas.toDataURL("image/png", ""); 144 } catch (e) { 145 return null; 146 } 147 }; 148 149 const ratio = args.snapshotScale; 150 let data = await drawToCanvas(ratio); 151 if (!data && ratio > 1.0) { 152 // If the user provided DPR or the window.devicePixelRatio was higher than 1, 153 // try again with a reduced ratio. 154 messages.push({ 155 level: "warn", 156 text: L10N.getStr("screenshotDPRDecreasedWarning"), 157 }); 158 data = await drawToCanvas(1.0); 159 } 160 if (!data) { 161 messages.push({ 162 level: "error", 163 text: L10N.getStr("screenshotRenderingError"), 164 }); 165 } 166 167 if (data && args.disableFlash !== true) { 168 simulateCameraFlash(browsingContext); 169 } 170 171 // Bug 1953285 - Incorrect message when taking a full page screenshot that's too large 172 // Previously Passing Incorrect value of width and height 173 // Now passing updated value of width and height i.e. renderWidth, renderHeight 174 // Took boolean _showScreenshotTruncationWarning to keep track 175 if (_showScreenshotTruncationWarning) { 176 messages.push({ 177 level: "warn", 178 text: L10N.getFormatStr("screenshotTruncationWarning", width, height), 179 }); 180 } 181 182 return { 183 data, 184 height, 185 width, 186 filename, 187 messages, 188 }; 189 } 190 191 exports.captureScreenshot = captureScreenshot; 192 193 /** 194 * We may have a filename specified in args, or we might have to generate 195 * one. 196 */ 197 function getFilename(defaultName) { 198 // Create a name for the file if not present 199 if (defaultName) { 200 return defaultName; 201 } 202 203 const date = new Date(); 204 const monthString = (date.getMonth() + 1).toString().padStart(2, "0"); 205 const dayString = date.getDate().toString().padStart(2, "0"); 206 const dateString = `${date.getFullYear()}-${monthString}-${dayString}`; 207 208 const timeString = date.toTimeString().replace(/:/g, ".").split(" ")[0]; 209 210 return ( 211 L10N.getFormatStr("screenshotGeneratedFilename", dateString, timeString) + 212 ".png" 213 ); 214 }