зеркало из https://github.com/mozilla/gecko-dev.git
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 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
797c55c4d9
Коммит
e2b30dea5c
|
@ -121,6 +121,7 @@ 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]
|
||||
[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:50000px; height:50000px; }
|
||||
.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
|
||||
);
|
||||
}
|
|
@ -1630,3 +1630,89 @@ async function clearOutput(hud, { keepStorage = false } = {}) {
|
|||
ui.clearOutput(!keepStorage);
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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";
|
||||
|
|
Загрузка…
Ссылка в новой задаче