mirror of
https://github.com/mozilla-firefox/firefox
synced 2026-08-10 11:48:50 +00:00
drawSnapshot only asked CrossProcessPaint for DrawView when no rect was passed. Without DrawView the paint also gets IgnoreViewportScrolling | DocumentRelative, which makes nsLayoutUtils::PaintFrame set the root scroll container as the display list builder's ignored scroll frame. ScrollContainerFrame::BuildDisplayList then only appends the scroll parts on that path when painting to a window, which a snapshot never is. The reftest harness needs an explicit rect, so it lost the root scrollbars, and drew position:fixed content at the scroll offset while the rest of the page was document relative. The default harness path doesn't have either problem because it calls drawWindow with DRAWWINDOW_DRAW_VIEW. Replace the trailing resetScrollPosition boolean on drawSnapshot with a DrawSnapshotOptions dictionary and add drawView to it, which sets the DrawView flag even when a rect is given. Both reftest harnesses opt in. Every other caller keeps the document relative behaviour it has today, which is what thumbnails, Screenshots, DevTools and tabs.captureTab want; those call sites are mechanically converted from the positional boolean to the dictionary. Differential Revision: https://phabricator.services.mozilla.com/D314618
236 lines
6.9 KiB
JavaScript
236 lines
6.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/. */
|
|
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
|
|
});
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"canvasMaxArea",
|
|
"gfx.canvas.max-area"
|
|
);
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"canvasMaxSize",
|
|
"gfx.canvas.max-size"
|
|
);
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"compositorReadback",
|
|
"remote.screenshot.use_readback",
|
|
false
|
|
);
|
|
|
|
const CONTEXT_2D = "2d";
|
|
const BG_COLOUR = "rgb(255,255,255)";
|
|
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, unless the
|
|
* `remote.screenshot.use_readback` preference is set.
|
|
* @param {boolean=} options.drawView
|
|
* If true, the rectangle is relative to the visible viewport rather than to
|
|
* the page, and view level rendering such as the root scrollbars is
|
|
* included. Ignored when reading back. 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,
|
|
drawView = false,
|
|
} = {}
|
|
) {
|
|
if (lazy.compositorReadback && !readback) {
|
|
// Readback can only return the content area composited on screen, and its
|
|
// coordinates are relative to the chrome window rather than the document,
|
|
// so any requested region degrades to the whole content area.
|
|
const browser = browsingContext.top.embedderElement;
|
|
if (browser) {
|
|
readback = true;
|
|
({ left, top, width, height } = browser.getBoundingClientRect());
|
|
}
|
|
}
|
|
|
|
// FIXME(bug 1761032): This looks a bit sketchy, overrideDPPX doesn't
|
|
// influence rendering...
|
|
const scale = browsingContext.overrideDPPX || win.devicePixelRatio;
|
|
|
|
const canvasHeight = height * scale;
|
|
const canvasWidth = width * scale;
|
|
const canvasArea = canvasWidth * canvasHeight;
|
|
|
|
if (canvasWidth > lazy.canvasMaxSize) {
|
|
throw new lazy.error.UnsupportedOperationError(
|
|
`${canvasWidth} exceeds the maximum allowed screenshot width of ${lazy.canvasMaxSize} pixels`
|
|
);
|
|
}
|
|
|
|
if (canvasHeight > lazy.canvasMaxSize) {
|
|
throw new lazy.error.UnsupportedOperationError(
|
|
`${canvasHeight} exceeds the maximum allowed screenshot height of ${lazy.canvasMaxSize} pixels`
|
|
);
|
|
}
|
|
|
|
if (canvasArea > lazy.canvasMaxArea) {
|
|
throw new lazy.error.UnsupportedOperationError(
|
|
`${canvasArea} exceeds the maximum allowed screenshot area of ${lazy.canvasMaxArea} square pixels`
|
|
);
|
|
}
|
|
|
|
try {
|
|
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,
|
|
{ drawView }
|
|
);
|
|
|
|
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();
|
|
}
|
|
} catch (error) {
|
|
throw new lazy.error.UnknownError(
|
|
`Unable to capture screenshot: ${error.message}`
|
|
);
|
|
}
|
|
return canvas;
|
|
};
|
|
|
|
/**
|
|
* Encode the contents of an HTMLCanvasElement to a Base64 encoded string.
|
|
*
|
|
* @param {HTMLCanvasElement} canvas
|
|
* The canvas to encode.
|
|
* @param {string} type
|
|
* The image format to output such as `image/png`.
|
|
* @param {number} encoderOptions
|
|
* A number between 0 and 1 representing the image quality. Defaults to `1.0`.
|
|
*
|
|
* @returns {string}
|
|
* A Base64 encoded string.
|
|
*/
|
|
capture.toBase64 = function (canvas, type, encoderOptions = 1.0) {
|
|
let u = canvas.toDataURL(type, encoderOptions);
|
|
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, "image/png");
|
|
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("");
|
|
}
|