Bug 800170 - Modify mozbrowser's getScreenshot() so it takes max-width, max-height parameters. r=smaug
authorJustin Lebar <justin.lebar@gmail.com>
Wed, 17 Oct 2012 00:23:08 -0400
changeset 110640 ab6c7006403e163bd62b45e1c297fee9ece5b351
parent 110639 d53bd74897b7a4875fd831c222edbad013e13761
child 110641 9fba5058ab84be2e336debca29feed5672e0d8c7
push id93
push usernmatsakis@mozilla.com
push dateWed, 31 Oct 2012 21:26:57 +0000
reviewerssmaug
bugs800170
milestone19.0a1
Bug 800170 - Modify mozbrowser's getScreenshot() so it takes max-width, max-height parameters. r=smaug
dom/browser-element/BrowserElementChild.js
dom/browser-element/BrowserElementParent.js
dom/browser-element/mochitest/Makefile.in
dom/browser-element/mochitest/browserElement_BadScreenshot.js
dom/browser-element/mochitest/browserElement_DOMRequestError.js
dom/browser-element/mochitest/browserElement_GetScreenshot.js
dom/browser-element/mochitest/browserElement_OpenMixedProcess.js
dom/browser-element/mochitest/browserElement_XFrameOptionsAllowFrom.js
dom/browser-element/mochitest/browserElement_XFrameOptionsDeny.js
dom/browser-element/mochitest/test_browserElement_inproc_BadScreenshot.html
dom/browser-element/mochitest/test_browserElement_oop_BadScreenshot.html
--- a/dom/browser-element/BrowserElementChild.js
+++ b/dom/browser-element/BrowserElementChild.js
@@ -447,30 +447,66 @@ BrowserElementChild.prototype = {
     }
 
     debug("scroll event " + win);
     sendAsyncMsg("scroll", { top: win.scrollY, left: win.scrollX });
   },
 
   _recvGetScreenshot: function(data) {
     debug("Received getScreenshot message: (" + data.json.id + ")");
+
+    // You can think of the screenshotting algorithm as carrying out the
+    // following steps:
+    //
+    // - Let max-width be data.json.args.width, and let max-height be
+    //   data.json.args.height.
+    //
+    // - Let scale-width be the factor by which we'd need to downscale the
+    //   viewport so it would fit within max-width.  (If the viewport's width
+    //   is less than max-width, let scale-width be 1.) Compute scale-height
+    //   the same way.
+    //
+    // - Scale the viewport by max(scale-width, scale-height).  Now either the
+    //   viewport's width is no larger than max-width, the viewport's height is
+    //   no larger than max-height, or both.
+    //
+    // - Crop the viewport so its width is no larger than max-width and its
+    //   height is no larger than max-height.
+    //
+    // - Return a screenshot of the page's viewport scaled and cropped per
+    //   above.
+
+    let maxWidth = data.json.args.width;
+    let maxHeight = data.json.args.height;
+
+    let scaleWidth = Math.min(1, maxWidth / content.innerWidth);
+    let scaleHeight = Math.min(1, maxHeight / content.innerHeight);
+
+    let scale = Math.max(scaleWidth, scaleHeight);
+
+    let canvasWidth = Math.min(maxWidth, Math.round(content.innerWidth * scale));
+    let canvasHeight = Math.min(maxHeight, Math.round(content.innerHeight * scale));
+
     var canvas = content.document
       .createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+    canvas.mozOpaque = true;
+    canvas.width = canvasWidth;
+    canvas.height = canvasHeight;
+
     var ctx = canvas.getContext("2d");
-    canvas.mozOpaque = true;
-    canvas.height = content.innerHeight;
-    canvas.width = content.innerWidth;
-    ctx.drawWindow(content, 0, 0, content.innerWidth,
-                   content.innerHeight, "rgb(255,255,255)");
+    ctx.scale(scale, scale);
+    ctx.drawWindow(content, 0, 0, content.innerWidth, content.innerHeight,
+                   "rgb(255,255,255)");
+
     sendAsyncMsg('got-screenshot', {
       id: data.json.id,
       // Hack around the fact that we can't specify opaque PNG, this requires
       // us to unpremultiply the alpha channel which is expensive on ARM
       // processors because they lack a hardware integer division instruction.
-      rv: canvas.toDataURL("image/jpeg")
+      successRv: canvas.toDataURL("image/jpeg")
     });
   },
 
   _recvFireCtxCallback: function(data) {
     debug("Received fireCtxCallback message: (" + data.json.menuitem + ")");
     // We silently ignore if the embedder uses an incorrect id in the callback
     if (data.json.menuitem in this._ctxHandlers) {
       this._ctxHandlers[data.json.menuitem].click();
@@ -545,25 +581,25 @@ BrowserElementChild.prototype = {
                          json.rotationAngles, json.forces, json.count,
                          json.modifiers);
   },
 
   _recvCanGoBack: function(data) {
     var webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
     sendAsyncMsg('got-can-go-back', {
       id: data.json.id,
-      rv: webNav.canGoBack
+      successRv: webNav.canGoBack
     });
   },
 
   _recvCanGoForward: function(data) {
     var webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
     sendAsyncMsg('got-can-go-forward', {
       id: data.json.id,
-      rv: webNav.canGoForward
+      successRv: webNav.canGoForward
     });
   },
 
   _recvGoBack: function(data) {
     try {
       docShell.QueryInterface(Ci.nsIWebNavigation).goBack();
     } catch(e) {
       // Silently swallow errors; these happen when we can't go back.
--- a/dom/browser-element/BrowserElementParent.js
+++ b/dom/browser-element/BrowserElementParent.js
@@ -238,17 +238,17 @@ function BrowserElementParent(frameLoade
   defineMethod('sendMouseEvent', this._sendMouseEvent);
   if (getBoolPref(TOUCH_EVENTS_ENABLED_PREF, false)) {
     defineMethod('sendTouchEvent', this._sendTouchEvent);
   }
   defineMethod('goBack', this._goBack);
   defineMethod('goForward', this._goForward);
   defineMethod('reload', this._reload);
   defineMethod('stop', this._stop);
-  defineDOMRequestMethod('getScreenshot', 'get-screenshot');
+  defineMethod('getScreenshot', this._getScreenshot);
   defineDOMRequestMethod('getCanGoBack', 'get-can-go-back');
   defineDOMRequestMethod('getCanGoForward', 'get-can-go-forward');
 
   // Listen to mozvisibilitychange on the iframe's owner window, and forward it
   // down to the child.
   this._window.addEventListener('mozvisibilitychange',
                                 this._ownerVisibilityChange.bind(this),
                                 /* useCapture = */ false,
@@ -465,44 +465,60 @@ BrowserElementParent.prototype = {
                                   { bubbles: true,
                                     cancelable: cancelable });
   },
 
   /**
    * Kick off a DOMRequest in the child process.
    *
    * We'll fire an event called |msgName| on the child process, passing along
-   * an object with a single field, id, containing the ID of this request.
+   * an object with two fields:
+   *
+   *  - id:  the ID of this request.
+   *  - arg: arguments to pass to the child along with this request.
    *
    * We expect the child to pass the ID back to us upon completion of the
-   * request; see _gotDOMRequestResult.
+   * request.  See _gotDOMRequestResult.
    */
-  _sendDOMRequest: function(msgName) {
+  _sendDOMRequest: function(msgName, args) {
     let id = 'req_' + this._domRequestCounter++;
     let req = Services.DOMRequest.createRequest(this._window);
-    if (this._sendAsyncMsg(msgName, {id: id})) {
+    if (this._sendAsyncMsg(msgName, {id: id, args: args})) {
       this._pendingDOMRequests[id] = req;
     } else {
       Services.DOMRequest.fireErrorAsync(req, "fail");
     }
     return req;
   },
 
   /**
-   * Called when the child process finishes handling a DOMRequest.  We expect
-   * data.json to have two fields:
+   * Called when the child process finishes handling a DOMRequest.  data.json
+   * must have the fields [id, successRv], if the DOMRequest was successful, or
+   * [id, errorMsg], if the request was not successful.
    *
-   *  - id: the ID of the DOM request (see _sendDOMRequest), and
-   *  - rv: the request's return value.
+   * The fields have the following meanings:
+   *
+   *  - id:        the ID of the DOM request (see _sendDOMRequest)
+   *  - successRv: the request's return value, if the request succeeded
+   *  - errorMsg:  the message to pass to DOMRequest.fireError(), if the request
+   *               failed.
    *
    */
   _gotDOMRequestResult: function(data) {
     let req = this._pendingDOMRequests[data.json.id];
     delete this._pendingDOMRequests[data.json.id];
-    Services.DOMRequest.fireSuccess(req, data.json.rv);
+
+    if ('successRv' in data.json) {
+      debug("Successful gotDOMRequestResult.");
+      Services.DOMRequest.fireSuccess(req, data.json.successRv);
+    }
+    else {
+      debug("Got error in gotDOMRequestResult.");
+      Services.DOMRequest.fireErrorAsync(req, data.json.errorMsg);
+    }
   },
 
   _setVisible: function(visible) {
     this._sendAsyncMsg('set-visible', {visible: visible});
   },
 
   _sendMouseEvent: function(type, x, y, button, clickCount, modifiers) {
     this._sendAsyncMsg("send-mouse-event", {
@@ -543,16 +559,28 @@ BrowserElementParent.prototype = {
   _reload: function(hardReload) {
     this._sendAsyncMsg('reload', {hardReload: hardReload});
   },
 
   _stop: function() {
     this._sendAsyncMsg('stop');
   },
 
+  _getScreenshot: function(_width, _height) {
+    let width = parseInt(_width);
+    let height = parseInt(_height);
+    if (isNaN(width) || isNaN(height) || width < 0 || height < 0) {
+      throw Components.Exception("Invalid argument",
+                                 Cr.NS_ERROR_INVALID_ARG);
+    }
+
+    return this._sendDOMRequest('get-screenshot',
+                                {width: width, height: height});
+  },
+
   _fireKeyEvent: function(data) {
     let evt = this._window.document.createEvent("KeyboardEvent");
     evt.initKeyEvent(data.json.type, true, true, this._window,
                      false, false, false, false, // modifiers
                      data.json.keyCode,
                      data.json.charCode);
 
     this._frameElement.dispatchEvent(evt);
--- a/dom/browser-element/mochitest/Makefile.in
+++ b/dom/browser-element/mochitest/Makefile.in
@@ -38,16 +38,18 @@ MOCHITEST_FILES = \
 		file_browserElement_AppWindowNamespace.html \
 		browserElement_BrowserWindowNamespace.js \
 		test_browserElement_inproc_BrowserWindowNamespace.html \
 		file_browserElement_BrowserWindowNamespace.html \
 		browserElement_Iconchange.js \
 		test_browserElement_inproc_Iconchange.html \
 		browserElement_GetScreenshot.js \
 		test_browserElement_inproc_GetScreenshot.html \
+		browserElement_BadScreenshot.js \
+		test_browserElement_inproc_BadScreenshot.html \
 		browserElement_SetVisible.js \
 		test_browserElement_inproc_SetVisible.html \
 		browserElement_SetVisibleFrames.js \
 		test_browserElement_inproc_SetVisibleFrames.html \
 		file_browserElement_SetVisibleFrames_Outer.html \
 		file_browserElement_SetVisibleFrames_Inner.html \
 		browserElement_SetVisibleFrames2.js \
 		test_browserElement_inproc_SetVisibleFrames2.html \
@@ -164,16 +166,17 @@ MOCHITEST_FILES += \
 		test_browserElement_oop_DataURI.html \
 		test_browserElement_oop_ErrorSecurity.html \
 		test_browserElement_oop_Titlechange.html \
 		test_browserElement_oop_AppWindowNamespace.html \
 		test_browserElement_oop_BrowserWindowNamespace.html \
 		test_browserElement_oop_TopBarrier.html \
 		test_browserElement_oop_Iconchange.html \
 		test_browserElement_oop_GetScreenshot.html \
+		test_browserElement_oop_BadScreenshot.html \
 		test_browserElement_oop_SetVisible.html \
 		test_browserElement_oop_SetVisibleFrames.html \
 		test_browserElement_oop_SetVisibleFrames2.html \
 		test_browserElement_oop_KeyEvents.html \
 		test_browserElement_oop_XFrameOptions.html \
 		test_browserElement_oop_XFrameOptionsDeny.html \
 		test_browserElement_oop_XFrameOptionsSameOrigin.html \
 		test_browserElement_oop_XFrameOptionsAllowFrom.html \
new file mode 100644
--- /dev/null
+++ b/dom/browser-element/mochitest/browserElement_BadScreenshot.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the public domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Bug 800170 - Test that we get errors when we pass bad arguments to
+// mozbrowser's getScreenshot.
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+var iframe;
+var numPendingTests = 0;
+
+// Call iframe.getScreenshot with the given args.  If expectSuccess is true, we
+// expect the screenshot's onsuccess handler to fire.  Otherwise, we expect
+// getScreenshot() to throw an exception.
+function checkScreenshotResult(expectSuccess, args) {
+  var req;
+  try {
+    req = iframe.getScreenshot.apply(iframe, args);
+  }
+  catch(e) {
+    ok(!expectSuccess, "getScreenshot(" + JSON.stringify(args) + ") threw an exception.");
+    return;
+  }
+
+  numPendingTests++;
+  req.onsuccess = function() {
+    ok(expectSuccess, "getScreenshot(" + JSON.stringify(args) + ") succeeded.");
+    numPendingTests--;
+    if (numPendingTests == 0) {
+      SimpleTest.finish();
+    }
+  };
+
+  // We never expect to see onerror.
+  req.onerror = function() {
+    ok(false, "getScreenshot(" + JSON.stringify(args) + ") ran onerror.");
+    numPendingTests--;
+    if (numPendingTests == 0) {
+      SimpleTest.finish();
+    }
+  };
+}
+
+function runTest() {
+  dump("XXX runTest\n");
+  browserElementTestHelpers.setEnabledPref(true);
+  browserElementTestHelpers.addPermission();
+
+  iframe = document.createElement('iframe');
+  iframe.mozbrowser = true;
+  document.body.appendChild(iframe);
+  iframe.src = 'data:text/html,<html>' +
+    '<body style="background:green">hello</body></html>';
+
+  iframe.addEventListener('mozbrowserfirstpaint', function() {
+    // This one should succeed.
+    checkScreenshotResult(true, [100, 100]);
+
+    // These should fail.
+    checkScreenshotResult(false, []);
+    checkScreenshotResult(false, [100]);
+    checkScreenshotResult(false, ['a', 100]);
+    checkScreenshotResult(false, [100, 'a']);
+    checkScreenshotResult(false, [-1, 100]);
+    checkScreenshotResult(false, [100, -1]);
+
+    if (numPendingTests == 0) {
+      SimpleTest.finish();
+    }
+  });
+}
+
+runTest();
--- a/dom/browser-element/mochitest/browserElement_DOMRequestError.js
+++ b/dom/browser-element/mochitest/browserElement_DOMRequestError.js
@@ -22,17 +22,17 @@ function runTest() {
       var error = false;
       if (beforeRun)
         beforeRun();
       function testEnd() {
         is(isErrorExpected, error);
         SimpleTest.executeSoon(nextTest);
       }
 
-      var domRequest = iframe1.getScreenshot();
+      var domRequest = iframe1.getScreenshot(1000, 1000);
       domRequest.onsuccess = function(e) {
         testEnd();
       }
       domRequest.onerror = function(e) {
         error = true;
         testEnd();
       }
     };
--- a/dom/browser-element/mochitest/browserElement_GetScreenshot.js
+++ b/dom/browser-element/mochitest/browserElement_GetScreenshot.js
@@ -32,45 +32,43 @@ function runTest() {
     }
     else if (screenshots.length === 2) {
       ok(true, 'Got updated screenshot after source page changed');
       SimpleTest.finish();
     }
   }
 
   // We continually take screenshots until we get one that we are
-  // happy with
+  // happy with.
   function waitForScreenshot(filter) {
 
     function screenshotLoaded(e) {
       if (filter(e.target.result)) {
         screenshotTaken(e.target.result);
         return;
       }
       if (--attempts === 0) {
         ok(false, 'Timed out waiting for correct screenshot');
         SimpleTest.finish();
       } else {
         content.document.defaultView.setTimeout(function() {
-          iframe1.getScreenshot().onsuccess = screenshotLoaded;
+          iframe1.getScreenshot(1000, 1000).onsuccess = screenshotLoaded;
         }, 200);
       }
     }
 
     var attempts = 10;
-    iframe1.getScreenshot().onsuccess = screenshotLoaded;
+    iframe1.getScreenshot(1000, 1000).onsuccess = screenshotLoaded;
   }
 
   function iframeLoadedHandler() {
     numLoaded++;
     if (numLoaded === 2) {
       waitForScreenshot(function(screenshot) {
         return screenshot !== 'data:,';
       });
     }
   }
 
   iframe1.addEventListener('mozbrowserloadend', iframeLoadedHandler);
 }
 
 addEventListener('load', function() { SimpleTest.executeSoon(runTest); });
-
-
--- a/dom/browser-element/mochitest/browserElement_OpenMixedProcess.js
+++ b/dom/browser-element/mochitest/browserElement_OpenMixedProcess.js
@@ -50,34 +50,34 @@ function runTest() {
       ok(true, e.detail.message);
     }
     else if (e.detail.message.startsWith('fail')) {
       ok(false, e.detail.message);
     }
     else if (e.detail.message == 'finish') {
       // We assume here that iframe is completely blank, and spin until popup's
       // screenshot is not the same as iframe.
-      iframe.getScreenshot().onsuccess = function(e) {
+      iframe.getScreenshot(1000, 1000).onsuccess = function(e) {
         test2(popup, e.target.result, popup);
       };
     }
     else {
       ok(false, e.detail.message, "Unexpected message!");
     }
   });
 
   document.body.appendChild(iframe);
   iframe.src = 'file_browserElement_OpenMixedProcess.html';
 }
 
 var prevScreenshot;
 function test2(popup, blankScreenshot) {
   // Take screenshots of popup until it doesn't equal blankScreenshot (or we
   // time out).
-  popup.getScreenshot().onsuccess = function(e) {
+  popup.getScreenshot(1000, 1000).onsuccess = function(e) {
     var screenshot = e.target.result;
     if (screenshot != blankScreenshot) {
       SimpleTest.finish();
       return;
     }
 
     if (screenshot != prevScreenshot) {
       prevScreenshot = screenshot;
--- a/dom/browser-element/mochitest/browserElement_XFrameOptionsAllowFrom.js
+++ b/dom/browser-element/mochitest/browserElement_XFrameOptionsAllowFrom.js
@@ -20,29 +20,29 @@ function runTest() {
   // The innermost page we load will fire an alert when it successfully loads.
   iframe.addEventListener('mozbrowsershowmodalprompt', function(e) {
     switch (e.detail.message) {
     case 'step 1':
       // Make the page wait for us to unblock it (which we do after we finish
       // taking the screenshot).
       e.preventDefault();
 
-      iframe.getScreenshot().onsuccess = function(sshot) {
+      iframe.getScreenshot(1000, 1000).onsuccess = function(sshot) {
         if (initialScreenshot == null)
           initialScreenshot = sshot.target.result;
         e.detail.unblock();
       };
       break;
     case 'step 2':
       ok(false, 'cross origin page loaded');
       break;
     case 'finish':
       // The page has now attempted to load the X-Frame-Options page; take
       // another screenshot.
-      iframe.getScreenshot().onsuccess = function(sshot) {
+      iframe.getScreenshot(1000, 1000).onsuccess = function(sshot) {
         is(sshot.target.result, initialScreenshot, "Screenshots should be identical");
         SimpleTest.finish();
       };
       break;
     }
   });
 
   document.body.appendChild(iframe);
--- a/dom/browser-element/mochitest/browserElement_XFrameOptionsDeny.js
+++ b/dom/browser-element/mochitest/browserElement_XFrameOptionsDeny.js
@@ -23,25 +23,25 @@ function runTest() {
 
   iframe.addEventListener('mozbrowsershowmodalprompt', function(e) {
     switch (e.detail.message) {
     case 'step 1':
       // Make the page wait for us to unblock it (which we do after we finish
       // taking the screenshot).
       e.preventDefault();
 
-      iframe.getScreenshot().onsuccess = function(sshot) {
+      iframe.getScreenshot(1000, 1000).onsuccess = function(sshot) {
         initialScreenshot = sshot.target.result;
         e.detail.unblock();
       };
       break;
     case 'step 2':
       // The page has now attempted to load the X-Frame-Options page; take
       // another screenshot.
-      iframe.getScreenshot().onsuccess = function(sshot) {
+      iframe.getScreenshot(1000, 1000).onsuccess = function(sshot) {
         is(sshot.target.result, initialScreenshot, "Screenshots should be identical");
         SimpleTest.finish();
       };
       break;
     }
   });
 
   document.body.appendChild(iframe);
new file mode 100644
--- /dev/null
+++ b/dom/browser-element/mochitest/test_browserElement_inproc_BadScreenshot.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for Bug 800170</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="browserElementTestHelpers.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<script type="application/javascript;version=1.7" src="browserElement_BadScreenshot.js">
+</script>
+</body>
+</html>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/browser-element/mochitest/test_browserElement_oop_BadScreenshot.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for Bug 800170</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="browserElementTestHelpers.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<script type="application/javascript;version=1.7" src="browserElement_BadScreenshot.js">
+</script>
+</body>
+</html>
\ No newline at end of file