From c99e9821ef2fa670300f2ea66d0f0af5f835bde1 Mon Sep 17 00:00:00 2001 From: Tomislav Jovanovic Date: Thu, 10 Sep 2020 19:38:17 +0000 Subject: [PATCH] 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 --- .../components/extensions/schemas/tabs.json | 4 +- .../extensions/test/browser/browser.ini | 1 - .../browser/browser_ext_tabs_captureTab.js | 109 ++++++--- .../browser_ext_tabs_captureVisibleTab.js | 206 ------------------ .../components/extensions/schemas/tabs.json | 2 +- .../test_ext_tabs_captureVisibleTab.html | 4 +- .../extensions/parent/ext-tabs-base.js | 20 +- .../extensions/schemas/extension_types.json | 18 +- 8 files changed, 110 insertions(+), 254 deletions(-) delete mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_captureVisibleTab.js diff --git a/browser/components/extensions/schemas/tabs.json b/browser/components/extensions/schemas/tabs.json index 54404a694c3c..b14ed7b92481 100644 --- a/browser/components/extensions/schemas/tabs.json +++ b/browser/components/extensions/schemas/tabs.json @@ -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": [""], "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)[<all_urls>] permission to use this method.", + "description": "Captures an area of the currently active tab in the specified window. You must have $(topic:declare_permissions)[<all_urls>] permission to use this method.", "permissions": [""], "async": "callback", "parameters": [ diff --git a/browser/components/extensions/test/browser/browser.ini b/browser/components/extensions/test/browser/browser.ini index ab5392bffefa..c1f167b2c4ae 100644 --- a/browser/components/extensions/test/browser/browser.ini +++ b/browser/components/extensions/test/browser/browser.ini @@ -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] diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_captureTab.js b/browser/components/extensions/test/browser/browser_ext_tabs_captureTab.js index 3538d8011cda..5165dcc8736a 100644 --- a/browser/components/extensions/test/browser/browser_ext_tabs_captureTab.js +++ b/browser/components/extensions/test/browser/browser_ext_tabs_captureTab.js @@ -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: [""], - }, + 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: [""], + }, - 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 = ` + +
+
+ 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: ` - -
-
- 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 "" permission should not have access to captureVisibleTab' + ); + browser.test.notifyPass("captureVisibleTabPermissions"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("captureVisibleTabPermissions"); await extension.unload(); }); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_captureVisibleTab.js b/browser/components/extensions/test/browser/browser_ext_tabs_captureVisibleTab.js deleted file mode 100644 index 6288ffc1ae1a..000000000000 --- a/browser/components/extensions/test/browser/browser_ext_tabs_captureVisibleTab.js +++ /dev/null @@ -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 = ` - - - - - -
- - - `; - - 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: [""], - }, - - 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 "" permission should not have access to captureVisibleTab' - ); - browser.test.notifyPass("captureVisibleTabPermissions"); - }, - }); - - await extension.startup(); - - await extension.awaitFinish("captureVisibleTabPermissions"); - - await extension.unload(); -}); diff --git a/mobile/android/components/extensions/schemas/tabs.json b/mobile/android/components/extensions/schemas/tabs.json index 5a53a37f74d1..f279e127412e 100644 --- a/mobile/android/components/extensions/schemas/tabs.json +++ b/mobile/android/components/extensions/schemas/tabs.json @@ -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)[<all_urls>] permission to use this method.", + "description": "Captures an area of the currently active tab in the specified window. You must have $(topic:declare_permissions)[<all_urls>] permission to use this method.", "permissions": [""], "async": "callback", "parameters": [ diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_captureVisibleTab.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_captureVisibleTab.html index 104228b4f9f8..630b8bcfe86d 100644 --- a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_captureVisibleTab.html +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_captureVisibleTab.html @@ -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}` ); diff --git a/toolkit/components/extensions/parent/ext-tabs-base.js b/toolkit/components/extensions/parent/ext-tabs-base.js index 1c92fb2c8a11..42beed785663 100644 --- a/toolkit/components/extensions/parent/ext-tabs-base.js +++ b/toolkit/components/extensions/parent/ext-tabs-base.js @@ -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} */ - 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; diff --git a/toolkit/components/extensions/schemas/extension_types.json b/toolkit/components/extensions/schemas/extension_types.json index 43f7b8dfdb99..e94ecd9e0e62 100644 --- a/toolkit/components/extensions/schemas/extension_types.json +++ b/toolkit/components/extensions/schemas/extension_types.json @@ -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 \"jpeg\", 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 devicePixelRatio." } } },