gecko-dev/remote/shared/Capture.sys.mjs

204 lines
5.9 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/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
Log: "chrome://remote/content/shared/Log.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
const CONTEXT_2D = "2d";
const BG_COLOUR = "rgb(255,255,255)";
const MAX_CANVAS_DIMENSION = 32767;
const MAX_CANVAS_AREA = 472907776;
const PNG_MIME = "image/png";
const XHTML_NS = "http://www.w3.org/1999/xhtml";
/**
* Provides primitives to capture screenshots.
*
* @namespace
*/
export const capture = {};
capture.Format = {
Base64: 0,
Hash: 1,
};
/**
* Draw a rectangle off the framebuffer.
*
* @param {DOMWindow} win
* The DOM window used for the framebuffer, and providing the interfaces
* for creating an HTMLCanvasElement.
* @param {BrowsingContext} browsingContext
* The BrowsingContext from which the snapshot should be taken.
* @param {number} left
* The left, X axis offset of the rectangle.
* @param {number} top
* The top, Y axis offset of the rectangle.
* @param {number} width
* The width dimension of the rectangle to paint.
* @param {number} height
* The height dimension of the rectangle to paint.
* @param {object=} options
* @param {HTMLCanvasElement=} options.canvas
* Optional canvas to reuse for the screenshot.
* @param {number=} options.flags
* Optional integer representing flags to pass to drawWindow; these
* are defined on CanvasRenderingContext2D.
* @param {number=} options.dX
* Horizontal offset between the browser window and content area. Defaults to 0.
* @param {number=} options.dY
* Vertical offset between the browser window and content area. Defaults to 0.
* @param {boolean=} options.readback
* If true, read back a snapshot of the pixel data currently in the
* compositor/window. Defaults to false.
*
* @returns {HTMLCanvasElement}
* The canvas on which the selection from the window's framebuffer
* has been painted on.
*/
capture.canvas = async function (
win,
browsingContext,
left,
top,
width,
height,
{ canvas = null, flags = null, dX = 0, dY = 0, readback = false } = {}
) {
// FIXME(bug 1761032): This looks a bit sketchy, overrideDPPX doesn't
// influence rendering...
const scale = browsingContext.overrideDPPX || win.devicePixelRatio;
let canvasHeight = height * scale;
let canvasWidth = width * scale;
// Cap the screenshot size for width and height at 2^16 pixels,
// which is the maximum allowed canvas size. Higher dimensions will
// trigger exceptions in Gecko.
if (canvasWidth > MAX_CANVAS_DIMENSION) {
lazy.logger.warn(
"Limiting screen capture width to maximum allowed " +
MAX_CANVAS_DIMENSION +
" pixels"
);
width = Math.floor(MAX_CANVAS_DIMENSION / scale);
canvasWidth = width * scale;
}
if (canvasHeight > MAX_CANVAS_DIMENSION) {
lazy.logger.warn(
"Limiting screen capture height to maximum allowed " +
MAX_CANVAS_DIMENSION +
" pixels"
);
height = Math.floor(MAX_CANVAS_DIMENSION / scale);
canvasHeight = height * scale;
}
// If the area is larger, reduce the height to keep the full width.
if (canvasWidth * canvasHeight > MAX_CANVAS_AREA) {
lazy.logger.warn(
"Limiting screen capture area to maximum allowed " +
MAX_CANVAS_AREA +
" pixels"
);
height = Math.floor(MAX_CANVAS_AREA / (canvasWidth * scale));
canvasHeight = height * scale;
}
if (canvas === null) {
canvas = win.document.createElementNS(XHTML_NS, "canvas");
canvas.width = canvasWidth;
canvas.height = canvasHeight;
}
const ctx = canvas.getContext(CONTEXT_2D);
if (readback) {
if (flags === null) {
flags =
ctx.DRAWWINDOW_DRAW_CARET |
ctx.DRAWWINDOW_DRAW_VIEW |
ctx.DRAWWINDOW_USE_WIDGET_LAYERS;
}
// drawWindow doesn't take scaling into account.
ctx.scale(scale, scale);
ctx.drawWindow(win, left + dX, top + dY, width, height, BG_COLOUR, flags);
} else {
let rect = new DOMRect(left, top, width, height);
let snapshot = await browsingContext.currentWindowGlobal.drawSnapshot(
rect,
scale,
BG_COLOUR
);
ctx.drawImage(snapshot, 0, 0);
// 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;
};
/**
* Encode the contents of an HTMLCanvasElement to a Base64 encoded string.
*
* @param {HTMLCanvasElement} canvas
* The canvas to encode.
*
* @returns {string}
* A Base64 encoded string.
*/
capture.toBase64 = function (canvas) {
let u = canvas.toDataURL(PNG_MIME);
return u.substring(u.indexOf(",") + 1);
};
/**
* Hash the contents of an HTMLCanvasElement to a SHA-256 hex digest.
*
* @param {HTMLCanvasElement} canvas
* The canvas to encode.
*
* @returns {string}
* A hex digest of the SHA-256 hash of the base64 encoded string.
*/
capture.toHash = function (canvas) {
let u = capture.toBase64(canvas);
let buffer = new TextEncoder().encode(u);
return crypto.subtle.digest("SHA-256", buffer).then(hash => hex(hash));
};
/**
* Convert buffer into to hex.
*
* @param {ArrayBuffer} buffer
* The buffer containing the data to convert to hex.
*
* @returns {string}
* A hex digest of the input buffer.
*/
function hex(buffer) {
let hexCodes = [];
let view = new DataView(buffer);
for (let i = 0; i < view.byteLength; i += 4) {
let value = view.getUint32(i);
let stringValue = value.toString(16);
let padding = "00000000";
let paddedValue = (padding + stringValue).slice(-padding.length);
hexCodes.push(paddedValue);
}
return hexCodes.join("");
}