From ccc7783ad33726ae479242262a7064ec785e3c8d Mon Sep 17 00:00:00 2001 From: Tomislav Jovanovic Date: Tue, 25 Aug 2020 11:30:52 +0000 Subject: [PATCH] Bug 1636508 - Make tabs.captureTab compatible with Fission r=mattwoodrow,robwu,geckoview-reviewers,agi Also fix WindowGlobalParent.drawSnapshot() to render the currently visible viewport when called with a null rect, and clarify the webidl comment. Differential Revision: https://phabricator.services.mozilla.com/D87971 --- .../extensions/test/browser/browser.ini | 1 + .../browser/browser_ext_tabs_captureTab.js | 134 +++++++++++------- .../browser_ext_tabs_captureVisibleTab.js | 15 +- .../extensions/test/browser/file_green.html | 3 + dom/chrome-webidl/WindowGlobalActors.webidl | 5 +- dom/ipc/WindowGlobalParent.cpp | 9 +- .../test_ext_tabs_captureVisibleTab.html | 12 +- .../extensions/ExtensionContent.jsm | 35 ----- .../extensions/ExtensionProcessScript.jsm | 1 - .../extensions/parent/ext-tabs-base.js | 48 +++---- 10 files changed, 134 insertions(+), 129 deletions(-) create mode 100644 browser/components/extensions/test/browser/file_green.html diff --git a/browser/components/extensions/test/browser/browser.ini b/browser/components/extensions/test/browser/browser.ini index a0bb887e7acd..ab5392bffefa 100644 --- a/browser/components/extensions/test/browser/browser.ini +++ b/browser/components/extensions/test/browser/browser.ini @@ -36,6 +36,7 @@ support-files = file_title.html file_indexedDB.html file_serviceWorker.html + file_green.html webNav_createdTarget.html webNav_createdTargetSource.html webNav_createdTargetSource_subframe.html 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 6cafafe5490b..3538d8011cda 100644 --- a/browser/components/extensions/test/browser/browser_ext_tabs_captureTab.js +++ b/browser/components/extensions/test/browser/browser_ext_tabs_captureTab.js @@ -2,35 +2,13 @@ /* 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)}`; +async function runTest({ html, fullZoom = 1, coords }) { + let url = `data:text/html,${encodeURIComponent(html)}#scroll`; 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}` - ); + tab.linkedBrowser.fullZoom = fullZoom; + async function background(coords) { try { let [tab] = await browser.tabs.query({ currentWindow: true, @@ -68,17 +46,18 @@ async function runTest(options) { ); [jpeg, png] = await Promise.all(promises); - let tabDims = `${tab.width}\u00d7${tab.height}`; - let images = { jpeg, png }; for (let format of Object.keys(images)) { let img = images[format]; - let dims = `${img.width}\u00d7${img.height}`; - browser.test.assertEq( - tabDims, - dims, - `${format} dimensions are correct` + // 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"); @@ -89,19 +68,9 @@ async function runTest(options) { 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) { + x = (x + img.width) % img.width; + y = (y + img.height) % img.height; let imageData = ctx.getImageData(x, y, 1, 1).data; if (format == "png") { @@ -153,7 +122,7 @@ async function runTest(options) { permissions: [""], }, - background: `(${background})(${JSON.stringify(options)})`, + background: `(${background})(${JSON.stringify(coords)})`, }); await extension.startup(); @@ -165,14 +134,77 @@ async function runTest(options) { BrowserTestUtils.removeTab(tab); } -add_task(async function testCaptureTab() { - await runTest({ color: [0, 0, 0], fullZoom: 1 }); +function testEdgeToEdge({ color, fullZoom }) { + let neutral = [0xaa, 0xaa, 0xaa]; - await runTest({ color: [0, 0, 0], fullZoom: 2 }); + let html = ` + + + + + +
+ + + `; - await runTest({ color: [0, 0, 0], fullZoom: 0.5 }); + // 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 }, + { x: -1, y: -1, color }, + { x: 300, y: 200, color: neutral }, + ]; - await runTest({ color: [255, 255, 255], fullZoom: 1 }); + info(`Test edge to edge color ${color} at fullZoom=${fullZoom}`); + return 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 }); +}); + +// 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. + `, + coords: [ + { x: 50, y: 50, color: [255, 0, 0] }, + { x: 50, y: -50, color: [255, 0, 0] }, + { x: -50, y: -50, color: [255, 255, 255] }, + ], + }); +}); + +// Test OOP iframes are captured, for Fission compatibility. +add_task(async function testOOPiframe() { + await runTest({ + html: ` + + + `, + coords: [ + { x: 50, y: 50, color: [0, 255, 0] }, + { x: 50, y: -50, color: [255, 255, 255] }, + { x: -50, y: 50, color: [255, 255, 255] }, + ], + }); }); add_task(async function testCaptureTabPermissions() { diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_captureVisibleTab.js b/browser/components/extensions/test/browser/browser_ext_tabs_captureVisibleTab.js index d9bf192936c3..6288ffc1ae1a 100644 --- a/browser/components/extensions/test/browser/browser_ext_tabs_captureVisibleTab.js +++ b/browser/components/extensions/test/browser/browser_ext_tabs_captureVisibleTab.js @@ -74,17 +74,18 @@ async function runTest(options) { ); [jpeg, png] = await Promise.all(promises); - let tabDims = `${tab.width}\u00d7${tab.height}`; - let images = { jpeg, png }; for (let format of Object.keys(images)) { let img = images[format]; - let dims = `${img.width}\u00d7${img.height}`; - browser.test.assertEq( - tabDims, - dims, - `${format} dimensions are correct` + // 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"); diff --git a/browser/components/extensions/test/browser/file_green.html b/browser/components/extensions/test/browser/file_green.html new file mode 100644 index 000000000000..20755c5b56ff --- /dev/null +++ b/browser/components/extensions/test/browser/file_green.html @@ -0,0 +1,3 @@ + +Super green test page + diff --git a/dom/chrome-webidl/WindowGlobalActors.webidl b/dom/chrome-webidl/WindowGlobalActors.webidl index c754974a044f..1e6c0d89361b 100644 --- a/dom/chrome-webidl/WindowGlobalActors.webidl +++ b/dom/chrome-webidl/WindowGlobalActors.webidl @@ -81,9 +81,8 @@ interface WindowGlobalParent : WindowContext { /** * Renders a region of the frame into an image bitmap. * - * @param rect Specify the area of the window to render, in CSS pixels. This - * is relative to the current scroll position. If null, the entire viewport - * is rendered. + * @param rect Specify the area of the document to render, in CSS pixels, + * relative to the page. If null, the currently visible viewport is rendered. * @param scale The scale to render the window at. Use devicePixelRatio * to have comparable rendering to the OS. * @param backgroundColor The background color to use. diff --git a/dom/ipc/WindowGlobalParent.cpp b/dom/ipc/WindowGlobalParent.cpp index 2f2ef45c611b..3a5485ff00dd 100644 --- a/dom/ipc/WindowGlobalParent.cpp +++ b/dom/ipc/WindowGlobalParent.cpp @@ -623,8 +623,13 @@ already_AddRefed WindowGlobalParent::DrawSnapshot( return nullptr; } - if (!gfx::CrossProcessPaint::Start(this, aRect, (float)aScale, color, - gfx::CrossProcessPaintFlags::None, + gfx::CrossProcessPaintFlags flags = gfx::CrossProcessPaintFlags::None; + if (!aRect) { + // If no explicit Rect was passed, we want the currently visible viewport. + flags = gfx::CrossProcessPaintFlags::DrawView; + } + + if (!gfx::CrossProcessPaint::Start(this, aRect, (float)aScale, color, flags, promise)) { aRv = NS_ERROR_FAILURE; return nullptr; 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 fb08e7fb0bdd..104228b4f9f8 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 @@ -63,14 +63,20 @@ function* runTest(options) { })); [jpeg, png] = await Promise.all(promises); - const tabDims = `${tab.width}\u00d7${tab.height}`; const images = {jpeg, png}; for (const format of Object.keys(images)) { const img = images[format]; - const dims = `${img.width}\u00d7${img.height}`; - browser.test.assertEq(tabDims, dims, `${format} dimensions are correct`); + // 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}` + ); const canvas = document.createElement("canvas"); canvas.width = img.width; diff --git a/toolkit/components/extensions/ExtensionContent.jsm b/toolkit/components/extensions/ExtensionContent.jsm index d3ba1477aae9..ab1146ae5404 100644 --- a/toolkit/components/extensions/ExtensionContent.jsm +++ b/toolkit/components/extensions/ExtensionContent.jsm @@ -1101,34 +1101,6 @@ var ExtensionContent = { return context; }, - handleExtensionCapture(global, width, height, options) { - let win = global.content; - - const XHTML_NS = "http://www.w3.org/1999/xhtml"; - let canvas = win.document.createElementNS(XHTML_NS, "canvas"); - canvas.width = width; - canvas.height = height; - canvas.mozOpaque = true; - - let ctx = canvas.getContext("2d"); - - // We need to scale the image to the visible size of the browser, - // in order for the result to appear as the user sees it when - // settings like full zoom come into play. - ctx.scale(canvas.width / win.innerWidth, canvas.height / win.innerHeight); - - ctx.drawWindow( - win, - win.scrollX, - win.scrollY, - win.innerWidth, - win.innerHeight, - "#fff" - ); - - return canvas.toDataURL(`image/${options.format}`, options.quality / 100); - }, - handleDetectLanguage(global, target) { let doc = target.content.document; @@ -1217,13 +1189,6 @@ var ExtensionContent = { async receiveMessage(global, name, target, data, recipient) { switch (name) { - case "Extension:Capture": - return this.handleExtensionCapture( - global, - data.width, - data.height, - data.options - ); case "Extension:DetectLanguage": return this.handleDetectLanguage(global, target); case "WebNavigation:GetFrame": diff --git a/toolkit/components/extensions/ExtensionProcessScript.jsm b/toolkit/components/extensions/ExtensionProcessScript.jsm index 81c288b21055..001ac0c8ac4c 100644 --- a/toolkit/components/extensions/ExtensionProcessScript.jsm +++ b/toolkit/components/extensions/ExtensionProcessScript.jsm @@ -69,7 +69,6 @@ class ExtensionGlobal { this.frameData = null; - MessageChannel.addListener(global, "Extension:Capture", this); MessageChannel.addListener(global, "Extension:DetectLanguage", this); MessageChannel.addListener(global, "WebNavigation:GetFrame", this); MessageChannel.addListener(global, "WebNavigation:GetAllFrames", this); diff --git a/toolkit/components/extensions/parent/ext-tabs-base.js b/toolkit/components/extensions/parent/ext-tabs-base.js index 9011cae3e71d..1c92fb2c8a11 100644 --- a/toolkit/components/extensions/parent/ext-tabs-base.js +++ b/toolkit/components/extensions/parent/ext-tabs-base.js @@ -7,16 +7,11 @@ /* globals EventEmitter */ -ChromeUtils.defineModuleGetter( - this, - "PrivateBrowsingUtils", - "resource://gre/modules/PrivateBrowsingUtils.jsm" -); -ChromeUtils.defineModuleGetter( - this, - "Services", - "resource://gre/modules/Services.jsm" -); +XPCOMUtils.defineLazyModuleGetters(this, { + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm", + Services: "resource://gre/modules/Services.jsm", +}); + XPCOMUtils.defineLazyPreferenceGetter( this, "containersEnabled", @@ -112,7 +107,7 @@ class TabBase { } /** - * Capture the visible area of this tab, and return the result as a data: URL. + * Capture the visible area of this tab, and return the result as a data: URI. * * @param {BaseContext} context * The extension context for which to perform the capture. @@ -127,24 +122,23 @@ class TabBase { * * @returns {Promise} */ - capture(context, options = null) { - if (!options) { - options = {}; - } - if (options.format == null) { - options.format = "png"; - } - if (options.quality == null) { - options.quality = 92; - } + async capture(context, options = null) { + let { ZoomManager, devicePixelRatio } = this.nativeTab.ownerGlobal; + let scale = ZoomManager.getZoomForBrowser(this.browser) * devicePixelRatio; - let message = { - options, - width: this.width, - height: this.height, - }; + let wgp = this.browsingContext.currentWindowGlobal; + let image = await wgp.drawSnapshot(null, scale, "white"); - return this.sendMessage(context, "Extension:Capture", message); + let win = Services.appShell.hiddenDOMWindow; + let canvas = win.document.createElement("canvas"); + canvas.width = image.width; + canvas.height = image.height; + + let ctx = canvas.getContext("2d", { alpha: false }); + ctx.drawImage(image, 0, 0); + image.close(); + + return canvas.toDataURL(`image/${options?.format}`, options?.quality / 100); } /**