HeadlessShell.sys.mjs (7663B)
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 file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { E10SUtils } from "resource://gre/modules/E10SUtils.sys.mjs"; 6 import { HiddenFrame } from "resource://gre/modules/HiddenFrame.sys.mjs"; 7 8 // Refrences to the progress listeners to keep them from being gc'ed 9 // before they are called. 10 const progressListeners = new Set(); 11 12 export class ScreenshotParent extends JSWindowActorParent { 13 getDimensions(params) { 14 return this.sendQuery("GetDimensions", params); 15 } 16 } 17 18 ChromeUtils.registerWindowActor("Screenshot", { 19 parent: { 20 esModuleURI: "moz-src:///browser/components/shell/HeadlessShell.sys.mjs", 21 }, 22 child: { 23 esModuleURI: "moz-src:///browser/components/shell/ScreenshotChild.sys.mjs", 24 }, 25 }); 26 27 function loadContentWindow(browser, url) { 28 let uri = URL.parse(url)?.URI; 29 if (!uri) { 30 let err = new Error(`Invalid URL passed to loadContentWindow(): ${url}`); 31 console.error(err); 32 return Promise.reject(err); 33 } 34 35 const principal = Services.scriptSecurityManager.getSystemPrincipal(); 36 return new Promise(resolve => { 37 let oa = E10SUtils.predictOriginAttributes({ 38 browser, 39 }); 40 let loadURIOptions = { 41 triggeringPrincipal: principal, 42 remoteType: E10SUtils.getRemoteTypeForURI( 43 url, 44 true, 45 false, 46 E10SUtils.DEFAULT_REMOTE_TYPE, 47 null, 48 oa 49 ), 50 }; 51 browser.loadURI(uri, loadURIOptions); 52 let { webProgress } = browser; 53 54 let progressListener = { 55 onLocationChange(progress, request, location, flags) { 56 // Ignore inner-frame events 57 if (!progress.isTopLevel) { 58 return; 59 } 60 // Ignore events that don't change the document 61 if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) { 62 return; 63 } 64 // Ignore the initial about:blank, unless about:blank is requested 65 if (location.spec == "about:blank" && uri.spec != "about:blank") { 66 return; 67 } 68 69 progressListeners.delete(progressListener); 70 webProgress.removeProgressListener(progressListener); 71 resolve(); 72 }, 73 QueryInterface: ChromeUtils.generateQI([ 74 "nsIWebProgressListener", 75 "nsISupportsWeakReference", 76 ]), 77 }; 78 progressListeners.add(progressListener); 79 webProgress.addProgressListener( 80 progressListener, 81 Ci.nsIWebProgress.NOTIFY_LOCATION 82 ); 83 }); 84 } 85 86 async function takeScreenshot( 87 fullWidth, 88 fullHeight, 89 contentWidth, 90 contentHeight, 91 path, 92 url 93 ) { 94 let frame; 95 try { 96 frame = new HiddenFrame(); 97 let windowlessBrowser = await frame.get(); 98 99 let doc = windowlessBrowser.document; 100 let browser = doc.createXULElement("browser"); 101 browser.setAttribute("remote", "true"); 102 browser.setAttribute("type", "content"); 103 browser.style.width = `${contentWidth}px`; 104 browser.style.minWidth = `${contentWidth}px`; 105 browser.style.height = `${contentHeight}px`; 106 browser.style.minHeight = `${contentHeight}px`; 107 browser.setAttribute("maychangeremoteness", "true"); 108 doc.documentElement.appendChild(browser); 109 110 await loadContentWindow(browser, url); 111 112 let actor = 113 browser.browsingContext.currentWindowGlobal.getActor("Screenshot"); 114 let dimensions = await actor.getDimensions(); 115 116 let canvas = doc.createElementNS( 117 "http://www.w3.org/1999/xhtml", 118 "html:canvas" 119 ); 120 let context = canvas.getContext("2d"); 121 let width = dimensions.innerWidth; 122 let height = dimensions.innerHeight; 123 if (fullWidth) { 124 width += dimensions.scrollMaxX - dimensions.scrollMinX; 125 } 126 if (fullHeight) { 127 height += dimensions.scrollMaxY - dimensions.scrollMinY; 128 } 129 canvas.width = width; 130 canvas.height = height; 131 let rect = new DOMRect(0, 0, width, height); 132 133 let snapshot = 134 await browser.browsingContext.currentWindowGlobal.drawSnapshot( 135 rect, 136 1, 137 "rgb(255, 255, 255)" 138 ); 139 context.drawImage(snapshot, 0, 0); 140 141 snapshot.close(); 142 143 let blob = await new Promise(resolve => canvas.toBlob(resolve)); 144 145 let reader = await new Promise(resolve => { 146 let fr = new FileReader(); 147 fr.onloadend = () => resolve(fr); 148 fr.readAsArrayBuffer(blob); 149 }); 150 151 await IOUtils.write(path, new Uint8Array(reader.result)); 152 dump("Screenshot saved to: " + path + "\n"); 153 } catch (e) { 154 dump("Failure taking screenshot: " + e + "\n"); 155 } finally { 156 if (frame) { 157 frame.destroy(); 158 } 159 } 160 } 161 162 export let HeadlessShell = { 163 async handleCmdLineArgs(cmdLine, URLlist) { 164 try { 165 // Don't quit even though we don't create a window 166 Services.startup.enterLastWindowClosingSurvivalArea(); 167 168 // Default options 169 let fullWidth = true; 170 let fullHeight = true; 171 // Most common screen resolution of Firefox users 172 let contentWidth = 1366; 173 let contentHeight = 768; 174 175 // Parse `window-size` 176 try { 177 var dimensionsStr = cmdLine.handleFlagWithParam("window-size", true); 178 } catch (e) { 179 dump("expected format: --window-size width[,height]\n"); 180 return; 181 } 182 if (dimensionsStr) { 183 let success; 184 let dimensions = dimensionsStr.split(",", 2); 185 if (dimensions.length == 1) { 186 success = dimensions[0] > 0; 187 if (success) { 188 fullWidth = false; 189 fullHeight = true; 190 contentWidth = dimensions[0]; 191 } 192 } else { 193 success = dimensions[0] > 0 && dimensions[1] > 0; 194 if (success) { 195 fullWidth = false; 196 fullHeight = false; 197 contentWidth = dimensions[0]; 198 contentHeight = dimensions[1]; 199 } 200 } 201 202 if (!success) { 203 dump("expected format: --window-size width[,height]\n"); 204 return; 205 } 206 } 207 208 let urlOrFileToSave = null; 209 try { 210 urlOrFileToSave = cmdLine.handleFlagWithParam("screenshot", true); 211 } catch (e) { 212 // We know that the flag exists so we only get here if there was no parameter. 213 cmdLine.handleFlag("screenshot", true); // Remove `screenshot` 214 } 215 216 // Assume that the remaining arguments that do not start 217 // with a hyphen are URLs 218 for (let i = 0; i < cmdLine.length; ++i) { 219 const argument = cmdLine.getArgument(i); 220 if (argument.startsWith("-")) { 221 dump(`Warning: unrecognized command line flag ${argument}\n`); 222 // To emulate the pre-nsICommandLine behavior, we ignore 223 // the argument after an unrecognized flag. 224 ++i; 225 } else { 226 URLlist.push(argument); 227 } 228 } 229 230 let path = null; 231 if (urlOrFileToSave && !URLlist.length) { 232 // URL was specified next to "-screenshot" 233 // Example: -screenshot https://www.example.com -attach-console 234 URLlist.push(urlOrFileToSave); 235 } else { 236 path = urlOrFileToSave; 237 } 238 239 if (!path) { 240 path = PathUtils.join(cmdLine.workingDirectory.path, "screenshot.png"); 241 } 242 243 if (URLlist.length == 1) { 244 await takeScreenshot( 245 fullWidth, 246 fullHeight, 247 contentWidth, 248 contentHeight, 249 path, 250 URLlist[0] 251 ); 252 } else { 253 dump("expected exactly one URL when using `screenshot`\n"); 254 } 255 } finally { 256 Services.startup.exitLastWindowClosingSurvivalArea(); 257 Services.startup.quit(Ci.nsIAppStartup.eForceQuit); 258 } 259 }, 260 };