Bug 766661 - Attempt to prevent screenshot failures and warn user on errors r=nchevobbe

The changes made here are meant to make the screenshot code in DevTools
closer to how the Firefox Screenshots addon works.

1. It cuts off large images to 10000x10000
2. It reduces drp to 1 for fullpage images

When those things happen, a warning is logged in the content console so
the user is aware that they did happen.

Finally, because there are still cases when taking a screenshot could fail,
an error is logged in the content console when this happens.

Differential Revision: https://phabricator.services.mozilla.com/D62945
This commit is contained in:
Patrick Brosset 2020-04-27 15:38:05 +00:00
Родитель a2b2871573
Коммит 8f3092b436
7 изменённых файлов: 302 добавлений и 96 удалений

Просмотреть файл

@ -135,6 +135,8 @@ skip-if = (os == "win" && processor == "aarch64") # disabled on aarch64 due to 1
[browser_jsterm_screenshot_command_clipboard.js]
[browser_jsterm_screenshot_command_user.js]
[browser_jsterm_screenshot_command_file.js]
[browser_jsterm_screenshot_command_warnings.js]
skip-if = (os == "win" && os_version == "6.1") # Getting the clipboard image dimensions throws an exception
[browser_jsterm_selfxss.js]
[browser_jsterm_syntax_highlight_output.js]
skip-if = (os == "win" && processor == "aarch64") # disabled on aarch64 due to 1531574

Просмотреть файл

@ -173,84 +173,3 @@ async function getContentSize() {
info(`content size: ${contentSize.innerWidth}x${contentSize.innerHeight}`);
return contentSize;
}
async function getImageSizeFromClipboard() {
const clipid = Ci.nsIClipboard;
const clip = Cc["@mozilla.org/widget/clipboard;1"].getService(clipid);
const trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
Ci.nsITransferable
);
const flavor = "image/png";
trans.init(null);
trans.addDataFlavor(flavor);
clip.getData(trans, clipid.kGlobalClipboard);
const data = {};
trans.getTransferData(flavor, data);
ok(data.value, "screenshot exists");
let image = data.value;
// Due to the differences in how images could be stored in the clipboard the
// checks below are needed. The clipboard could already provide the image as
// byte streams or as image container. If it's not possible obtain a
// byte stream, the function throws.
if (image instanceof Ci.imgIContainer) {
image = Cc["@mozilla.org/image/tools;1"]
.getService(Ci.imgITools)
.encodeImage(image, flavor);
}
if (!(image instanceof Ci.nsIInputStream)) {
throw new Error("Unable to read image data");
}
const binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
Ci.nsIBinaryInputStream
);
binaryStream.setInputStream(image);
const available = binaryStream.available();
const buffer = new ArrayBuffer(available);
is(
binaryStream.readArrayBuffer(available, buffer),
available,
"Read expected amount of data"
);
// We are going to load the image in the content page to measure its size.
// We don't want to insert the image directly in the browser's document
// (which is value of the global `document` here). Doing so might push the
// toolbox upwards, shrink the content page and fail the fullpage screenshot
// test.
return SpecialPowers.spawn(gBrowser.selectedBrowser, [buffer], async function(
_buffer
) {
const img = content.document.createElement("img");
const loaded = new Promise(r => {
img.addEventListener("load", r, { once: true });
});
// Build a URL from the buffer passed to the ContentTask
const url = content.URL.createObjectURL(
new Blob([_buffer], { type: "image/png" })
);
// Load the image
img.src = url;
content.document.documentElement.appendChild(img);
info("Waiting for the clipboard image to load in the content page");
await loaded;
// Remove the image and revoke the URL.
img.remove();
content.URL.revokeObjectURL(url);
return {
width: img.width,
height: img.height,
};
});
}

Просмотреть файл

@ -0,0 +1,114 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// Test that screenshot command leads to the proper warning and error messages in the
// console when necessary.
"use strict";
// We create a very big page here in order to make the :screenshot command fail on
// purpose.
const TEST_URI = `data:text/html;charset=utf8,
<style>
body { margin:0; }
.big { width:20000px; height:20000px; }
.small { width:5px; height:5px; }
</style>
<div class="big"></div>
<div class="small"></div>`;
add_task(async function() {
await addTab(TEST_URI);
const hud = await openConsole();
ok(hud, "web console opened");
await testTruncationWarning(hud);
await testDPRWarning(hud);
await testErrorMessage(hud);
});
async function testTruncationWarning(hud) {
info("Check that large screenshots get cut off if necessary");
let onMessages = waitForMessages({
hud,
messages: [
{ text: "Screenshot copied to clipboard." },
{
text:
"The image was cut off to 10000×10000 as the resulting image was too large",
},
],
});
// Note, we put the screenshot in the clipboard so we can easily measure the resulting
// image. We also pass --dpr 1 so we don't need to worry about different machines having
// different screen resolutions.
execute(hud, ":screenshot --clipboard --selector .big --dpr 1");
await onMessages;
let { width, height } = await getImageSizeFromClipboard();
is(width, 10000, "The resulting image is 10000px wide");
is(height, 10000, "The resulting image is 10000px high");
onMessages = waitForMessages({
hud,
messages: [{ text: "Screenshot copied to clipboard." }],
});
execute(hud, ":screenshot --clipboard --selector .small --dpr 1");
await onMessages;
({ width, height } = await getImageSizeFromClipboard());
is(width, 5, "The resulting image is 5px wide");
is(height, 5, "The resulting image is 5px high");
}
async function testDPRWarning(hud) {
info("Check that fullpage screenshots are taken at dpr 1");
// This is only relevant on machines that actually have a dpr that's higher than 1. If
// the current test machine already has a dpr of 1, then the command won't change it and
// no warning will be displayed in the console.
const machineDPR = await getMachineDPR();
if (machineDPR <= 1) {
info("This machine already has a dpr of 1, no need to test this");
return;
}
const onMessages = waitForMessages({
hud,
messages: [
{ text: "Screenshot copied to clipboard." },
{
text:
"The image was cut off to 10000×10000 as the resulting image was too large",
},
{
text:
"The device pixel ratio was reduced to 1 as the resulting image was too large",
},
],
});
execute(hud, ":screenshot --clipboard --fullpage");
await onMessages;
ok(true, "Expected messages were displayed");
}
async function testErrorMessage(hud) {
info("Check that when a screenshot fails, an error message is displayed");
await executeAndWaitForMessage(
hud,
":screenshot --clipboard --dpr 1000",
"Error creating the image. The resulting image was probably too large."
);
}
function getMachineDPR() {
return SpecialPowers.spawn(
gBrowser.selectedBrowser,
[],
() => content.devicePixelRatio
);
}

Просмотреть файл

@ -1802,3 +1802,89 @@ function selectTargetInContextSelector(hud, targetLabel) {
itemToSelect.click();
}
/**
* A helper that returns the size of the image that was just put into the clipboard by the
* :screenshot command.
* @return The {width, height} dimension object.
*/
async function getImageSizeFromClipboard() {
const clipid = Ci.nsIClipboard;
const clip = Cc["@mozilla.org/widget/clipboard;1"].getService(clipid);
const trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
Ci.nsITransferable
);
const flavor = "image/png";
trans.init(null);
trans.addDataFlavor(flavor);
clip.getData(trans, clipid.kGlobalClipboard);
const data = {};
trans.getTransferData(flavor, data);
ok(data.value, "screenshot exists");
let image = data.value;
// Due to the differences in how images could be stored in the clipboard the
// checks below are needed. The clipboard could already provide the image as
// byte streams or as image container. If it's not possible obtain a
// byte stream, the function throws.
if (image instanceof Ci.imgIContainer) {
image = Cc["@mozilla.org/image/tools;1"]
.getService(Ci.imgITools)
.encodeImage(image, flavor);
}
if (!(image instanceof Ci.nsIInputStream)) {
throw new Error("Unable to read image data");
}
const binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
Ci.nsIBinaryInputStream
);
binaryStream.setInputStream(image);
const available = binaryStream.available();
const buffer = new ArrayBuffer(available);
is(
binaryStream.readArrayBuffer(available, buffer),
available,
"Read expected amount of data"
);
// We are going to load the image in the content page to measure its size.
// We don't want to insert the image directly in the browser's document
// (which is value of the global `document` here). Doing so might push the
// toolbox upwards, shrink the content page and fail the fullpage screenshot
// test.
return SpecialPowers.spawn(gBrowser.selectedBrowser, [buffer], async function(
_buffer
) {
const img = content.document.createElement("img");
const loaded = new Promise(r => {
img.addEventListener("load", r, { once: true });
});
// Build a URL from the buffer passed to the ContentTask
const url = content.URL.createObjectURL(
new Blob([_buffer], { type: "image/png" })
);
// Load the image
img.src = url;
content.document.documentElement.appendChild(img);
info("Waiting for the clipboard image to load in the content page");
await loaded;
// Remove the image and revoke the URL.
img.remove();
content.URL.revokeObjectURL(url);
return {
width: img.width,
height: img.height,
};
});
}

Просмотреть файл

@ -114,3 +114,20 @@ inspectNodeDesc=CSS selector
# parameter to the 'inspect' command. Displayed when the --help flag is
# passed to the `screenshot command.
inspectNodeManual=A CSS selector for use with document.querySelector which identifies a single element
# LOCALIZATION NOTE (screenshotTruncationWarning) Text displayed to user when the image
# that would be created by the screenshot is too big and needs to be truncated to avoid
# errors.
# The first parameter is the width of the final image and the second parameter is the
# height of the image.
screenshotTruncationWarning=The image was cut off to %1$S×%2$S as the resulting image was too large
# LOCALIZATION NOTE (screenshotDPRDecreasedWarning) Text displayed to user when the
# screenshot they want to take is for the full page and the Device Pixel Ratio is
# decreased to 1, to avoid creating images that are too big which may cause errors.
screenshotDPRDecreasedWarning=The device pixel ratio was reduced to 1 as the resulting image was too large
# LOCALIZATION NOTE (screenshotRenderingError) Text displayed to user upon
# encountering an error while rendering the screenshot. This most often happens when the
# resulting image is too large to be rendered.
screenshotRenderingError=Error creating the image. The resulting image was probably too large.

Просмотреть файл

@ -3,14 +3,22 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { Cu } = require("chrome");
const { Cu, Cc, Ci } = require("chrome");
const Services = require("Services");
const { LocalizationHelper } = require("devtools/shared/l10n");
loader.lazyRequireGetter(this, "getRect", "devtools/shared/layout/utils", true);
const CONTAINER_FLASHING_DURATION = 500;
const STRINGS_URI = "devtools/shared/locales/screenshot.properties";
const L10N = new LocalizationHelper(STRINGS_URI);
loader.lazyRequireGetter(this, "getRect", "devtools/shared/layout/utils", true);
// 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/build/buildSettings.js
const MAX_IMAGE_WIDTH = 10000;
const MAX_IMAGE_HEIGHT = 10000;
/**
* This function is called to simulate camera effects
@ -88,36 +96,66 @@ function createScreenshotDataURL(document, args) {
height -= scrollbarHeight.value;
}
let ratio;
if (args.fullpage) {
// Always take fullpage screenshots at dpr=1 to avoid failures.
ratio = 1;
// Warn the user if they had provided a higher dpr or have a high resolution monitor.
if ((args.dpr && args.dpr > 1) || window.devicePixelRatio > 1) {
logWarningInPage(L10N.getStr("screenshotDPRDecreasedWarning"), window);
}
} else {
ratio = args.dpr ? args.dpr : window.devicePixelRatio;
}
// Truncate the width and height if necessary.
if (width > MAX_IMAGE_WIDTH || height > MAX_IMAGE_HEIGHT) {
width = Math.min(width, MAX_IMAGE_WIDTH);
height = Math.min(height, MAX_IMAGE_HEIGHT);
logWarningInPage(
L10N.getFormatStr("screenshotTruncationWarning", width, height),
window
);
}
const canvas = document.createElementNS(
"http://www.w3.org/1999/xhtml",
"canvas"
);
const ctx = canvas.getContext("2d");
const ratio = args.dpr ? args.dpr : window.devicePixelRatio;
canvas.width = width * ratio;
canvas.height = height * ratio;
ctx.scale(ratio, ratio);
ctx.drawWindow(window, left, top, width, height, "#fff");
const data = canvas.toDataURL("image/png", "");
// 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.
let data = null;
try {
canvas.width = width * ratio;
canvas.height = height * ratio;
ctx.scale(ratio, ratio);
ctx.drawWindow(window, left, top, width, height, "#fff");
data = canvas.toDataURL("image/png", "");
} catch (e) {
logErrorInPage(L10N.getStr("screenshotRenderingError"), window);
}
// See comment above on bug 961832
if (args.fullpage) {
window.scrollTo(currentX, currentY);
}
simulateCameraFlash(document);
if (data) {
simulateCameraFlash(document);
}
return Promise.resolve({
destinations: [],
data: data,
height: height,
width: width,
filename: filename,
data,
height,
width,
filename,
});
}
exports.createScreenshotDataURL = createScreenshotDataURL;
/**
* We may have a filename specified in args, or we might have to generate
* one.
@ -150,3 +188,23 @@ function getFilename(defaultName) {
".png"
);
}
function logInPage(text, flags, window) {
const scriptError = Cc["@mozilla.org/scripterror;1"].createInstance(
Ci.nsIScriptError
);
scriptError.initWithWindowID(
text,
null,
null,
0,
0,
flags,
"screenshot",
window.windowUtils.currentInnerWindowID
);
Services.console.logMessage(scriptError);
}
const logErrorInPage = (text, window) => logInPage(text, 0, window);
const logWarningInPage = (text, window) => logInPage(text, 1, window);

Просмотреть файл

@ -116,6 +116,11 @@ function getFormattedHelpData() {
* Response messages from processing the screenshot
*/
function saveScreenshot(window, args = {}, value) {
// Guard against missing image data.
if (!value.data) {
return [];
}
if (args.help) {
const message = getFormattedHelpData();
// Wrap message in an array so that the return value is consistant with save
@ -225,6 +230,11 @@ function saveToClipboard(base64URI) {
async function saveToFile(image) {
let filename = image.filename;
// Guard against missing image data.
if (!image.data) {
return "";
}
// Check there is a .png extension to filename
if (!filename.match(/.png$/i)) {
filename += ".png";