Source code

Revision control

Copy as Markdown

Other Tools

/* 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(
"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"
);
}