tor-browser

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

Capture.sys.mjs (6231B)


      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 file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 const lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  Log: "chrome://remote/content/shared/Log.sys.mjs",
      9 });
     10 
     11 ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
     12 
     13 const CONTEXT_2D = "2d";
     14 const BG_COLOUR = "rgb(255,255,255)";
     15 const MAX_CANVAS_DIMENSION = 32767;
     16 const MAX_CANVAS_AREA = 472907776;
     17 const XHTML_NS = "http://www.w3.org/1999/xhtml";
     18 
     19 /**
     20 * Provides primitives to capture screenshots.
     21 *
     22 * @namespace
     23 */
     24 export const capture = {};
     25 
     26 capture.Format = {
     27  Base64: 0,
     28  Hash: 1,
     29 };
     30 
     31 /**
     32 * Draw a rectangle off the framebuffer.
     33 *
     34 * @param {DOMWindow} win
     35 *     The DOM window used for the framebuffer, and providing the interfaces
     36 *     for creating an HTMLCanvasElement.
     37 * @param {BrowsingContext} browsingContext
     38 *     The BrowsingContext from which the snapshot should be taken.
     39 * @param {number} left
     40 *     The left, X axis offset of the rectangle.
     41 * @param {number} top
     42 *     The top, Y axis offset of the rectangle.
     43 * @param {number} width
     44 *     The width dimension of the rectangle to paint.
     45 * @param {number} height
     46 *     The height dimension of the rectangle to paint.
     47 * @param {object=} options
     48 * @param {HTMLCanvasElement=} options.canvas
     49 *     Optional canvas to reuse for the screenshot.
     50 * @param {number=} options.flags
     51 *     Optional integer representing flags to pass to drawWindow; these
     52 *     are defined on CanvasRenderingContext2D.
     53 * @param {number=} options.dX
     54 *     Horizontal offset between the browser window and content area. Defaults to 0.
     55 * @param {number=} options.dY
     56 *     Vertical offset between the browser window and content area. Defaults to 0.
     57 * @param {boolean=} options.readback
     58 *     If true, read back a snapshot of the pixel data currently in the
     59 *     compositor/window. Defaults to false.
     60 *
     61 * @returns {HTMLCanvasElement}
     62 *     The canvas on which the selection from the window's framebuffer
     63 *     has been painted on.
     64 */
     65 capture.canvas = async function (
     66  win,
     67  browsingContext,
     68  left,
     69  top,
     70  width,
     71  height,
     72  { canvas = null, flags = null, dX = 0, dY = 0, readback = false } = {}
     73 ) {
     74  // FIXME(bug 1761032): This looks a bit sketchy, overrideDPPX doesn't
     75  // influence rendering...
     76  const scale = browsingContext.overrideDPPX || win.devicePixelRatio;
     77 
     78  let canvasHeight = height * scale;
     79  let canvasWidth = width * scale;
     80 
     81  // Cap the screenshot size for width and height at 2^16 pixels,
     82  // which is the maximum allowed canvas size. Higher dimensions will
     83  // trigger exceptions in Gecko.
     84  if (canvasWidth > MAX_CANVAS_DIMENSION) {
     85    lazy.logger.warn(
     86      "Limiting screen capture width to maximum allowed " +
     87        MAX_CANVAS_DIMENSION +
     88        " pixels"
     89    );
     90    width = Math.floor(MAX_CANVAS_DIMENSION / scale);
     91    canvasWidth = width * scale;
     92  }
     93 
     94  if (canvasHeight > MAX_CANVAS_DIMENSION) {
     95    lazy.logger.warn(
     96      "Limiting screen capture height to maximum allowed " +
     97        MAX_CANVAS_DIMENSION +
     98        " pixels"
     99    );
    100    height = Math.floor(MAX_CANVAS_DIMENSION / scale);
    101    canvasHeight = height * scale;
    102  }
    103 
    104  // If the area is larger, reduce the height to keep the full width.
    105  if (canvasWidth * canvasHeight > MAX_CANVAS_AREA) {
    106    lazy.logger.warn(
    107      "Limiting screen capture area to maximum allowed " +
    108        MAX_CANVAS_AREA +
    109        " pixels"
    110    );
    111    height = Math.floor(MAX_CANVAS_AREA / (canvasWidth * scale));
    112    canvasHeight = height * scale;
    113  }
    114 
    115  if (canvas === null) {
    116    canvas = win.document.createElementNS(XHTML_NS, "canvas");
    117    canvas.width = canvasWidth;
    118    canvas.height = canvasHeight;
    119  }
    120 
    121  const ctx = canvas.getContext(CONTEXT_2D);
    122 
    123  if (readback) {
    124    if (flags === null) {
    125      flags =
    126        ctx.DRAWWINDOW_DRAW_CARET |
    127        ctx.DRAWWINDOW_DRAW_VIEW |
    128        ctx.DRAWWINDOW_USE_WIDGET_LAYERS;
    129    }
    130 
    131    // drawWindow doesn't take scaling into account.
    132    ctx.scale(scale, scale);
    133    ctx.drawWindow(win, left + dX, top + dY, width, height, BG_COLOUR, flags);
    134  } else {
    135    let rect = new DOMRect(left, top, width, height);
    136    let snapshot = await browsingContext.currentWindowGlobal.drawSnapshot(
    137      rect,
    138      scale,
    139      BG_COLOUR
    140    );
    141 
    142    ctx.drawImage(snapshot, 0, 0);
    143 
    144    // Bug 1574935 - Huge dimensions can trigger an OOM because multiple copies
    145    // of the bitmap will exist in memory. Force the removal of the snapshot
    146    // because it is no longer needed.
    147    snapshot.close();
    148  }
    149 
    150  return canvas;
    151 };
    152 
    153 /**
    154 * Encode the contents of an HTMLCanvasElement to a Base64 encoded string.
    155 *
    156 * @param {HTMLCanvasElement} canvas
    157 *     The canvas to encode.
    158 * @param {string} type
    159 *     The image format to output such as `image/png`.
    160 * @param {number} encoderOptions
    161 *     A number between 0 and 1 representing the image quality. Defaults to `1.0`.
    162 *
    163 * @returns {string}
    164 *     A Base64 encoded string.
    165 */
    166 capture.toBase64 = function (canvas, type, encoderOptions = 1.0) {
    167  let u = canvas.toDataURL(type, encoderOptions);
    168  return u.substring(u.indexOf(",") + 1);
    169 };
    170 
    171 /**
    172 * Hash the contents of an HTMLCanvasElement to a SHA-256 hex digest.
    173 *
    174 * @param {HTMLCanvasElement} canvas
    175 *     The canvas to encode.
    176 *
    177 * @returns {string}
    178 *     A hex digest of the SHA-256 hash of the base64 encoded string.
    179 */
    180 capture.toHash = function (canvas) {
    181  let u = capture.toBase64(canvas, "image/png");
    182  let buffer = new TextEncoder().encode(u);
    183  return crypto.subtle.digest("SHA-256", buffer).then(hash => hex(hash));
    184 };
    185 
    186 /**
    187 * Convert buffer into to hex.
    188 *
    189 * @param {ArrayBuffer} buffer
    190 *     The buffer containing the data to convert to hex.
    191 *
    192 * @returns {string}
    193 *     A hex digest of the input buffer.
    194 */
    195 function hex(buffer) {
    196  let hexCodes = [];
    197  let view = new DataView(buffer);
    198  for (let i = 0; i < view.byteLength; i += 4) {
    199    let value = view.getUint32(i);
    200    let stringValue = value.toString(16);
    201    let padding = "00000000";
    202    let paddedValue = (padding + stringValue).slice(-padding.length);
    203    hexCodes.push(paddedValue);
    204  }
    205  return hexCodes.join("");
    206 }