Bug 1636508 - Add options to select captureTab area and scale r=robwu,geckoview-reviewers,agi

Also fix test for HiDPI monitors, and refactor it to remove duplicate code for captureVisibleTab.

Differential Revision: https://phabricator.services.mozilla.com/D89128
This commit is contained in:
Tomislav Jovanovic 2020-09-10 19:38:17 +00:00
parent cfd0790e49
commit c99e9821ef
8 changed files with 110 additions and 254 deletions

View File

@ -1120,7 +1120,7 @@
{
"name": "captureTab",
"type": "function",
"description": "Captures the visible area of a specified tab. You must have $(topic:declare_permissions)[<all_urls>] permission to use this method.",
"description": "Captures an area of a specified tab. You must have $(topic:declare_permissions)[<all_urls>] permission to use this method.",
"permissions": ["<all_urls>"],
"async": true,
"parameters": [
@ -1141,7 +1141,7 @@
{
"name": "captureVisibleTab",
"type": "function",
"description": "Captures the visible area of the currently active tab in the specified window. You must have $(topic:declare_permissions)[&lt;all_urls&gt;] permission to use this method.",
"description": "Captures an area of the currently active tab in the specified window. You must have $(topic:declare_permissions)[&lt;all_urls&gt;] permission to use this method.",
"permissions": ["<all_urls>"],
"async": "callback",
"parameters": [

View File

@ -219,7 +219,6 @@ skip-if = !e10s || debug || asan || (os == "win" && processor == "aarch64") # wi
[browser_ext_tabs_attention.js]
[browser_ext_tabs_audio.js]
[browser_ext_tabs_captureTab.js]
[browser_ext_tabs_captureVisibleTab.js]
[browser_ext_tabs_create.js]
skip-if = os == "linux" && debug && bits == 32 # Bug 1350189
[browser_ext_tabs_create_url.js]

View File

@ -2,24 +2,26 @@
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
async function runTest({ html, fullZoom = 1, coords }) {
async function runTest({ html, fullZoom, coords, rect, scale }) {
let url = `data:text/html,${encodeURIComponent(html)}#scroll`;
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url, true);
tab.linkedBrowser.fullZoom = fullZoom;
tab.linkedBrowser.fullZoom = fullZoom ?? 1;
async function background(coords) {
async function background({ coords, rect, scale, method }) {
try {
let [tab] = await browser.tabs.query({
currentWindow: true,
active: true,
});
let id = method === "captureVisibleTab" ? tab.windowId : tab.id;
let [jpeg, png, ...pngs] = await Promise.all([
browser.tabs.captureTab(tab.id, { format: "jpeg", quality: 95 }),
browser.tabs.captureTab(tab.id, { format: "png", quality: 95 }),
browser.tabs.captureTab(tab.id, { quality: 95 }),
browser.tabs.captureTab(tab.id),
browser.tabs[method](id, { format: "jpeg", quality: 95, rect, scale }),
browser.tabs[method](id, { format: "png", quality: 95, rect, scale }),
browser.tabs[method](id, { quality: 95, rect, scale }),
browser.tabs[method](id, { rect, scale }),
]);
browser.test.assertTrue(
@ -45,6 +47,9 @@ async function runTest({ html, fullZoom = 1, coords }) {
})
);
let width = (rect?.width ?? tab.width) * (scale ?? devicePixelRatio);
let height = (rect?.height ?? tab.height) * (scale ?? devicePixelRatio);
[jpeg, png] = await Promise.all(promises);
let images = { jpeg, png };
for (let format of Object.keys(images)) {
@ -52,12 +57,12 @@ async function runTest({ html, fullZoom = 1, coords }) {
// WGP.drawSnapshot() deals in int coordinates, and rounds down.
browser.test.assertTrue(
Math.abs(tab.width - img.width) <= 1,
`${format} ok image width: ${img.width}, from a tab: ${tab.width}`
Math.abs(width - img.width) <= 1,
`${format} ok image width: ${img.width}, expected: ${width}`
);
browser.test.assertTrue(
Math.abs(tab.height - img.height) <= 1,
`${format} ok image height ${img.height}, from a tab: ${tab.height}`
Math.abs(height - img.height) <= 1,
`${format} ok image height ${img.height}, expected: ${height}`
);
let canvas = document.createElement("canvas");
@ -117,24 +122,27 @@ async function runTest({ html, fullZoom = 1, coords }) {
}
}
let extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["<all_urls>"],
},
for (let method of ["captureTab", "captureVisibleTab"]) {
let options = { coords, rect, scale, method };
info(`Testing configuration: ${JSON.stringify(options)}`);
background: `(${background})(${JSON.stringify(coords)})`,
});
let extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["<all_urls>"],
},
await extension.startup();
background: `(${background})(${JSON.stringify(options)})`,
});
await extension.awaitFinish("captureTab");
await extension.unload();
await extension.startup();
await extension.awaitFinish("captureTab");
await extension.unload();
}
BrowserTestUtils.removeTab(tab);
}
function testEdgeToEdge({ color, fullZoom }) {
async function testEdgeToEdge({ color, fullZoom }) {
let neutral = [0xaa, 0xaa, 0xaa];
let html = `
@ -162,28 +170,27 @@ function testEdgeToEdge({ color, fullZoom }) {
];
info(`Test edge to edge color ${color} at fullZoom=${fullZoom}`);
return runTest({ html, fullZoom, coords });
await runTest({ html, fullZoom, coords });
}
add_task(async function testCaptureEdgeToEdge() {
await testEdgeToEdge({ color: [0, 0, 0], fullZoom: 1 });
await testEdgeToEdge({ color: [0, 0, 0], fullZoom: 2 });
await testEdgeToEdge({ color: [0, 0, 0], fullZoom: 0.5 });
await testEdgeToEdge({ color: [255, 255, 255], fullZoom: 1 });
});
const tallDoc = `<!DOCTYPE html>
<meta charset=utf-8>
<div style="background: yellow; width: 50%; height: 500px;"></div>
<div id=scroll style="background: red; width: 25%; height: 5000px;"></div>
Opened with the #scroll fragment, scrolls the div ^ into view.
`;
// Test currently visible viewport is captured if scrolling is involved.
add_task(async function testScrolledViewport() {
await runTest({
html: `<!DOCTYPE html>
<meta charset=utf-8>
<div style="background: yellow; width: 50%; height: 500px;"></div>
<div id=scroll style="background: red; width: 25%; height: 5000px;"></div>
Opened with the #scroll fragment, scrolls the div ^ into view.
`,
html: tallDoc,
coords: [
{ x: 50, y: 50, color: [255, 0, 0] },
{ x: 50, y: -50, color: [255, 0, 0] },
@ -192,6 +199,21 @@ add_task(async function testScrolledViewport() {
});
});
// Test rect and scale options.
add_task(async function testRectAndScale() {
await runTest({
html: tallDoc,
rect: { x: 50, y: 50, width: 10, height: 1000 },
scale: 4,
coords: [
{ x: 0, y: 0, color: [255, 255, 0] },
{ x: -1, y: 0, color: [255, 255, 0] },
{ x: 0, y: -1, color: [255, 0, 0] },
{ x: -1, y: -1, color: [255, 0, 0] },
],
});
});
// Test OOP iframes are captured, for Fission compatibility.
add_task(async function testOOPiframe() {
await runTest({
@ -224,8 +246,27 @@ add_task(async function testCaptureTabPermissions() {
});
await extension.startup();
await extension.awaitFinish("captureTabPermissions");
await extension.unload();
});
add_task(async function testCaptureVisibleTabPermissions() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["tabs"],
},
background() {
browser.test.assertEq(
undefined,
browser.tabs.captureVisibleTab,
'Extension without "<all_urls>" permission should not have access to captureVisibleTab'
);
browser.test.notifyPass("captureVisibleTabPermissions");
},
});
await extension.startup();
await extension.awaitFinish("captureVisibleTabPermissions");
await extension.unload();
});

View File

@ -1,206 +0,0 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
async function runTest(options) {
options.neutral = [0xaa, 0xaa, 0xaa];
let html = `
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"></head>
<body style="background-color: rgb(${options.color})">
<!-- Fill most of the image with a neutral color to test edge-to-edge scaling. -->
<div style="position: absolute;
left: 2px;
right: 2px;
top: 2px;
bottom: 2px;
background: rgb(${options.neutral});"></div>
</body>
</html>
`;
let url = `data:text/html,${encodeURIComponent(html)}`;
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url, true);
tab.linkedBrowser.fullZoom = options.fullZoom;
async function background(options) {
browser.test.log(
`Test color ${options.color} at fullZoom=${options.fullZoom}`
);
try {
let [tab] = await browser.tabs.query({
currentWindow: true,
active: true,
});
let [jpeg, png, ...pngs] = await Promise.all([
browser.tabs.captureVisibleTab(tab.windowId, {
format: "jpeg",
quality: 95,
}),
browser.tabs.captureVisibleTab(tab.windowId, {
format: "png",
quality: 95,
}),
browser.tabs.captureVisibleTab(tab.windowId, { quality: 95 }),
browser.tabs.captureVisibleTab(tab.windowId),
]);
browser.test.assertTrue(
pngs.every(url => url == png),
"All PNGs are identical"
);
browser.test.assertTrue(
jpeg.startsWith("data:image/jpeg;base64,"),
"jpeg is JPEG"
);
browser.test.assertTrue(
png.startsWith("data:image/png;base64,"),
"png is PNG"
);
let promises = [jpeg, png].map(
url =>
new Promise(resolve => {
let img = new Image();
img.src = url;
img.onload = () => resolve(img);
})
);
[jpeg, png] = await Promise.all(promises);
let images = { jpeg, png };
for (let format of Object.keys(images)) {
let img = images[format];
// WGP.drawSnapshot() deals in int coordinates, and rounds down.
browser.test.assertTrue(
Math.abs(tab.width - img.width) <= 1,
`${format} ok image width: ${img.width}, from a tab: ${tab.width}`
);
browser.test.assertTrue(
Math.abs(tab.height - img.height) <= 1,
`${format} ok image height ${img.height}, from a tab: ${tab.height}`
);
let canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
canvas.mozOpaque = true;
let ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
// Check the colors of the first and last pixels of the image, to make
// sure we capture the entire frame, and scale it correctly.
let coords = [
{ x: 0, y: 0, color: options.color },
{ x: img.width - 1, y: img.height - 1, color: options.color },
{
x: (img.width / 2) | 0,
y: (img.height / 2) | 0,
color: options.neutral,
},
];
for (let { x, y, color } of coords) {
let imageData = ctx.getImageData(x, y, 1, 1).data;
if (format == "png") {
browser.test.assertEq(
`rgba(${color},255)`,
`rgba(${[...imageData]})`,
`${format} image color is correct at (${x}, ${y})`
);
} else {
// Allow for some deviation in JPEG version due to lossy compression.
const SLOP = 3;
browser.test.log(
`Testing ${format} image color at (${x}, ${y}), have rgba(${[
...imageData,
]}), expecting approx. rgba(${color},255)`
);
browser.test.assertTrue(
Math.abs(color[0] - imageData[0]) <= SLOP,
`${format} image color.red is correct at (${x}, ${y})`
);
browser.test.assertTrue(
Math.abs(color[1] - imageData[1]) <= SLOP,
`${format} image color.green is correct at (${x}, ${y})`
);
browser.test.assertTrue(
Math.abs(color[2] - imageData[2]) <= SLOP,
`${format} image color.blue is correct at (${x}, ${y})`
);
browser.test.assertEq(
255,
imageData[3],
`${format} image color.alpha is correct at (${x}, ${y})`
);
}
}
}
browser.test.notifyPass("captureVisibleTab");
} catch (e) {
browser.test.fail(`Error: ${e} :: ${e.stack}`);
browser.test.notifyFail("captureVisibleTab");
}
}
let extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["<all_urls>"],
},
background: `(${background})(${JSON.stringify(options)})`,
});
await extension.startup();
await extension.awaitFinish("captureVisibleTab");
await extension.unload();
BrowserTestUtils.removeTab(tab);
}
add_task(async function testCaptureVisibleTab() {
await runTest({ color: [0, 0, 0], fullZoom: 1 });
await runTest({ color: [0, 0, 0], fullZoom: 2 });
await runTest({ color: [0, 0, 0], fullZoom: 0.5 });
await runTest({ color: [255, 255, 255], fullZoom: 1 });
});
add_task(async function testCaptureVisibleTabPermissions() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["tabs"],
},
background() {
browser.test.assertEq(
undefined,
browser.tabs.captureVisibleTab,
'Extension without "<all_urls>" permission should not have access to captureVisibleTab'
);
browser.test.notifyPass("captureVisibleTabPermissions");
},
});
await extension.startup();
await extension.awaitFinish("captureVisibleTabPermissions");
await extension.unload();
});

View File

@ -836,7 +836,7 @@
{
"name": "captureVisibleTab",
"type": "function",
"description": "Captures the visible area of the currently active tab in the specified window. You must have $(topic:declare_permissions)[&lt;all_urls&gt;] permission to use this method.",
"description": "Captures an area of the currently active tab in the specified window. You must have $(topic:declare_permissions)[&lt;all_urls&gt;] permission to use this method.",
"permissions": ["<all_urls>"],
"async": "callback",
"parameters": [

View File

@ -70,11 +70,11 @@ function* runTest(options) {
// WGP.drawSnapshot() deals in int coordinates, and rounds down.
browser.test.assertTrue(
Math.abs(tab.width - img.width) <= 1,
Math.abs(tab.width * devicePixelRatio - img.width) <= 1,
`${format} ok image width: ${img.width}, from a tab: ${tab.width}`
);
browser.test.assertTrue(
Math.abs(tab.height - img.height) <= 1,
Math.abs(tab.height * devicePixelRatio - img.height) <= 1,
`${format} ok image height ${img.height}, from a tab: ${tab.height}`
);

View File

@ -119,18 +119,24 @@ class TabBase {
* @param {integer} [options.quality = 92]
* The quality at which to encode the captured image data, ranging from
* 0 to 100. Has no effect for the "png" format.
*
* @param {DOMRectInit} [options.rect]
* Area of the document to render, in CSS pixels, relative to the page.
* If null, the currently visible viewport is rendered.
* @param {number} [options.scale]
* The scale to render at, defaults to devicePixelRatio.
* @returns {Promise<string>}
*/
async capture(context, options = null) {
let { ZoomManager, devicePixelRatio } = this.nativeTab.ownerGlobal;
let scale = ZoomManager.getZoomForBrowser(this.browser) * devicePixelRatio;
async capture(context, options) {
let win = this.nativeTab.ownerGlobal;
let scale = options?.scale || win.devicePixelRatio;
let zoom = win.ZoomManager.getZoomForBrowser(this.browser);
let rect = options?.rect && win.DOMRect.fromRect(options.rect);
let wgp = this.browsingContext.currentWindowGlobal;
let image = await wgp.drawSnapshot(null, scale, "white");
let image = await wgp.drawSnapshot(rect, scale * zoom, "white");
let win = Services.appShell.hiddenDOMWindow;
let canvas = win.document.createElement("canvas");
let doc = Services.appShell.hiddenDOMWindow.document;
let canvas = doc.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;

View File

@ -16,7 +16,7 @@
{
"id": "ImageDetails",
"type": "object",
"description": "Details about the format and quality of an image.",
"description": "Details about the format, quality, area and scale of the capture.",
"properties": {
"format": {
"$ref": "ImageFormat",
@ -29,6 +29,22 @@
"minimum": 0,
"maximum": 100,
"description": "When format is <code>\"jpeg\"</code>, controls the quality of the resulting image. This value is ignored for PNG images. As quality is decreased, the resulting image will have more visual artifacts, and the number of bytes needed to store it will decrease."
},
"rect": {
"type": "object",
"optional": true,
"description": "The area of the document to capture, in CSS pixels, relative to the page. If omitted, capture the visible viewport.",
"properties": {
"x": {"type": "number"},
"y": {"type": "number"},
"width": {"type": "number"},
"height": {"type": "number"}
}
},
"scale": {
"type": "number",
"optional": true,
"description": "The scale of the resulting image. Defaults to <code>devicePixelRatio</code>."
}
}
},