Bug 1464461 - implement screenshot command in console panel; r=nchevobbe,ochameau
authoryulia <ystartsev@mozilla.com>
Mon, 04 Jun 2018 17:46:48 +0200
changeset 424827 1269a7d4b143
parent 424826 7d0fb7a6b9fa
child 424828 f1f577e0d6f4
push id65865
push userystartsev@mozilla.com
push dateTue, 03 Jul 2018 13:51:00 +0000
treeherderautoland@dc54472ca3bf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnchevobbe, ochameau
bugs1464461
milestone63.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1464461 - implement screenshot command in console panel; r=nchevobbe,ochameau MozReview-Commit-ID: 8MDZglPqTz9
devtools/client/webconsole/components/JSTerm.js
devtools/server/actors/webconsole.js
devtools/server/actors/webconsole/moz.build
devtools/server/actors/webconsole/screenshot.js
devtools/server/actors/webconsole/utils.js
devtools/shared/locales/en-US/screenshot.properties
devtools/shared/webconsole/moz.build
devtools/shared/webconsole/screenshot-helper.js
--- a/devtools/client/webconsole/components/JSTerm.js
+++ b/devtools/client/webconsole/components/JSTerm.js
@@ -14,16 +14,17 @@ loader.lazyRequireGetter(this, "defer", 
 loader.lazyRequireGetter(this, "Debugger", "Debugger");
 loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
 loader.lazyRequireGetter(this, "AutocompletePopup", "devtools/client/shared/autocomplete-popup");
 loader.lazyRequireGetter(this, "PropTypes", "devtools/client/shared/vendor/react-prop-types");
 loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
 loader.lazyRequireGetter(this, "KeyCodes", "devtools/client/shared/keycodes", true);
 loader.lazyRequireGetter(this, "Editor", "devtools/client/sourceeditor/editor");
 loader.lazyRequireGetter(this, "Telemetry", "devtools/client/shared/telemetry");
+loader.lazyRequireGetter(this, "processScreenshot", "devtools/shared/webconsole/screenshot-helper");
 
 const l10n = require("devtools/client/webconsole/webconsole-l10n");
 
 const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers";
 
 function gSequenceId() {
   return gSequenceId.n++;
 }
@@ -326,17 +327,17 @@ class JSTerm extends Component {
    *
    * @private
    * @param function [callback]
    *        Optional function to invoke when the evaluation result is added to
    *        the output.
    * @param object response
    *        The message received from the server.
    */
-  _executeResultCallback(callback, response) {
+  async _executeResultCallback(callback, response) {
     if (!this.hud) {
       return;
     }
     if (response.error) {
       console.error("Evaluation error " + response.error + ": " + response.message);
       return;
     }
     let errorMessage = response.exceptionMessage;
@@ -368,16 +369,22 @@ class JSTerm extends Component {
           }
           break;
         case "help":
           this.hud.owner.openLink(HELP_URL);
           break;
         case "copyValueToClipboard":
           clipboardHelper.copyString(helperResult.value);
           break;
+        case "screenshotOutput":
+          const { args, value } = helperResult;
+          const results = await processScreenshot(this.hud.window, args, value);
+          this.screenshotNotify(results);
+          // early return as screenshot notify has dispatched all necessary messages
+          return;
       }
     }
 
     // Hide undefined results coming from JSTerm helper functions.
     if (!errorMessage && result && typeof result == "object" &&
       result.type == "undefined" &&
       helperResult && !helperHasRawOutput) {
       callback && callback();
@@ -394,16 +401,21 @@ class JSTerm extends Component {
       helperResult: {
         type: "inspectObject",
         object: objectActor
       }
     }, true);
     return this.hud.consoleOutput;
   }
 
+  screenshotNotify(results) {
+    const wrappedResults = results.map(result => ({ result }));
+    this.hud.consoleOutput.dispatchMessagesAdd(wrappedResults);
+  }
+
   /**
    * Execute a string. Execution happens asynchronously in the content process.
    *
    * @param string [executeString]
    *        The string you want to execute. If this is not provided, the current
    *        user input is used - taken from |this.getInputValue()|.
    * @param function [callback]
    *        Optional function to invoke when the result is displayed.
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -870,16 +870,17 @@ WebConsoleActor.prototype =
     };
   },
 
   /**
    * Handler for the "evaluateJSAsync" request. This method evaluates the given
    * JavaScript string and sends back a packet with a unique ID.
    * The result will be returned later as an unsolicited `evaluationResult`,
    * that can be associated back to this request via the `resultID` field.
+   * Cannot be async, see Comment two on Bug #1452920
    *
    * @param object request
    *        The JSON request object received from the Web Console client.
    * @return object
    *         The response packet to send to with the unique id in the
    *         `resultID` field.
    */
   evaluateJSAsync: function(request) {
@@ -892,16 +893,49 @@ WebConsoleActor.prototype =
       from: this.actorID,
       resultID: resultID
     });
 
     // Then, execute the script that may pause.
     const response = this.evaluateJS(request);
     response.resultID = resultID;
 
+    this._waitForHelperResultAndSend(response).catch(e =>
+      DevToolsUtils.reportException(
+        "evaluateJSAsync",
+        Error(`Encountered error while waiting for Helper Result: ${e}`)
+      )
+    );
+  },
+
+  /**
+   * In order to have asynchronous commands such as screenshot, we have to be
+   * able to handle promises in the helper result. This method handles waiting
+   * for the promise, and then dispatching the result
+   *
+   *
+   * @private
+   * @param object response
+   *         The response packet to send to with the unique id in the
+   *         `resultID` field, and potentially a promise in the helperResult
+   *         field.
+   *
+   * @return object
+   *         The response packet to send to with the unique id in the
+   *         `resultID` field, with a sanitized helperResult field.
+   */
+  _waitForHelperResultAndSend: async function(response) {
+    // Wait for asynchronous command completion before sending back the response
+    if (
+      response.helperResult &&
+      typeof response.helperResult.then == "function"
+    ) {
+      response.helperResult = await response.helperResult;
+    }
+
     // Finally, send an unsolicited evaluationResult packet with
     // the normal return value
     this.conn.sendActorEvent(this.actorID, "evaluationResult", response);
   },
 
   /**
    * Handler for the "evaluateJS" request. This method evaluates the given
    * JavaScript string and sends back the result.
--- a/devtools/server/actors/webconsole/moz.build
+++ b/devtools/server/actors/webconsole/moz.build
@@ -2,11 +2,12 @@
 # vim: set filetype=python:
 # 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/.
 
 DevToolsModules(
     'content-process-forward.js',
     'listeners.js',
+    'screenshot.js',
     'utils.js',
     'worker-listeners.js',
 )
copy from devtools/shared/gcli/commands/screenshot.js
copy to devtools/server/actors/webconsole/screenshot.js
--- a/devtools/shared/gcli/commands/screenshot.js
+++ b/devtools/server/actors/webconsole/screenshot.js
@@ -1,240 +1,37 @@
 /* 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 { Cc, Ci, Cr, Cu } = require("chrome");
-const ChromeUtils = require("ChromeUtils");
-const l10n = require("gcli/l10n");
-const Services = require("Services");
-const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");
+const { Ci, Cu } = require("chrome");
 const { getRect } = require("devtools/shared/layout/utils");
-const defer = require("devtools/shared/defer");
-const { Task } = require("devtools/shared/task");
-
-loader.lazyRequireGetter(this, "openContentLink", "devtools/client/shared/link", true);
-
-loader.lazyImporter(this, "Downloads", "resource://gre/modules/Downloads.jsm");
-loader.lazyImporter(this, "OS", "resource://gre/modules/osfile.jsm");
-loader.lazyImporter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm");
-loader.lazyImporter(this, "PrivateBrowsingUtils",
-                          "resource://gre/modules/PrivateBrowsingUtils.jsm");
-
-// String used as an indication to generate default file name in the following
-// format: "Screen Shot yyyy-mm-dd at HH.MM.SS.png"
-const FILENAME_DEFAULT_VALUE = " ";
-const CONTAINER_FLASHING_DURATION = 500;
-
-/*
- * There are 2 commands and 1 converter here. The 2 commands are nearly
- * identical except that one runs on the client and one in the server.
- *
- * The server command is hidden, and is designed to be called from the client
- * command.
- */
-
-/**
- * Both commands have the same initial filename parameter
- */
-const filenameParam = {
-  name: "filename",
-  type: "string",
-  defaultValue: FILENAME_DEFAULT_VALUE,
-  description: l10n.lookup("screenshotFilenameDesc"),
-  manual: l10n.lookup("screenshotFilenameManual")
-};
+const { LocalizationHelper } = require("devtools/shared/L10N");
 
-/**
- * Both commands have almost the same set of standard optional parameters, except for the
- * type of the --selector option, which can be a node only on the server.
- */
-const getScreenshotCommandParams = function(isClient) {
-  return {
-    group: l10n.lookup("screenshotGroupOptions"),
-    params: [
-      {
-        name: "clipboard",
-        type: "boolean",
-        description: l10n.lookup("screenshotClipboardDesc"),
-        manual: l10n.lookup("screenshotClipboardManual")
-      },
-      {
-        name: "imgur",
-        type: "boolean",
-        description: l10n.lookup("screenshotImgurDesc"),
-        manual: l10n.lookup("screenshotImgurManual")
-      },
-      {
-        name: "delay",
-        type: { name: "number", min: 0 },
-        defaultValue: 0,
-        description: l10n.lookup("screenshotDelayDesc"),
-        manual: l10n.lookup("screenshotDelayManual")
-      },
-      {
-        name: "dpr",
-        type: { name: "number", min: 0, allowFloat: true },
-        defaultValue: 0,
-        description: l10n.lookup("screenshotDPRDesc"),
-        manual: l10n.lookup("screenshotDPRManual")
-      },
-      {
-        name: "fullpage",
-        type: "boolean",
-        description: l10n.lookup("screenshotFullPageDesc"),
-        manual: l10n.lookup("screenshotFullPageManual")
-      },
-      {
-        name: "selector",
-        // On the client side, don't try to parse the selector as a node as it will
-        // trigger an unsafe CPOW.
-        type: isClient ? "string" : "node",
-        defaultValue: null,
-        description: l10n.lookup("inspectNodeDesc"),
-        manual: l10n.lookup("inspectNodeManual")
-      },
-      {
-        name: "file",
-        type: "boolean",
-        description: l10n.lookup("screenshotFileDesc"),
-        manual: l10n.lookup("screenshotFileManual"),
-      },
-    ]
-  };
-};
-
-const clientScreenshotParams = getScreenshotCommandParams(true);
-const serverScreenshotParams = getScreenshotCommandParams(false);
+const CONTAINER_FLASHING_DURATION = 500;
+const STRINGS_URI = "devtools/shared/locales/screenshot.properties";
+const L10N = new LocalizationHelper(STRINGS_URI);
 
-exports.items = [
-  {
-    /**
-     * Format an 'imageSummary' (as output by the screenshot command).
-     * An 'imageSummary' is a simple JSON object that looks like this:
-     *
-     * {
-     *   destinations: [ "..." ], // Required array of descriptions of the
-     *                            // locations of the result image (the command
-     *                            // can have multiple outputs)
-     *   data: "...",             // Optional Base64 encoded image data
-     *   width:1024, height:768,  // Dimensions of the image data, required
-     *                            // if data != null
-     *   filename: "...",         // If set, clicking the image will open the
-     *                            // folder containing the given file
-     *   href: "...",             // If set, clicking the image will open the
-     *                            // link in a new tab
-     * }
-     */
-    item: "converter",
-    from: "imageSummary",
-    to: "dom",
-    exec: function(imageSummary, context) {
-      const document = context.document;
-      const root = document.createElement("div");
-
-      // Add a line to the result for each destination
-      imageSummary.destinations.forEach(destination => {
-        const title = document.createElement("div");
-        title.textContent = destination;
-        root.appendChild(title);
-      });
-
-      // Add the thumbnail image
-      if (imageSummary.data != null) {
-        const image = context.document.createElement("div");
-        const previewHeight = parseInt(256 * imageSummary.height / imageSummary.width,
-                                       10);
-        const style = "" +
-            "width: 256px;" +
-            "height: " + previewHeight + "px;" +
-            "max-height: 256px;" +
-            "background-image: url('" + imageSummary.data + "');" +
-            "background-size: 256px " + previewHeight + "px;" +
-            "margin: 4px;" +
-            "display: block;";
-        image.setAttribute("style", style);
-        root.appendChild(image);
-      }
-
-      // Click handler
-      if (imageSummary.href || imageSummary.filename) {
-        root.style.cursor = "pointer";
-        root.addEventListener("click", () => {
-          if (imageSummary.href) {
-            openContentLink(imageSummary.href);
-          } else if (imageSummary.filename) {
-            const file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
-            file.initWithPath(imageSummary.filename);
-            file.reveal();
-          }
-        });
-      }
-
-      return root;
-    }
-  },
-  {
-    item: "command",
-    runAt: "client",
-    name: "screenshot",
-    description: l10n.lookup("screenshotDesc"),
-    manual: l10n.lookup("screenshotManual"),
-    returnType: "imageSummary",
-    buttonId: "command-button-screenshot",
-    buttonClass: "command-button",
-    tooltipText: l10n.lookup("screenshotTooltipPage"),
-    params: [
-      filenameParam,
-      clientScreenshotParams,
-    ],
-    exec: function(args, context) {
-      // Re-execute the command on the server
-      const command = context.typed.replace(/^screenshot/, "screenshot_server");
-      const capture = context.updateExec(command).then(output => {
-        return output.error ? Promise.reject(output.data) : output.data;
-      });
-
-      simulateCameraEffect(context.environment.chromeDocument, "shutter");
-      return capture.then(saveScreenshot.bind(null, args, context));
-    },
-  },
-  {
-    item: "command",
-    runAt: "server",
-    name: "screenshot_server",
-    hidden: true,
-    returnType: "imageSummary",
-    params: [
-      filenameParam,
-      serverScreenshotParams,
-    ],
-    exec: function(args, context) {
-      return captureScreenshot(args, context.environment.document);
-    },
+exports.screenshot = function takeAsyncScreenshot(owner, args = {}) {
+  if (args.help) {
+    // Early return as help will be handled on the client side.
+    return null;
   }
-];
+  return captureScreenshot(args, owner.window.document);
+};
 
 /**
  * This function is called to simulate camera effects
  */
-function simulateCameraEffect(document, effect) {
+function simulateCameraFlash(document) {
   const window = document.defaultView;
-  if (effect === "shutter") {
-    if (Services.prefs.getBoolPref("devtools.screenshot.audio.enabled")) {
-      const audioCamera = new window.Audio("resource://devtools/client/themes/audio/shutter.wav");
-      audioCamera.play();
-    }
-  }
-  if (effect == "flash") {
-    const frames = Cu.cloneInto({ opacity: [ 0, 1 ] }, window);
-    document.documentElement.animate(frames, CONTAINER_FLASHING_DURATION);
-  }
+  const frames = Cu.cloneInto({ opacity: [ 0, 1 ] }, window);
+  document.documentElement.animate(frames, CONTAINER_FLASHING_DURATION);
 }
 
 /**
  * This function simply handles the --delay argument before calling
  * createScreenshotData
  */
 function captureScreenshot(args, document) {
   if (args.delay > 0) {
@@ -243,36 +40,16 @@ function captureScreenshot(args, documen
         createScreenshotData(document, args).then(resolve, reject);
       }, args.delay * 1000);
     });
   }
   return createScreenshotData(document, args);
 }
 
 /**
- * There are several possible destinations for the screenshot, SKIP is used
- * in saveScreenshot() whenever one of them is not used
- */
-const SKIP = Promise.resolve();
-
-/**
- * Save the captured screenshot to one of several destinations.
- */
-function saveScreenshot(args, context, reply) {
-  const fileNeeded = args.filename != FILENAME_DEFAULT_VALUE ||
-    (!args.imgur && !args.clipboard) || args.file;
-
-  return Promise.all([
-    args.clipboard ? saveToClipboard(context, reply) : SKIP,
-    args.imgur ? uploadToImgur(reply) : SKIP,
-    fileNeeded ? saveToFile(context, reply) : SKIP,
-  ]).then(() => reply);
-}
-
-/**
  * This does the dirty work of creating a base64 string out of an
  * area of the browser window
  */
 function createScreenshotData(document, args) {
   const window = document.defaultView;
   let left = 0;
   let top = 0;
   let width;
@@ -285,17 +62,18 @@ function createScreenshotData(document, 
   if (args.fullpage) {
     // Bug 961832: GCLI screenshot shows fixed position element in wrong
     // position if we don't scroll to top
     window.scrollTo(0, 0);
     width = window.innerWidth + window.scrollMaxX - window.scrollMinX;
     height = window.innerHeight + window.scrollMaxY - window.scrollMinY;
     filename = filename.replace(".png", "-fullpage.png");
   } else if (args.selector) {
-    ({ top, left, width, height } = getRect(window, args.selector, window));
+    const node = window.document.querySelector(args.selector);
+    ({ top, left, width, height } = getRect(window, node, window));
   } else {
     left = window.scrollX;
     top = window.scrollY;
     width = window.innerWidth;
     height = window.innerHeight;
   }
 
   // Only adjust for scrollbars when considering the full window
@@ -318,278 +96,46 @@ function createScreenshotData(document, 
   ctx.drawWindow(window, left, top, width, height, "#fff");
   const data = canvas.toDataURL("image/png", "");
 
   // See comment above on bug 961832
   if (args.fullpage) {
     window.scrollTo(currentX, currentY);
   }
 
-  simulateCameraEffect(document, "flash");
+  simulateCameraFlash(document);
 
   return Promise.resolve({
     destinations: [],
     data: data,
     height: height,
     width: width,
     filename: filename,
   });
 }
 
 /**
  * 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 != FILENAME_DEFAULT_VALUE) {
+  if (defaultName) {
     return defaultName;
   }
 
   const date = new Date();
   let dateString = date.getFullYear() + "-" + (date.getMonth() + 1) +
                   "-" + date.getDate();
   dateString = dateString.split("-").map(function(part) {
     if (part.length == 1) {
       part = "0" + part;
     }
     return part;
   }).join("-");
 
   const timeString = date.toTimeString().replace(/:/g, ".").split(" ")[0];
-  return l10n.lookupFormat("screenshotGeneratedFilename",
-                           [ dateString, timeString ]) + ".png";
-}
-
-/**
- * Save the image data to the clipboard. This returns a promise, so it can
- * be treated exactly like imgur / file processing, but it's really sync
- * for now.
- */
-function saveToClipboard(context, reply) {
-  return new Promise(resolve => {
-    try {
-      const channel = NetUtil.newChannel({
-        uri: reply.data,
-        loadUsingSystemPrincipal: true,
-        contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE
-      });
-      const input = channel.open2();
-
-      const loadContext = context.environment.chromeWindow
-                                 .QueryInterface(Ci.nsIInterfaceRequestor)
-                                 .getInterface(Ci.nsIWebNavigation)
-                                 .QueryInterface(Ci.nsILoadContext);
-
-      const callback = {
-        onImageReady(container, status) {
-          if (!container) {
-            console.error("imgTools.decodeImageAsync failed");
-            reply.destinations.push(l10n.lookup("screenshotErrorCopying"));
-            resolve();
-            return;
-          }
-
-          try {
-            const wrapped = Cc["@mozilla.org/supports-interface-pointer;1"]
-                              .createInstance(Ci.nsISupportsInterfacePointer);
-            wrapped.data = container;
-
-            const trans = Cc["@mozilla.org/widget/transferable;1"]
-                            .createInstance(Ci.nsITransferable);
-            trans.init(loadContext);
-            trans.addDataFlavor(channel.contentType);
-            trans.setTransferData(channel.contentType, wrapped, -1);
-
-            Services.clipboard.setData(trans, null, Ci.nsIClipboard.kGlobalClipboard);
-
-            reply.destinations.push(l10n.lookup("screenshotCopied"));
-          } catch (ex) {
-            console.error(ex);
-            reply.destinations.push(l10n.lookup("screenshotErrorCopying"));
-          }
-          resolve();
-        }
-      };
-
-      const threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
-      const imgTools = Cc["@mozilla.org/image/tools;1"]
-                          .getService(Ci.imgITools);
-      imgTools.decodeImageAsync(input, channel.contentType, callback,
-                                threadManager.currentThread);
-    } catch (ex) {
-      console.error(ex);
-      reply.destinations.push(l10n.lookup("screenshotErrorCopying"));
-      resolve();
-    }
-  });
-}
-
-/**
- * Upload screenshot data to Imgur, returning a promise of a URL (as a string)
- */
-function uploadToImgur(reply) {
-  return new Promise((resolve, reject) => {
-    const xhr = new XMLHttpRequest();
-    const fd = new FormData();
-    fd.append("image", reply.data.split(",")[1]);
-    fd.append("type", "base64");
-    fd.append("title", reply.filename);
-
-    const postURL = Services.prefs.getCharPref("devtools.gcli.imgurUploadURL");
-    const clientID = "Client-ID " +
-                     Services.prefs.getCharPref("devtools.gcli.imgurClientID");
-
-    xhr.open("POST", postURL);
-    xhr.setRequestHeader("Authorization", clientID);
-    xhr.send(fd);
-    xhr.responseType = "json";
-
-    xhr.onreadystatechange = function() {
-      if (xhr.readyState == 4) {
-        if (xhr.status == 200) {
-          reply.href = xhr.response.data.link;
-          reply.destinations.push(l10n.lookupFormat("screenshotImgurUploaded",
-                                                    [ reply.href ]));
-        } else {
-          reply.destinations.push(l10n.lookup("screenshotImgurError"));
-        }
-
-        resolve();
-      }
-    };
-  });
+  return L10N.getFormatStr(
+    "screenshotGeneratedFilename",
+    dateString,
+    timeString
+  ) + ".png";
 }
-
-/**
- * Progress listener that forwards calls to a transfer object.
- *
- * This is used below in saveToFile to forward progress updates from the
- * nsIWebBrowserPersist object that does the actual saving to the nsITransfer
- * which just represents the operation for the Download Manager.  This keeps the
- * Download Manager updated on saving progress and completion, so that it gives
- * visual feedback from the downloads toolbar button when the save is done.
- *
- * It also allows the browser window to show auth prompts if needed (should not
- * be needed for saving screenshots).
- *
- * This code is borrowed directly from contentAreaUtils.js.
- */
-function DownloadListener(win, transfer) {
-  this.window = win;
-  this.transfer = transfer;
-
-  // For most method calls, forward to the transfer object.
-  for (const name in transfer) {
-    if (name != "QueryInterface" &&
-        name != "onStateChange") {
-      this[name] = (...args) => transfer[name].apply(transfer, args);
-    }
-  }
-
-  // Allow saveToFile to await completion for error handling
-  this._completedDeferred = defer();
-  this.completed = this._completedDeferred.promise;
-}
-
-DownloadListener.prototype = {
-  QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor",
-                                          "nsIWebProgressListener",
-                                          "nsIWebProgressListener2"]),
-
-  getInterface: function(iid) {
-    if (iid.equals(Ci.nsIAuthPrompt) ||
-        iid.equals(Ci.nsIAuthPrompt2)) {
-      const ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]
-                 .getService(Ci.nsIPromptFactory);
-      return ww.getPrompt(this.window, iid);
-    }
-
-    throw Cr.NS_ERROR_NO_INTERFACE;
-  },
-
-  onStateChange: function(webProgress, request, state, status) {
-    // Check if the download has completed
-    if ((state & Ci.nsIWebProgressListener.STATE_STOP) &&
-        (state & Ci.nsIWebProgressListener.STATE_IS_NETWORK)) {
-      if (status == Cr.NS_OK) {
-        this._completedDeferred.resolve();
-      } else {
-        this._completedDeferred.reject();
-      }
-    }
-
-    this.transfer.onStateChange.apply(this.transfer, arguments);
-  }
-};
-
-/**
- * Save the screenshot data to disk, returning a promise which is resolved on
- * completion.
- */
-var saveToFile = Task.async(function* (context, reply) {
-  const document = context.environment.chromeDocument;
-  const window = context.environment.chromeWindow;
-
-  // Check there is a .png extension to filename
-  if (!reply.filename.match(/.png$/i)) {
-    reply.filename += ".png";
-  }
-
-  const downloadsDir = yield Downloads.getPreferredDownloadsDirectory();
-  const downloadsDirExists = yield OS.File.exists(downloadsDir);
-  if (downloadsDirExists) {
-    // If filename is absolute, it will override the downloads directory and
-    // still be applied as expected.
-    reply.filename = OS.Path.join(downloadsDir, reply.filename);
-  }
-
-  const sourceURI = Services.io.newURI(reply.data);
-  const targetFile = new FileUtils.File(reply.filename);
-  const targetFileURI = Services.io.newFileURI(targetFile);
-
-  // Create download and track its progress.
-  // This is adapted from saveURL in contentAreaUtils.js, but simplified greatly
-  // and modified to allow saving to arbitrary paths on disk.  Using these
-  // objects as opposed to just writing with OS.File allows us to tie into the
-  // download manager to record a download entry and to get visual feedback from
-  // the downloads toolbar button when the save is done.
-  const nsIWBP = Ci.nsIWebBrowserPersist;
-  const flags = nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
-                nsIWBP.PERSIST_FLAGS_FORCE_ALLOW_COOKIES |
-                nsIWBP.PERSIST_FLAGS_BYPASS_CACHE |
-                nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
-  const isPrivate =
-    PrivateBrowsingUtils.isContentWindowPrivate(document.defaultView);
-  const persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
-                  .createInstance(Ci.nsIWebBrowserPersist);
-  persist.persistFlags = flags;
-  const tr = Cc["@mozilla.org/transfer;1"].createInstance(Ci.nsITransfer);
-  tr.init(sourceURI,
-          targetFileURI,
-          "",
-          null,
-          null,
-          null,
-          persist,
-          isPrivate);
-  const listener = new DownloadListener(window, tr);
-  persist.progressListener = listener;
-  persist.savePrivacyAwareURI(sourceURI,
-                              0,
-                              document.documentURIObject,
-                              Ci.nsIHttpChannel.REFERRER_POLICY_UNSET,
-                              null,
-                              null,
-                              targetFileURI,
-                              isPrivate);
-
-  try {
-    // Await successful completion of the save via the listener
-    yield listener.completed;
-    reply.destinations.push(l10n.lookup("screenshotSavedToFile") +
-                            ` "${reply.filename}"`);
-  } catch (ex) {
-    console.error(ex);
-    reply.destinations.push(l10n.lookup("screenshotErrorSavingToFile") + " " +
-                            reply.filename);
-  }
-});
--- a/devtools/server/actors/webconsole/utils.js
+++ b/devtools/server/actors/webconsole/utils.js
@@ -1,18 +1,19 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
-/* vim: set ft= javascript ts=2 et sw=2 tw=80: */
 /* 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 {Ci, Cu} = require("chrome");
 
+loader.lazyRequireGetter(this, "screenshot", "devtools/server/actors/webconsole/screenshot", true);
+
 // Note that this is only used in WebConsoleCommands, see $0 and pprint().
 if (!isWorker) {
   loader.lazyImporter(this, "VariablesView", "resource://devtools/client/shared/widgets/VariablesView.jsm");
 }
 
 const CONSOLE_WORKER_IDS = exports.CONSOLE_WORKER_IDS = [
   "SharedWorker",
   "ServiceWorker",
@@ -588,16 +589,38 @@ WebConsoleCommands._registerOriginal("co
   }
   owner.helperResult = {
     type: "copyValueToClipboard",
     value: payload,
   };
 });
 
 /**
+ * Take a screenshot of a page.
+ *
+ * @param object args
+ *               The arguments to be passed to the screenshot
+ * @return void
+ */
+WebConsoleCommands._registerOriginal("screenshot", function(owner, args) {
+  owner.helperResult = (async () => {
+    // creates data for saving the screenshot
+    const value = await screenshot(owner, args);
+    return {
+      type: "screenshotOutput",
+      value,
+      // pass args through to the client, so that the client can take care of copying
+      // and saving the screenshot data on the client machine instead of on the
+      // remote machine
+      args
+    };
+  })();
+});
+
+/**
  * (Internal only) Add the bindings to |owner.sandbox|.
  * This is intended to be used by the WebConsole actor only.
   *
   * @param object owner
   *        The owning object.
   */
 function addWebConsoleCommands(owner) {
   // Not supporting extra commands in workers yet.  This should be possible to
new file mode 100644
--- /dev/null
+++ b/devtools/shared/locales/en-US/screenshot.properties
@@ -0,0 +1,116 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside Web Console commands.
+# The Web Console command line is available from the Web Developer sub-menu
+# -> 'Web Console'.
+#
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (screenshotDesc) A very short description of the
+# 'screenshot' command. Displayed when the --help flag is passed to
+# the screenshot command.
+screenshotDesc=Save an image of the page
+
+# LOCALIZATION NOTE (screenshotFilenameDesc) A very short string to describe
+# the 'filename' parameter to the 'screenshot' command. Displayed when the
+# --help flag is passed to the screenshot command.
+screenshotFilenameDesc=Destination filename
+
+# LOCALIZATION NOTE (screenshotFilenameManual) A fuller description of the
+# 'filename' parameter to the 'screenshot' command.
+screenshotFilenameManual=The name of the file (should have a ‘.png’ extension) to which we write the screenshot.
+
+# LOCALIZATION NOTE (screenshotClipboardDesc) A very short string to describe
+# the 'clipboard' parameter to the 'screenshot' command. Displayed when the
+# --help flag is passed to the screenshot command.
+screenshotClipboardDesc=Copy screenshot to clipboard? (true/false)
+
+# LOCALIZATION NOTE (screenshotClipboardManual) A fuller description of the
+# 'clipboard' parameter to the 'screenshot' command. Displayed when the
+# --help flag is passed to the screenshot command.
+screenshotClipboardManual=True if you want to copy the screenshot instead of saving it to a file.
+
+# LOCALIZATION NOTE (screenshotGroupOptions) A label for the optional options of
+# the screenshot command. Displayed when the --help flag is passed to the
+# screenshot command.
+screenshotGroupOptions=Options
+
+# LOCALIZATION NOTE (screenshotDelayDesc) A very short string to describe
+# the 'delay' parameter to the 'screenshot' command. Displayed when the
+# --help flag is passed to the screenshot command.
+screenshotDelayDesc=Delay (seconds)
+
+# LOCALIZATION NOTE (screenshotDelayManual) A fuller description of the
+# 'delay' parameter to the 'screenshot' command. Displayed when the
+# --help flag is passed to the screenshot command.
+screenshotDelayManual=The time to wait (in seconds) before the screenshot is taken
+
+# LOCALIZATION NOTE (screenshotDPRDesc) A very short string to describe
+# the 'dpr' parameter to the 'screenshot' command. Displayed when the
+# --help flag is passed to the `screenshot command.
+screenshotDPRDesc=Device pixel ratio
+
+# LOCALIZATION NOTE (screenshotDPRManual) A fuller description of the
+# 'dpr' parameter to the 'screenshot' command. Displayed when the
+# --help flag is passed to the `screenshot command.
+screenshotDPRManual=The device pixel ratio to use when taking the screenshot
+
+# LOCALIZATION NOTE (screenshotFullPageDesc) A very short string to describe
+# the 'fullpage' parameter to the 'screenshot' command. Displayed when the
+# --help flag is passed to the `screenshot command.
+screenshotFullPageDesc=Entire webpage? (true/false)
+
+# LOCALIZATION NOTE (screenshotFullPageManual) A fuller description of the
+# 'fullpage' parameter to the 'screenshot' command. Displayed when the
+# --help flag is passed to the `screenshot command.
+screenshotFullPageManual=True if the screenshot should also include parts of the webpage which are outside the current scrolled bounds.
+
+# LOCALIZATION NOTE (screenshotFileDesc) A very short string to describe
+# the 'file' parameter to the 'screenshot' command. Displayed when the
+# --help flag is passed to the `screenshot command.
+screenshotFileDesc=Save to file? (true/false)
+
+# LOCALIZATION NOTE (screenshotFileManual) A fuller description of the
+# 'file' parameter to the 'screenshot' command. Displayed when the
+# --help flag is passed to the `screenshot command.
+screenshotFileManual=True if the screenshot should save the file even when other options are enabled (eg. clipboard).
+
+# LOCALIZATION NOTE (screenshotGeneratedFilename) The auto generated filename
+# when no file name is provided. The first argument (%1$S) is the date string
+# in yyyy-mm-dd format and the second argument (%2$S) is the time string
+# in HH.MM.SS format. Please don't add the extension here.
+screenshotGeneratedFilename=Screen Shot %1$S at %2$S
+
+# LOCALIZATION NOTE (screenshotErrorSavingToFile) Text displayed to user upon
+# encountering error while saving the screenshot to the file specified.
+# The argument (%1$S) is the filename.
+screenshotErrorSavingToFile=Error saving to %1$S
+
+# LOCALIZATION NOTE (screenshotSavedToFile) Text displayed to user when the
+# screenshot is successfully saved to the file specified.
+# The argument (%1$S) is the filename.
+screenshotSavedToFile=Saved to %1$S
+
+# LOCALIZATION NOTE (screenshotErrorCopying) Text displayed to user upon
+# encountering error while copying the screenshot to clipboard.
+screenshotErrorCopying=Error occurred while copying screenshot to clipboard.
+
+# LOCALIZATION NOTE (screenshotCopied) Text displayed to user when the
+# screenshot is successfully copied to the clipboard.
+screenshotCopied=Screenshot copied to clipboard.
+
+# LOCALIZATION NOTE (inspectNodeDesc) A very short string to describe the
+# 'node' parameter to the 'inspect' command. Displayed when the
+# --help flag is passed to the `screenshot command.
+inspectNodeDesc=CSS selector
+
+# LOCALIZATION NOTE (inspectNodeManual) A fuller description of the 'node'
+# parameter to the 'inspect' command. Displayed when the --help flag is
+# passed to the `screenshot command.
+inspectNodeManual=A CSS selector for use with document.querySelector which identifies a single element
--- a/devtools/shared/webconsole/moz.build
+++ b/devtools/shared/webconsole/moz.build
@@ -8,10 +8,11 @@ if CONFIG['OS_TARGET'] != 'Android':
     MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini']
     XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
 
 DevToolsModules(
     'client.js',
     'js-property-provider.js',
     'network-helper.js',
     'network-monitor.js',
+    'screenshot-helper.js',
     'throttle.js',
 )
copy from devtools/shared/gcli/commands/screenshot.js
copy to devtools/shared/webconsole/screenshot-helper.js
--- a/devtools/shared/gcli/commands/screenshot.js
+++ b/devtools/shared/webconsole/screenshot-helper.js
@@ -1,498 +1,290 @@
 /* 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 { Cc, Ci, Cr, Cu } = require("chrome");
+const { Cc, Ci, Cr } = require("chrome");
 const ChromeUtils = require("ChromeUtils");
-const l10n = require("gcli/l10n");
+const { LocalizationHelper } = require("devtools/shared/l10n");
 const Services = require("Services");
 const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");
-const { getRect } = require("devtools/shared/layout/utils");
-const defer = require("devtools/shared/defer");
-const { Task } = require("devtools/shared/task");
-
-loader.lazyRequireGetter(this, "openContentLink", "devtools/client/shared/link", true);
 
 loader.lazyImporter(this, "Downloads", "resource://gre/modules/Downloads.jsm");
 loader.lazyImporter(this, "OS", "resource://gre/modules/osfile.jsm");
 loader.lazyImporter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm");
 loader.lazyImporter(this, "PrivateBrowsingUtils",
                           "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
-// String used as an indication to generate default file name in the following
-// format: "Screen Shot yyyy-mm-dd at HH.MM.SS.png"
-const FILENAME_DEFAULT_VALUE = " ";
-const CONTAINER_FLASHING_DURATION = 500;
-
-/*
- * There are 2 commands and 1 converter here. The 2 commands are nearly
- * identical except that one runs on the client and one in the server.
- *
- * The server command is hidden, and is designed to be called from the client
- * command.
- */
-
-/**
- * Both commands have the same initial filename parameter
- */
-const filenameParam = {
-  name: "filename",
-  type: "string",
-  defaultValue: FILENAME_DEFAULT_VALUE,
-  description: l10n.lookup("screenshotFilenameDesc"),
-  manual: l10n.lookup("screenshotFilenameManual")
-};
+const STRINGS_URI = "devtools/shared/locales/screenshot.properties";
+const L10N = new LocalizationHelper(STRINGS_URI);
 
-/**
- * Both commands have almost the same set of standard optional parameters, except for the
- * type of the --selector option, which can be a node only on the server.
- */
-const getScreenshotCommandParams = function(isClient) {
-  return {
-    group: l10n.lookup("screenshotGroupOptions"),
-    params: [
-      {
-        name: "clipboard",
-        type: "boolean",
-        description: l10n.lookup("screenshotClipboardDesc"),
-        manual: l10n.lookup("screenshotClipboardManual")
-      },
-      {
-        name: "imgur",
-        type: "boolean",
-        description: l10n.lookup("screenshotImgurDesc"),
-        manual: l10n.lookup("screenshotImgurManual")
-      },
-      {
-        name: "delay",
-        type: { name: "number", min: 0 },
-        defaultValue: 0,
-        description: l10n.lookup("screenshotDelayDesc"),
-        manual: l10n.lookup("screenshotDelayManual")
-      },
-      {
-        name: "dpr",
-        type: { name: "number", min: 0, allowFloat: true },
-        defaultValue: 0,
-        description: l10n.lookup("screenshotDPRDesc"),
-        manual: l10n.lookup("screenshotDPRManual")
-      },
-      {
-        name: "fullpage",
-        type: "boolean",
-        description: l10n.lookup("screenshotFullPageDesc"),
-        manual: l10n.lookup("screenshotFullPageManual")
-      },
-      {
-        name: "selector",
-        // On the client side, don't try to parse the selector as a node as it will
-        // trigger an unsafe CPOW.
-        type: isClient ? "string" : "node",
-        defaultValue: null,
-        description: l10n.lookup("inspectNodeDesc"),
-        manual: l10n.lookup("inspectNodeManual")
-      },
-      {
-        name: "file",
-        type: "boolean",
-        description: l10n.lookup("screenshotFileDesc"),
-        manual: l10n.lookup("screenshotFileManual"),
-      },
-    ]
-  };
-};
-
-const clientScreenshotParams = getScreenshotCommandParams(true);
-const serverScreenshotParams = getScreenshotCommandParams(false);
-
-exports.items = [
+const screenshotDescription = L10N.getStr("screenshotDesc");
+const screenshotGroupOptions = L10N.getStr("screenshotGroupOptions");
+const screenshotCommandParams = [
   {
-    /**
-     * Format an 'imageSummary' (as output by the screenshot command).
-     * An 'imageSummary' is a simple JSON object that looks like this:
-     *
-     * {
-     *   destinations: [ "..." ], // Required array of descriptions of the
-     *                            // locations of the result image (the command
-     *                            // can have multiple outputs)
-     *   data: "...",             // Optional Base64 encoded image data
-     *   width:1024, height:768,  // Dimensions of the image data, required
-     *                            // if data != null
-     *   filename: "...",         // If set, clicking the image will open the
-     *                            // folder containing the given file
-     *   href: "...",             // If set, clicking the image will open the
-     *                            // link in a new tab
-     * }
-     */
-    item: "converter",
-    from: "imageSummary",
-    to: "dom",
-    exec: function(imageSummary, context) {
-      const document = context.document;
-      const root = document.createElement("div");
-
-      // Add a line to the result for each destination
-      imageSummary.destinations.forEach(destination => {
-        const title = document.createElement("div");
-        title.textContent = destination;
-        root.appendChild(title);
-      });
-
-      // Add the thumbnail image
-      if (imageSummary.data != null) {
-        const image = context.document.createElement("div");
-        const previewHeight = parseInt(256 * imageSummary.height / imageSummary.width,
-                                       10);
-        const style = "" +
-            "width: 256px;" +
-            "height: " + previewHeight + "px;" +
-            "max-height: 256px;" +
-            "background-image: url('" + imageSummary.data + "');" +
-            "background-size: 256px " + previewHeight + "px;" +
-            "margin: 4px;" +
-            "display: block;";
-        image.setAttribute("style", style);
-        root.appendChild(image);
-      }
-
-      // Click handler
-      if (imageSummary.href || imageSummary.filename) {
-        root.style.cursor = "pointer";
-        root.addEventListener("click", () => {
-          if (imageSummary.href) {
-            openContentLink(imageSummary.href);
-          } else if (imageSummary.filename) {
-            const file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
-            file.initWithPath(imageSummary.filename);
-            file.reveal();
-          }
-        });
-      }
-
-      return root;
-    }
+    name: "clipboard",
+    type: "boolean",
+    description: L10N.getStr("screenshotClipboardDesc"),
+    manual: L10N.getStr("screenshotClipboardManual")
+  },
+  {
+    name: "delay",
+    type: "number",
+    description: L10N.getStr("screenshotDelayDesc"),
+    manual: L10N.getStr("screenshotDelayManual")
   },
   {
-    item: "command",
-    runAt: "client",
-    name: "screenshot",
-    description: l10n.lookup("screenshotDesc"),
-    manual: l10n.lookup("screenshotManual"),
-    returnType: "imageSummary",
-    buttonId: "command-button-screenshot",
-    buttonClass: "command-button",
-    tooltipText: l10n.lookup("screenshotTooltipPage"),
-    params: [
-      filenameParam,
-      clientScreenshotParams,
-    ],
-    exec: function(args, context) {
-      // Re-execute the command on the server
-      const command = context.typed.replace(/^screenshot/, "screenshot_server");
-      const capture = context.updateExec(command).then(output => {
-        return output.error ? Promise.reject(output.data) : output.data;
-      });
-
-      simulateCameraEffect(context.environment.chromeDocument, "shutter");
-      return capture.then(saveScreenshot.bind(null, args, context));
-    },
+    name: "dpr",
+    type: "number",
+    description: L10N.getStr("screenshotDPRDesc"),
+    manual: L10N.getStr("screenshotDPRManual")
+  },
+  {
+    name: "fullpage",
+    type: "boolean",
+    description: L10N.getStr("screenshotFullPageDesc"),
+    manual: L10N.getStr("screenshotFullPageManual")
   },
   {
-    item: "command",
-    runAt: "server",
-    name: "screenshot_server",
-    hidden: true,
-    returnType: "imageSummary",
-    params: [
-      filenameParam,
-      serverScreenshotParams,
-    ],
-    exec: function(args, context) {
-      return captureScreenshot(args, context.environment.document);
-    },
+    name: "selector",
+    type: "string",
+    description: L10N.getStr("inspectNodeDesc"),
+    manual: L10N.getStr("inspectNodeManual")
+  },
+  {
+    name: "file",
+    type: "boolean",
+    description: L10N.getStr("screenshotFileDesc"),
+    manual: L10N.getStr("screenshotFileManual"),
+  },
+  {
+    name: "filename",
+    type: "string",
+    description: L10N.getStr("screenshotFilenameDesc"),
+    manual: L10N.getStr("screenshotFilenameManual")
   }
 ];
 
 /**
- * This function is called to simulate camera effects
+ * Creates a string from an object for use when screenshot is passed the `--help` argument
+ *
+ * @param object param
+ *        The param object to be formatted.
+ * @return string
+ *         The formatted information from the param object as a string
+ */
+function formatHelpField(param) {
+  const padding = " ".repeat(5);
+  return Object.entries(param).map(([key, value]) => {
+    if (key === "name") {
+      const name = `${padding}--${value}`;
+      return name;
+    }
+    return `${padding.repeat(2)}${key}: ${value}`;
+  }).join("\n");
+}
+
+/**
+ * Creates a string response from the screenshot options for use when
+ * screenshot is passed the `--help` argument
+ *
+ * @return string
+ *         The formatted information from the param object as a string
  */
-function simulateCameraEffect(document, effect) {
+function getFormattedHelpData() {
+  const formattedParams = screenshotCommandParams
+    .map(formatHelpField)
+    .join("\n\n");
+
+  return `${screenshotDescription}\n${screenshotGroupOptions}\n\n${formattedParams}`;
+}
+
+/**
+ * Main entry point in this file; Takes the original arguments that `:screenshot` was
+ * called with and the image value from the server, and uses the client window to save
+ * the screenshot to the remote debugging machine's memory or clipboard.
+ *
+ * @param object window
+ *        The Debugger Client window.
+ *
+ * @param object args
+ *        The original args with which the screenshot
+ *        was called.
+ * @param object value
+ *        an object with a image value and file name
+ *
+ * @return string[]
+ *         Response messages from processing the screenshot
+ */
+function processScreenshot(window, args = {}, value) {
+  if (args.help) {
+    const message = getFormattedHelpData();
+    // Wrap meesage in an array so that the return value is consistant with saveScreenshot
+    return [message];
+  }
+  simulateCameraShutter(window.document);
+  return saveScreenshot(window, args, value);
+}
+
+/**
+ * This function is called to simulate camera effects
+ *
+ * @param object document
+ *        The Debugger Client document.
+ */
+function simulateCameraShutter(document) {
   const window = document.defaultView;
-  if (effect === "shutter") {
-    if (Services.prefs.getBoolPref("devtools.screenshot.audio.enabled")) {
-      const audioCamera = new window.Audio("resource://devtools/client/themes/audio/shutter.wav");
-      audioCamera.play();
-    }
-  }
-  if (effect == "flash") {
-    const frames = Cu.cloneInto({ opacity: [ 0, 1 ] }, window);
-    document.documentElement.animate(frames, CONTAINER_FLASHING_DURATION);
+  if (Services.prefs.getBoolPref("devtools.screenshot.audio.enabled")) {
+    const audioCamera = new window.Audio("resource://devtools/client/themes/audio/shutter.wav");
+    audioCamera.play();
   }
 }
 
 /**
- * This function simply handles the --delay argument before calling
- * createScreenshotData
+ * Save the captured screenshot to one of several destinations.
+ *
+ * @param object window
+ *        The Debugger Client window.
+ *
+ * @param object args
+ *        The original args with which the screenshot was called.
+ *
+ * @param object image
+ *        The image object that was sent from the server.
+ *
+ * @return string[]
+ *         Response messages from processing the screenshot.
  */
-function captureScreenshot(args, document) {
-  if (args.delay > 0) {
-    return new Promise((resolve, reject) => {
-      document.defaultView.setTimeout(() => {
-        createScreenshotData(document, args).then(resolve, reject);
-      }, args.delay * 1000);
-    });
-  }
-  return createScreenshotData(document, args);
-}
-
-/**
- * There are several possible destinations for the screenshot, SKIP is used
- * in saveScreenshot() whenever one of them is not used
- */
-const SKIP = Promise.resolve();
-
-/**
- * Save the captured screenshot to one of several destinations.
- */
-function saveScreenshot(args, context, reply) {
-  const fileNeeded = args.filename != FILENAME_DEFAULT_VALUE ||
-    (!args.imgur && !args.clipboard) || args.file;
+async function saveScreenshot(window, args, image) {
+  const fileNeeded = args.filename ||
+    !args.clipboard || args.file;
+  const results = [];
 
-  return Promise.all([
-    args.clipboard ? saveToClipboard(context, reply) : SKIP,
-    args.imgur ? uploadToImgur(reply) : SKIP,
-    fileNeeded ? saveToFile(context, reply) : SKIP,
-  ]).then(() => reply);
-}
-
-/**
- * This does the dirty work of creating a base64 string out of an
- * area of the browser window
- */
-function createScreenshotData(document, args) {
-  const window = document.defaultView;
-  let left = 0;
-  let top = 0;
-  let width;
-  let height;
-  const currentX = window.scrollX;
-  const currentY = window.scrollY;
-
-  let filename = getFilename(args.filename);
-
-  if (args.fullpage) {
-    // Bug 961832: GCLI screenshot shows fixed position element in wrong
-    // position if we don't scroll to top
-    window.scrollTo(0, 0);
-    width = window.innerWidth + window.scrollMaxX - window.scrollMinX;
-    height = window.innerHeight + window.scrollMaxY - window.scrollMinY;
-    filename = filename.replace(".png", "-fullpage.png");
-  } else if (args.selector) {
-    ({ top, left, width, height } = getRect(window, args.selector, window));
-  } else {
-    left = window.scrollX;
-    top = window.scrollY;
-    width = window.innerWidth;
-    height = window.innerHeight;
+  if (args.clipboard) {
+    const result = await saveToClipboard(window, image.data);
+    results.push(result);
   }
 
-  // Only adjust for scrollbars when considering the full window
-  if (!args.selector) {
-    const winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                         .getInterface(Ci.nsIDOMWindowUtils);
-    const scrollbarHeight = {};
-    const scrollbarWidth = {};
-    winUtils.getScrollbarSize(false, scrollbarWidth, scrollbarHeight);
-    width -= scrollbarWidth.value;
-    height -= scrollbarHeight.value;
-  }
-
-  const canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
-  const ctx = canvas.getContext("2d");
-  const ratio = args.dpr ? args.dpr : window.devicePixelRatio;
-  canvas.width = width * ratio;
-  canvas.height = height * ratio;
-  ctx.scale(ratio, ratio);
-  ctx.drawWindow(window, left, top, width, height, "#fff");
-  const data = canvas.toDataURL("image/png", "");
-
-  // See comment above on bug 961832
-  if (args.fullpage) {
-    window.scrollTo(currentX, currentY);
+  if (fileNeeded) {
+    const result = await saveToFile(window, image);
+    results.push(result);
   }
-
-  simulateCameraEffect(document, "flash");
-
-  return Promise.resolve({
-    destinations: [],
-    data: data,
-    height: height,
-    width: width,
-    filename: filename,
-  });
-}
-
-/**
- * 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 != FILENAME_DEFAULT_VALUE) {
-    return defaultName;
-  }
-
-  const date = new Date();
-  let dateString = date.getFullYear() + "-" + (date.getMonth() + 1) +
-                  "-" + date.getDate();
-  dateString = dateString.split("-").map(function(part) {
-    if (part.length == 1) {
-      part = "0" + part;
-    }
-    return part;
-  }).join("-");
-
-  const timeString = date.toTimeString().replace(/:/g, ".").split(" ")[0];
-  return l10n.lookupFormat("screenshotGeneratedFilename",
-                           [ dateString, timeString ]) + ".png";
+  return results;
 }
 
 /**
  * Save the image data to the clipboard. This returns a promise, so it can
- * be treated exactly like imgur / file processing, but it's really sync
- * for now.
+ * be treated exactly like file processing.
+ *
+ * @param object window
+ *        The Debugger Client window.
+ *
+ * @param string data
+ *        The image data encoded in base64 that was sent from the server.
+ *
+ * @return string
+ *         Response message from processing the screenshot.
  */
-function saveToClipboard(context, reply) {
+function saveToClipboard(window, data) {
   return new Promise(resolve => {
     try {
       const channel = NetUtil.newChannel({
-        uri: reply.data,
+        uri: data,
         loadUsingSystemPrincipal: true,
         contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE
       });
       const input = channel.open2();
 
-      const loadContext = context.environment.chromeWindow
-                                 .QueryInterface(Ci.nsIInterfaceRequestor)
-                                 .getInterface(Ci.nsIWebNavigation)
-                                 .QueryInterface(Ci.nsILoadContext);
+      const loadContext = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                                .getInterface(Ci.nsIWebNavigation)
+                                .QueryInterface(Ci.nsILoadContext);
 
       const callback = {
         onImageReady(container, status) {
           if (!container) {
             console.error("imgTools.decodeImageAsync failed");
-            reply.destinations.push(l10n.lookup("screenshotErrorCopying"));
-            resolve();
+            resolve(L10N.getStr("screenshotErrorCopying"));
             return;
           }
 
           try {
             const wrapped = Cc["@mozilla.org/supports-interface-pointer;1"]
                               .createInstance(Ci.nsISupportsInterfacePointer);
             wrapped.data = container;
 
             const trans = Cc["@mozilla.org/widget/transferable;1"]
                             .createInstance(Ci.nsITransferable);
             trans.init(loadContext);
             trans.addDataFlavor(channel.contentType);
             trans.setTransferData(channel.contentType, wrapped, -1);
 
             Services.clipboard.setData(trans, null, Ci.nsIClipboard.kGlobalClipboard);
 
-            reply.destinations.push(l10n.lookup("screenshotCopied"));
+            resolve(L10N.getStr("screenshotCopied"));
           } catch (ex) {
             console.error(ex);
-            reply.destinations.push(l10n.lookup("screenshotErrorCopying"));
+            resolve(L10N.getStr("screenshotErrorCopying"));
           }
-          resolve();
         }
       };
 
       const threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
       const imgTools = Cc["@mozilla.org/image/tools;1"]
                           .getService(Ci.imgITools);
       imgTools.decodeImageAsync(input, channel.contentType, callback,
                                 threadManager.currentThread);
     } catch (ex) {
       console.error(ex);
-      reply.destinations.push(l10n.lookup("screenshotErrorCopying"));
-      resolve();
+      resolve(L10N.getStr("screenshotErrorCopying"));
     }
   });
 }
 
 /**
- * Upload screenshot data to Imgur, returning a promise of a URL (as a string)
- */
-function uploadToImgur(reply) {
-  return new Promise((resolve, reject) => {
-    const xhr = new XMLHttpRequest();
-    const fd = new FormData();
-    fd.append("image", reply.data.split(",")[1]);
-    fd.append("type", "base64");
-    fd.append("title", reply.filename);
-
-    const postURL = Services.prefs.getCharPref("devtools.gcli.imgurUploadURL");
-    const clientID = "Client-ID " +
-                     Services.prefs.getCharPref("devtools.gcli.imgurClientID");
-
-    xhr.open("POST", postURL);
-    xhr.setRequestHeader("Authorization", clientID);
-    xhr.send(fd);
-    xhr.responseType = "json";
-
-    xhr.onreadystatechange = function() {
-      if (xhr.readyState == 4) {
-        if (xhr.status == 200) {
-          reply.href = xhr.response.data.link;
-          reply.destinations.push(l10n.lookupFormat("screenshotImgurUploaded",
-                                                    [ reply.href ]));
-        } else {
-          reply.destinations.push(l10n.lookup("screenshotImgurError"));
-        }
-
-        resolve();
-      }
-    };
-  });
-}
-
-/**
  * Progress listener that forwards calls to a transfer object.
  *
  * This is used below in saveToFile to forward progress updates from the
  * nsIWebBrowserPersist object that does the actual saving to the nsITransfer
- * which just represents the operation for the Download Manager.  This keeps the
+ * which just represents the operation for the Download Manager. This keeps the
  * Download Manager updated on saving progress and completion, so that it gives
  * visual feedback from the downloads toolbar button when the save is done.
  *
  * It also allows the browser window to show auth prompts if needed (should not
  * be needed for saving screenshots).
  *
  * This code is borrowed directly from contentAreaUtils.js.
+ *
+ * @param object win
+ *        The Debugger Client window.
+ *
+ * @param object transfer
+ *        The transfer object.
+ *
  */
 function DownloadListener(win, transfer) {
   this.window = win;
   this.transfer = transfer;
 
   // For most method calls, forward to the transfer object.
   for (const name in transfer) {
     if (name != "QueryInterface" &&
         name != "onStateChange") {
       this[name] = (...args) => transfer[name].apply(transfer, args);
     }
   }
 
   // Allow saveToFile to await completion for error handling
-  this._completedDeferred = defer();
-  this.completed = this._completedDeferred.promise;
+  this._completedDeferred = {};
+  this.completed = new Promise((resolve, reject) => {
+    this._completedDeferred.resolve = resolve;
+    this._completedDeferred.reject = reject;
+  });
 }
 
 DownloadListener.prototype = {
   QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor",
                                           "nsIWebProgressListener",
                                           "nsIWebProgressListener2"]),
 
   getInterface: function(iid) {
@@ -519,41 +311,50 @@ DownloadListener.prototype = {
 
     this.transfer.onStateChange.apply(this.transfer, arguments);
   }
 };
 
 /**
  * Save the screenshot data to disk, returning a promise which is resolved on
  * completion.
+ *
+ * @param object window
+ *        The Debugger Client window.
+ *
+ * @param object image
+ *        The image object that was sent from the server.
+ *
+ * @return string
+ *         Response message from processing the screenshot.
  */
-var saveToFile = Task.async(function* (context, reply) {
-  const document = context.environment.chromeDocument;
-  const window = context.environment.chromeWindow;
+async function saveToFile(window, image) {
+  const document = window.document;
+  let filename = image.filename;
 
   // Check there is a .png extension to filename
-  if (!reply.filename.match(/.png$/i)) {
-    reply.filename += ".png";
+  if (!filename.match(/.png$/i)) {
+    filename += ".png";
   }
 
-  const downloadsDir = yield Downloads.getPreferredDownloadsDirectory();
-  const downloadsDirExists = yield OS.File.exists(downloadsDir);
+  const downloadsDir = await Downloads.getPreferredDownloadsDirectory();
+  const downloadsDirExists = await OS.File.exists(downloadsDir);
   if (downloadsDirExists) {
     // If filename is absolute, it will override the downloads directory and
     // still be applied as expected.
-    reply.filename = OS.Path.join(downloadsDir, reply.filename);
+    filename = OS.Path.join(downloadsDir, filename);
   }
 
-  const sourceURI = Services.io.newURI(reply.data);
-  const targetFile = new FileUtils.File(reply.filename);
+  const sourceURI = Services.io.newURI(image.data);
+  const targetFile = new FileUtils.File(filename);
   const targetFileURI = Services.io.newFileURI(targetFile);
 
   // Create download and track its progress.
   // This is adapted from saveURL in contentAreaUtils.js, but simplified greatly
-  // and modified to allow saving to arbitrary paths on disk.  Using these
+  // and modified to allow saving to arbitrary paths on disk. Using these
   // objects as opposed to just writing with OS.File allows us to tie into the
   // download manager to record a download entry and to get visual feedback from
   // the downloads toolbar button when the save is done.
   const nsIWBP = Ci.nsIWebBrowserPersist;
   const flags = nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
                 nsIWBP.PERSIST_FLAGS_FORCE_ALLOW_COOKIES |
                 nsIWBP.PERSIST_FLAGS_BYPASS_CACHE |
                 nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
@@ -579,17 +380,17 @@ var saveToFile = Task.async(function* (c
                               Ci.nsIHttpChannel.REFERRER_POLICY_UNSET,
                               null,
                               null,
                               targetFileURI,
                               isPrivate);
 
   try {
     // Await successful completion of the save via the listener
-    yield listener.completed;
-    reply.destinations.push(l10n.lookup("screenshotSavedToFile") +
-                            ` "${reply.filename}"`);
+    await listener.completed;
+    return L10N.getFormatStr("screenshotSavedToFile", filename);
   } catch (ex) {
     console.error(ex);
-    reply.destinations.push(l10n.lookup("screenshotErrorSavingToFile") + " " +
-                            reply.filename);
+    return L10N.getFormatStr("screenshotErrorSavingToFile", filename);
   }
-});
+}
+
+module.exports = processScreenshot;