gecko-dev/devtools/shared/screenshot/save.js
2018-10-19 12:55:39 +00:00

250 lines
7.3 KiB
JavaScript

/* 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 } = require("chrome");
const { LocalizationHelper } = require("devtools/shared/l10n");
const Services = require("Services");
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");
const STRINGS_URI = "devtools/shared/locales/screenshot.properties";
const L10N = new LocalizationHelper(STRINGS_URI);
const screenshotDescription = L10N.getStr("screenshotDesc");
const screenshotGroupOptions = L10N.getStr("screenshotGroupOptions");
const screenshotCommandParams = [
{
name: "clipboard",
type: "boolean",
description: L10N.getStr("screenshotClipboardDesc"),
manual: L10N.getStr("screenshotClipboardManual"),
},
{
name: "delay",
type: "number",
description: L10N.getStr("screenshotDelayDesc"),
manual: L10N.getStr("screenshotDelayManual"),
},
{
name: "dpr",
type: "number",
description: L10N.getStr("screenshotDPRDesc"),
manual: L10N.getStr("screenshotDPRManual"),
},
{
name: "fullpage",
type: "boolean",
description: L10N.getStr("screenshotFullPageDesc"),
manual: L10N.getStr("screenshotFullPageManual"),
},
{
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"),
},
];
/**
* 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 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 add
* and audio effect.
*
* @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 saveScreenshot(window, args = {}, value) {
if (args.help) {
const message = getFormattedHelpData();
// Wrap message in an array so that the return value is consistant with save
return [message];
}
simulateCameraShutter(window);
return save(args, value);
}
/**
* This function is called to simulate camera effects
*
* @param object document
* The Debugger Client document.
*/
function simulateCameraShutter(window) {
if (Services.prefs.getBoolPref("devtools.screenshot.audio.enabled")) {
const audioCamera = new window.Audio("resource://devtools/client/themes/audio/shutter.wav");
audioCamera.play();
}
}
/**
* Save the captured screenshot to one of several destinations.
*
* @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.
*/
async function save(args, image) {
const fileNeeded = args.filename ||
!args.clipboard || args.file;
const results = [];
if (args.clipboard) {
const result = saveToClipboard(image.data);
results.push(result);
}
if (fileNeeded) {
const result = await saveToFile(image);
results.push(result);
}
return results;
}
/**
* Save the image data to the clipboard. This returns a promise, so it can
* be treated exactly like file processing.
*
* @param string base64URI
* The image data encoded in a base64 URI that was sent from the server.
*
* @return string
* Response message from processing the screenshot.
*/
function saveToClipboard(base64URI) {
try {
const imageTools = Cc["@mozilla.org/image/tools;1"]
.getService(Ci.imgITools);
const base64Data = base64URI.replace("data:image/png;base64,", "");
const image = atob(base64Data);
const img = imageTools.decodeImageFromBuffer(image, image.length, "image/png");
const transferable = Cc["@mozilla.org/widget/transferable;1"]
.createInstance(Ci.nsITransferable);
transferable.init(null);
transferable.addDataFlavor("image/png");
transferable.setTransferData("image/png", img, -1);
Services.clipboard.setData(transferable, null, Services.clipboard.kGlobalClipboard);
return L10N.getStr("screenshotCopied");
} catch (ex) {
console.error(ex);
return L10N.getStr("screenshotErrorCopying");
}
}
/**
* Save the screenshot data to disk, returning a promise which is resolved on
* completion.
*
* @param object image
* The image object that was sent from the server.
*
* @return string
* Response message from processing the screenshot.
*/
async function saveToFile(image) {
let filename = image.filename;
// Check there is a .png extension to filename
if (!filename.match(/.png$/i)) {
filename += ".png";
}
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.
filename = OS.Path.join(downloadsDir, filename);
}
const sourceURI = Services.io.newURI(image.data);
const targetFile = new FileUtils.File(filename);
// Create download and track its progress.
try {
const download = await Downloads.createDownload({
source: sourceURI,
target: targetFile,
});
const list = await Downloads.getList(Downloads.ALL);
// add the download to the download list in the Downloads list in the Browser UI
list.add(download);
// Await successful completion of the save via the download manager
await download.start();
return L10N.getFormatStr("screenshotSavedToFile", filename);
} catch (ex) {
console.error(ex);
return L10N.getFormatStr("screenshotErrorSavingToFile", filename);
}
}
module.exports = saveScreenshot;