tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

capture-screenshot.js (7800B)


      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
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
      8 
      9 const CONTAINER_FLASHING_DURATION = 500;
     10 const STRINGS_URI = "devtools/shared/locales/screenshot.properties";
     11 const L10N = new LocalizationHelper(STRINGS_URI);
     12 
     13 // These values are used to truncate the resulting image if the captured area is bigger.
     14 // This is to avoid failing to produce a screenshot at all.
     15 // It is recommended to keep these values in sync with the corresponding screenshots features
     16 // values in browser/components/screenshots/ScreenshotsUtils.sys.mjs.
     17 //
     18 // TODO(Bug 1942439): Change the consts and related truncation warning logic to align it to the new consts
     19 // used by ScreenshotsUtils.sys.mjs, which does not use the same approach nor the MAX_IMAGE_WIDTH
     20 // and MAX_IMAGE_HEIGHT consts that the screenshots addon was originally using.
     21 const MAX_IMAGE_WIDTH = 10000;
     22 const MAX_IMAGE_HEIGHT = 10000;
     23 
     24 /**
     25 * This function is called to simulate camera effects
     26 *
     27 * @param {BrowsingContext} browsingContext: The browsing context associated with the
     28 *                          browser element we want to animate.
     29 */
     30 function simulateCameraFlash(browsingContext) {
     31  // If there's no topFrameElement (it can happen if the screenshot is taken from the
     32  // browser toolbox), use the top chrome window document element.
     33  const node =
     34    browsingContext.topFrameElement ||
     35    browsingContext.topChromeWindow.document.documentElement;
     36 
     37  if (!node) {
     38    console.error(
     39      "Can't find an element to play the camera flash animation on for the following browsing context:",
     40      browsingContext
     41    );
     42    return;
     43  }
     44 
     45  // Don't take a screenshot if the user prefers reduced motion.
     46  if (node.ownerGlobal.matchMedia("(prefers-reduced-motion)").matches) {
     47    return;
     48  }
     49 
     50  node.animate([{ opacity: 0 }, { opacity: 1 }], {
     51    duration: CONTAINER_FLASHING_DURATION,
     52  });
     53 }
     54 
     55 /**
     56 * Take a screenshot of a browser element given its browsingContext.
     57 *
     58 * @param {object} args
     59 * @param {number} args.delay: Number of seconds to wait before taking the screenshot
     60 * @param {object | null} args.rect: Object with left, top, width and height properties
     61 *                      representing the rect **inside the browser element** that should
     62 *                      be rendered. If null, the current viewport of the element will be rendered.
     63 * @param {boolean} args.fullpage: Should the screenshot be the height of the whole page
     64 * @param {string} args.filename: Expected filename for the screenshot
     65 * @param {number} args.snapshotScale: Scale that will be used by `drawSnapshot` to take the screenshot.
     66 *                 ⚠️ Note that the scale might be decreased if the resulting image would
     67 *                 be too big to draw safely. A warning message will be returned if that's
     68 *                 the case.
     69 * @param {number} args.fileScale: Scale of the exported file. Defaults to args.snapshotScale.
     70 * @param {boolean} args.disableFlash: Set to true to disable the flash animation when the
     71 *                  screenshot is taken.
     72 * @param {BrowsingContext} browsingContext
     73 * @returns {object} object with the following properties:
     74 *          - data {String}: The dataURL representing the screenshot
     75 *          - height {Number}: Height of the resulting screenshot
     76 *          - width {Number}: Width of the resulting screenshot
     77 *          - filename {String}: Filename of the resulting screenshot
     78 *          - messages {Array<Object{text, level}>}: An array of object representing the
     79 *            different messages and their level that should be displayed to the user.
     80 */
     81 async function captureScreenshot(args, browsingContext) {
     82  const messages = [];
     83 
     84  let filename = getFilename(args.filename);
     85 
     86  if (args.fullpage) {
     87    filename = filename.replace(".png", "-fullpage.png");
     88  }
     89 
     90  let { left, top, width, height } = args.rect || {};
     91  let _showScreenshotTruncationWarning = false;
     92 
     93  // Truncate the width and height if necessary.
     94  if (width && (width > MAX_IMAGE_WIDTH || height > MAX_IMAGE_HEIGHT)) {
     95    _showScreenshotTruncationWarning = true;
     96    width = Math.min(width, MAX_IMAGE_WIDTH);
     97    height = Math.min(height, MAX_IMAGE_HEIGHT);
     98  }
     99 
    100  let rect = null;
    101  if (args.rect) {
    102    rect = new globalThis.DOMRect(
    103      Math.round(left),
    104      Math.round(top),
    105      Math.round(width),
    106      Math.round(height)
    107    );
    108  }
    109 
    110  const document = browsingContext.topChromeWindow.document;
    111  const canvas = document.createElementNS(
    112    "http://www.w3.org/1999/xhtml",
    113    "canvas"
    114  );
    115 
    116  const drawToCanvas = async actualRatio => {
    117    // Even after decreasing width, height and ratio, there may still be cases where the
    118    // hardware fails at creating the image. Let's catch this so we can at least show an
    119    // error message to the user.
    120    try {
    121      const snapshot = await browsingContext.currentWindowGlobal.drawSnapshot(
    122        rect,
    123        actualRatio,
    124        "rgb(255,255,255)",
    125        args.fullpage
    126      );
    127 
    128      const fileScale = args.fileScale || actualRatio;
    129      const renderingWidth = (snapshot.width / actualRatio) * fileScale;
    130      const renderingHeight = (snapshot.height / actualRatio) * fileScale;
    131      canvas.width = renderingWidth;
    132      canvas.height = renderingHeight;
    133      width = renderingWidth;
    134      height = renderingHeight;
    135      const ctx = canvas.getContext("2d");
    136      ctx.drawImage(snapshot, 0, 0, renderingWidth, renderingHeight);
    137 
    138      // Bug 1574935 - Huge dimensions can trigger an OOM because multiple copies
    139      // of the bitmap will exist in memory. Force the removal of the snapshot
    140      // because it is no longer needed.
    141      snapshot.close();
    142 
    143      return canvas.toDataURL("image/png", "");
    144    } catch (e) {
    145      return null;
    146    }
    147  };
    148 
    149  const ratio = args.snapshotScale;
    150  let data = await drawToCanvas(ratio);
    151  if (!data && ratio > 1.0) {
    152    // If the user provided DPR or the window.devicePixelRatio was higher than 1,
    153    // try again with a reduced ratio.
    154    messages.push({
    155      level: "warn",
    156      text: L10N.getStr("screenshotDPRDecreasedWarning"),
    157    });
    158    data = await drawToCanvas(1.0);
    159  }
    160  if (!data) {
    161    messages.push({
    162      level: "error",
    163      text: L10N.getStr("screenshotRenderingError"),
    164    });
    165  }
    166 
    167  if (data && args.disableFlash !== true) {
    168    simulateCameraFlash(browsingContext);
    169  }
    170 
    171  // Bug 1953285 - Incorrect message when taking a full page screenshot that's too large
    172  // Previously Passing Incorrect value of width and height
    173  // Now passing updated value of width and height i.e. renderWidth, renderHeight
    174  // Took boolean _showScreenshotTruncationWarning to keep track
    175  if (_showScreenshotTruncationWarning) {
    176    messages.push({
    177      level: "warn",
    178      text: L10N.getFormatStr("screenshotTruncationWarning", width, height),
    179    });
    180  }
    181 
    182  return {
    183    data,
    184    height,
    185    width,
    186    filename,
    187    messages,
    188  };
    189 }
    190 
    191 exports.captureScreenshot = captureScreenshot;
    192 
    193 /**
    194 * We may have a filename specified in args, or we might have to generate
    195 * one.
    196 */
    197 function getFilename(defaultName) {
    198  // Create a name for the file if not present
    199  if (defaultName) {
    200    return defaultName;
    201  }
    202 
    203  const date = new Date();
    204  const monthString = (date.getMonth() + 1).toString().padStart(2, "0");
    205  const dayString = date.getDate().toString().padStart(2, "0");
    206  const dateString = `${date.getFullYear()}-${monthString}-${dayString}`;
    207 
    208  const timeString = date.toTimeString().replace(/:/g, ".").split(" ")[0];
    209 
    210  return (
    211    L10N.getFormatStr("screenshotGeneratedFilename", dateString, timeString) +
    212    ".png"
    213  );
    214 }