From 3dce432d4f459d2ef643465cca085e14237dc355 Mon Sep 17 00:00:00 2001 From: Henrik Skupin Date: Wed, 16 Oct 2019 19:36:56 +0000 Subject: [PATCH] Bug 1563746 - [remote] Implement Page.captureScreenshot. r=remote-protocol-reviewers,maja_zf,ato Differential Revision: https://phabricator.services.mozilla.com/D49203 --HG-- extra : moz-landing-system : lando --- remote/domains/content/Page.jsm | 9 ++ remote/domains/parent/Page.jsm | 85 ++++++++++++++++++ remote/test/browser/browser.ini | 1 + .../browser/browser_page_captureScreenshot.js | 89 +++++++++++++++++++ 4 files changed, 184 insertions(+) create mode 100644 remote/test/browser/browser_page_captureScreenshot.js diff --git a/remote/domains/content/Page.jsm b/remote/domains/content/Page.jsm index 599239c9e7ef..ccdd691b2ecf 100644 --- a/remote/domains/content/Page.jsm +++ b/remote/domains/content/Page.jsm @@ -58,6 +58,15 @@ class Page extends ContentProcessDomain { } } + _viewportRect() { + return new DOMRect( + this.content.pageXOffset, + this.content.pageYOffset, + this.content.innerWidth, + this.content.innerHeight + ); + } + async navigate({ url, referrer, transitionType, frameId } = {}) { if (frameId && frameId != this.content.windowUtils.outerWindowID) { throw new UnsupportedError("frameId not supported"); diff --git a/remote/domains/parent/Page.jsm b/remote/domains/parent/Page.jsm index 4249a05d9e1b..9315a719f817 100644 --- a/remote/domains/parent/Page.jsm +++ b/remote/domains/parent/Page.jsm @@ -12,6 +12,9 @@ const { DialogHandler } = ChromeUtils.import( const { Domain } = ChromeUtils.import( "chrome://remote/content/domains/Domain.jsm" ); +const { UnsupportedError } = ChromeUtils.import( + "chrome://remote/content/Error.jsm" +); class Page extends Domain { constructor(session) { @@ -32,6 +35,88 @@ class Page extends Domain { // commands + /** + * Capture page screenshot. + * + * @param {Object} options + * @param {Viewport=} options.clip (not supported) + * Capture the screenshot of a given region only. + * @param {string=} options.format (not supported) + * Image compression format. Defaults to "png". + * @param {number=} options.quality (not supported) + * Compression quality from range [0..100] (jpeg only). Defaults to 100. + * + * @return {string} + * Base64-encoded image data. + */ + async captureScreenshot(options = {}) { + if (options.clip) { + throw new UnsupportedError("clip not supported"); + } + if (options.format) { + throw new UnsupportedError("format not supported"); + } + if (options.fromSurface) { + throw new UnsupportedError("fromSurface not supported"); + } + if (options.quality) { + throw new UnsupportedError("quality not supported"); + } + + const MAX_CANVAS_DIMENSION = 32767; + const MAX_CANVAS_AREA = 472907776; + + // Retrieve the browsing context of the content browser + const { browsingContext, window } = this.session.target; + const scale = window.devicePixelRatio; + + const rect = await this.executeInChild("_viewportRect"); + + let canvasWidth = rect.width * scale; + let canvasHeight = rect.height * scale; + + // Cap the screenshot size based on maximum allowed canvas sizes. + // Using higher dimensions would trigger exceptions in Gecko. + // + // See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#Maximum_canvas_size + if (canvasWidth > MAX_CANVAS_DIMENSION) { + rect.width = Math.floor(MAX_CANVAS_DIMENSION / scale); + canvasWidth = rect.width * scale; + } + if (canvasHeight > MAX_CANVAS_DIMENSION) { + rect.height = Math.floor(MAX_CANVAS_DIMENSION / scale); + canvasHeight = rect.height * scale; + } + // If the area is larger, reduce the height to keep the full width. + if (canvasWidth * canvasHeight > MAX_CANVAS_AREA) { + rect.height = Math.floor(MAX_CANVAS_AREA / (canvasWidth * scale)); + canvasHeight = rect.height * scale; + } + + const snapshot = await browsingContext.currentWindowGlobal.drawSnapshot( + rect, + scale, + "rgb(255,255,255)" + ); + + const canvas = window.document.createElementNS( + "http://www.w3.org/1999/xhtml", + "canvas" + ); + canvas.width = canvasWidth; + canvas.height = canvasHeight; + + const ctx = canvas.getContext("2d"); + ctx.drawImage(snapshot, 0, 0); + + // Bug 1574935 - Huge dimensions can trigger an OOM because multiple copies + // of the bitmap will exist in memory. Force the removal of the snapshot + // because it is no longer needed. + snapshot.close(); + + return canvas.toDataURL(); + } + async enable() { if (this.enabled) { return; diff --git a/remote/test/browser/browser.ini b/remote/test/browser/browser.ini index 6cb361e74dc1..acff10388fb5 100644 --- a/remote/test/browser/browser.ini +++ b/remote/test/browser/browser.ini @@ -16,6 +16,7 @@ support-files = [browser_main_target.js] [browser_network_requestWillBeSent.js] [browser_page_bringToFront.js] +[browser_page_captureScreenshot.js] [browser_page_frameNavigated.js] [browser_page_frameNavigated_iframe.js] [browser_page_javascriptDialog_alert.js] diff --git a/remote/test/browser/browser_page_captureScreenshot.js b/remote/test/browser/browser_page_captureScreenshot.js new file mode 100644 index 000000000000..a7aaab2ad173 --- /dev/null +++ b/remote/test/browser/browser_page_captureScreenshot.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +async function getDevicePixelRatio() { + return ContentTask.spawn(gBrowser.selectedBrowser, null, function() { + return content.devicePixelRatio; + }); +} + +async function getImageDetails(client, image) { + return ContentTask.spawn(gBrowser.selectedBrowser, image, async function( + image + ) { + let infoPromise = new Promise(resolve => { + const img = new content.Image(); + img.addEventListener( + "load", + () => { + resolve({ + width: img.width, + height: img.height, + }); + }, + { once: true } + ); + img.src = image; + }); + return infoPromise; + }); +} + +async function getViewportRect() { + return ContentTask.spawn(gBrowser.selectedBrowser, null, function() { + return { + left: content.pageXOffset, + top: content.pageYOffset, + width: content.innerWidth, + height: content.innerHeight, + }; + }); +} + +add_task(async function testScreenshotWithDocumentSmallerThanViewport() { + const doc = toDataURL("
Hello world"); + const { client, tab } = await setupForURL(doc); + + const { Page } = client; + info("Check that captureScreenshot() captures the viewport by default"); + const screenshot = await Page.captureScreenshot(); + + const scale = await getDevicePixelRatio(); + const viewportRect = await getViewportRect(); + const { width, height } = await getImageDetails(client, screenshot); + + is(width, (viewportRect.width - viewportRect.left) * scale); + is(height, (viewportRect.height - viewportRect.top) * scale); + + await client.close(); + ok(true, "The client is closed"); + + BrowserTestUtils.removeTab(tab); + + await RemoteAgent.close(); +}); + +add_task(async function testScreenshotWithDocumentLargerThanViewport() { + const doc = toDataURL("
Hello world"); + const { client, tab } = await setupForURL(doc); + + const { Page } = client; + info("Check that captureScreenshot() captures the viewport by default"); + const screenshot = await Page.captureScreenshot(); + + const scale = await getDevicePixelRatio(); + const viewportRect = await getViewportRect(); + const { width, height } = await getImageDetails(client, screenshot); + + is(width, (viewportRect.width - viewportRect.left) * scale); + is(height, (viewportRect.height - viewportRect.top) * scale); + + await client.close(); + ok(true, "The client is closed"); + + BrowserTestUtils.removeTab(tab); + + await RemoteAgent.close(); +});