summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/utils/capture-screenshot.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/utils/capture-screenshot.js')
-rw-r--r--devtools/server/actors/utils/capture-screenshot.js200
1 files changed, 200 insertions, 0 deletions
diff --git a/devtools/server/actors/utils/capture-screenshot.js b/devtools/server/actors/utils/capture-screenshot.js
new file mode 100644
index 0000000000..e7b46620b2
--- /dev/null
+++ b/devtools/server/actors/utils/capture-screenshot.js
@@ -0,0 +1,200 @@
+/* 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(
+ "http://www.w3.org/1999/xhtml",
+ "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"
+ );
+}