Merge fx-team to m-c.
authorRyan VanderMeulen <ryanvm@gmail.com>
Fri, 31 Jan 2014 21:04:30 -0500
changeset 182390 cee541ab3322ae9d6670c9c2c0fea33388b827a9
parent 182348 00b86eca0baf0971b35b33f223f32c35ab6fdf8f (current diff)
parent 182389 f5d04ca4795d025e45d436820b9a289704d765d7 (diff)
child 182415 45e2143238b9e1cfc67896e5eb7055abbee5bb4a
push id3343
push userffxbld
push dateMon, 17 Mar 2014 21:55:32 +0000
treeherdermozilla-beta@2f7d3415f79f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone29.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c.
new file mode 100644
--- /dev/null
+++ b/b2g/chrome/content/devtools.js
@@ -0,0 +1,310 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+'use strict';
+
+const WIDGET_PANEL_LOG_PREFIX = 'WidgetPanel';
+
+XPCOMUtils.defineLazyGetter(this, 'DebuggerClient', function() {
+  return Cu.import('resource://gre/modules/devtools/dbg-client.jsm', {}).DebuggerClient;
+});
+
+XPCOMUtils.defineLazyGetter(this, 'WebConsoleUtils', function() {
+  let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+  return devtools.require("devtools/toolkit/webconsole/utils").Utils;
+});
+
+/**
+ * The Widget Panel is an on-device developer tool that displays widgets,
+ * showing visual debug information about apps. Each widget corresponds to a
+ * metric as tracked by a metric watcher (e.g. consoleWatcher).
+ */
+let devtoolsWidgetPanel = {
+
+  _apps: new Map(),
+  _urls: new Map(),
+  _client: null,
+  _webappsActor: null,
+  _watchers: [],
+
+  /**
+   * This method registers a metric watcher that will watch one or more metrics
+   * of apps that are being tracked. A watcher must implement the trackApp(app)
+   * and untrackApp(app) methods, add entries to the app.metrics map, keep them
+   * up-to-date, and call app.display() when values were changed.
+   */
+  registerWatcher: function dwp_registerWatcher(watcher) {
+    this._watchers.unshift(watcher);
+  },
+
+  init: function dwp_init() {
+    if (this._client)
+      return;
+
+    if (!DebuggerServer.initialized) {
+      RemoteDebugger.start();
+    }
+
+    this._client = new DebuggerClient(DebuggerServer.connectPipe());
+    this._client.connect((type, traits) => {
+
+      // FIXME(Bug 962577) see below.
+      this._client.listTabs((res) => {
+        this._webappsActor = res.webappsActor;
+
+        for (let w of this._watchers) {
+          if (w.init) {
+            w.init(this._client);
+          }
+        }
+
+        Services.obs.addObserver(this, 'remote-browser-frame-pending', false);
+        Services.obs.addObserver(this, 'in-process-browser-or-app-frame-shown', false);
+        Services.obs.addObserver(this, 'message-manager-disconnect', false);
+      });
+    });
+  },
+
+  uninit: function dwp_uninit() {
+    if (!this._client)
+      return;
+
+    for (let manifest of this._apps.keys()) {
+      this.untrackApp(manifest);
+    }
+
+    Services.obs.removeObserver(this, 'remote-browser-frame-pending');
+    Services.obs.removeObserver(this, 'in-process-browser-or-app-frame-shown');
+    Services.obs.removeObserver(this, 'message-manager-disconnect');
+
+    this._client.close();
+    delete this._client;
+  },
+
+  /**
+   * This method will ask all registered watchers to track and update metrics
+   * on an app.
+   */
+  trackApp: function dwp_trackApp(manifestURL) {
+    if (this._apps.has(manifestURL))
+      return;
+
+    // FIXME(Bug 962577) Factor getAppActor and watchApps out of webappsActor.
+    this._client.request({
+      to: this._webappsActor,
+      type: 'getAppActor',
+      manifestURL: manifestURL
+    }, (res) => {
+      if (res.error) {
+        return;
+      }
+
+      let app = new App(manifestURL, res.actor);
+      this._apps.set(manifestURL, app);
+
+      for (let w of this._watchers) {
+        w.trackApp(app);
+      }
+    });
+  },
+
+  untrackApp: function dwp_untrackApp(manifestURL) {
+    let app = this._apps.get(manifestURL);
+    if (app) {
+      for (let w of this._watchers) {
+        w.untrackApp(app);
+      }
+
+      // Delete the metrics and call display() to clean up the front-end.
+      delete app.metrics;
+      app.display();
+
+      this._apps.delete(manifestURL);
+    }
+  },
+
+  observe: function dwp_observe(subject, topic, data) {
+    if (!this._client)
+      return;
+
+    let manifestURL;
+
+    switch(topic) {
+
+      // listen for frame creation in OOP (device) as well as in parent process (b2g desktop)
+      case 'remote-browser-frame-pending':
+      case 'in-process-browser-or-app-frame-shown':
+        let frameLoader = subject;
+        // get a ref to the app <iframe>
+        frameLoader.QueryInterface(Ci.nsIFrameLoader);
+        manifestURL = frameLoader.ownerElement.appManifestURL;
+        if (!manifestURL) // Ignore all frames but apps
+          return;
+        this.trackApp(manifestURL);
+        this._urls.set(frameLoader.messageManager, manifestURL);
+        break;
+
+      // Every time an iframe is destroyed, its message manager also is
+      case 'message-manager-disconnect':
+        let mm = subject;
+        manifestURL = this._urls.get(mm);
+        if (!manifestURL)
+          return;
+        this.untrackApp(manifestURL);
+        this._urls.delete(mm);
+        break;
+    }
+  },
+
+  log: function dwp_log(message) {
+    dump(WIDGET_PANEL_LOG_PREFIX + ': ' + message + '\n');
+  }
+
+};
+
+
+/**
+ * An App object represents all there is to know about a Firefox OS app that is
+ * being tracked, e.g. its manifest information, current values of watched
+ * metrics, and how to update these values on the front-end.
+ */
+function App(manifest, actor) {
+  this.manifest = manifest;
+  this.actor = actor;
+  this.metrics = new Map();
+}
+
+App.prototype = {
+
+  display: function app_display() {
+    let data = {manifestURL: this.manifest, metrics: []};
+    let metrics = this.metrics;
+
+    if (metrics && metrics.size > 0) {
+      for (let name of metrics.keys()) {
+        data.metrics.push({name: name, value: metrics.get(name)});
+      }
+    }
+
+    shell.sendCustomEvent('widget-panel-update', data);
+    // FIXME(after bug 963239 lands) return event.isDefaultPrevented();
+    return false;
+  }
+
+};
+
+
+/**
+ * The Console Watcher tracks the following metrics in apps: errors, warnings,
+ * and reflows.
+ */
+let consoleWatcher = {
+
+  _apps: new Map(),
+  _client: null,
+
+  init: function cw_init(client) {
+    this._client = client;
+    this.consoleListener = this.consoleListener.bind(this);
+
+    client.addListener('logMessage', this.consoleListener);
+    client.addListener('pageError', this.consoleListener);
+    client.addListener('consoleAPICall', this.consoleListener);
+    client.addListener('reflowActivity', this.consoleListener);
+  },
+
+  trackApp: function cw_trackApp(app) {
+    app.metrics.set('reflows', 0);
+    app.metrics.set('warnings', 0);
+    app.metrics.set('errors', 0);
+
+    this._client.request({
+      to: app.actor.consoleActor,
+      type: 'startListeners',
+      listeners: ['LogMessage', 'PageError', 'ConsoleAPI', 'ReflowActivity']
+    }, (res) => {
+      this._apps.set(app.actor.consoleActor, app);
+    });
+  },
+
+  untrackApp: function cw_untrackApp(app) {
+    this._client.request({
+      to: app.actor.consoleActor,
+      type: 'stopListeners',
+      listeners: ['LogMessage', 'PageError', 'ConsoleAPI', 'ReflowActivity']
+    }, (res) => { });
+
+    this._apps.delete(app.actor.consoleActor);
+  },
+
+  bump: function cw_bump(app, metric) {
+    let metrics = app.metrics;
+    metrics.set(metric, metrics.get(metric) + 1);
+  },
+
+  consoleListener: function cw_consoleListener(type, packet) {
+    let app = this._apps.get(packet.from);
+    let output = '';
+
+    switch (packet.type) {
+
+      case 'pageError':
+        let pageError = packet.pageError;
+        if (pageError.warning || pageError.strict) {
+          this.bump(app, 'warnings');
+          output = 'warning (';
+        } else {
+          this.bump(app, 'errors');
+          output += 'error (';
+        }
+        let {errorMessage, sourceName, category, lineNumber, columnNumber} = pageError;
+        output += category + '): "' + (errorMessage.initial || errorMessage) +
+          '" in ' + sourceName + ':' + lineNumber + ':' + columnNumber;
+        break;
+
+      case 'consoleAPICall':
+        switch (packet.message.level) {
+          case 'error':
+            this.bump(app, 'errors');
+            output = 'error (console)';
+            break;
+          case 'warn':
+            this.bump(app, 'warnings');
+            output = 'warning (console)';
+            break;
+          default:
+            return;
+        }
+        break;
+
+      case 'reflowActivity':
+        this.bump(app, 'reflows');
+        let {start, end, sourceURL} = packet;
+        let duration = Math.round((end - start) * 100) / 100;
+        output = 'reflow: ' + duration + 'ms';
+        if (sourceURL) {
+          output += ' ' + this.formatSourceURL(packet);
+        }
+        break;
+    }
+
+    if (!app.display()) {
+      // If the information was not displayed, log it.
+      devtoolsWidgetPanel.log(output);
+    }
+  },
+
+  formatSourceURL: function cw_formatSourceURL(packet) {
+    // Abbreviate source URL
+    let source = WebConsoleUtils.abbreviateSourceURL(packet.sourceURL);
+
+    // Add function name and line number
+    let {functionName, sourceLine} = packet;
+    source = 'in ' + (functionName || '<anonymousFunction>') +
+      ', ' + source + ':' + sourceLine;
+
+    return source;
+  }
+};
+devtoolsWidgetPanel.registerWatcher(consoleWatcher);
--- a/b2g/chrome/content/settings.js
+++ b/b2g/chrome/content/settings.js
@@ -212,16 +212,34 @@ Components.utils.import('resource://gre/
     'deviceinfo.update_channel': update_channel,
     'deviceinfo.hardware': hardware_info,
     'deviceinfo.firmware_revision': firmware_revision,
     'deviceinfo.product_model': product_model
   }
   window.navigator.mozSettings.createLock().set(setting);
 })();
 
+// =================== DevTools ====================
+
+let devtoolsWidgetPanel;
+SettingsListener.observe('devtools.overlay', false, (value) => {
+  if (value) {
+    if (!devtoolsWidgetPanel) {
+      let scope = {};
+      Services.scriptloader.loadSubScript('chrome://browser/content/devtools.js', scope);
+      devtoolsWidgetPanel = scope.devtoolsWidgetPanel;
+    }
+    devtoolsWidgetPanel.init();
+  } else {
+    if (devtoolsWidgetPanel) {
+      devtoolsWidgetPanel.uninit();
+    }
+  }
+});
+
 // =================== Debugger / ADB ====================
 
 #ifdef MOZ_WIDGET_GONK
 let AdbController = {
   DEBUG: false,
   locked: undefined,
   remoteDebuggerEnabled: undefined,
   lockEnabled: undefined,
--- a/b2g/chrome/jar.mn
+++ b/b2g/chrome/jar.mn
@@ -8,16 +8,17 @@ chrome.jar:
 % content branding %content/branding/
 % content browser %content/
 
   content/arrow.svg                     (content/arrow.svg)
 * content/dbg-browser-actors.js         (content/dbg-browser-actors.js)
 * content/settings.js                   (content/settings.js)
 * content/shell.html                    (content/shell.html)
 * content/shell.js                      (content/shell.js)
+  content/devtools.js                   (content/devtools.js)
 #ifndef ANDROID
   content/desktop.js                    (content/desktop.js)
   content/screen.js                     (content/screen.js)
   content/runapp.js                     (content/runapp.js)
 #endif
 * content/content.css                   (content/content.css)
   content/touchcontrols.css             (content/touchcontrols.css)
 
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1113,17 +1113,17 @@ pref("devtools.debugger.chrome-enabled",
 pref("devtools.debugger.chrome-debugging-host", "localhost");
 pref("devtools.debugger.chrome-debugging-port", 6080);
 pref("devtools.debugger.remote-host", "localhost");
 pref("devtools.debugger.remote-timeout", 20000);
 pref("devtools.debugger.pause-on-exceptions", false);
 pref("devtools.debugger.ignore-caught-exceptions", true);
 pref("devtools.debugger.source-maps-enabled", true);
 pref("devtools.debugger.pretty-print-enabled", true);
-pref("devtools.debugger.auto-pretty-print", true);
+pref("devtools.debugger.auto-pretty-print", false);
 pref("devtools.debugger.tracer", false);
 
 // The default Debugger UI settings
 pref("devtools.debugger.ui.panes-sources-width", 200);
 pref("devtools.debugger.ui.panes-instruments-width", 300);
 pref("devtools.debugger.ui.panes-visible-on-startup", false);
 pref("devtools.debugger.ui.variables-sorting-enabled", true);
 pref("devtools.debugger.ui.variables-only-enum-visible", false);
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -891,23 +891,25 @@ chatbox:-moz-full-screen-ancestor > .cha
   background: transparent;
   border: none;
   transition: opacity 300ms;
   /* The popup inherits -moz-image-region from the button, must reset it */
   -moz-image-region: auto;
 }
 
 /* Customize mode */
-#tab-view-deck {
-  transition-property: padding;
+#navigator-toolbox > toolbar:not(#TabsToolbar),
+#content-deck {
+  transition-property: margin-left, margin-right;
   transition-duration: 150ms;
   transition-timing-function: ease-out;
 }
 
-#tab-view-deck[fastcustomizeanimation] {
+#tab-view-deck[fastcustomizeanimation] #navigator-toolbox > toolbar:not(#TabsToolbar),
+#tab-view-deck[fastcustomizeanimation] #content-deck {
   transition-duration: 1ms;
   transition-timing-function: linear;
 }
 
 #PanelUI-contents > .panel-customization-placeholder > .panel-customization-placeholder-child {
   list-style-image: none;
 }
 
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -4363,18 +4363,16 @@ var TabsInTitlebar = {
     // of various elements, it's important that the layout be more or less
     // settled before updating the titlebar. So instead of listening to
     // DOMMenuBarActive and DOMMenuBarInactive, we use a MutationObserver to
     // watch the "invalid" attribute directly.
     let menu = document.getElementById("toolbar-menubar");
     this._menuObserver = new MutationObserver(this._onMenuMutate);
     this._menuObserver.observe(menu, {attributes: true});
 
-    gNavToolbox.addEventListener("customization-transitionend", this);
-
     this.onAreaReset = function(aArea) {
       if (aArea == CustomizableUI.AREA_TABSTRIP || aArea == CustomizableUI.AREA_MENUBAR)
         this._update(true);
     };
     this.onWidgetAdded = this.onWidgetRemoved = function(aWidgetId, aArea) {
       if (aArea == CustomizableUI.AREA_TABSTRIP || aArea == CustomizableUI.AREA_MENUBAR)
         this._update(true);
     };
@@ -4411,22 +4409,16 @@ var TabsInTitlebar = {
   },
 
 #ifdef CAN_DRAW_IN_TITLEBAR
   observe: function (subject, topic, data) {
     if (topic == "nsPref:changed")
       this._readPref();
   },
 
-  handleEvent: function(ev) {
-    if (ev.type == "customization-transitionend") {
-      this._update(true);
-    }
-  },
-
   _onMenuMutate: function (aMutations) {
     for (let mutation of aMutations) {
       if (mutation.attributeName == "inactive" ||
           mutation.attributeName == "autohide") {
         TabsInTitlebar._update(true);
         return;
       }
     }
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -218,17 +218,18 @@
         </vbox>
       </hbox>
     </panel>
     <panel id="UITourHighlightContainer"
            hidden="true"
            noautofocus="true"
            noautohide="true"
            flip="none"
-           consumeoutsideclicks="false">
+           consumeoutsideclicks="false"
+           mousethrough="always">
       <box id="UITourHighlight"></box>
     </panel>
 
     <panel id="social-share-panel"
            class="social-panel"
            type="arrow"
            orient="horizontal"
            onpopupshowing="SocialShare.onShowing()"
--- a/browser/base/content/test/social/browser_blocklist.js
+++ b/browser/base/content/test/social/browser_blocklist.js
@@ -2,17 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 // a place for miscellaneous social tests
 
 let SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
 
 const URI_EXTENSION_BLOCKLIST_DIALOG = "chrome://mozapps/content/extensions/blocklist.xul";
-let blocklistURL = "http://example.org/browser/browser/base/content/test/social/blocklist.xml";
+let blocklistURL = "http://example.com/browser/browser/base/content/test/social/blocklist.xml";
 
 let manifest = { // normal provider
   name: "provider ok",
   origin: "https://example.com",
   sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html",
   workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js",
   iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png"
 };
@@ -21,16 +21,21 @@ let manifest_bad = { // normal provider
   origin: "https://test1.example.com",
   sidebarURL: "https://test1.example.com/browser/browser/base/content/test/social/social_sidebar.html",
   workerURL: "https://test1.example.com/browser/browser/base/content/test/social/social_worker.js",
   iconURL: "https://test1.example.com/browser/browser/base/content/test/general/moz.png"
 };
 
 function test() {
   waitForExplicitFinish();
+  // turn on logging for nsBlocklistService.js
+  Services.prefs.setBoolPref("extensions.logging.enabled", true);
+  registerCleanupFunction(function () {
+    Services.prefs.clearUserPref("extensions.logging.enabled");
+  });
 
   runSocialTests(tests, undefined, undefined, function () {
     resetBlocklist(finish); //restore to original pref
   });
 }
 
 var tests = {
   testSimpleBlocklist: function(next) {
@@ -40,62 +45,64 @@ var tests = {
       ok(!Services.blocklist.isAddonBlocklisted(SocialService.createWrapper(manifest)), "not blocking 'good'");
       resetBlocklist(function() {
         ok(!Services.blocklist.isAddonBlocklisted(SocialService.createWrapper(manifest_bad)), "blocklist cleared");
         next();
       });
     });
   },
   testAddingNonBlockedProvider: function(next) {
-    function finish(isgood) {
+    function finishTest(isgood) {
       ok(isgood, "adding non-blocked provider ok");
       Services.prefs.clearUserPref("social.manifest.good");
       resetBlocklist(next);
     }
     setManifestPref("social.manifest.good", manifest);
     setAndUpdateBlocklist(blocklistURL, function() {
       try {
         SocialService.addProvider(manifest, function(provider) {
           try {
             SocialService.removeProvider(provider.origin, function() {
               ok(true, "added and removed provider");
-              finish(true);
+              finishTest(true);
             });
           } catch(e) {
             ok(false, "SocialService.removeProvider threw exception: " + e);
-            finish(false);
+            finishTest(false);
           }
         });
       } catch(e) {
         ok(false, "SocialService.addProvider threw exception: " + e);
-        finish(false);
+        finishTest(false);
       }
     });
   },
   testAddingBlockedProvider: function(next) {
-    function finish(good) {
+    function finishTest(good) {
       ok(good, "Unable to add blocklisted provider");
       Services.prefs.clearUserPref("social.manifest.blocked");
       resetBlocklist(next);
     }
     setManifestPref("social.manifest.blocked", manifest_bad);
     setAndUpdateBlocklist(blocklistURL, function() {
       try {
         SocialService.addProvider(manifest_bad, function(provider) {
-          ok(false, "SocialService.addProvider should throw blocklist exception");
-          finish(false);
+          SocialService.removeProvider(provider.origin, function() {
+            ok(false, "SocialService.addProvider should throw blocklist exception");
+            finishTest(false);
+          });
         });
       } catch(e) {
         ok(true, "SocialService.addProvider should throw blocklist exception: " + e);
-        finish(true);
+        finishTest(true);
       }
     });
   },
   testInstallingBlockedProvider: function(next) {
-    function finish(good) {
+    function finishTest(good) {
       ok(good, "Unable to add blocklisted provider");
       Services.prefs.clearUserPref("social.whitelist");
       resetBlocklist(next);
     }
     let activationURL = manifest_bad.origin + "/browser/browser/base/content/test/social/social_activate.html"
     addTab(activationURL, function(tab) {
       let doc = tab.linkedBrowser.contentDocument;
       let installFrom = doc.nodePrincipal.origin;
@@ -103,54 +110,52 @@ var tests = {
       // the blocklist inside installProvider.
       Services.prefs.setCharPref("social.whitelist", installFrom);
       setAndUpdateBlocklist(blocklistURL, function() {
         try {
           // expecting an exception when attempting to install a hard blocked
           // provider
           Social.installProvider(doc, manifest_bad, function(addonManifest) {
             gBrowser.removeTab(tab);
-            finish(false);
+            finishTest(false);
           });
         } catch(e) {
           gBrowser.removeTab(tab);
-          finish(true);
+          finishTest(true);
         }
       });
     });
   },
   testBlockingExistingProvider: function(next) {
-    let windowWasClosed = false;
-    function finish() {
-      waitForCondition(function() windowWasClosed, function() {
-        Services.wm.removeListener(listener);
-        next();
-      }, "blocklist dialog was closed");
-    }
-
     let listener = {
       _window: null,
       onOpenWindow: function(aXULWindow) {
         Services.wm.removeListener(this);
         this._window = aXULWindow;
         let domwindow = aXULWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                                   .getInterface(Ci.nsIDOMWindow);
 
         domwindow.addEventListener("load", function _load() {
           domwindow.removeEventListener("load", _load, false);
 
           domwindow.addEventListener("unload", function _unload() {
             domwindow.removeEventListener("unload", _unload, false);
             info("blocklist window was closed");
-            windowWasClosed = true;
+            Services.wm.removeListener(listener);
+            next();
           }, false);
 
           is(domwindow.document.location.href, URI_EXTENSION_BLOCKLIST_DIALOG, "dialog opened and focused");
-          domwindow.close();
-
+          // wait until after load to cancel so the dialog has initalized. we
+          // don't want to accept here since that restarts the browser.
+          executeSoon(() => {
+            let cancelButton = domwindow.document.documentElement.getButton("cancel");
+            info("***** hit the cancel button\n");
+            cancelButton.doCommand();
+          });
         }, false);
       },
       onCloseWindow: function(aXULWindow) { },
       onWindowTitleChange: function(aXULWindow, aNewTitle) { }
     };
 
     Services.wm.addListener(listener);
 
@@ -162,21 +167,21 @@ var tests = {
         SocialService.registerProviderListener(function providerListener(topic, origin, providers) {
           if (topic != "provider-disabled")
             return;
           SocialService.unregisterProviderListener(providerListener);
           is(origin, provider.origin, "provider disabled");
           SocialService.getProvider(provider.origin, function(p) {
             ok(p == null, "blocklisted provider disabled");
             Services.prefs.clearUserPref("social.manifest.blocked");
-            resetBlocklist(finish);
+            resetBlocklist();
           });
         });
         // no callback - the act of updating should cause the listener above
         // to fire.
         setAndUpdateBlocklist(blocklistURL);
       });
     } catch(e) {
       ok(false, "unable to add provider " + e);
-      finish();
+      next();
     }
   }
 }
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -21,19 +21,21 @@
                        oncommand="gFxAccounts.toggle(event);"
                        hidden="true"/>
 
         <hbox id="PanelUI-footer-inner">
           <toolbarbutton id="PanelUI-customize" label="&appMenuCustomize.label;"
                          exitLabel="&appMenuCustomizeExit.label;"
                          tooltiptext="&appMenuCustomize.tooltip;"
                          exitTooltiptext="&appMenuCustomizeExit.tooltip;"
+                         closemenu="none"
                          oncommand="gCustomizeMode.toggle();"/>
           <toolbarseparator/>
           <toolbarbutton id="PanelUI-help" label="&helpMenu.label;"
+                         closemenu="none"
                          tooltiptext="&appMenuHelp.tooltip;"
                          oncommand="PanelUI.showHelpView(this.parentNode);"/>
           <toolbarseparator/>
           <toolbarbutton id="PanelUI-quit"
 #ifdef XP_WIN
                          label="&quitApplicationCmdWin.label;"
 #else
 #ifdef XP_MACOSX
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -56,30 +56,28 @@ const PanelUI = {
   },
 
   _addEventListeners: function() {
     for (let event of this.kEvents) {
       this.panel.addEventListener(event, this);
     }
 
     this.helpView.addEventListener("ViewShowing", this._onHelpViewShow, false);
-    this.helpView.addEventListener("ViewHiding", this._onHelpViewHide, false);
     this._eventListenersAdded = true;
   },
 
   uninit: function() {
     if (!this._eventListenersAdded) {
       return;
     }
 
     for (let event of this.kEvents) {
       this.panel.removeEventListener(event, this);
     }
     this.helpView.removeEventListener("ViewShowing", this._onHelpViewShow);
-    this.helpView.removeEventListener("ViewHiding", this._onHelpViewHide);
     this.menuButton.removeEventListener("mousedown", this);
     this.menuButton.removeEventListener("keypress", this);
   },
 
   /**
    * Customize mode extracts the mainView and puts it somewhere else while the
    * user customizes. Upon completion, this function can be called to put the
    * panel back to where it belongs in normal browsing mode.
@@ -162,19 +160,16 @@ const PanelUI = {
       return;
     }
 
     this.panel.hidePopup();
   },
 
   handleEvent: function(aEvent) {
     switch (aEvent.type) {
-      case "command":
-        this.onCommandHandler(aEvent);
-        break;
       case "popupshowing":
         // Fall through
       case "popupshown":
         // Fall through
       case "popuphiding":
         // Fall through
       case "popuphidden":
         this._updatePanelButton(aEvent.target);
@@ -414,22 +409,16 @@ const PanelUI = {
         if (!node.hasAttribute(attrName))
           continue;
         button.setAttribute(attrName, node.getAttribute(attrName));
       }
       button.setAttribute("class", "subviewbutton");
       fragment.appendChild(button);
     }
     items.appendChild(fragment);
-
-    this.addEventListener("command", PanelUI);
-  },
-
-  _onHelpViewHide: function(aEvent) {
-    this.removeEventListener("command", PanelUI);
   },
 
   _updateQuitTooltip: function() {
 #ifndef XP_WIN
 #ifdef XP_MACOSX
     let tooltipId = "quit-button.tooltiptext.mac";
     let brands = Services.strings.createBundle("chrome://branding/locale/brand.properties");
     let stringArgs = [brands.GetStringFromName("brandShortName")];
--- a/browser/components/customizableui/src/CustomizableUI.jsm
+++ b/browser/components/customizableui/src/CustomizableUI.jsm
@@ -630,43 +630,43 @@ let CustomizableUIInternal = {
     if (node) {
       return [ CustomizableUI.PROVIDER_XUL, node ];
     }
 
     LOG("No node for " + aWidgetId + " found.");
     return [null, null];
   },
 
-  registerMenuPanel: function(aPanel) {
+  registerMenuPanel: function(aPanelContents) {
     if (gBuildAreas.has(CustomizableUI.AREA_PANEL) &&
-        gBuildAreas.get(CustomizableUI.AREA_PANEL).has(aPanel)) {
+        gBuildAreas.get(CustomizableUI.AREA_PANEL).has(aPanelContents)) {
       return;
     }
 
-    let document = aPanel.ownerDocument;
-
-    aPanel.toolbox = document.getElementById("navigator-toolbox");
-    aPanel.customizationTarget = aPanel;
-
-    this.addPanelCloseListeners(aPanel);
+    let document = aPanelContents.ownerDocument;
+
+    aPanelContents.toolbox = document.getElementById("navigator-toolbox");
+    aPanelContents.customizationTarget = aPanelContents;
+
+    this.addPanelCloseListeners(this._getPanelForNode(aPanelContents));
 
     let placements = gPlacements.get(CustomizableUI.AREA_PANEL);
-    this.buildArea(CustomizableUI.AREA_PANEL, placements, aPanel);
-    for (let child of aPanel.children) {
+    this.buildArea(CustomizableUI.AREA_PANEL, placements, aPanelContents);
+    for (let child of aPanelContents.children) {
       if (child.localName != "toolbarbutton") {
         if (child.localName == "toolbaritem") {
-          this.ensureButtonContextMenu(child, aPanel);
+          this.ensureButtonContextMenu(child, aPanelContents);
         }
         continue;
       }
-      this.ensureButtonContextMenu(child, aPanel);
+      this.ensureButtonContextMenu(child, aPanelContents);
       child.setAttribute("wrap", "true");
     }
 
-    this.registerBuildArea(CustomizableUI.AREA_PANEL, aPanel);
+    this.registerBuildArea(CustomizableUI.AREA_PANEL, aPanelContents);
   },
 
   onWidgetAdded: function(aWidgetId, aArea, aPosition) {
     this.insertNode(aWidgetId, aArea, aPosition, true);
   },
 
   onWidgetRemoved: function(aWidgetId, aArea) {
     let areaNodes = gBuildAreas.get(aArea);
@@ -1309,21 +1309,30 @@ let CustomizableUIInternal = {
       }
       let isInteractive = this._isOnInteractiveElement(aEvent);
       LOG("maybeAutoHidePanel: interactive ? " + isInteractive);
       if (isInteractive) {
         return;
       }
     }
 
-    if (aEvent.target.getAttribute("closemenu") == "none" ||
-        aEvent.target.getAttribute("widget-type") == "view") {
+    if (aEvent.originalTarget.getAttribute("closemenu") == "none" ||
+        aEvent.originalTarget.getAttribute("widget-type") == "view") {
       return;
     }
 
+    if (aEvent.originalTarget.getAttribute("closemenu") == "single") {
+      let panel = this._getPanelForNode(aEvent.originalTarget);
+      let multiview = panel.querySelector("panelmultiview");
+      if (multiview.showingSubView) {
+        multiview.showMainView();
+        return;
+      }
+    }
+
     // If we get here, we can actually hide the popup:
     this.hidePanelForNode(aEvent.target);
   },
 
   getUnusedWidgets: function(aWindowPalette) {
     let window = aWindowPalette.ownerDocument.defaultView;
     let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window);
     // We use a Set because there can be overlap between the widgets in
--- a/browser/components/customizableui/src/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/src/CustomizableWidgets.jsm
@@ -186,21 +186,19 @@ const CustomizableWidgets = [{
                                                      "menuRestoreAllWindowsSubview.label");
       separator = doc.getElementById("PanelUI-recentlyClosedWindows-separator");
       elementCount = windowsFragment.childElementCount;
       separator.hidden = !elementCount;
       while (--elementCount >= 0) {
         windowsFragment.children[elementCount].classList.add("subviewbutton");
       }
       recentlyClosedWindows.appendChild(windowsFragment);
-      aEvent.target.addEventListener("command", win.PanelUI);
     },
     onViewHiding: function(aEvent) {
       LOG("History view is being hidden!");
-      aEvent.target.removeEventListener("command", win.PanelUI);
     }
   }, {
     id: "privatebrowsing-button",
     shortcutId: "key_privatebrowsing",
     defaultArea: CustomizableUI.AREA_PANEL,
     onCommand: function(e) {
       if (e.target && e.target.ownerDocument && e.target.ownerDocument.defaultView) {
         let win = e.target.ownerDocument.defaultView;
@@ -288,33 +286,31 @@ const CustomizableWidgets = [{
           let attrVal = node.getAttribute(attr);
           if (attrVal)
             item.setAttribute(attr, attrVal);
         }
         fragment.appendChild(item);
       }
       items.appendChild(fragment);
 
-      aEvent.target.addEventListener("command", win.PanelUI);
     },
     onViewHiding: function(aEvent) {
       let doc = aEvent.target.ownerDocument;
       let win = doc.defaultView;
       let items = doc.getElementById("PanelUI-developerItems");
       let parent = items.parentNode;
       // We'll take the container out of the document before cleaning it out
       // to avoid reflowing each time we remove something.
       parent.removeChild(items);
 
       while (items.firstChild) {
         items.firstChild.remove();
       }
 
       parent.appendChild(items);
-      aEvent.target.removeEventListener("command", win.PanelUI);
     }
   }, {
     id: "add-ons-button",
     shortcutId: "key_openAddons",
     tooltiptext: "add-ons-button.tooltiptext2",
     defaultArea: CustomizableUI.AREA_PANEL,
     onCommand: function(aEvent) {
       let win = aEvent.target &&
--- a/browser/components/customizableui/src/CustomizeMode.jsm
+++ b/browser/components/customizableui/src/CustomizeMode.jsm
@@ -169,16 +169,23 @@ CustomizeMode.prototype = {
 
       // Same goes for the menu button - if we're customizing, a mousedown to the
       // menu button means a quick exit from customization mode.
       window.PanelUI.hide();
       window.PanelUI.menuButton.addEventListener("mousedown", this);
       window.PanelUI.menuButton.open = true;
       window.PanelUI.beginBatchUpdate();
 
+      // The menu panel is lazy, and registers itself when the popup shows. We
+      // need to force the menu panel to register itself, or else customization
+      // is really not going to work. We pass "true" to ensureRegistered to
+      // indicate that we're handling calling startBatchUpdate and
+      // endBatchUpdate.
+      yield window.PanelUI.ensureReady(true);
+
       // Hide the palette before starting the transition for increased perf.
       this.visiblePalette.hidden = true;
 
       // Move the mainView in the panel to the holder so that we can see it
       // while customizing.
       let mainView = window.PanelUI.mainView;
       let panelHolder = document.getElementById("customization-panelHolder");
       panelHolder.appendChild(mainView);
@@ -195,23 +202,16 @@ CustomizeMode.prototype = {
       customizer.parentNode.selectedPanel = customizer;
       customizer.hidden = false;
 
       yield this._doTransition(true);
 
       // Let everybody in this window know that we're about to customize.
       this.dispatchToolboxEvent("customizationstarting");
 
-      // The menu panel is lazy, and registers itself when the popup shows. We
-      // need to force the menu panel to register itself, or else customization
-      // is really not going to work. We pass "true" to ensureRegistered to
-      // indicate that we're handling calling startBatchUpdate and
-      // endBatchUpdate.
-      yield window.PanelUI.ensureReady(true);
-
       this._mainViewContext = mainView.getAttribute("context");
       if (this._mainViewContext) {
         mainView.removeAttribute("context");
       }
 
       this._showPanelCustomizationPlaceholders();
 
       yield this._wrapToolbarItems();
@@ -422,36 +422,40 @@ CustomizeMode.prototype = {
    * order - customize-entered, customize-exiting, pre-customization mode.
    *
    * When in the customize-entering, customize-entered, or customize-exiting
    * phases, there is a "customizing" attribute set on the main-window to simplify
    * excluding certain styles while in any phase of customize mode.
    */
   _doTransition: function(aEntering) {
     let deferred = Promise.defer();
-    let deck = this.document.getElementById("tab-view-deck");
+    let deck = this.document.getElementById("content-deck");
 
     let customizeTransitionEnd = function(aEvent) {
       if (aEvent != "timedout" &&
-          (aEvent.originalTarget != deck || aEvent.propertyName != "padding-bottom")) {
+          (aEvent.originalTarget != deck || aEvent.propertyName != "margin-left")) {
         return;
       }
       this.window.clearTimeout(catchAllTimeout);
-      deck.removeEventListener("transitionend", customizeTransitionEnd);
+      // Bug 962677: We let the event loop breathe for before we do the final
+      // stage of the transition to improve perceived performance.
+      this.window.setTimeout(function () {
+        deck.removeEventListener("transitionend", customizeTransitionEnd);
 
-      if (!aEntering) {
-        this.document.documentElement.removeAttribute("customize-exiting");
-        this.document.documentElement.removeAttribute("customizing");
-      } else {
-        this.document.documentElement.setAttribute("customize-entered", true);
-        this.document.documentElement.removeAttribute("customize-entering");
-      }
-      this.dispatchToolboxEvent("customization-transitionend", aEntering);
+        if (!aEntering) {
+          this.document.documentElement.removeAttribute("customize-exiting");
+          this.document.documentElement.removeAttribute("customizing");
+        } else {
+          this.document.documentElement.setAttribute("customize-entered", true);
+          this.document.documentElement.removeAttribute("customize-entering");
+        }
+        this.dispatchToolboxEvent("customization-transitionend", aEntering);
 
-      deferred.resolve();
+        deferred.resolve();
+      }.bind(this), 0);
     }.bind(this);
     deck.addEventListener("transitionend", customizeTransitionEnd);
 
     if (gDisableAnimation) {
       deck.setAttribute("fastcustomizeanimation", true);
     }
     if (aEntering) {
       this.document.documentElement.setAttribute("customizing", true);
--- a/browser/components/customizableui/test/browser_934113_menubar_removable.js
+++ b/browser/components/customizableui/test/browser_934113_menubar_removable.js
@@ -5,21 +5,26 @@
 "use strict";
 
 // Attempting to drag the menubar to the navbar shouldn't work.
 add_task(function() {
   yield startCustomizing();
   let menuItems = document.getElementById("menubar-items");
   let navbar = document.getElementById("nav-bar");
   let menubar = document.getElementById("toolbar-menubar");
+  // Force the menu to be shown.
+  const kAutohide = menubar.getAttribute("autohide");
+  menubar.setAttribute("autohide", "false");
   simulateItemDrag(menuItems, navbar.customizationTarget);
+
   is(getAreaWidgetIds("nav-bar").indexOf("menubar-items"), -1, "Menu bar shouldn't be in the navbar.");
   ok(!navbar.querySelector("#menubar-items"), "Shouldn't find menubar items in the navbar.");
   ok(menubar.querySelector("#menubar-items"), "Should find menubar items in the menubar.");
   isnot(getAreaWidgetIds("toolbar-menubar").indexOf("menubar-items"), -1,
         "Menubar items shouldn't be missing from the navbar.");
+  menubar.setAttribute("autohide", kAutohide);
   yield endCustomizing();
 });
 
 add_task(function asyncCleanup() {
   yield endCustomizing();
   yield resetCustomization();
 });
--- a/browser/components/privatebrowsing/content/aboutPrivateBrowsing.xhtml
+++ b/browser/components/privatebrowsing/content/aboutPrivateBrowsing.xhtml
@@ -2,46 +2,35 @@
 <!--
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 -->
 <!DOCTYPE html [
   <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
   %htmlDTD;
-  <!ENTITY % netErrorDTD SYSTEM "chrome://global/locale/netError.dtd">
-  %netErrorDTD;
   <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
   %globalDTD;
+  <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+  %brandDTD;
   <!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
   %browserDTD;
-#ifdef XP_MACOSX
-  <!ENTITY basePBMenu.label   "&fileMenu.label;">
-#else
-  <!ENTITY basePBMenu.label   "<span class='appMenuButton'>&brandShortName;</span><span class='fileMenu'>&fileMenu.label;</span>">
-#endif
   <!ENTITY % privatebrowsingpageDTD SYSTEM "chrome://browser/locale/aboutPrivateBrowsing.dtd">
   %privatebrowsingpageDTD;
 ]>
 
 <html xmlns="http://www.w3.org/1999/xhtml">
   <head>
     <link rel="stylesheet" href="chrome://global/skin/netError.css" type="text/css" media="all"/>
     <link rel="stylesheet" href="chrome://browser/skin/aboutPrivateBrowsing.css" type="text/css" media="all"/>
     <style type="text/css"><![CDATA[
       body.normal .showPrivate,
       body.private .showNormal {
         display: none;
       }
-      body.appMenuButtonVisible .fileMenu {
-        display: none;
-      }
-      body.appMenuButtonInvisible .appMenuButton {
-        display: none;
-      }
     ]]></style>
     <script type="application/javascript;version=1.7"><![CDATA[
       const Cc = Components.classes;
       const Ci = Components.interfaces;
 
       Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
 
       if (!PrivateBrowsingUtils.isWindowPrivate(window)) {
@@ -77,23 +66,16 @@
 
         // Set up the help link
         let moreInfoURL = Cc["@mozilla.org/toolkit/URLFormatterService;1"].
                           getService(Ci.nsIURLFormatter).
                           formatURLPref("app.support.baseURL");
         let moreInfoLink = document.getElementById("moreInfoLink");
         if (moreInfoLink)
           moreInfoLink.setAttribute("href", moreInfoURL + "private-browsing");
-
-        // Show the correct menu structure based on whether the App Menu button is
-        // shown or not.
-        var menuBar = mainWindow.document.getElementById("toolbar-menubar");
-        var appMenuButtonIsVisible = menuBar.getAttribute("autohide") == "true";
-        document.body.classList.add(appMenuButtonIsVisible ? "appMenuButtonVisible" :
-                                                             "appMenuButtonInvisible");
       }, false);
 
       function openPrivateWindow() {
         mainWindow.OpenBrowserWindow({private: true});
       }
     ]]></script>
   </head>
 
@@ -129,17 +111,17 @@
                   id="startPrivateBrowsing" label="&privatebrowsingpage.openPrivateWindow.label;"
                   accesskey="&privatebrowsingpage.openPrivateWindow.accesskey;"
                   oncommand="openPrivateWindow();"/>
         </div>
 
         <!-- Footer -->
         <div id="footerDesc">
           <p id="footerText" class="showPrivate">&privatebrowsingpage.howToStop3;</p>
-          <p id="footerTextNormal" class="showNormal">&privatebrowsingpage.howToStart3;</p>
+          <p id="footerTextNormal" class="showNormal">&privatebrowsingpage.howToStart4;</p>
         </div>
 
         <!-- More Info -->
         <div id="moreInfo" class="showPrivate">
           <p id="moreInfoText">
             &privatebrowsingpage.moreInfo;
           </p>
           <p id="moreInfoLinkContainer">
--- a/browser/components/sessionstore/src/SessionFile.jsm
+++ b/browser/components/sessionstore/src/SessionFile.jsm
@@ -228,10 +228,10 @@ let SessionWorker = (function () {
   };
 })();
 
 // Ensure that we can write sessionstore.js cleanly before the profile
 // becomes unaccessible.
 AsyncShutdown.profileBeforeChange.addBlocker(
   "SessionFile: Finish writing the latest sessionstore.js",
   function() {
-    return SessionFile._latestWrite;
+    return SessionFileInternal._latestWrite;
   });
--- a/browser/devtools/debugger/debugger-panes.js
+++ b/browser/devtools/debugger/debugger-panes.js
@@ -1867,17 +1867,24 @@ VariableBubbleView.prototype = {
     // machinery to display it.
     if (VariablesView.isPrimitive({ value: objectActor })) {
       let className = VariablesView.getClass(objectActor);
       let textContent = VariablesView.getString(objectActor);
       this._tooltip.setTextContent({
         messages: [textContent],
         messagesClass: className,
         containerClass: "plain"
-      });
+      }, [{
+        label: L10N.getStr('addWatchExpressionButton'),
+        className: "dbg-expression-button",
+        command: () => {
+          DebuggerView.VariableBubble.hideContents();
+          DebuggerView.WatchExpressions.addExpression(evalPrefix, true);
+        }
+      }]);
     } else {
       this._tooltip.setVariableContent(objectActor, {
         searchPlaceholder: L10N.getStr("emptyPropertiesFilterText"),
         searchEnabled: Prefs.variablesSearchboxVisible,
         eval: (variable, value) => {
           let string = variable.evaluationMacro(variable, value);
           DebuggerController.StackFrames.evaluate(string);
           DebuggerView.VariableBubble.hideContents();
@@ -1889,17 +1896,24 @@ VariableBubbleView.prototype = {
         getterOrSetterEvalMacro: this._getGetterOrSetterEvalMacro(evalPrefix),
         overrideValueEvalMacro: this._getOverrideValueEvalMacro(evalPrefix)
       }, {
         fetched: (aEvent, aType) => {
           if (aType == "properties") {
             window.emit(EVENTS.FETCHED_BUBBLE_PROPERTIES);
           }
         }
-      });
+      }, [{
+        label: L10N.getStr("addWatchExpressionButton"),
+        className: "dbg-expression-button",
+        command: () => {
+          DebuggerView.VariableBubble.hideContents();
+          DebuggerView.WatchExpressions.addExpression(evalPrefix, true);
+        }
+      }]);
     }
 
     this._tooltip.show(this._markedText.anchor);
   },
 
   /**
    * Hides the inspection popup.
    */
@@ -2026,38 +2040,49 @@ WatchExpressionsView.prototype = Heritag
     this.widget.removeEventListener("click", this._onClick, false);
   },
 
   /**
    * Adds a watch expression in this container.
    *
    * @param string aExpression [optional]
    *        An optional initial watch expression text.
+   * @param boolean aSkipUserInput [optional]
+   *        Pass true to avoid waiting for additional user input
+   *        on the watch expression.
    */
-  addExpression: function(aExpression = "") {
+  addExpression: function(aExpression = "", aSkipUserInput = false) {
     // Watch expressions are UI elements which benefit from visible panes.
     DebuggerView.showInstrumentsPane();
 
     // Create the element node for the watch expression item.
     let itemView = this._createItemView(aExpression);
 
     // Append a watch expression item to this container.
     let expressionItem = this.push([itemView.container], {
       index: 0, /* specifies on which position should the item be appended */
       attachment: {
         view: itemView,
         initialExpression: aExpression,
         currentExpression: "",
       }
     });
 
-    // Automatically focus the new watch expression input.
-    expressionItem.attachment.view.inputNode.select();
-    expressionItem.attachment.view.inputNode.focus();
-    DebuggerView.Variables.parentNode.scrollTop = 0;
+    // Automatically focus the new watch expression input
+    // if additional user input is desired.
+    if (!aSkipUserInput) {
+      expressionItem.attachment.view.inputNode.select();
+      expressionItem.attachment.view.inputNode.focus();
+      DebuggerView.Variables.parentNode.scrollTop = 0;
+    }
+    // Otherwise, add and evaluate the new watch expression immediately.
+    else {
+      this.toggleContents(false);
+      this._onBlur({ target: expressionItem.attachment.view.inputNode });
+    }
   },
 
   /**
    * Changes the watch expression corresponding to the specified variable item.
    * This function is called whenever a watch expression's code is edited in
    * the variables view container.
    *
    * @param Variable aVar
--- a/browser/devtools/debugger/debugger.xul
+++ b/browser/devtools/debugger/debugger.xul
@@ -301,17 +301,18 @@
   </keyset>
 
   <vbox id="body"
         class="theme-body"
         layout="horizontal"
         flex="1">
     <toolbar id="debugger-toolbar"
              class="devtools-toolbar">
-      <hbox id="debugger-controls">
+      <hbox id="debugger-controls"
+            class="devtools-toolbarbutton-group">
         <toolbarbutton id="resume"
                        class="devtools-toolbarbutton"
                        tabindex="0"/>
         <toolbarbutton id="step-over"
                        class="devtools-toolbarbutton"
                        tabindex="0"/>
         <toolbarbutton id="step-in"
                        class="devtools-toolbarbutton"
@@ -349,17 +350,18 @@
           <tab id="sources-tab" label="&debuggerUI.tabs.sources;"/>
           <tab id="callstack-tab" label="&debuggerUI.tabs.callstack;"/>
           <tab id="tracer-tab" label="&debuggerUI.tabs.traces;" hidden="true"/>
         </tabs>
         <tabpanels flex="1">
           <tabpanel id="sources-tabpanel">
             <vbox id="sources" flex="1"/>
             <toolbar id="sources-toolbar" class="devtools-toolbar">
-              <hbox id="sources-controls">
+              <hbox id="sources-controls"
+                    class="devtools-toolbarbutton-group">
                 <toolbarbutton id="black-box"
                                class="devtools-toolbarbutton"
                                tooltiptext="&debuggerUI.sources.blackBoxTooltip;"
                                command="blackBoxCommand"/>
                 <toolbarbutton id="pretty-print"
                                class="devtools-toolbarbutton devtools-monospace"
                                label="{}"
                                tooltiptext="&debuggerUI.sources.prettyPrint;"
--- a/browser/devtools/debugger/test/browser.ini
+++ b/browser/devtools/debugger/test/browser.ini
@@ -59,16 +59,17 @@ support-files =
   doc_scope-variable.html
   doc_scope-variable-2.html
   doc_scope-variable-3.html
   doc_script-switching-01.html
   doc_script-switching-02.html
   doc_step-out.html
   doc_tracing-01.html
   doc_watch-expressions.html
+  doc_watch-expression-button.html
   doc_with-frame.html
   head.js
   sjs_random-javascript.sjs
   testactors.js
 
 [browser_dbg_aaa_run_first_leaktest.js]
 [browser_dbg_auto-pretty-print-01.js]
 [browser_dbg_auto-pretty-print-02.js]
@@ -239,16 +240,18 @@ support-files =
 [browser_dbg_variables-view-popup-03.js]
 [browser_dbg_variables-view-popup-04.js]
 [browser_dbg_variables-view-popup-05.js]
 [browser_dbg_variables-view-popup-06.js]
 [browser_dbg_variables-view-popup-07.js]
 [browser_dbg_variables-view-popup-08.js]
 [browser_dbg_variables-view-popup-09.js]
 [browser_dbg_variables-view-popup-10.js]
+[browser_dbg_variables-view-popup-11.js]
+[browser_dbg_variables-view-popup-12.js]
 [browser_dbg_variables-view-reexpand-01.js]
 [browser_dbg_variables-view-reexpand-02.js]
 [browser_dbg_variables-view-webidl.js]
 [browser_dbg_watch-expressions-01.js]
 [browser_dbg_watch-expressions-02.js]
 [browser_dbg_chrome-create.js]
 skip-if = os == "linux" # Bug 847558
 [browser_dbg_on-pause-raise.js]
--- a/browser/devtools/debugger/test/browser_dbg_auto-pretty-print-01.js
+++ b/browser/devtools/debugger/test/browser_dbg_auto-pretty-print-01.js
@@ -6,16 +6,19 @@
 const TAB_URL = EXAMPLE_URL + "doc_auto-pretty-print-01.html";
 
 let gTab, gDebuggee, gPanel, gDebugger;
 let gEditor, gSources, gPrefs, gOptions, gView;
 
 let gFirstSourceLabel = "code_ugly-5.js";
 let gSecondSourceLabel = "code_ugly-6.js";
 
+let gOriginalPref = Services.prefs.getBoolPref("devtools.debugger.auto-pretty-print");
+Services.prefs.setBoolPref("devtools.debugger.auto-pretty-print", true);
+
 function test(){
   initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gEditor = gDebugger.DebuggerView.editor;
     gSources = gDebugger.DebuggerView.Sources;
@@ -99,9 +102,10 @@ registerCleanupFunction(function() {
   gDebuggee = null;
   gPanel = null;
   gDebugger = null;
   gEditor = null;
   gSources = null;
   gOptions = null;
   gPrefs = null;
   gView = null;
-});
\ No newline at end of file
+  Services.prefs.setBoolPref("devtools.debugger.auto-pretty-print", gOriginalPref);
+});
--- a/browser/devtools/debugger/test/browser_dbg_auto-pretty-print-02.js
+++ b/browser/devtools/debugger/test/browser_dbg_auto-pretty-print-02.js
@@ -10,16 +10,19 @@
 const TAB_URL = EXAMPLE_URL + "doc_auto-pretty-print-02.html";
 
 let gTab, gDebuggee, gPanel, gDebugger;
 let gEditor, gSources, gPrefs, gOptions, gView;
 
 let gFirstSourceLabel = "code_ugly-6.js";
 let gSecondSourceLabel = "code_ugly-7.js";
 
+let gOriginalPref = Services.prefs.getBoolPref("devtools.debugger.auto-pretty-print");
+Services.prefs.setBoolPref("devtools.debugger.auto-pretty-print", true);
+
 function test(){
   initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gEditor = gDebugger.DebuggerView.editor;
     gSources = gDebugger.DebuggerView.Sources;
@@ -102,9 +105,10 @@ registerCleanupFunction(function() {
   gDebuggee = null;
   gPanel = null;
   gDebugger = null;
   gEditor = null;
   gSources = null;
   gOptions = null;
   gPrefs = null;
   gView = null;
+  Services.prefs.setBoolPref("devtools.debugger.auto-pretty-print", gOriginalPref);
 });
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_variables-view-popup-11.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the watch expression button is added in variable view popup.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_watch-expression-button.html";
+
+function test() {
+  Task.spawn(function() {
+    let [tab, debuggee, panel] = yield initDebugger(TAB_URL);
+    let win = panel.panelWin;
+    let events = win.EVENTS;
+    let watch = win.DebuggerView.WatchExpressions;
+    let bubble = win.DebuggerView.VariableBubble;
+    let tooltip = bubble._tooltip.panel;
+
+    let label = win.L10N.getStr("addWatchExpressionButton");
+    let className = "dbg-expression-button";
+
+    function testExpressionButton(aLabel, aClassName, aExpression) {
+      ok(tooltip.querySelector("button"),
+        "There should be a button available in variable view popup.");
+      is(tooltip.querySelector("button").label, aLabel,
+        "The button available is labeled correctly.");
+      is(tooltip.querySelector("button").className, aClassName,
+        "The button available is styled correctly.");
+
+      tooltip.querySelector("button").click();
+
+      ok(!tooltip.querySelector("button"),
+        "There should be no button available in variable view popup.");
+      ok(watch.getItemAtIndex(0),
+        "The expression at index 0 should be available.");
+      is(watch.getItemAtIndex(0).attachment.initialExpression, aExpression,
+        "The expression at index 0 is correct.");
+    }
+
+    // Allow this generator function to yield first.
+    executeSoon(() => debuggee.start());
+    yield waitForSourceAndCaretAndScopes(panel, ".html", 19);
+
+    // Inspect primitive value variable.
+    yield openVarPopup(panel, { line: 15, ch: 12 });
+    let popupHiding = once(tooltip, "popuphiding");
+    let expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
+    testExpressionButton(label, className, "a");
+    yield promise.all([popupHiding, expressionsEvaluated]);
+    ok(true, "The new watch expressions were re-evaluated and the panel got hidden (1).");
+
+    // Inspect non primitive value variable.
+    let expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
+    yield openVarPopup(panel, { line: 16, ch: 12 }, true);
+    yield expressionsEvaluated;
+    ok(true, "The watch expressions were re-evaluated when a new panel opened (1).");
+
+    let popupHiding = once(tooltip, "popuphiding");
+    let expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
+    testExpressionButton(label, className, "b");
+    yield promise.all([popupHiding, expressionsEvaluated]);
+    ok(true, "The new watch expressions were re-evaluated and the panel got hidden (2).");
+
+    // Inspect property of an object.
+    let expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
+    yield openVarPopup(panel, { line: 17, ch: 10 });
+    yield expressionsEvaluated;
+    ok(true, "The watch expressions were re-evaluated when a new panel opened (2).");
+
+    let popupHiding = once(tooltip, "popuphiding");
+    let expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
+    testExpressionButton(label, className, "b.a");
+    yield promise.all([popupHiding, expressionsEvaluated]);
+    ok(true, "The new watch expressions were re-evaluated and the panel got hidden (3).");
+
+    yield resumeDebuggerThenCloseAndFinish(panel);
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_variables-view-popup-12.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the clicking "Watch" button twice, for the same expression, only adds it
+ * once.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_watch-expression-button.html";
+
+function test() {
+  Task.spawn(function() {
+    let [tab, debuggee, panel] = yield initDebugger(TAB_URL);
+    let win = panel.panelWin;
+    let events = win.EVENTS;
+    let watch = win.DebuggerView.WatchExpressions;
+    let bubble = win.DebuggerView.VariableBubble;
+    let tooltip = bubble._tooltip.panel;
+
+    function verifyContent(aExpression, aItemCount) {
+
+      ok(watch.getItemAtIndex(0),
+        "The expression at index 0 should be available.");
+      is(watch.getItemAtIndex(0).attachment.initialExpression, aExpression,
+        "The expression at index 0 is correct.");
+      is(watch.itemCount, aItemCount,
+        "The expression count is correct.");
+    }
+
+    // Allow this generator function to yield first.
+    executeSoon(() => debuggee.start());
+    yield waitForSourceAndCaretAndScopes(panel, ".html", 19);
+
+    // Inspect primitive value variable.
+    yield openVarPopup(panel, { line: 15, ch: 12 });
+    let popupHiding = once(tooltip, "popuphiding");
+    let expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
+    tooltip.querySelector("button").click();
+    verifyContent("a", 1);
+    yield promise.all([popupHiding, expressionsEvaluated]);
+    ok(true, "The new watch expressions were re-evaluated and the panel got hidden (1).");
+
+    // Inspect property of an object.
+    let expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
+    yield openVarPopup(panel, { line: 17, ch: 10 });
+    yield expressionsEvaluated;
+    ok(true, "The watch expressions were re-evaluated when a new panel opened (1).");
+
+    let popupHiding = once(tooltip, "popuphiding");
+    let expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
+    tooltip.querySelector("button").click();
+    verifyContent("b.a", 2);
+    yield promise.all([popupHiding, expressionsEvaluated]);
+    ok(true, "The new watch expressions were re-evaluated and the panel got hidden (2).");
+
+    // Re-inspect primitive value variable.
+    let expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
+    yield openVarPopup(panel, { line: 15, ch: 12 });
+    yield expressionsEvaluated;
+    ok(true, "The watch expressions were re-evaluated when a new panel opened (2).");
+
+    let popupHiding = once(tooltip, "popuphiding");
+    let expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
+    tooltip.querySelector("button").click();
+    verifyContent("b.a", 2);
+    yield promise.all([popupHiding, expressionsEvaluated]);
+    ok(true, "The new watch expressions were re-evaluated and the panel got hidden (3).");
+
+    yield resumeDebuggerThenCloseAndFinish(panel);
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/doc_watch-expression-button.html
@@ -0,0 +1,31 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Debugger test page</title>
+  </head>
+
+  <body>
+    <button onclick="start()">Click me!</button>
+
+    <script type="text/javascript">
+      function test() {
+        var a = 1;
+        var b = { a: a };
+        b.a = 2;
+        debugger;
+      }
+
+      function start() {
+        var e  = eval('test();');
+      }
+
+      var button = document.querySelector("button");
+      var buttonAsProto = Object.create(button);
+    </script>
+  </body>
+
+</html>
--- a/browser/devtools/profiler/profiler.xul
+++ b/browser/devtools/profiler/profiler.xul
@@ -19,17 +19,18 @@
 
   <script type="application/javascript;version=1.8"
           src="chrome://browser/content/devtools/theme-switching.js"/>
   <script type="text/javascript" src="sidebar.js"/>
   <box flex="1" id="profiler-chrome"
     class="devtools-responsive-container theme-body">
     <vbox class="profiler-sidebar theme-sidebar">
       <toolbar class="devtools-toolbar">
-        <hbox id="profiler-controls">
+        <hbox id="profiler-controls"
+              class="devtools-toolbarbutton-group">
           <toolbarbutton id="profiler-start"
             tooltiptext="&startProfiler.tooltip;"
             class="devtools-toolbarbutton"
             disabled="true"/>
           <toolbarbutton id="profiler-import"
             class="devtools-toolbarbutton"
             disabled="true"
             label="&importProfile.label;"/>
--- a/browser/devtools/shared/widgets/Tooltip.js
+++ b/browser/devtools/shared/widgets/Tooltip.js
@@ -409,32 +409,47 @@ Tooltip.prototype = {
    *        A list of text messages.
    * @param {string} messagesClass [optional]
    *        A style class for the text messages.
    * @param {string} containerClass [optional]
    *        A style class for the text messages container.
    * @param {boolean} isAlertTooltip [optional]
    *        Pass true to add an alert image for your tooltip.
    */
-  setTextContent: function({ messages, messagesClass, containerClass, isAlertTooltip }) {
+  setTextContent: function(
+    {
+      messages,
+      messagesClass,
+      containerClass,
+      isAlertTooltip
+    },
+    extraButtons = []) {
     messagesClass = messagesClass || "default-tooltip-simple-text-colors";
     containerClass = containerClass || "default-tooltip-simple-text-colors";
 
     let vbox = this.doc.createElement("vbox");
     vbox.className = "devtools-tooltip-simple-text-container " + containerClass;
     vbox.setAttribute("flex", "1");
 
     for (let text of messages) {
       let description = this.doc.createElement("description");
       description.setAttribute("flex", "1");
       description.className = "devtools-tooltip-simple-text " + messagesClass;
       description.textContent = text;
       vbox.appendChild(description);
     }
 
+    for (let { label, className, command } of extraButtons) {
+      let button = this.doc.createElement("button");
+      button.className = className;
+      button.setAttribute("label", label);
+      button.addEventListener("command", command);
+      vbox.appendChild(button);
+    }
+
     if (isAlertTooltip) {
       let hbox = this.doc.createElement("hbox");
       hbox.setAttribute("align", "start");
 
       let alertImg = this.doc.createElement("image");
       alertImg.className = "devtools-tooltip-alert-icon";
       hbox.appendChild(alertImg);
       hbox.appendChild(vbox);
@@ -462,40 +477,42 @@ Tooltip.prototype = {
    *        Otherwise, if a variable was previously inspected, its widget
    *        will be reused.
    */
   setVariableContent: function(
     objectActor,
     viewOptions = {},
     controllerOptions = {},
     relayEvents = {},
-    reuseCachedWidget = true) {
+    extraButtons = []) {
 
-    if (reuseCachedWidget && this._cachedVariablesView) {
-      var [vbox, widget] = this._cachedVariablesView;
-    } else {
-      var vbox = this.doc.createElement("vbox");
-      vbox.className = "devtools-tooltip-variables-view-box";
-      vbox.setAttribute("flex", "1");
+    let vbox = this.doc.createElement("vbox");
+    vbox.className = "devtools-tooltip-variables-view-box";
+    vbox.setAttribute("flex", "1");
+
+    let innerbox = this.doc.createElement("vbox");
+    innerbox.className = "devtools-tooltip-variables-view-innerbox";
+    innerbox.setAttribute("flex", "1");
+    vbox.appendChild(innerbox);
 
-      let innerbox = this.doc.createElement("vbox");
-      innerbox.className = "devtools-tooltip-variables-view-innerbox";
-      innerbox.setAttribute("flex", "1");
-      vbox.appendChild(innerbox);
-
-      var widget = new VariablesView(innerbox, viewOptions);
+    for (let { label, className, command } of extraButtons) {
+      let button = this.doc.createElement("button");
+      button.className = className;
+      button.setAttribute("label", label);
+      button.addEventListener("command", command);
+      vbox.appendChild(button);
+    }
 
-      // Analyzing state history isn't useful with transient object inspectors.
-      widget.commitHierarchy = () => {};
+    let widget = new VariablesView(innerbox, viewOptions);
 
-      for (let e in relayEvents) widget.on(e, relayEvents[e]);
-      VariablesViewController.attach(widget, controllerOptions);
+    // Analyzing state history isn't useful with transient object inspectors.
+    widget.commitHierarchy = () => {};
 
-      this._cachedVariablesView = [vbox, widget];
-    }
+    for (let e in relayEvents) widget.on(e, relayEvents[e]);
+    VariablesViewController.attach(widget, controllerOptions);
 
     // Some of the view options are allowed to change between uses.
     widget.searchPlaceholder = viewOptions.searchPlaceholder;
     widget.searchEnabled = viewOptions.searchEnabled;
 
     // Use the object actor's grip to display it as a variable in the widget.
     // The controller options are allowed to change between uses.
     widget.controller.setSingleVariable(
--- a/browser/extensions/pdfjs/README.mozilla
+++ b/browser/extensions/pdfjs/README.mozilla
@@ -1,4 +1,4 @@
 This is the pdf.js project output, https://github.com/mozilla/pdf.js
 
-Current extension version is: 0.8.934
+Current extension version is: 0.8.990
 
--- a/browser/extensions/pdfjs/content/build/pdf.js
+++ b/browser/extensions/pdfjs/content/build/pdf.js
@@ -15,18 +15,18 @@
  * limitations under the License.
  */
 
 // Initializing PDFJS global object (if still undefined)
 if (typeof PDFJS === 'undefined') {
   (typeof window !== 'undefined' ? window : this).PDFJS = {};
 }
 
-PDFJS.version = '0.8.934';
-PDFJS.build = 'c80df60';
+PDFJS.version = '0.8.990';
+PDFJS.build = '54f6291';
 
 (function pdfjsWrapper() {
   // Use strict in our context only - users might not want it
   'use strict';
 
 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
 /* Copyright 2012 Mozilla Foundation
@@ -2000,420 +2000,16 @@ var LabCS = (function LabCSClosure() {
     },
     usesZeroToOneRange: false
   };
   return LabCS;
 })();
 
 
 
-var PatternType = {
-  AXIAL: 2,
-  RADIAL: 3
-};
-
-var Pattern = (function PatternClosure() {
-  // Constructor should define this.getPattern
-  function Pattern() {
-    error('should not call Pattern constructor');
-  }
-
-  Pattern.prototype = {
-    // Input: current Canvas context
-    // Output: the appropriate fillStyle or strokeStyle
-    getPattern: function Pattern_getPattern(ctx) {
-      error('Should not call Pattern.getStyle: ' + ctx);
-    }
-  };
-
-  Pattern.shadingFromIR = function Pattern_shadingFromIR(raw) {
-    return Shadings[raw[0]].fromIR(raw);
-  };
-
-  Pattern.parseShading = function Pattern_parseShading(shading, matrix, xref,
-                                                       res) {
-
-    var dict = isStream(shading) ? shading.dict : shading;
-    var type = dict.get('ShadingType');
-
-    switch (type) {
-      case PatternType.AXIAL:
-      case PatternType.RADIAL:
-        // Both radial and axial shadings are handled by RadialAxial shading.
-        return new Shadings.RadialAxial(dict, matrix, xref, res);
-      default:
-        UnsupportedManager.notify(UNSUPPORTED_FEATURES.shadingPattern);
-        return new Shadings.Dummy();
-    }
-  };
-  return Pattern;
-})();
-
-var Shadings = {};
-
-// A small number to offset the first/last color stops so we can insert ones to
-// support extend.  Number.MIN_VALUE appears to be too small and breaks the
-// extend. 1e-7 works in FF but chrome seems to use an even smaller sized number
-// internally so we have to go bigger.
-Shadings.SMALL_NUMBER = 1e-2;
-
-// Radial and axial shading have very similar implementations
-// If needed, the implementations can be broken into two classes
-Shadings.RadialAxial = (function RadialAxialClosure() {
-  function RadialAxial(dict, matrix, xref, res, ctx) {
-    this.matrix = matrix;
-    this.coordsArr = dict.get('Coords');
-    this.shadingType = dict.get('ShadingType');
-    this.type = 'Pattern';
-    this.ctx = ctx;
-    var cs = dict.get('ColorSpace', 'CS');
-    cs = ColorSpace.parse(cs, xref, res);
-    this.cs = cs;
-
-    var t0 = 0.0, t1 = 1.0;
-    if (dict.has('Domain')) {
-      var domainArr = dict.get('Domain');
-      t0 = domainArr[0];
-      t1 = domainArr[1];
-    }
-
-    var extendStart = false, extendEnd = false;
-    if (dict.has('Extend')) {
-      var extendArr = dict.get('Extend');
-      extendStart = extendArr[0];
-      extendEnd = extendArr[1];
-    }
-
-    if (this.shadingType === PatternType.RADIAL &&
-       (!extendStart || !extendEnd)) {
-      // Radial gradient only currently works if either circle is fully within
-      // the other circle.
-      var x1 = this.coordsArr[0];
-      var y1 = this.coordsArr[1];
-      var r1 = this.coordsArr[2];
-      var x2 = this.coordsArr[3];
-      var y2 = this.coordsArr[4];
-      var r2 = this.coordsArr[5];
-      var distance = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
-      if (r1 <= r2 + distance &&
-          r2 <= r1 + distance) {
-        warn('Unsupported radial gradient.');
-      }
-    }
-
-    this.extendStart = extendStart;
-    this.extendEnd = extendEnd;
-
-    var fnObj = dict.get('Function');
-    var fn;
-    if (isArray(fnObj)) {
-      var fnArray = [];
-      for (var j = 0, jj = fnObj.length; j < jj; j++) {
-        var obj = xref.fetchIfRef(fnObj[j]);
-        if (!isPDFFunction(obj)) {
-          error('Invalid function');
-        }
-        fnArray.push(PDFFunction.parse(xref, obj));
-      }
-      fn = function radialAxialColorFunction(arg) {
-        var out = [];
-        for (var i = 0, ii = fnArray.length; i < ii; i++) {
-          out.push(fnArray[i](arg)[0]);
-        }
-        return out;
-      };
-    } else {
-      if (!isPDFFunction(fnObj)) {
-        error('Invalid function');
-      }
-      fn = PDFFunction.parse(xref, fnObj);
-    }
-
-    // 10 samples seems good enough for now, but probably won't work
-    // if there are sharp color changes. Ideally, we would implement
-    // the spec faithfully and add lossless optimizations.
-    var diff = t1 - t0;
-    var step = diff / 10;
-
-    var colorStops = this.colorStops = [];
-
-    // Protect against bad domains so we don't end up in an infinte loop below.
-    if (t0 >= t1 || step <= 0) {
-      // Acrobat doesn't seem to handle these cases so we'll ignore for
-      // now.
-      info('Bad shading domain.');
-      return;
-    }
-
-    for (var i = t0; i <= t1; i += step) {
-      var rgbColor = cs.getRgb(fn([i]), 0);
-      var cssColor = Util.makeCssRgb(rgbColor);
-      colorStops.push([(i - t0) / diff, cssColor]);
-    }
-
-    var background = 'transparent';
-    if (dict.has('Background')) {
-      var rgbColor = cs.getRgb(dict.get('Background'), 0);
-      background = Util.makeCssRgb(rgbColor);
-    }
-
-    if (!extendStart) {
-      // Insert a color stop at the front and offset the first real color stop
-      // so it doesn't conflict with the one we insert.
-      colorStops.unshift([0, background]);
-      colorStops[1][0] += Shadings.SMALL_NUMBER;
-    }
-    if (!extendEnd) {
-      // Same idea as above in extendStart but for the end.
-      colorStops[colorStops.length - 1][0] -= Shadings.SMALL_NUMBER;
-      colorStops.push([1, background]);
-    }
-
-    this.colorStops = colorStops;
-  }
-
-  RadialAxial.fromIR = function RadialAxial_fromIR(raw) {
-    var type = raw[1];
-    var colorStops = raw[2];
-    var p0 = raw[3];
-    var p1 = raw[4];
-    var r0 = raw[5];
-    var r1 = raw[6];
-    return {
-      type: 'Pattern',
-      getPattern: function RadialAxial_getPattern(ctx) {
-        var grad;
-        if (type == PatternType.AXIAL)
-          grad = ctx.createLinearGradient(p0[0], p0[1], p1[0], p1[1]);
-        else if (type == PatternType.RADIAL)
-          grad = ctx.createRadialGradient(p0[0], p0[1], r0, p1[0], p1[1], r1);
-
-        for (var i = 0, ii = colorStops.length; i < ii; ++i) {
-          var c = colorStops[i];
-          grad.addColorStop(c[0], c[1]);
-        }
-        return grad;
-      }
-    };
-  };
-
-  RadialAxial.prototype = {
-    getIR: function RadialAxial_getIR() {
-      var coordsArr = this.coordsArr;
-      var type = this.shadingType;
-      if (type == PatternType.AXIAL) {
-        var p0 = [coordsArr[0], coordsArr[1]];
-        var p1 = [coordsArr[2], coordsArr[3]];
-        var r0 = null;
-        var r1 = null;
-      } else if (type == PatternType.RADIAL) {
-        var p0 = [coordsArr[0], coordsArr[1]];
-        var p1 = [coordsArr[3], coordsArr[4]];
-        var r0 = coordsArr[2];
-        var r1 = coordsArr[5];
-      } else {
-        error('getPattern type unknown: ' + type);
-      }
-
-      var matrix = this.matrix;
-      if (matrix) {
-        p0 = Util.applyTransform(p0, matrix);
-        p1 = Util.applyTransform(p1, matrix);
-      }
-
-      return ['RadialAxial', type, this.colorStops, p0, p1, r0, r1];
-    }
-  };
-
-  return RadialAxial;
-})();
-
-Shadings.Dummy = (function DummyClosure() {
-  function Dummy() {
-    this.type = 'Pattern';
-  }
-
-  Dummy.fromIR = function Dummy_fromIR() {
-    return {
-      type: 'Pattern',
-      getPattern: function Dummy_fromIR_getPattern() {
-        return 'hotpink';
-      }
-    };
-  };
-
-  Dummy.prototype = {
-    getIR: function Dummy_getIR() {
-      return ['Dummy'];
-    }
-  };
-  return Dummy;
-})();
-
-var TilingPattern = (function TilingPatternClosure() {
-  var PaintType = {
-    COLORED: 1,
-    UNCOLORED: 2
-  };
-
-  var MAX_PATTERN_SIZE = 3000; // 10in @ 300dpi shall be enough
-
-  function TilingPattern(IR, color, ctx, objs, commonObjs, baseTransform) {
-    this.name = IR[1][0].name;
-    this.operatorList = IR[2];
-    this.matrix = IR[3] || [1, 0, 0, 1, 0, 0];
-    this.bbox = IR[4];
-    this.xstep = IR[5];
-    this.ystep = IR[6];
-    this.paintType = IR[7];
-    this.tilingType = IR[8];
-    this.color = color;
-    this.objs = objs;
-    this.commonObjs = commonObjs;
-    this.baseTransform = baseTransform;
-    this.type = 'Pattern';
-    this.ctx = ctx;
-  }
-
-  TilingPattern.getIR = function TilingPattern_getIR(operatorList, dict, args) {
-    var matrix = dict.get('Matrix');
-    var bbox = dict.get('BBox');
-    var xstep = dict.get('XStep');
-    var ystep = dict.get('YStep');
-    var paintType = dict.get('PaintType');
-    var tilingType = dict.get('TilingType');
-
-    return [
-      'TilingPattern', args, operatorList, matrix, bbox, xstep, ystep,
-      paintType, tilingType
-    ];
-  };
-
-  TilingPattern.prototype = {
-    createPatternCanvas: function TilinPattern_createPatternCanvas(owner) {
-      var operatorList = this.operatorList;
-      var bbox = this.bbox;
-      var xstep = this.xstep;
-      var ystep = this.ystep;
-      var paintType = this.paintType;
-      var tilingType = this.tilingType;
-      var color = this.color;
-      var objs = this.objs;
-      var commonObjs = this.commonObjs;
-      var ctx = this.ctx;
-
-      info('TilingType: ' + tilingType);
-
-      var x0 = bbox[0], y0 = bbox[1], x1 = bbox[2], y1 = bbox[3];
-
-      var topLeft = [x0, y0];
-      // we want the canvas to be as large as the step size
-      var botRight = [x0 + xstep, y0 + ystep];
-
-      var width = botRight[0] - topLeft[0];
-      var height = botRight[1] - topLeft[1];
-
-      // Obtain scale from matrix and current transformation matrix.
-      var matrixScale = Util.singularValueDecompose2dScale(this.matrix);
-      var curMatrixScale = Util.singularValueDecompose2dScale(
-                             this.baseTransform);
-      var combinedScale = [matrixScale[0] * curMatrixScale[0],
-                           matrixScale[1] * curMatrixScale[1]];
-
-      // MAX_PATTERN_SIZE is used to avoid OOM situation.
-      // Use width and height values that are as close as possible to the end
-      // result when the pattern is used. Too low value makes the pattern look
-      // blurry. Too large value makes it look too crispy.
-      width = Math.min(Math.ceil(Math.abs(width * combinedScale[0])),
-                       MAX_PATTERN_SIZE);
-
-      height = Math.min(Math.ceil(Math.abs(height * combinedScale[1])),
-                        MAX_PATTERN_SIZE);
-
-      var tmpCanvas = CachedCanvases.getCanvas('pattern', width, height, true);
-      var tmpCtx = tmpCanvas.context;
-      var graphics = new CanvasGraphics(tmpCtx, commonObjs, objs);
-      graphics.groupLevel = owner.groupLevel;
-
-      this.setFillAndStrokeStyleToContext(tmpCtx, paintType, color);
-
-      this.setScale(width, height, xstep, ystep);
-      this.transformToScale(graphics);
-
-      // transform coordinates to pattern space
-      var tmpTranslate = [1, 0, 0, 1, -topLeft[0], -topLeft[1]];
-      graphics.transform.apply(graphics, tmpTranslate);
-
-      this.clipBbox(graphics, bbox, x0, y0, x1, y1);
-
-      graphics.executeOperatorList(operatorList);
-      return tmpCanvas.canvas;
-    },
-
-    setScale: function TilingPattern_setScale(width, height, xstep, ystep) {
-      this.scale = [width / xstep, height / ystep];
-    },
-
-    transformToScale: function TilingPattern_transformToScale(graphics) {
-      var scale = this.scale;
-      var tmpScale = [scale[0], 0, 0, scale[1], 0, 0];
-      graphics.transform.apply(graphics, tmpScale);
-    },
-
-    scaleToContext: function TilingPattern_scaleToContext() {
-      var scale = this.scale;
-      this.ctx.scale(1 / scale[0], 1 / scale[1]);
-    },
-
-    clipBbox: function clipBbox(graphics, bbox, x0, y0, x1, y1) {
-      if (bbox && isArray(bbox) && 4 == bbox.length) {
-        var bboxWidth = x1 - x0;
-        var bboxHeight = y1 - y0;
-        graphics.rectangle(x0, y0, bboxWidth, bboxHeight);
-        graphics.clip();
-        graphics.endPath();
-      }
-    },
-
-    setFillAndStrokeStyleToContext:
-      function setFillAndStrokeStyleToContext(context, paintType, color) {
-      switch (paintType) {
-        case PaintType.COLORED:
-          var ctx = this.ctx;
-          context.fillStyle = ctx.fillStyle;
-          context.strokeStyle = ctx.strokeStyle;
-          break;
-        case PaintType.UNCOLORED:
-          var rgbColor = ColorSpace.singletons.rgb.getRgb(color, 0);
-          var cssColor = Util.makeCssRgb(rgbColor);
-          context.fillStyle = cssColor;
-          context.strokeStyle = cssColor;
-          break;
-        default:
-          error('Unsupported paint type: ' + paintType);
-      }
-    },
-
-    getPattern: function TilingPattern_getPattern(ctx, owner) {
-      var temporaryPatternCanvas = this.createPatternCanvas(owner);
-
-      var ctx = this.ctx;
-      ctx.setTransform.apply(ctx, this.baseTransform);
-      ctx.transform.apply(ctx, this.matrix);
-      this.scaleToContext();
-
-      return ctx.createPattern(temporaryPatternCanvas, 'repeat');
-    }
-  };
-
-  return TilingPattern;
-})();
-
-
-
 var PDFFunction = (function PDFFunctionClosure() {
   var CONSTRUCT_SAMPLED = 0;
   var CONSTRUCT_INTERPOLATED = 2;
   var CONSTRUCT_STICHED = 3;
   var CONSTRUCT_POSTSCRIPT = 4;
 
   return {
     getSampleArray: function PDFFunction_getSampleArray(size, outputSize, bps,
@@ -2854,19 +2450,18 @@ var PostScriptStack = (function PostScri
       for (i = c, j = r; i < j; i++, j--) {
         t = stack[i]; stack[i] = stack[j]; stack[j] = t;
       }
     }
   };
   return PostScriptStack;
 })();
 var PostScriptEvaluator = (function PostScriptEvaluatorClosure() {
-  function PostScriptEvaluator(operators, operands) {
+  function PostScriptEvaluator(operators) {
     this.operators = operators;
-    this.operands = operands;
   }
   PostScriptEvaluator.prototype = {
     execute: function PostScriptEvaluator_execute(initialStack) {
       var stack = new PostScriptStack(initialStack);
       var counter = 0;
       var operators = this.operators;
       var length = operators.length;
       var operator, a, b;
@@ -3085,210 +2680,16 @@ var PostScriptEvaluator = (function Post
         }
       }
       return stack.stack;
     }
   };
   return PostScriptEvaluator;
 })();
 
-var PostScriptParser = (function PostScriptParserClosure() {
-  function PostScriptParser(lexer) {
-    this.lexer = lexer;
-    this.operators = [];
-    this.token = null;
-    this.prev = null;
-  }
-  PostScriptParser.prototype = {
-    nextToken: function PostScriptParser_nextToken() {
-      this.prev = this.token;
-      this.token = this.lexer.getToken();
-    },
-    accept: function PostScriptParser_accept(type) {
-      if (this.token.type == type) {
-        this.nextToken();
-        return true;
-      }
-      return false;
-    },
-    expect: function PostScriptParser_expect(type) {
-      if (this.accept(type))
-        return true;
-      error('Unexpected symbol: found ' + this.token.type + ' expected ' +
-            type + '.');
-    },
-    parse: function PostScriptParser_parse() {
-      this.nextToken();
-      this.expect(PostScriptTokenTypes.LBRACE);
-      this.parseBlock();
-      this.expect(PostScriptTokenTypes.RBRACE);
-      return this.operators;
-    },
-    parseBlock: function PostScriptParser_parseBlock() {
-      while (true) {
-        if (this.accept(PostScriptTokenTypes.NUMBER)) {
-          this.operators.push(this.prev.value);
-        } else if (this.accept(PostScriptTokenTypes.OPERATOR)) {
-          this.operators.push(this.prev.value);
-        } else if (this.accept(PostScriptTokenTypes.LBRACE)) {
-          this.parseCondition();
-        } else {
-          return;
-        }
-      }
-    },
-    parseCondition: function PostScriptParser_parseCondition() {
-      // Add two place holders that will be updated later
-      var conditionLocation = this.operators.length;
-      this.operators.push(null, null);
-
-      this.parseBlock();
-      this.expect(PostScriptTokenTypes.RBRACE);
-      if (this.accept(PostScriptTokenTypes.IF)) {
-        // The true block is right after the 'if' so it just falls through on
-        // true else it jumps and skips the true block.
-        this.operators[conditionLocation] = this.operators.length;
-        this.operators[conditionLocation + 1] = 'jz';
-      } else if (this.accept(PostScriptTokenTypes.LBRACE)) {
-        var jumpLocation = this.operators.length;
-        this.operators.push(null, null);
-        var endOfTrue = this.operators.length;
-        this.parseBlock();
-        this.expect(PostScriptTokenTypes.RBRACE);
-        this.expect(PostScriptTokenTypes.IFELSE);
-        // The jump is added at the end of the true block to skip the false
-        // block.
-        this.operators[jumpLocation] = this.operators.length;
-        this.operators[jumpLocation + 1] = 'j';
-
-        this.operators[conditionLocation] = endOfTrue;
-        this.operators[conditionLocation + 1] = 'jz';
-      } else {
-        error('PS Function: error parsing conditional.');
-      }
-    }
-  };
-  return PostScriptParser;
-})();
-
-var PostScriptTokenTypes = {
-  LBRACE: 0,
-  RBRACE: 1,
-  NUMBER: 2,
-  OPERATOR: 3,
-  IF: 4,
-  IFELSE: 5
-};
-
-var PostScriptToken = (function PostScriptTokenClosure() {
-  function PostScriptToken(type, value) {
-    this.type = type;
-    this.value = value;
-  }
-
-  var opCache = {};
-
-  PostScriptToken.getOperator = function PostScriptToken_getOperator(op) {
-    var opValue = opCache[op];
-    if (opValue)
-      return opValue;
-
-    return opCache[op] = new PostScriptToken(PostScriptTokenTypes.OPERATOR, op);
-  };
-
-  PostScriptToken.LBRACE = new PostScriptToken(PostScriptTokenTypes.LBRACE,
-                                                '{');
-  PostScriptToken.RBRACE = new PostScriptToken(PostScriptTokenTypes.RBRACE,
-                                                '}');
-  PostScriptToken.IF = new PostScriptToken(PostScriptTokenTypes.IF, 'IF');
-  PostScriptToken.IFELSE = new PostScriptToken(PostScriptTokenTypes.IFELSE,
-                                                'IFELSE');
-  return PostScriptToken;
-})();
-
-var PostScriptLexer = (function PostScriptLexerClosure() {
-  function PostScriptLexer(stream) {
-    this.stream = stream;
-    this.nextChar();
-  }
-  PostScriptLexer.prototype = {
-    nextChar: function PostScriptLexer_nextChar() {
-      return (this.currentChar = this.stream.getByte());
-    },
-    getToken: function PostScriptLexer_getToken() {
-      var s = '';
-      var comment = false;
-      var ch = this.currentChar;
-
-      // skip comments
-      while (true) {
-        if (ch < 0) {
-          return EOF;
-        }
-
-        if (comment) {
-          if (ch === 0x0A || ch === 0x0D) {
-            comment = false;
-          }
-        } else if (ch == 0x25) { // '%'
-          comment = true;
-        } else if (!Lexer.isSpace(ch)) {
-          break;
-        }
-        ch = this.nextChar();
-      }
-      switch (ch | 0) {
-        case 0x30: case 0x31: case 0x32: case 0x33: case 0x34: // '0'-'4'
-        case 0x35: case 0x36: case 0x37: case 0x38: case 0x39: // '5'-'9'
-        case 0x2B: case 0x2D: case 0x2E: // '+', '-', '.'
-          return new PostScriptToken(PostScriptTokenTypes.NUMBER,
-                                      this.getNumber());
-        case 0x7B: // '{'
-          this.nextChar();
-          return PostScriptToken.LBRACE;
-        case 0x7D: // '}'
-          this.nextChar();
-          return PostScriptToken.RBRACE;
-      }
-      // operator
-      var str = String.fromCharCode(ch);
-      while ((ch = this.nextChar()) >= 0 && // and 'A'-'Z', 'a'-'z'
-             ((ch >= 0x41 && ch <= 0x5A) || (ch >= 0x61 && ch <= 0x7A))) {
-        str += String.fromCharCode(ch);
-      }
-      switch (str.toLowerCase()) {
-        case 'if':
-          return PostScriptToken.IF;
-        case 'ifelse':
-          return PostScriptToken.IFELSE;
-        default:
-          return PostScriptToken.getOperator(str);
-      }
-    },
-    getNumber: function PostScriptLexer_getNumber() {
-      var ch = this.currentChar;
-      var str = String.fromCharCode(ch);
-      while ((ch = this.nextChar()) >= 0) {
-        if ((ch >= 0x30 && ch <= 0x39) || // '0'-'9'
-             ch === 0x2D || ch === 0x2E) { // '-', '.'
-          str += String.fromCharCode(ch);
-        } else {
-          break;
-        }
-      }
-      var value = parseFloat(str);
-      if (isNaN(value))
-        error('Invalid floating point number: ' + value);
-      return value;
-    }
-  };
-  return PostScriptLexer;
-})();
-
-
 
 var Annotation = (function AnnotationClosure() {
   // 12.5.5: Algorithm: Appearance streams
   function getTransformMatrix(rect, bbox, matrix) {
     var bounds = Util.getAxialAlignedBoundingBox(bbox, matrix);
     var minX = bounds[0];
     var minY = bounds[1];
     var maxX = bounds[2];
@@ -4266,20 +3667,21 @@ var PDFDocumentProxy = (function PDFDocu
      */
     getData: function PDFDocumentProxy_getData() {
       var promise = new PDFJS.LegacyPromise();
       this.transport.getData(promise);
       return promise;
     },
     /**
      * @return {Promise} A promise that is resolved when the document's data
-     * is loaded
+     * is loaded. It is resolved with an {Object} that contains the length
+     * property that indicates size of the PDF data in bytes.
      */
-    dataLoaded: function PDFDocumentProxy_dataLoaded() {
-      return this.transport.dataLoaded();
+    getDownloadInfo: function PDFDocumentProxy_getDownloadInfo() {
+      return this.transport.downloadInfoPromise;
     },
     /**
      * Cleans up resources allocated by the document, e.g. created @font-face.
      */
     cleanup: function PDFDocumentProxy_cleanup() {
       this.transport.startCleanup();
     },
     /**
@@ -4542,17 +3944,17 @@ var WorkerTransport = (function WorkerTr
 
     this.workerReadyPromise = workerReadyPromise;
     this.progressCallback = progressCallback;
     this.commonObjs = new PDFObjects();
 
     this.pageCache = [];
     this.pagePromises = [];
     this.embeddedFontsUsed = false;
-
+    this.downloadInfoPromise = new PDFJS.LegacyPromise();
     this.passwordCallback = null;
 
     // If worker support isn't disabled explicit and the browser has worker
     // support, create a new web worker and test if it/the browser fullfills
     // all requirements to run parts of pdf.js in a web worker.
     // Right now, the requirement is, that an Uint8Array is still an Uint8Array
     // as it arrives on the worker. Chrome added this with version 15.
     if (!globalScope.PDFJS.disableWorker && typeof Worker !== 'undefined') {
@@ -4712,16 +4114,20 @@ var WorkerTransport = (function WorkerTr
       messageHandler.on('MissingPDF', function transportMissingPDF(data) {
         this.workerReadyPromise.reject(data.exception.message, data.exception);
       }, this);
 
       messageHandler.on('UnknownError', function transportUnknownError(data) {
         this.workerReadyPromise.reject(data.exception.message, data.exception);
       }, this);
 
+      messageHandler.on('DataLoaded', function transportPage(data) {
+        this.downloadInfoPromise.resolve(data);
+      }, this);
+
       messageHandler.on('GetPage', function transportPage(data) {
         var pageInfo = data.pageInfo;
         var page = new PDFPageProxy(pageInfo, this);
         this.pageCache[pageInfo.pageIndex] = page;
         var promise = this.pagePromises[pageInfo.pageIndex];
         promise.resolve(page);
       }, this);
 
@@ -4878,28 +4284,16 @@ var WorkerTransport = (function WorkerTr
     },
 
     getData: function WorkerTransport_getData(promise) {
       this.messageHandler.send('GetData', null, function(data) {
         promise.resolve(data);
       });
     },
 
-    dataLoaded: function WorkerTransport_dataLoaded() {
-      if (this.dataLoadedPromise) {
-        return this.dataLoadedPromise;
-      }
-      var promise = new PDFJS.LegacyPromise();
-      this.messageHandler.send('DataLoaded', null, function(args) {
-        promise.resolve(args);
-      });
-      this.dataLoadedPromise = promise;
-      return promise;
-    },
-
     getPage: function WorkerTransport_getPage(pageNumber, promise) {
       var pageIndex = pageNumber - 1;
       if (pageIndex in this.pagePromises)
         return this.pagePromises[pageIndex];
       var promise = new PDFJS.LegacyPromise();
       this.pagePromises[pageIndex] = promise;
       this.messageHandler.send('GetPageRequest', { pageIndex: pageIndex });
       return promise;
@@ -5619,16 +5013,17 @@ var CanvasExtraState = (function CanvasE
     this.strokeColorObj = null;
     // Default fore and background colors
     this.fillColor = '#000000';
     this.strokeColor = '#000000';
     // Note: fill alpha applies to all non-stroking operations
     this.fillAlpha = 1;
     this.strokeAlpha = 1;
     this.lineWidth = 1;
+    this.activeSMask = null; // nonclonable field (see the save method below)
 
     this.old = old;
   }
 
   CanvasExtraState.prototype = {
     clone: function CanvasExtraState_clone() {
       return Object.create(this);
     },
@@ -5659,16 +5054,19 @@ var CanvasGraphics = (function CanvasGra
     this.imageLayer = imageLayer;
     this.groupStack = [];
     this.processingType3 = null;
     // Patterns are painted relative to the initial page/form transform, see pdf
     // spec 8.7.2 NOTE 1.
     this.baseTransform = null;
     this.baseTransformStack = [];
     this.groupLevel = 0;
+    this.smaskStack = [];
+    this.smaskCounter = 0;
+    this.tempSMask = null;
     if (canvasCtx) {
       addContextCurrentTransform(canvasCtx);
     }
   }
 
   function putBinaryImageData(ctx, imgData) {
     if (typeof ImageData !== 'undefined' && imgData instanceof ImageData) {
       ctx.putImageData(imgData, 0, 0);
@@ -5676,55 +5074,107 @@ var CanvasGraphics = (function CanvasGra
     }
 
     // Put the image data to the canvas in chunks, rather than putting the
     // whole image at once.  This saves JS memory, because the ImageData object
     // is smaller. It also possibly saves C++ memory within the implementation
     // of putImageData(). (E.g. in Firefox we make two short-lived copies of
     // the data passed to putImageData()). |n| shouldn't be too small, however,
     // because too many putImageData() calls will slow things down.
-
-    var rowsInFullChunks = 16;
-    var fullChunks = (imgData.height / rowsInFullChunks) | 0;
-    var rowsInLastChunk = imgData.height - fullChunks * rowsInFullChunks;
-    var elemsInFullChunks = imgData.width * rowsInFullChunks * 4;
-    var elemsInLastChunk = imgData.width * rowsInLastChunk * 4;
-
-    var chunkImgData = ctx.createImageData(imgData.width, rowsInFullChunks);
+    //
+    // Note: as written, if the last chunk is partial, the putImageData() call
+    // will (conceptually) put pixels past the bounds of the canvas.  But
+    // that's ok; any such pixels are ignored.
+
+    var height = imgData.height, width = imgData.width;
+    var fullChunkHeight = 16;
+    var fracChunks = height / fullChunkHeight;
+    var fullChunks = Math.floor(fracChunks);
+    var totalChunks = Math.ceil(fracChunks);
+    var partialChunkHeight = height - fullChunks * fullChunkHeight;
+
+    var chunkImgData = ctx.createImageData(width, fullChunkHeight);
     var srcPos = 0;
     var src = imgData.data;
     var dst = chunkImgData.data;
-    var haveSetAndSubarray = 'set' in dst && 'subarray' in src;
-
-    // Do all the full-size chunks.
-    for (var i = 0; i < fullChunks; i++) {
-      if (haveSetAndSubarray) {
-        dst.set(src.subarray(srcPos, srcPos + elemsInFullChunks));
-        srcPos += elemsInFullChunks;
-      } else {
-        for (var j = 0; j < elemsInFullChunks; j++) {
-          chunkImgData.data[j] = imgData.data[srcPos++];
+
+    // There are multiple forms in which the pixel data can be passed, and
+    // imgData.kind tells us which one this is.
+
+    if (imgData.kind === 'grayscale_1bpp') {
+      // Grayscale, 1 bit per pixel (i.e. black-and-white).
+      var srcData = imgData.data;
+      var destData = chunkImgData.data;
+      var destDataLength = destData.length;
+      var origLength = imgData.origLength;
+      for (var i = 3; i < destDataLength; i += 4) {
+        destData[i] = 255;
+      }
+      for (var i = 0; i < totalChunks; i++) {
+        var thisChunkHeight =
+          (i < fullChunks) ? fullChunkHeight : partialChunkHeight;
+        var destPos = 0;
+        for (var j = 0; j < thisChunkHeight; j++) {
+          var mask = 0;
+          var srcByte = 0;
+          for (var k = 0; k < width; k++, destPos += 4) {
+            if (mask === 0) {
+              if (srcPos >= origLength) {
+                break;
+              }
+              srcByte = srcData[srcPos++];
+              mask = 128;
+            }
+
+            if ((srcByte & mask)) {
+              destData[destPos] = 255;
+              destData[destPos + 1] = 255;
+              destData[destPos + 2] = 255;
+            } else {
+              destData[destPos] = 0;
+              destData[destPos + 1] = 0;
+              destData[destPos + 2] = 0;
+            }
+
+            mask >>= 1;
+          }
         }
-      }
-      ctx.putImageData(chunkImgData, 0, i * rowsInFullChunks);
-    }
-
-    // Do the final, partial chunk, if required.
-    if (rowsInLastChunk !== 0) {
-      if (haveSetAndSubarray) {
-        dst.set(src.subarray(srcPos, srcPos + elemsInLastChunk));
-        srcPos += elemsInLastChunk;
-      } else {
-        for (var j = 0; j < elemsInLastChunk; j++) {
-          chunkImgData.data[j] = imgData.data[srcPos++];
+        if (destPos < destDataLength) {
+          // We ran out of input. Make all remaining pixels transparent.
+          destPos += 3;
+          do {
+            destData[destPos] = 0;
+            destPos += 4;
+          } while (destPos < destDataLength);
         }
-      }
-      // This (conceptually) puts pixels past the bounds of the canvas.  But
-      // that's ok; any such pixels are ignored.
-      ctx.putImageData(chunkImgData, 0, fullChunks * rowsInFullChunks);
+
+        ctx.putImageData(chunkImgData, 0, i * fullChunkHeight);
+      }
+
+    } else if (imgData.kind === 'rgba_32bpp') {
+      // RGBA, 32-bits per pixel.
+      var haveSetAndSubarray = 'set' in dst && 'subarray' in src;
+
+      for (var i = 0; i < totalChunks; i++) {
+        var thisChunkHeight =
+          (i < fullChunks) ? fullChunkHeight : partialChunkHeight;
+        var elemsInThisChunk = imgData.width * thisChunkHeight * 4;
+        if (haveSetAndSubarray) {
+          dst.set(src.subarray(srcPos, srcPos + elemsInThisChunk));
+          srcPos += elemsInThisChunk;
+        } else {
+          for (var j = 0; j < elemsInThisChunk; j++) {
+            chunkImgData.data[j] = imgData.data[srcPos++];
+          }
+        }
+        ctx.putImageData(chunkImgData, 0, i * fullChunkHeight);
+      }
+
+    } else {
+        error('bad image kind: ' + imgData.kind);
     }
   }
 
   function putBinaryImageMask(ctx, imgData) {
     var width = imgData.width, height = imgData.height;
     var tmpImgData = ctx.createImageData(width, height);
     var data = imgData.data;
     var tmpImgDataPixels = tmpImgData.data;
@@ -5765,16 +5215,83 @@ var CanvasGraphics = (function CanvasGra
       destCtx.setLineDash(sourceCtx.getLineDash());
       destCtx.lineDashOffset =  sourceCtx.lineDashOffset;
     } else if ('mozDash' in sourceCtx) {
       destCtx.mozDash = sourceCtx.mozDash;
       destCtx.mozDashOffset = sourceCtx.mozDashOffset;
     }
   }
 
+  function composeSMask(ctx, smask, layerCtx) {
+    var mask = smask.canvas;
+    var maskCtx = smask.context;
+    var width = mask.width, height = mask.height;
+
+    var addBackdropFn;
+    if (smask.backdrop) {
+      var cs = smask.colorSpace || ColorSpace.singletons.rgb;
+      var backdrop = cs.getRgb(smask.backdrop, 0);
+      addBackdropFn = function (r0, g0, b0, bytes) {
+        var length = bytes.length;
+        for (var i = 3; i < length; i += 4) {
+          var alpha = bytes[i] / 255;
+          if (alpha === 0) {
+            bytes[i - 3] = r0;
+            bytes[i - 2] = g0;
+            bytes[i - 1] = b0;
+          } else if (alpha < 1) {
+            var alpha_ = 1 - alpha;
+            bytes[i - 3] = (bytes[i - 3] * alpha + r0 * alpha_) | 0;
+            bytes[i - 2] = (bytes[i - 2] * alpha + g0 * alpha_) | 0;
+            bytes[i - 1] = (bytes[i - 1] * alpha + b0 * alpha_) | 0;
+          }
+        }
+      }.bind(null, backdrop[0], backdrop[1], backdrop[2]);
+    } else {
+      addBackdropFn = function () {};
+    }
+
+    var composeFn;
+    if (smask.subtype === 'Luminosity') {
+      composeFn = function (maskDataBytes, layerDataBytes) {
+        var length = maskDataBytes.length;
+        for (var i = 3; i < length; i += 4) {
+          var y = ((maskDataBytes[i - 3] * 77) +     // * 0.3 / 255 * 0x10000
+                   (maskDataBytes[i - 2] * 152) +    // * 0.59 ....
+                   (maskDataBytes[i - 1] * 28)) | 0; // * 0.11 ....
+          layerDataBytes[i] = (layerDataBytes[i] * y) >> 16;
+        }
+      };
+    } else {
+      composeFn = function (maskDataBytes, layerDataBytes) {
+        var length = maskDataBytes.length;
+        for (var i = 3; i < length; i += 4) {
+          var alpha = maskDataBytes[i];
+          layerDataBytes[i] = (layerDataBytes[i] * alpha / 255) | 0;
+        }
+      };
+    }
+
+    // processing image in chunks to save memory
+    var chunkSize = 16;
+    for (var row = 0; row < height; row += chunkSize) {
+      var chunkHeight = Math.min(chunkSize, height - row);
+      var maskData = maskCtx.getImageData(0, row, width, chunkHeight);
+      var layerData = layerCtx.getImageData(0, row, width, chunkHeight);
+
+      addBackdropFn(maskData.data);
+      composeFn(maskData.data, layerData.data);
+
+      maskCtx.putImageData(layerData, 0, row);
+    }
+
+    ctx.setTransform(1, 0, 0, 1, 0, 0);
+    ctx.drawImage(mask, smask.offsetX, smask.offsetY);
+  }
+
   var LINE_CAP_STYLES = ['butt', 'round', 'square'];
   var LINE_JOIN_STYLES = ['miter', 'round', 'bevel'];
   var NORMAL_CLIP = {};
   var EO_CLIP = {};
 
   CanvasGraphics.prototype = {
 
     beginDrawing: function CanvasGraphics_beginDrawing(viewport, transparency) {
@@ -5791,20 +5308,22 @@ var CanvasGraphics = (function CanvasGra
         this.ctx.mozOpaque = true;
         this.ctx.save();
         this.ctx.fillStyle = 'rgb(255, 255, 255)';
         this.ctx.fillRect(0, 0, width, height);
         this.ctx.restore();
       }
 
       var transform = viewport.transform;
-      this.baseTransform = transform.slice();
+
       this.ctx.save();
       this.ctx.transform.apply(this.ctx, transform);
 
+      this.baseTransform = this.ctx.mozCurrentTransform.slice();
+
       if (this.textLayer) {
         this.textLayer.beginLayout();
       }
       if (this.imageLayer) {
         this.imageLayer.beginLayout();
       }
     },
 
@@ -5973,28 +5492,80 @@ var CanvasGraphics = (function CanvasGra
               if (this.ctx.globalCompositeOperation !== mode) {
                 warn('globalCompositeOperation "' + mode +
                      '" is not supported');
               }
             } else {
               this.ctx.globalCompositeOperation = 'source-over';
             }
             break;
+          case 'SMask':
+            if (this.current.activeSMask) {
+              this.endSMaskGroup();
+            }
+            this.current.activeSMask = value ? this.tempSMask : null;
+            if (this.current.activeSMask) {
+              this.beginSMaskGroup();
+            }
+            this.tempSMask = null;
+            break;
         }
       }
     },
+    beginSMaskGroup: function CanvasGraphics_beginSMaskGroup() {
+
+      var activeSMask = this.current.activeSMask;
+      var drawnWidth = activeSMask.canvas.width;
+      var drawnHeight = activeSMask.canvas.height;
+      var cacheId = 'smaskGroupAt' + this.groupLevel;
+      var scratchCanvas = CachedCanvases.getCanvas(
+        cacheId, drawnWidth, drawnHeight, true);
+
+      var currentCtx = this.ctx;
+      var currentTransform = currentCtx.mozCurrentTransform;
+      this.ctx.save();
+
+      var groupCtx = scratchCanvas.context;
+      groupCtx.translate(-activeSMask.offsetX, -activeSMask.offsetY);
+      groupCtx.transform.apply(groupCtx, currentTransform);
+
+      copyCtxState(currentCtx, groupCtx);
+      this.ctx = groupCtx;
+      this.setGState([
+        ['BM', 'Normal'],
+        ['ca', 1],
+        ['CA', 1]
+      ]);
+      this.groupStack.push(currentCtx);
+      this.groupLevel++;
+    },
+    endSMaskGroup: function CanvasGraphics_endSMaskGroup() {
+      var groupCtx = this.ctx;
+      this.groupLevel--;
+      this.ctx = this.groupStack.pop();
+
+      composeSMask(this.ctx, this.current.activeSMask, groupCtx);
+      this.ctx.restore();
+    },
     save: function CanvasGraphics_save() {
       this.ctx.save();
       var old = this.current;
       this.stateStack.push(old);
       this.current = old.clone();
+      if (this.current.activeSMask) {
+        this.current.activeSMask = null;
+      }
     },
     restore: function CanvasGraphics_restore() {
       var prev = this.stateStack.pop();
       if (prev) {
+        if (this.current.activeSMask) {
+          this.endSMaskGroup();
+        }
+
         this.current = prev;
         this.ctx.restore();
       }
     },
     transform: function CanvasGraphics_transform(a, b, c, d, e, f) {
       this.ctx.transform(a, b, c, d, e, f);
     },
 
@@ -6618,20 +6189,18 @@ var CanvasGraphics = (function CanvasGra
         var color;
         if (base) {
           var baseComps = base.numComps;
 
           color = base.getRgb(args, 0);
         }
         var pattern = new TilingPattern(IR, color, this.ctx, this.objs,
                                         this.commonObjs, this.baseTransform);
-      } else if (IR[0] == 'RadialAxial' || IR[0] == 'Dummy') {
-        var pattern = Pattern.shadingFromIR(IR);
       } else {
-        error('Unkown IR type ' + IR[0]);
+        var pattern = getShadingPatternFromIR(IR);
       }
       return pattern;
     },
     setStrokeColorN: function CanvasGraphics_setStrokeColorN(/*...*/) {
       var cs = this.current.strokeColorSpace;
 
       if (cs.name == 'Pattern') {
         this.current.strokeColor = this.getColorN_Pattern(arguments, cs);
@@ -6701,18 +6270,18 @@ var CanvasGraphics = (function CanvasGra
       this.ctx.fillStyle = color;
       this.current.fillColor = color;
     },
 
     shadingFill: function CanvasGraphics_shadingFill(patternIR) {
       var ctx = this.ctx;
 
       this.save();
-      var pattern = Pattern.shadingFromIR(patternIR);
-      ctx.fillStyle = pattern.getPattern(ctx, this);
+      var pattern = getShadingPatternFromIR(patternIR);
+      ctx.fillStyle = pattern.getPattern(ctx, this, true);
 
       var inv = ctx.mozCurrentTransformInverse;
       if (inv) {
         var canvas = ctx.canvas;
         var width = canvas.width;
         var height = canvas.height;
 
         var bl = Util.applyTransform([0, 0], inv);
@@ -6814,36 +6383,54 @@ var CanvasGraphics = (function CanvasGra
                           currentCtx.canvas.width,
                           currentCtx.canvas.height];
       bounds = Util.intersect(bounds, canvasBounds) || [0, 0, 0, 0];
       // Use ceil in case we're between sizes so we don't create canvas that is
       // too small and make the canvas at least 1x1 pixels.
       var drawnWidth = Math.max(Math.ceil(bounds[2] - bounds[0]), 1);
       var drawnHeight = Math.max(Math.ceil(bounds[3] - bounds[1]), 1);
 
+      var cacheId = 'groupAt' + this.groupLevel;
+      if (group.smask) {
+        // Using two cache entries is case if masks are used one after another.
+        cacheId +=  '_smask_' + ((this.smaskCounter++) % 2);
+      }
       var scratchCanvas = CachedCanvases.getCanvas(
-        'groupAt' + this.groupLevel, drawnWidth, drawnHeight, true);
+        cacheId, drawnWidth, drawnHeight, true);
       var groupCtx = scratchCanvas.context;
+
       // Since we created a new canvas that is just the size of the bounding box
       // we have to translate the group ctx.
       var offsetX = bounds[0];
       var offsetY = bounds[1];
       groupCtx.translate(-offsetX, -offsetY);
       groupCtx.transform.apply(groupCtx, currentTransform);
 
-      // Setup the current ctx so when the group is popped we draw it the right
-      // location.
-      currentCtx.setTransform(1, 0, 0, 1, 0, 0);
-      currentCtx.translate(offsetX, offsetY);
+      if (group.smask) {
+        // Saving state and cached mask to be used in setGState.
+        this.smaskStack.push({
+          canvas: scratchCanvas.canvas,
+          context: groupCtx,
+          offsetX: offsetX,
+          offsetY: offsetY,
+          subtype: group.smask.subtype,
+          backdrop: group.smask.backdrop,
+          colorSpace: group.colorSpace && ColorSpace.fromIR(group.colorSpace)
+        });
+      } else {
+        // Setup the current ctx so when the group is popped we draw it at the
+        // right location.
+        currentCtx.setTransform(1, 0, 0, 1, 0, 0);
+        currentCtx.translate(offsetX, offsetY);
+      }
       // The transparency group inherits all off the current graphics state
       // except the blend mode, soft mask, and alpha constants.
       copyCtxState(currentCtx, groupCtx);
       this.ctx = groupCtx;
       this.setGState([
-        ['SMask', 'None'],
         ['BM', 'Normal'],
         ['ca', 1],
         ['CA', 1]
       ]);
       this.groupStack.push(currentCtx);
       this.groupLevel++;
     },
 
@@ -6853,17 +6440,21 @@ var CanvasGraphics = (function CanvasGra
       this.ctx = this.groupStack.pop();
       // Turn off image smoothing to avoid sub pixel interpolation which can
       // look kind of blurry for some pdfs.
       if ('imageSmoothingEnabled' in this.ctx) {
         this.ctx.imageSmoothingEnabled = false;
       } else {
         this.ctx.mozImageSmoothingEnabled = false;
       }
-      this.ctx.drawImage(groupCtx.canvas, 0, 0);
+      if (group.smask) {
+        this.tempSMask = this.smaskStack.pop();
+      } else {
+        this.ctx.drawImage(groupCtx.canvas, 0, 0);
+      }
       this.restore();
     },
 
     beginAnnotations: function CanvasGraphics_beginAnnotations() {
       this.save();
       this.current = new CanvasExtraState();
     },
 
@@ -7173,16 +6764,405 @@ var CanvasGraphics = (function CanvasGra
     CanvasGraphics.prototype[OPS[op]] = CanvasGraphics.prototype[op];
   }
 
   return CanvasGraphics;
 })();
 
 
 
+var ShadingIRs = {};
+
+ShadingIRs.RadialAxial = {
+  fromIR: function RadialAxial_fromIR(raw) {
+    var type = raw[1];
+    var colorStops = raw[2];
+    var p0 = raw[3];
+    var p1 = raw[4];
+    var r0 = raw[5];
+    var r1 = raw[6];
+    return {
+      type: 'Pattern',
+      getPattern: function RadialAxial_getPattern(ctx) {
+        var grad;
+        if (type === 'axial') {
+          grad = ctx.createLinearGradient(p0[0], p0[1], p1[0], p1[1]);
+        } else if (type === 'radial') {
+          grad = ctx.createRadialGradient(p0[0], p0[1], r0, p1[0], p1[1], r1);
+        }
+
+        for (var i = 0, ii = colorStops.length; i < ii; ++i) {
+          var c = colorStops[i];
+          grad.addColorStop(c[0], c[1]);
+        }
+        return grad;
+      }
+    };
+  }
+};
+
+var createMeshCanvas = (function createMeshCanvasClosure() {
+  function drawTriangle(data, context, p1, p2, p3, c1, c2, c3) {
+    // Very basic Gouraud-shaded triangle rasterization algorithm.
+    var coords = context.coords, colors = context.colors;
+    var bytes = data.data, rowSize = data.width * 4;
+    var tmp;
+    if (coords[p1 * 2 + 1] > coords[p2 * 2 + 1]) {
+      tmp = p1; p1 = p2; p2 = tmp; tmp = c1; c1 = c2; c2 = tmp;
+    }
+    if (coords[p2 * 2 + 1] > coords[p3 * 2 + 1]) {
+      tmp = p2; p2 = p3; p3 = tmp; tmp = c2; c2 = c3; c3 = tmp;
+    }
+    if (coords[p1 * 2 + 1] > coords[p2 * 2 + 1]) {
+      tmp = p1; p1 = p2; p2 = tmp; tmp = c1; c1 = c2; c2 = tmp;
+    }
+    var x1 = (coords[p1 * 2] + context.offsetX) * context.scaleX;
+    var y1 = (coords[p1 * 2 + 1] + context.offsetY) * context.scaleY;
+    var x2 = (coords[p2 * 2] + context.offsetX) * context.scaleX;
+    var y2 = (coords[p2 * 2 + 1] + context.offsetY) * context.scaleY;
+    var x3 = (coords[p3 * 2] + context.offsetX) * context.scaleX;
+    var y3 = (coords[p3 * 2 + 1] + context.offsetY) * context.scaleY;
+    if (y1 >= y3) {
+      return;
+    }
+    var c1i = c1 * 3, c2i = c2 * 3, c3i = c3 * 3;
+    var c1r = colors[c1i], c1g = colors[c1i + 1], c1b = colors[c1i + 2];
+    var c2r = colors[c2i], c2g = colors[c2i + 1], c2b = colors[c2i + 2];
+    var c3r = colors[c3i], c3g = colors[c3i + 1], c3b = colors[c3i + 2];
+
+    var minY = Math.round(y1), maxY = Math.round(y3);
+    var xa, car, cag, cab;
+    var xb, cbr, cbg, cbb;
+    var k;
+    for (var y = minY; y <= maxY; y++) {
+      if (y < y2) {
+        k = y < y1 ? 0 : y1 === y2 ? 1 : (y1 - y) / (y1 - y2);
+        xa = x1 - (x1 - x2) * k;
+        car = c1r - (c1r - c2r) * k;
+        cag = c1g - (c1g - c2g) * k;
+        cab = c1b - (c1b - c2b) * k;
+      } else {
+        k = y > y3 ? 1 : y2 === y3 ? 0 : (y2 - y) / (y2 - y3);
+        xa = x2 - (x2 - x3) * k;
+        car = c2r - (c2r - c3r) * k;
+        cag = c2g - (c2g - c3g) * k;
+        cab = c2b - (c2b - c3b) * k;
+      }
+      k = y < y1 ? 0 : y > y3 ? 1 : (y1 - y) / (y1 - y3);
+      xb = x1 - (x1 - x3) * k;
+      cbr = c1r - (c1r - c3r) * k;
+      cbg = c1g - (c1g - c3g) * k;
+      cbb = c1b - (c1b - c3b) * k;
+      var x1_ = Math.round(Math.min(xa, xb));
+      var x2_ = Math.round(Math.max(xa, xb));
+      var j = rowSize * y + x1_ * 4;
+      for (var x = x1_; x <= x2_; x++) {
+        k = (xa - x) / (xa - xb);
+        k = k < 0 ? 0 : k > 1 ? 1 : k;
+        bytes[j++] = (car - (car - cbr) * k) | 0;
+        bytes[j++] = (cag - (cag - cbg) * k) | 0;
+        bytes[j++] = (cab - (cab - cbb) * k) | 0;
+        bytes[j++] = 255;
+      }
+    }
+  }
+
+  function drawFigure(data, figure, context) {
+    var ps = figure.coords;
+    var cs = figure.colors;
+    switch (figure.type) {
+      case 'lattice':
+        var verticesPerRow = figure.verticesPerRow;
+        var rows = Math.floor(ps.length / verticesPerRow) - 1;
+        var cols = verticesPerRow - 1;
+        for (var i = 0; i < rows; i++) {
+          var q = i * verticesPerRow;
+          for (var j = 0; j < cols; j++, q++) {
+            drawTriangle(data, context,
+              ps[q], ps[q + 1], ps[q + verticesPerRow],
+              cs[q], cs[q + 1], cs[q + verticesPerRow]);
+            drawTriangle(data, context,
+              ps[q + verticesPerRow + 1], ps[q + 1], ps[q + verticesPerRow],
+              cs[q + verticesPerRow + 1], cs[q + 1], cs[q + verticesPerRow]);
+          }
+        }
+        break;
+      case 'triangles':
+        for (var i = 0, ii = ps.length; i < ii; i += 3) {
+          drawTriangle(data, context,
+            ps[i], ps[i + 1], ps[i + 2],
+            cs[i], cs[i + 1], cs[i + 2]);
+        }
+        break;
+      default:
+        error('illigal figure');
+        break;
+    }
+  }
+
+  function createMeshCanvas(bounds, combinesScale, coords, colors, figures,
+                            backgroundColor) {
+    // we will increase scale on some weird factor to let antialiasing take
+    // care of "rough" edges
+    var EXPECTED_SCALE = 1.1;
+    // MAX_PATTERN_SIZE is used to avoid OOM situation.
+    var MAX_PATTERN_SIZE = 3000; // 10in @ 300dpi shall be enough
+
+    var boundsWidth = bounds[2] - bounds[0];
+    var boundsHeight = bounds[3] - bounds[1];
+
+    var width = Math.min(Math.ceil(Math.abs(boundsWidth * combinesScale[0] *
+      EXPECTED_SCALE)), MAX_PATTERN_SIZE);
+    var height = Math.min(Math.ceil(Math.abs(boundsHeight * combinesScale[1] *
+      EXPECTED_SCALE)), MAX_PATTERN_SIZE);
+    var scaleX = width / boundsWidth;
+    var scaleY = height / boundsHeight;
+
+    var tmpCanvas = CachedCanvases.getCanvas('mesh', width, height, false);
+    var tmpCtx = tmpCanvas.context;
+    if (backgroundColor) {
+      tmpCtx.fillStyle = makeCssRgb(backgroundColor);
+      tmpCtx.fillRect(0, 0, width, height);
+    }
+
+    var context = {
+      coords: coords,
+      colors: colors,
+      offsetX: -bounds[0],
+      offsetY: -bounds[1],
+      scaleX: scaleX,
+      scaleY: scaleY
+    };
+
+    var data = tmpCtx.getImageData(0, 0, width, height);
+    for (var i = 0; i < figures.length; i++) {
+      drawFigure(data, figures[i], context);
+    }
+    tmpCtx.putImageData(data, 0, 0);
+
+    return {canvas: tmpCanvas.canvas, scaleX: 1 / scaleX, scaleY: 1 / scaleY};
+  }
+  return createMeshCanvas;
+})();
+
+ShadingIRs.Mesh = {
+  fromIR: function Mesh_fromIR(raw) {
+    var type = raw[1];
+    var coords = raw[2];
+    var colors = raw[3];
+    var figures = raw[4];
+    var bounds = raw[5];
+    var matrix = raw[6];
+    var bbox = raw[7];
+    var background = raw[8];
+    return {
+      type: 'Pattern',
+      getPattern: function Mesh_getPattern(ctx, owner, shadingFill) {
+        var combinedScale;
+        // Obtain scale from matrix and current transformation matrix.
+        if (shadingFill) {
+          combinedScale = Util.singularValueDecompose2dScale(
+            ctx.mozCurrentTransform);
+        } else {
+          var matrixScale = Util.singularValueDecompose2dScale(matrix);
+          var curMatrixScale = Util.singularValueDecompose2dScale(
+            owner.baseTransform);
+          combinedScale = [matrixScale[0] * curMatrixScale[0],
+            matrixScale[1] * curMatrixScale[1]];
+        }
+
+
+        // Rasterizing on the main thread since sending/queue large canvases
+        // might cause OOM.
+        // TODO consider using WebGL or asm.js to perform rasterization
+        var temporaryPatternCanvas = createMeshCanvas(bounds, combinedScale,
+          coords, colors, figures, shadingFill ? null : background);
+
+        if (!shadingFill) {
+          ctx.setTransform.apply(ctx, owner.baseTransform);
+          if (matrix) {
+            ctx.transform.apply(ctx, matrix);
+          }
+        }
+
+        ctx.translate(bounds[0], bounds[1]);
+        ctx.scale(temporaryPatternCanvas.scaleX,
+                  temporaryPatternCanvas.scaleY);
+
+        return ctx.createPattern(temporaryPatternCanvas.canvas, 'no-repeat');
+      }
+    };
+  }
+};
+
+ShadingIRs.Dummy = {
+  fromIR: function Dummy_fromIR() {
+    return {
+      type: 'Pattern',
+      getPattern: function Dummy_fromIR_getPattern() {
+        return 'hotpink';
+      }
+    };
+  }
+};
+
+function getShadingPatternFromIR(raw) {
+  var shadingIR = ShadingIRs[raw[0]];
+  if (!shadingIR) {
+    error('Unknown IR type: ' + raw[0]);
+  }
+  return shadingIR.fromIR(raw);
+}
+
+var TilingPattern = (function TilingPatternClosure() {
+  var PaintType = {
+    COLORED: 1,
+    UNCOLORED: 2
+  };
+
+  var MAX_PATTERN_SIZE = 3000; // 10in @ 300dpi shall be enough
+
+  function TilingPattern(IR, color, ctx, objs, commonObjs, baseTransform) {
+    this.name = IR[1][0].name;
+    this.operatorList = IR[2];
+    this.matrix = IR[3] || [1, 0, 0, 1, 0, 0];
+    this.bbox = IR[4];
+    this.xstep = IR[5];
+    this.ystep = IR[6];
+    this.paintType = IR[7];
+    this.tilingType = IR[8];
+    this.color = color;
+    this.objs = objs;
+    this.commonObjs = commonObjs;
+    this.baseTransform = baseTransform;
+    this.type = 'Pattern';
+    this.ctx = ctx;
+  }
+
+  TilingPattern.prototype = {
+    createPatternCanvas: function TilinPattern_createPatternCanvas(owner) {
+      var operatorList = this.operatorList;
+      var bbox = this.bbox;
+      var xstep = this.xstep;
+      var ystep = this.ystep;
+      var paintType = this.paintType;
+      var tilingType = this.tilingType;
+      var color = this.color;
+      var objs = this.objs;
+      var commonObjs = this.commonObjs;
+      var ctx = this.ctx;
+
+      info('TilingType: ' + tilingType);
+
+      var x0 = bbox[0], y0 = bbox[1], x1 = bbox[2], y1 = bbox[3];
+
+      var topLeft = [x0, y0];
+      // we want the canvas to be as large as the step size
+      var botRight = [x0 + xstep, y0 + ystep];
+
+      var width = botRight[0] - topLeft[0];
+      var height = botRight[1] - topLeft[1];
+
+      // Obtain scale from matrix and current transformation matrix.
+      var matrixScale = Util.singularValueDecompose2dScale(this.matrix);
+      var curMatrixScale = Util.singularValueDecompose2dScale(
+        this.baseTransform);
+      var combinedScale = [matrixScale[0] * curMatrixScale[0],
+        matrixScale[1] * curMatrixScale[1]];
+
+      // MAX_PATTERN_SIZE is used to avoid OOM situation.
+      // Use width and height values that are as close as possible to the end
+      // result when the pattern is used. Too low value makes the pattern look
+      // blurry. Too large value makes it look too crispy.
+      width = Math.min(Math.ceil(Math.abs(width * combinedScale[0])),
+        MAX_PATTERN_SIZE);
+
+      height = Math.min(Math.ceil(Math.abs(height * combinedScale[1])),
+        MAX_PATTERN_SIZE);
+
+      var tmpCanvas = CachedCanvases.getCanvas('pattern', width, height, true);
+      var tmpCtx = tmpCanvas.context;
+      var graphics = new CanvasGraphics(tmpCtx, commonObjs, objs);
+      graphics.groupLevel = owner.groupLevel;
+
+      this.setFillAndStrokeStyleToContext(tmpCtx, paintType, color);
+
+      this.setScale(width, height, xstep, ystep);
+      this.transformToScale(graphics);
+
+      // transform coordinates to pattern space
+      var tmpTranslate = [1, 0, 0, 1, -topLeft[0], -topLeft[1]];
+      graphics.transform.apply(graphics, tmpTranslate);
+
+      this.clipBbox(graphics, bbox, x0, y0, x1, y1);
+
+      graphics.executeOperatorList(operatorList);
+      return tmpCanvas.canvas;
+    },
+
+    setScale: function TilingPattern_setScale(width, height, xstep, ystep) {
+      this.scale = [width / xstep, height / ystep];
+    },
+
+    transformToScale: function TilingPattern_transformToScale(graphics) {
+      var scale = this.scale;
+      var tmpScale = [scale[0], 0, 0, scale[1], 0, 0];
+      graphics.transform.apply(graphics, tmpScale);
+    },
+
+    scaleToContext: function TilingPattern_scaleToContext() {
+      var scale = this.scale;
+      this.ctx.scale(1 / scale[0], 1 / scale[1]);
+    },
+
+    clipBbox: function clipBbox(graphics, bbox, x0, y0, x1, y1) {
+      if (bbox && isArray(bbox) && 4 == bbox.length) {
+        var bboxWidth = x1 - x0;
+        var bboxHeight = y1 - y0;
+        graphics.rectangle(x0, y0, bboxWidth, bboxHeight);
+        graphics.clip();
+        graphics.endPath();
+      }
+    },
+
+    setFillAndStrokeStyleToContext:
+      function setFillAndStrokeStyleToContext(context, paintType, color) {
+        switch (paintType) {
+          case PaintType.COLORED:
+            var ctx = this.ctx;
+            context.fillStyle = ctx.fillStyle;
+            context.strokeStyle = ctx.strokeStyle;
+            break;
+          case PaintType.UNCOLORED:
+            var rgbColor = ColorSpace.singletons.rgb.getRgb(color, 0);
+            var cssColor = Util.makeCssRgb(rgbColor);
+            context.fillStyle = cssColor;
+            context.strokeStyle = cssColor;
+            break;
+          default:
+            error('Unsupported paint type: ' + paintType);
+        }
+      },
+
+    getPattern: function TilingPattern_getPattern(ctx, owner) {
+      var temporaryPatternCanvas = this.createPatternCanvas(owner);
+
+      var ctx = this.ctx;
+      ctx.setTransform.apply(ctx, this.baseTransform);
+      ctx.transform.apply(ctx, this.matrix);
+      this.scaleToContext();
+
+      return ctx.createPattern(temporaryPatternCanvas, 'repeat');
+    }
+  };
+
+  return TilingPattern;
+})();
+
+
 PDFJS.disableFontFace = false;
 
 var FontLoader = {
   insertRule: function fontLoaderInsertRule(rule) {
     var styleElement = document.getElementById('PDFJS_FONT_STYLE_TAG');
     if (!styleElement) {
         styleElement = document.createElement('style');
         styleElement.id = 'PDFJS_FONT_STYLE_TAG';
--- a/browser/extensions/pdfjs/content/build/pdf.worker.js
+++ b/browser/extensions/pdfjs/content/build/pdf.worker.js
@@ -15,18 +15,18 @@
  * limitations under the License.
  */
 
 // Initializing PDFJS global object (if still undefined)
 if (typeof PDFJS === 'undefined') {
   (typeof window !== 'undefined' ? window : this).PDFJS = {};
 }
 
-PDFJS.version = '0.8.934';
-PDFJS.build = 'c80df60';
+PDFJS.version = '0.8.990';
+PDFJS.build = '54f6291';
 
 (function pdfjsWrapper() {
   // Use strict in our context only - users might not want it
   'use strict';
 
 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
 /* Copyright 2012 Mozilla Foundation
@@ -2000,420 +2000,16 @@ var LabCS = (function LabCSClosure() {
     },
     usesZeroToOneRange: false
   };
   return LabCS;
 })();
 
 
 
-var PatternType = {
-  AXIAL: 2,
-  RADIAL: 3
-};
-
-var Pattern = (function PatternClosure() {
-  // Constructor should define this.getPattern
-  function Pattern() {
-    error('should not call Pattern constructor');
-  }
-
-  Pattern.prototype = {
-    // Input: current Canvas context
-    // Output: the appropriate fillStyle or strokeStyle
-    getPattern: function Pattern_getPattern(ctx) {
-      error('Should not call Pattern.getStyle: ' + ctx);
-    }
-  };
-
-  Pattern.shadingFromIR = function Pattern_shadingFromIR(raw) {
-    return Shadings[raw[0]].fromIR(raw);
-  };
-
-  Pattern.parseShading = function Pattern_parseShading(shading, matrix, xref,
-                                                       res) {
-
-    var dict = isStream(shading) ? shading.dict : shading;
-    var type = dict.get('ShadingType');
-
-    switch (type) {
-      case PatternType.AXIAL:
-      case PatternType.RADIAL:
-        // Both radial and axial shadings are handled by RadialAxial shading.
-        return new Shadings.RadialAxial(dict, matrix, xref, res);
-      default:
-        UnsupportedManager.notify(UNSUPPORTED_FEATURES.shadingPattern);
-        return new Shadings.Dummy();
-    }
-  };
-  return Pattern;
-})();
-
-var Shadings = {};
-
-// A small number to offset the first/last color stops so we can insert ones to
-// support extend.  Number.MIN_VALUE appears to be too small and breaks the
-// extend. 1e-7 works in FF but chrome seems to use an even smaller sized number
-// internally so we have to go bigger.
-Shadings.SMALL_NUMBER = 1e-2;
-
-// Radial and axial shading have very similar implementations
-// If needed, the implementations can be broken into two classes
-Shadings.RadialAxial = (function RadialAxialClosure() {
-  function RadialAxial(dict, matrix, xref, res, ctx) {
-    this.matrix = matrix;
-    this.coordsArr = dict.get('Coords');
-    this.shadingType = dict.get('ShadingType');
-    this.type = 'Pattern';
-    this.ctx = ctx;
-    var cs = dict.get('ColorSpace', 'CS');
-    cs = ColorSpace.parse(cs, xref, res);
-    this.cs = cs;
-
-    var t0 = 0.0, t1 = 1.0;
-    if (dict.has('Domain')) {
-      var domainArr = dict.get('Domain');
-      t0 = domainArr[0];
-      t1 = domainArr[1];
-    }
-
-    var extendStart = false, extendEnd = false;
-    if (dict.has('Extend')) {
-      var extendArr = dict.get('Extend');
-      extendStart = extendArr[0];
-      extendEnd = extendArr[1];
-    }
-
-    if (this.shadingType === PatternType.RADIAL &&
-       (!extendStart || !extendEnd)) {
-      // Radial gradient only currently works if either circle is fully within
-      // the other circle.
-      var x1 = this.coordsArr[0];
-      var y1 = this.coordsArr[1];
-      var r1 = this.coordsArr[2];
-      var x2 = this.coordsArr[3];
-      var y2 = this.coordsArr[4];
-      var r2 = this.coordsArr[5];
-      var distance = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
-      if (r1 <= r2 + distance &&
-          r2 <= r1 + distance) {
-        warn('Unsupported radial gradient.');
-      }
-    }
-
-    this.extendStart = extendStart;
-    this.extendEnd = extendEnd;
-
-    var fnObj = dict.get('Function');
-    var fn;
-    if (isArray(fnObj)) {
-      var fnArray = [];
-      for (var j = 0, jj = fnObj.length; j < jj; j++) {
-        var obj = xref.fetchIfRef(fnObj[j]);
-        if (!isPDFFunction(obj)) {
-          error('Invalid function');
-        }
-        fnArray.push(PDFFunction.parse(xref, obj));
-      }
-      fn = function radialAxialColorFunction(arg) {
-        var out = [];
-        for (var i = 0, ii = fnArray.length; i < ii; i++) {
-          out.push(fnArray[i](arg)[0]);
-        }
-        return out;
-      };
-    } else {
-      if (!isPDFFunction(fnObj)) {
-        error('Invalid function');
-      }
-      fn = PDFFunction.parse(xref, fnObj);
-    }
-
-    // 10 samples seems good enough for now, but probably won't work
-    // if there are sharp color changes. Ideally, we would implement
-    // the spec faithfully and add lossless optimizations.
-    var diff = t1 - t0;
-    var step = diff / 10;
-
-    var colorStops = this.colorStops = [];
-
-    // Protect against bad domains so we don't end up in an infinte loop below.
-    if (t0 >= t1 || step <= 0) {
-      // Acrobat doesn't seem to handle these cases so we'll ignore for
-      // now.
-      info('Bad shading domain.');
-      return;
-    }
-
-    for (var i = t0; i <= t1; i += step) {
-      var rgbColor = cs.getRgb(fn([i]), 0);
-      var cssColor = Util.makeCssRgb(rgbColor);
-      colorStops.push([(i - t0) / diff, cssColor]);
-    }
-
-    var background = 'transparent';
-    if (dict.has('Background')) {
-      var rgbColor = cs.getRgb(dict.get('Background'), 0);
-      background = Util.makeCssRgb(rgbColor);
-    }
-
-    if (!extendStart) {
-      // Insert a color stop at the front and offset the first real color stop
-      // so it doesn't conflict with the one we insert.
-      colorStops.unshift([0, background]);
-      colorStops[1][0] += Shadings.SMALL_NUMBER;
-    }
-    if (!extendEnd) {
-      // Same idea as above in extendStart but for the end.
-      colorStops[colorStops.length - 1][0] -= Shadings.SMALL_NUMBER;
-      colorStops.push([1, background]);
-    }
-
-    this.colorStops = colorStops;
-  }
-
-  RadialAxial.fromIR = function RadialAxial_fromIR(raw) {
-    var type = raw[1];
-    var colorStops = raw[2];
-    var p0 = raw[3];
-    var p1 = raw[4];
-    var r0 = raw[5];
-    var r1 = raw[6];
-    return {
-      type: 'Pattern',
-      getPattern: function RadialAxial_getPattern(ctx) {
-        var grad;
-        if (type == PatternType.AXIAL)
-          grad = ctx.createLinearGradient(p0[0], p0[1], p1[0], p1[1]);
-        else if (type == PatternType.RADIAL)
-          grad = ctx.createRadialGradient(p0[0], p0[1], r0, p1[0], p1[1], r1);
-
-        for (var i = 0, ii = colorStops.length; i < ii; ++i) {
-          var c = colorStops[i];
-          grad.addColorStop(c[0], c[1]);
-        }
-        return grad;
-      }
-    };
-  };
-
-  RadialAxial.prototype = {
-    getIR: function RadialAxial_getIR() {
-      var coordsArr = this.coordsArr;
-      var type = this.shadingType;
-      if (type == PatternType.AXIAL) {
-        var p0 = [coordsArr[0], coordsArr[1]];
-        var p1 = [coordsArr[2], coordsArr[3]];
-        var r0 = null;
-        var r1 = null;
-      } else if (type == PatternType.RADIAL) {
-        var p0 = [coordsArr[0], coordsArr[1]];
-        var p1 = [coordsArr[3], coordsArr[4]];
-        var r0 = coordsArr[2];
-        var r1 = coordsArr[5];
-      } else {
-        error('getPattern type unknown: ' + type);
-      }
-
-      var matrix = this.matrix;
-      if (matrix) {
-        p0 = Util.applyTransform(p0, matrix);
-        p1 = Util.applyTransform(p1, matrix);
-      }
-
-      return ['RadialAxial', type, this.colorStops, p0, p1, r0, r1];
-    }
-  };
-
-  return RadialAxial;
-})();
-
-Shadings.Dummy = (function DummyClosure() {
-  function Dummy() {
-    this.type = 'Pattern';
-  }
-
-  Dummy.fromIR = function Dummy_fromIR() {
-    return {
-      type: 'Pattern',
-      getPattern: function Dummy_fromIR_getPattern() {
-        return 'hotpink';
-      }
-    };
-  };
-
-  Dummy.prototype = {
-    getIR: function Dummy_getIR() {
-      return ['Dummy'];
-    }
-  };
-  return Dummy;
-})();
-
-var TilingPattern = (function TilingPatternClosure() {
-  var PaintType = {
-    COLORED: 1,
-    UNCOLORED: 2
-  };
-
-  var MAX_PATTERN_SIZE = 3000; // 10in @ 300dpi shall be enough
-
-  function TilingPattern(IR, color, ctx, objs, commonObjs, baseTransform) {
-    this.name = IR[1][0].name;
-    this.operatorList = IR[2];
-    this.matrix = IR[3] || [1, 0, 0, 1, 0, 0];
-    this.bbox = IR[4];
-    this.xstep = IR[5];
-    this.ystep = IR[6];
-    this.paintType = IR[7];
-    this.tilingType = IR[8];
-    this.color = color;
-    this.objs = objs;
-    this.commonObjs = commonObjs;
-    this.baseTransform = baseTransform;
-    this.type = 'Pattern';
-    this.ctx = ctx;
-  }
-
-  TilingPattern.getIR = function TilingPattern_getIR(operatorList, dict, args) {
-    var matrix = dict.get('Matrix');
-    var bbox = dict.get('BBox');
-    var xstep = dict.get('XStep');
-    var ystep = dict.get('YStep');
-    var paintType = dict.get('PaintType');
-    var tilingType = dict.get('TilingType');
-
-    return [
-      'TilingPattern', args, operatorList, matrix, bbox, xstep, ystep,
-      paintType, tilingType
-    ];
-  };
-
-  TilingPattern.prototype = {
-    createPatternCanvas: function TilinPattern_createPatternCanvas(owner) {
-      var operatorList = this.operatorList;
-      var bbox = this.bbox;
-      var xstep = this.xstep;
-      var ystep = this.ystep;
-      var paintType = this.paintType;
-      var tilingType = this.tilingType;
-      var color = this.color;
-      var objs = this.objs;
-      var commonObjs = this.commonObjs;
-      var ctx = this.ctx;
-
-      info('TilingType: ' + tilingType);
-
-      var x0 = bbox[0], y0 = bbox[1], x1 = bbox[2], y1 = bbox[3];
-
-      var topLeft = [x0, y0];
-      // we want the canvas to be as large as the step size
-      var botRight = [x0 + xstep, y0 + ystep];
-
-      var width = botRight[0] - topLeft[0];
-      var height = botRight[1] - topLeft[1];
-
-      // Obtain scale from matrix and current transformation matrix.
-      var matrixScale = Util.singularValueDecompose2dScale(this.matrix);
-      var curMatrixScale = Util.singularValueDecompose2dScale(
-                             this.baseTransform);
-      var combinedScale = [matrixScale[0] * curMatrixScale[0],
-                           matrixScale[1] * curMatrixScale[1]];
-
-      // MAX_PATTERN_SIZE is used to avoid OOM situation.
-      // Use width and height values that are as close as possible to the end
-      // result when the pattern is used. Too low value makes the pattern look
-      // blurry. Too large value makes it look too crispy.
-      width = Math.min(Math.ceil(Math.abs(width * combinedScale[0])),
-                       MAX_PATTERN_SIZE);
-
-      height = Math.min(Math.ceil(Math.abs(height * combinedScale[1])),
-                        MAX_PATTERN_SIZE);
-
-      var tmpCanvas = CachedCanvases.getCanvas('pattern', width, height, true);
-      var tmpCtx = tmpCanvas.context;
-      var graphics = new CanvasGraphics(tmpCtx, commonObjs, objs);
-      graphics.groupLevel = owner.groupLevel;
-
-      this.setFillAndStrokeStyleToContext(tmpCtx, paintType, color);
-
-      this.setScale(width, height, xstep, ystep);
-      this.transformToScale(graphics);
-
-      // transform coordinates to pattern space
-      var tmpTranslate = [1, 0, 0, 1, -topLeft[0], -topLeft[1]];
-      graphics.transform.apply(graphics, tmpTranslate);
-
-      this.clipBbox(graphics, bbox, x0, y0, x1, y1);
-
-      graphics.executeOperatorList(operatorList);
-      return tmpCanvas.canvas;
-    },
-
-    setScale: function TilingPattern_setScale(width, height, xstep, ystep) {
-      this.scale = [width / xstep, height / ystep];
-    },
-
-    transformToScale: function TilingPattern_transformToScale(graphics) {
-      var scale = this.scale;
-      var tmpScale = [scale[0], 0, 0, scale[1], 0, 0];
-      graphics.transform.apply(graphics, tmpScale);
-    },
-
-    scaleToContext: function TilingPattern_scaleToContext() {
-      var scale = this.scale;
-      this.ctx.scale(1 / scale[0], 1 / scale[1]);
-    },
-
-    clipBbox: function clipBbox(graphics, bbox, x0, y0, x1, y1) {
-      if (bbox && isArray(bbox) && 4 == bbox.length) {
-        var bboxWidth = x1 - x0;
-        var bboxHeight = y1 - y0;
-        graphics.rectangle(x0, y0, bboxWidth, bboxHeight);
-        graphics.clip();
-        graphics.endPath();
-      }
-    },
-
-    setFillAndStrokeStyleToContext:
-      function setFillAndStrokeStyleToContext(context, paintType, color) {
-      switch (paintType) {
-        case PaintType.COLORED:
-          var ctx = this.ctx;
-          context.fillStyle = ctx.fillStyle;
-          context.strokeStyle = ctx.strokeStyle;
-          break;
-        case PaintType.UNCOLORED:
-          var rgbColor = ColorSpace.singletons.rgb.getRgb(color, 0);
-          var cssColor = Util.makeCssRgb(rgbColor);
-          context.fillStyle = cssColor;
-          context.strokeStyle = cssColor;
-          break;
-        default:
-          error('Unsupported paint type: ' + paintType);
-      }
-    },
-
-    getPattern: function TilingPattern_getPattern(ctx, owner) {
-      var temporaryPatternCanvas = this.createPatternCanvas(owner);
-
-      var ctx = this.ctx;
-      ctx.setTransform.apply(ctx, this.baseTransform);
-      ctx.transform.apply(ctx, this.matrix);
-      this.scaleToContext();
-
-      return ctx.createPattern(temporaryPatternCanvas, 'repeat');
-    }
-  };
-
-  return TilingPattern;
-})();
-
-
-
 var PDFFunction = (function PDFFunctionClosure() {
   var CONSTRUCT_SAMPLED = 0;
   var CONSTRUCT_INTERPOLATED = 2;
   var CONSTRUCT_STICHED = 3;
   var CONSTRUCT_POSTSCRIPT = 4;
 
   return {
     getSampleArray: function PDFFunction_getSampleArray(size, outputSize, bps,
@@ -2854,19 +2450,18 @@ var PostScriptStack = (function PostScri
       for (i = c, j = r; i < j; i++, j--) {
         t = stack[i]; stack[i] = stack[j]; stack[j] = t;
       }
     }
   };
   return PostScriptStack;
 })();
 var PostScriptEvaluator = (function PostScriptEvaluatorClosure() {
-  function PostScriptEvaluator(operators, operands) {
+  function PostScriptEvaluator(operators) {
     this.operators = operators;
-    this.operands = operands;
   }
   PostScriptEvaluator.prototype = {
     execute: function PostScriptEvaluator_execute(initialStack) {
       var stack = new PostScriptStack(initialStack);
       var counter = 0;
       var operators = this.operators;
       var length = operators.length;
       var operator, a, b;
@@ -3085,210 +2680,16 @@ var PostScriptEvaluator = (function Post
         }
       }
       return stack.stack;
     }
   };
   return PostScriptEvaluator;
 })();
 
-var PostScriptParser = (function PostScriptParserClosure() {
-  function PostScriptParser(lexer) {
-    this.lexer = lexer;
-    this.operators = [];
-    this.token = null;
-    this.prev = null;
-  }
-  PostScriptParser.prototype = {
-    nextToken: function PostScriptParser_nextToken() {
-      this.prev = this.token;
-      this.token = this.lexer.getToken();
-    },
-    accept: function PostScriptParser_accept(type) {
-      if (this.token.type == type) {
-        this.nextToken();
-        return true;
-      }
-      return false;
-    },
-    expect: function PostScriptParser_expect(type) {
-      if (this.accept(type))
-        return true;
-      error('Unexpected symbol: found ' + this.token.type + ' expected ' +
-            type + '.');
-    },
-    parse: function PostScriptParser_parse() {
-      this.nextToken();
-      this.expect(PostScriptTokenTypes.LBRACE);
-      this.parseBlock();
-      this.expect(PostScriptTokenTypes.RBRACE);
-      return this.operators;
-    },
-    parseBlock: function PostScriptParser_parseBlock() {
-      while (true) {
-        if (this.accept(PostScriptTokenTypes.NUMBER)) {
-          this.operators.push(this.prev.value);
-        } else if (this.accept(PostScriptTokenTypes.OPERATOR)) {
-          this.operators.push(this.prev.value);
-        } else if (this.accept(PostScriptTokenTypes.LBRACE)) {
-          this.parseCondition();
-        } else {
-          return;
-        }
-      }
-    },
-    parseCondition: function PostScriptParser_parseCondition() {
-      // Add two place holders that will be updated later
-      var conditionLocation = this.operators.length;
-      this.operators.push(null, null);
-
-      this.parseBlock();
-      this.expect(PostScriptTokenTypes.RBRACE);
-      if (this.accept(PostScriptTokenTypes.IF)) {
-        // The true block is right after the 'if' so it just falls through on
-        // true else it jumps and skips the true block.
-        this.operators[conditionLocation] = this.operators.length;
-        this.operators[conditionLocation + 1] = 'jz';
-      } else if (this.accept(PostScriptTokenTypes.LBRACE)) {
-        var jumpLocation = this.operators.length;
-        this.operators.push(null, null);
-        var endOfTrue = this.operators.length;
-        this.parseBlock();
-        this.expect(PostScriptTokenTypes.RBRACE);
-        this.expect(PostScriptTokenTypes.IFELSE);
-        // The jump is added at the end of the true block to skip the false
-        // block.
-        this.operators[jumpLocation] = this.operators.length;
-        this.operators[jumpLocation + 1] = 'j';
-
-        this.operators[conditionLocation] = endOfTrue;
-        this.operators[conditionLocation + 1] = 'jz';
-      } else {
-        error('PS Function: error parsing conditional.');
-      }
-    }
-  };
-  return PostScriptParser;
-})();
-
-var PostScriptTokenTypes = {
-  LBRACE: 0,
-  RBRACE: 1,
-  NUMBER: 2,
-  OPERATOR: 3,
-  IF: 4,
-  IFELSE: 5
-};
-
-var PostScriptToken = (function PostScriptTokenClosure() {
-  function PostScriptToken(type, value) {
-    this.type = type;
-    this.value = value;
-  }
-
-  var opCache = {};
-
-  PostScriptToken.getOperator = function PostScriptToken_getOperator(op) {
-    var opValue = opCache[op];
-    if (opValue)
-      return opValue;
-
-    return opCache[op] = new PostScriptToken(PostScriptTokenTypes.OPERATOR, op);
-  };
-
-  PostScriptToken.LBRACE = new PostScriptToken(PostScriptTokenTypes.LBRACE,
-                                                '{');
-  PostScriptToken.RBRACE = new PostScriptToken(PostScriptTokenTypes.RBRACE,
-                                                '}');
-  PostScriptToken.IF = new PostScriptToken(PostScriptTokenTypes.IF, 'IF');
-  PostScriptToken.IFELSE = new PostScriptToken(PostScriptTokenTypes.IFELSE,
-                                                'IFELSE');
-  return PostScriptToken;
-})();
-
-var PostScriptLexer = (function PostScriptLexerClosure() {
-  function PostScriptLexer(stream) {
-    this.stream = stream;
-    this.nextChar();
-  }
-  PostScriptLexer.prototype = {
-    nextChar: function PostScriptLexer_nextChar() {
-      return (this.currentChar = this.stream.getByte());
-    },
-    getToken: function PostScriptLexer_getToken() {
-      var s = '';
-      var comment = false;
-      var ch = this.currentChar;
-
-      // skip comments
-      while (true) {
-        if (ch < 0) {
-          return EOF;
-        }
-
-        if (comment) {
-          if (ch === 0x0A || ch === 0x0D) {
-            comment = false;
-          }
-        } else if (ch == 0x25) { // '%'
-          comment = true;
-        } else if (!Lexer.isSpace(ch)) {
-          break;
-        }
-        ch = this.nextChar();
-      }
-      switch (ch | 0) {
-        case 0x30: case 0x31: case 0x32: case 0x33: case 0x34: // '0'-'4'
-        case 0x35: case 0x36: case 0x37: case 0x38: case 0x39: // '5'-'9'
-        case 0x2B: case 0x2D: case 0x2E: // '+', '-', '.'
-          return new PostScriptToken(PostScriptTokenTypes.NUMBER,
-                                      this.getNumber());
-        case 0x7B: // '{'
-          this.nextChar();
-          return PostScriptToken.LBRACE;
-        case 0x7D: // '}'
-          this.nextChar();
-          return PostScriptToken.RBRACE;
-      }
-      // operator
-      var str = String.fromCharCode(ch);
-      while ((ch = this.nextChar()) >= 0 && // and 'A'-'Z', 'a'-'z'
-             ((ch >= 0x41 && ch <= 0x5A) || (ch >= 0x61 && ch <= 0x7A))) {
-        str += String.fromCharCode(ch);
-      }
-      switch (str.toLowerCase()) {
-        case 'if':
-          return PostScriptToken.IF;
-        case 'ifelse':
-          return PostScriptToken.IFELSE;
-        default:
-          return PostScriptToken.getOperator(str);
-      }
-    },
-    getNumber: function PostScriptLexer_getNumber() {
-      var ch = this.currentChar;
-      var str = String.fromCharCode(ch);
-      while ((ch = this.nextChar()) >= 0) {
-        if ((ch >= 0x30 && ch <= 0x39) || // '0'-'9'
-             ch === 0x2D || ch === 0x2E) { // '-', '.'
-          str += String.fromCharCode(ch);
-        } else {
-          break;
-        }
-      }
-      var value = parseFloat(str);
-      if (isNaN(value))
-        error('Invalid floating point number: ' + value);
-      return value;
-    }
-  };
-  return PostScriptLexer;
-})();
-
-
 
 var Annotation = (function AnnotationClosure() {
   // 12.5.5: Algorithm: Appearance streams
   function getTransformMatrix(rect, bbox, matrix) {
     var bounds = Util.getAxialAlignedBoundingBox(bbox, matrix);
     var minX = bounds[0];
     var minY = bounds[1];
     var maxX = bounds[2];
@@ -14169,16 +13570,817 @@ var CipherTransformFactory = (function C
     }
   };
 
   return CipherTransformFactory;
 })();
 
 
 
+var PatternType = {
+  FUNCTION_BASED: 1,
+  AXIAL: 2,
+  RADIAL: 3,
+  FREE_FORM_MESH: 4,
+  LATTICE_FORM_MESH: 5,
+  COONS_PATCH_MESH: 6,
+  TENSOR_PATCH_MESH: 7
+};
+
+var Pattern = (function PatternClosure() {
+  // Constructor should define this.getPattern
+  function Pattern() {
+    error('should not call Pattern constructor');
+  }
+
+  Pattern.prototype = {
+    // Input: current Canvas context
+    // Output: the appropriate fillStyle or strokeStyle
+    getPattern: function Pattern_getPattern(ctx) {
+      error('Should not call Pattern.getStyle: ' + ctx);
+    }
+  };
+
+  Pattern.parseShading = function Pattern_parseShading(shading, matrix, xref,
+                                                       res) {
+
+    var dict = isStream(shading) ? shading.dict : shading;
+    var type = dict.get('ShadingType');
+
+    switch (type) {
+      case PatternType.AXIAL:
+      case PatternType.RADIAL:
+        // Both radial and axial shadings are handled by RadialAxial shading.
+        return new Shadings.RadialAxial(dict, matrix, xref, res);
+      case PatternType.FREE_FORM_MESH:
+      case PatternType.LATTICE_FORM_MESH:
+      case PatternType.COONS_PATCH_MESH:
+      case PatternType.TENSOR_PATCH_MESH:
+        return new Shadings.Mesh(shading, matrix, xref, res);
+      default:
+        UnsupportedManager.notify(UNSUPPORTED_FEATURES.shadingPattern);
+        return new Shadings.Dummy();
+    }
+  };
+  return Pattern;
+})();
+
+var Shadings = {};
+
+// A small number to offset the first/last color stops so we can insert ones to
+// support extend.  Number.MIN_VALUE appears to be too small and breaks the
+// extend. 1e-7 works in FF but chrome seems to use an even smaller sized number
+// internally so we have to go bigger.
+Shadings.SMALL_NUMBER = 1e-2;
+
+// Radial and axial shading have very similar implementations
+// If needed, the implementations can be broken into two classes
+Shadings.RadialAxial = (function RadialAxialClosure() {
+  function RadialAxial(dict, matrix, xref, res) {
+    this.matrix = matrix;
+    this.coordsArr = dict.get('Coords');
+    this.shadingType = dict.get('ShadingType');
+    this.type = 'Pattern';
+    var cs = dict.get('ColorSpace', 'CS');
+    cs = ColorSpace.parse(cs, xref, res);
+    this.cs = cs;
+
+    var t0 = 0.0, t1 = 1.0;
+    if (dict.has('Domain')) {
+      var domainArr = dict.get('Domain');
+      t0 = domainArr[0];
+      t1 = domainArr[1];
+    }
+
+    var extendStart = false, extendEnd = false;
+    if (dict.has('Extend')) {
+      var extendArr = dict.get('Extend');
+      extendStart = extendArr[0];
+      extendEnd = extendArr[1];
+    }
+
+    if (this.shadingType === PatternType.RADIAL &&
+       (!extendStart || !extendEnd)) {
+      // Radial gradient only currently works if either circle is fully within
+      // the other circle.
+      var x1 = this.coordsArr[0];
+      var y1 = this.coordsArr[1];
+      var r1 = this.coordsArr[2];
+      var x2 = this.coordsArr[3];
+      var y2 = this.coordsArr[4];
+      var r2 = this.coordsArr[5];
+      var distance = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
+      if (r1 <= r2 + distance &&
+          r2 <= r1 + distance) {
+        warn('Unsupported radial gradient.');
+      }
+    }
+
+    this.extendStart = extendStart;
+    this.extendEnd = extendEnd;
+
+    var fnObj = dict.get('Function');
+    var fn;
+    if (isArray(fnObj)) {
+      var fnArray = [];
+      for (var j = 0, jj = fnObj.length; j < jj; j++) {
+        var obj = xref.fetchIfRef(fnObj[j]);
+        if (!isPDFFunction(obj)) {
+          error('Invalid function');
+        }
+        fnArray.push(PDFFunction.parse(xref, obj));
+      }
+      fn = function radialAxialColorFunction(arg) {
+        var out = [];
+        for (var i = 0, ii = fnArray.length; i < ii; i++) {
+          out.push(fnArray[i](arg)[0]);
+        }
+        return out;
+      };
+    } else {
+      if (!isPDFFunction(fnObj)) {
+        error('Invalid function');
+      }
+      fn = PDFFunction.parse(xref, fnObj);
+    }
+
+    // 10 samples seems good enough for now, but probably won't work
+    // if there are sharp color changes. Ideally, we would implement
+    // the spec faithfully and add lossless optimizations.
+    var diff = t1 - t0;
+    var step = diff / 10;
+
+    var colorStops = this.colorStops = [];
+
+    // Protect against bad domains so we don't end up in an infinte loop below.
+    if (t0 >= t1 || step <= 0) {
+      // Acrobat doesn't seem to handle these cases so we'll ignore for
+      // now.
+      info('Bad shading domain.');
+      return;
+    }
+
+    for (var i = t0; i <= t1; i += step) {
+      var rgbColor = cs.getRgb(fn([i]), 0);
+      var cssColor = Util.makeCssRgb(rgbColor);
+      colorStops.push([(i - t0) / diff, cssColor]);
+    }
+
+    var background = 'transparent';
+    if (dict.has('Background')) {
+      var rgbColor = cs.getRgb(dict.get('Background'), 0);
+      background = Util.makeCssRgb(rgbColor);
+    }
+
+    if (!extendStart) {
+      // Insert a color stop at the front and offset the first real color stop
+      // so it doesn't conflict with the one we insert.
+      colorStops.unshift([0, background]);
+      colorStops[1][0] += Shadings.SMALL_NUMBER;
+    }
+    if (!extendEnd) {
+      // Same idea as above in extendStart but for the end.
+      colorStops[colorStops.length - 1][0] -= Shadings.SMALL_NUMBER;
+      colorStops.push([1, background]);
+    }
+
+    this.colorStops = colorStops;
+  }
+
+  RadialAxial.prototype = {
+    getIR: function RadialAxial_getIR() {
+      var coordsArr = this.coordsArr;
+      var shadingType = this.shadingType;
+      var type, p0, p1, r0, r1;
+      if (shadingType == PatternType.AXIAL) {
+        p0 = [coordsArr[0], coordsArr[1]];
+        p1 = [coordsArr[2], coordsArr[3]];
+        r0 = null;
+        r1 = null;
+        type = 'axial';
+      } else if (shadingType == PatternType.RADIAL) {
+        p0 = [coordsArr[0], coordsArr[1]];
+        p1 = [coordsArr[3], coordsArr[4]];
+        r0 = coordsArr[2];
+        r1 = coordsArr[5];
+        type = 'radial';
+      } else {
+        error('getPattern type unknown: ' + shadingType);
+      }
+
+      var matrix = this.matrix;
+      if (matrix) {
+        p0 = Util.applyTransform(p0, matrix);
+        p1 = Util.applyTransform(p1, matrix);
+      }
+
+      return ['RadialAxial', type, this.colorStops, p0, p1, r0, r1];
+    }
+  };
+
+  return RadialAxial;
+})();
+
+// All mesh shading. For now, they will be presented as set of the triangles
+// to be drawn on the canvas and rgb color for each vertex.
+Shadings.Mesh = (function MeshClosure() {
+  function MeshStreamReader(stream, context) {
+    this.stream = stream;
+    this.context = context;
+    this.buffer = 0;
+    this.bufferLength = 0;
+  }
+  MeshStreamReader.prototype = {
+    get hasData() {
+      if (this.stream.end) {
+        return this.stream.pos < this.stream.end;
+      }
+      if (this.bufferLength > 0) {
+        return true;
+      }
+      var nextByte = this.stream.getByte();
+      if (nextByte < 0) {
+        return false;
+      }
+      this.buffer = nextByte;
+      this.bufferLength = 8;
+      return true;
+    },
+    readBits: function MeshStreamReader_readBits(n) {
+      var buffer = this.buffer;
+      var bufferLength = this.bufferLength;
+      if (n === 32) {
+        if (bufferLength === 0) {
+          return ((this.stream.getByte() << 24) |
+            (this.stream.getByte() << 16) | (this.stream.getByte() << 8) |
+            this.stream.getByte()) >>> 0;
+        }
+        buffer = (buffer << 24) | (this.stream.getByte() << 16) |
+          (this.stream.getByte() << 8) | this.stream.getByte();
+        var nextByte = this.stream.getByte();
+        this.buffer = nextByte & ((1 << bufferLength) - 1);
+        return ((buffer << (8 - bufferLength)) |
+          ((nextByte & 0xFF) >> bufferLength)) >>> 0;
+      }
+      if (n === 8 && bufferLength === 0) {
+        return this.stream.getByte();
+      }
+      while (bufferLength < n) {
+        buffer = (buffer << 8) | this.stream.getByte();
+        bufferLength += 8;
+      }
+      bufferLength -= n;
+      this.bufferLength = bufferLength;
+      this.buffer = buffer & ((1 << bufferLength) - 1);
+      return buffer >> bufferLength;
+    },
+    align: function MeshStreamReader_align() {
+      this.buffer = 0;
+      this.bufferLength = 0;
+    },
+    readFlag: function MeshStreamReader_readFlag() {
+      return this.readBits(this.context.bitsPerFlag);
+    },
+    readCoordinate: function MeshStreamReader_readCoordinate() {
+      var bitsPerCoordinate = this.context.bitsPerCoordinate;
+      var xi = this.readBits(bitsPerCoordinate);
+      var yi = this.readBits(bitsPerCoordinate);
+      var decode = this.context.decode;
+      var scale = bitsPerCoordinate < 32 ? 1 / ((1 << bitsPerCoordinate) - 1) :
+        2.3283064365386963e-10; // 2 ^ -32
+      return [
+        xi * scale * (decode[1] - decode[0]) + decode[0],
+        yi * scale * (decode[3] - decode[2]) + decode[2]
+      ];
+    },
+    readComponents: function MeshStreamReader_readComponents() {
+      var numComps = this.context.numComps;
+      var bitsPerComponent = this.context.bitsPerComponent;
+      var scale = bitsPerComponent < 32 ? 1 / ((1 << bitsPerComponent) - 1) :
+        2.3283064365386963e-10; // 2 ^ -32
+      var decode = this.context.decode;
+      var components = [];
+      for (var i = 0, j = 4; i < numComps; i++, j += 2) {
+        var ci = this.readBits(bitsPerComponent);
+        components.push(ci * scale * (decode[j + 1] - decode[j]) + decode[j]);
+      }
+      if (this.context.colorFn) {
+        components = this.context.colorFn(components);
+      }
+      return this.context.colorSpace.getRgb(components, 0);
+    }
+  };
+
+  function decodeType4Shading(mesh, reader) {
+    var coords = mesh.coords;
+    var colors = mesh.colors;
+    var operators = [];
+    var ps = []; // not maintaining cs since that will match ps
+    var verticesLeft = 0; // assuming we have all data to start a new triangle
+    while (reader.hasData) {
+      var f = reader.readFlag();
+      var coord = reader.readCoordinate();
+      var color = reader.readComponents();
+      if (verticesLeft === 0) { // ignoring flags if we started a triangle
+        assert(0 <= f && f <= 2, 'Unknown type4 flag');
+        switch (f) {
+          case 0:
+            verticesLeft = 3;
+            break;
+          case 1:
+            ps.push(ps[ps.length - 2], ps[ps.length - 1]);
+            verticesLeft = 1;
+            break;
+          case 2:
+            ps.push(ps[ps.length - 3], ps[ps.length - 1]);
+            verticesLeft = 1;
+            break;
+        }
+        operators.push(f);
+      }
+      ps.push(coords.length);
+      coords.push(coord);
+      colors.push(color);
+      verticesLeft--;
+
+      reader.align();
+    }
+
+    var psPacked = new Int32Array(ps);
+
+    mesh.figures.push({
+      type: 'triangles',
+      coords: psPacked,
+      colors: psPacked
+    });
+  }
+
+  function decodeType5Shading(mesh, reader, verticesPerRow) {
+    var coords = mesh.coords;
+    var colors = mesh.colors;
+    var operators = [];
+    var ps = []; // not maintaining cs since that will match ps
+    while (reader.hasData) {
+      var coord = reader.readCoordinate();
+      var color = reader.readComponents();
+      ps.push(coords.length);
+      coords.push(coord);
+      colors.push(color);
+    }
+
+    var psPacked = new Int32Array(ps);
+
+    mesh.figures.push({
+      type: 'lattice',
+      coords: psPacked,
+      colors: psPacked,
+      verticesPerRow: verticesPerRow
+    });
+  }
+
+  var MIN_SPLIT_PATCH_CHUNKS_AMOUNT = 3;
+  var MAX_SPLIT_PATCH_CHUNKS_AMOUNT = 20;
+
+  var TRIANGLE_DENSITY = 20; // count of triangles per entire mesh bounds
+
+  var getB = (function getBClosure() {
+    function buildB(count) {
+      var lut = [];
+      for (var i = 0; i <= count; i++) {
+        var t = i / count, t_ = 1 - t;
+        lut.push(new Float32Array([t_ * t_ * t_, 3 * t * t_ * t_,
+          3 * t * t * t_, t * t * t]));
+      }
+      return lut;
+    }
+    var cache = [];
+    return function getB(count) {
+      if (!cache[count]) {
+        cache[count] = buildB(count);
+      }
+      return cache[count];
+    };
+  })();
+
+  function buildFigureFromPatch(mesh, index) {
+    var figure = mesh.figures[index];
+    assert(figure.type === 'patch', 'Unexpected patch mesh figure');
+
+    var coords = mesh.coords, colors = mesh.colors;
+    var pi = figure.coords;
+    var ci = figure.colors;
+
+    var figureMinX = Math.min(coords[pi[0]][0], coords[pi[3]][0],
+                              coords[pi[12]][0], coords[pi[15]][0]);
+    var figureMinY = Math.min(coords[pi[0]][1], coords[pi[3]][1],
+                              coords[pi[12]][1], coords[pi[15]][1]);
+    var figureMaxX = Math.max(coords[pi[0]][0], coords[pi[3]][0],
+                              coords[pi[12]][0], coords[pi[15]][0]);
+    var figureMaxY = Math.max(coords[pi[0]][1], coords[pi[3]][1],
+                              coords[pi[12]][1], coords[pi[15]][1]);
+    var splitXBy = Math.ceil((figureMaxX - figureMinX) * TRIANGLE_DENSITY /
+                             (mesh.bounds[2] - mesh.bounds[0]));
+    splitXBy = Math.max(MIN_SPLIT_PATCH_CHUNKS_AMOUNT,
+               Math.min(MAX_SPLIT_PATCH_CHUNKS_AMOUNT, splitXBy));
+    var splitYBy = Math.ceil((figureMaxY - figureMinY) * TRIANGLE_DENSITY /
+                             (mesh.bounds[3] - mesh.bounds[1]));
+    splitYBy = Math.max(MIN_SPLIT_PATCH_CHUNKS_AMOUNT,
+               Math.min(MAX_SPLIT_PATCH_CHUNKS_AMOUNT, splitYBy));
+
+    var verticesPerRow = splitXBy + 1;
+    var figureCoords = new Int32Array((splitYBy + 1) * verticesPerRow);
+    var figureColors = new Int32Array((splitYBy + 1) * verticesPerRow);
+    var k = 0;
+    var cl = new Uint8Array(3), cr = new Uint8Array(3);
+    var c0 = colors[ci[0]], c1 = colors[ci[1]],
+      c2 = colors[ci[2]], c3 = colors[ci[3]];
+    var bRow = getB(splitYBy), bCol = getB(splitXBy);
+    for (var row = 0; row <= splitYBy; row++) {
+      cl[0] = ((c0[0] * (splitYBy - row) + c2[0] * row) / splitYBy) | 0;
+      cl[1] = ((c0[1] * (splitYBy - row) + c2[1] * row) / splitYBy) | 0;
+      cl[2] = ((c0[2] * (splitYBy - row) + c2[2] * row) / splitYBy) | 0;
+
+      cr[0] = ((c1[0] * (splitYBy - row) + c3[0] * row) / splitYBy) | 0;
+      cr[1] = ((c1[1] * (splitYBy - row) + c3[1] * row) / splitYBy) | 0;
+      cr[2] = ((c1[2] * (splitYBy - row) + c3[2] * row) / splitYBy) | 0;
+
+      for (var col = 0; col <= splitXBy; col++, k++) {
+        if ((row === 0 || row === splitYBy) &&
+            (col === 0 || col === splitXBy)) {
+          continue;
+        }
+        var x = 0, y = 0;
+        var q = 0;
+        for (var i = 0; i <= 3; i++) {
+          for (var j = 0; j <= 3; j++, q++) {
+            var m = bRow[row][i] * bCol[col][j];
+            x += coords[pi[q]][0] * m;
+            y += coords[pi[q]][1] * m;
+          }
+        }
+        figureCoords[k] = coords.length;
+        coords.push([x, y]);
+        figureColors[k] = colors.length;
+        var newColor = new Uint8Array(3);
+        newColor[0] = ((cl[0] * (splitXBy - col) + cr[0] * col) / splitXBy) | 0;
+        newColor[1] = ((cl[1] * (splitXBy - col) + cr[1] * col) / splitXBy) | 0;
+        newColor[2] = ((cl[2] * (splitXBy - col) + cr[2] * col) / splitXBy) | 0;
+        colors.push(newColor);
+      }
+    }
+    figureCoords[0] = pi[0];
+    figureColors[0] = ci[0];
+    figureCoords[splitXBy] = pi[3];
+    figureColors[splitXBy] = ci[1];
+    figureCoords[verticesPerRow * splitYBy] = pi[12];
+    figureColors[verticesPerRow * splitYBy] = ci[2];
+    figureCoords[verticesPerRow * splitYBy + splitXBy] = pi[15];
+    figureColors[verticesPerRow * splitYBy + splitXBy] = ci[3];
+
+    mesh.figures[index] = {
+      type: 'lattice',
+      coords: figureCoords,
+      colors: figureColors,
+      verticesPerRow: verticesPerRow
+    };
+  }
+
+  function decodeType6Shading(mesh, reader) {
+    // A special case of Type 7. The p11, p12, p21, p22 automatically filled
+    var coords = mesh.coords;
+    var colors = mesh.colors;
+    var ps = new Int32Array(16); // p00, p10, ..., p30, p01, ..., p33
+    var cs = new Int32Array(4); // c00, c30, c03, c33
+    while (reader.hasData) {
+      var f = reader.readFlag();
+      assert(0 <= f && f <= 3, 'Unknown type6 flag');
+      var i, ii;
+      var pi = coords.length;
+      for (i = 0, ii = (f !== 0 ? 8 : 12); i < ii; i++) {
+        coords.push(reader.readCoordinate());
+      }
+      var ci = colors.length;
+      for (i = 0, ii = (f !== 0 ? 2 : 4); i < ii; i++) {
+        colors.push(reader.readComponents());
+      }
+      var tmp1, tmp2, tmp3, tmp4;
+      switch (f) {
+        case 0:
+          ps[12] = pi + 3; ps[13] = pi + 4;  ps[14] = pi + 5;  ps[15] = pi + 6;
+          ps[ 8] = pi + 2; /* values for 5, 6, 9, 10 are    */ ps[11] = pi + 7;
+          ps[ 4] = pi + 1; /* calculated below              */ ps[ 7] = pi + 8;
+          ps[ 0] = pi;     ps[ 1] = pi + 11; ps[ 2] = pi + 10; ps[ 3] = pi + 9;
+          cs[2] = ci + 1; cs[3] = ci + 2;
+          cs[0] = ci;     cs[1] = ci + 3;
+          break;
+        case 1:
+          tmp1 = ps[12]; tmp2 = ps[13]; tmp3 = ps[14]; tmp4 = ps[15];
+          ps[12] = pi + 5; ps[13] = pi + 4;  ps[14] = pi + 3;  ps[15] = pi + 2;
+          ps[ 8] = pi + 6; /* values for 5, 6, 9, 10 are    */ ps[11] = pi + 1;
+          ps[ 4] = pi + 7; /* calculated below              */ ps[ 7] = pi;
+          ps[ 0] = tmp1;   ps[ 1] = tmp2;    ps[ 2] = tmp3;    ps[ 3] = tmp4;
+          tmp1 = cs[2]; tmp2 = cs[3];
+          cs[2] = ci + 1; cs[3] = ci;
+          cs[0] = tmp1;   cs[1] = tmp2;
+          break;
+        case 2:
+          ps[12] = ps[15]; ps[13] = pi + 7; ps[14] = pi + 6;   ps[15] = pi + 5;
+          ps[ 8] = ps[11]; /* values for 5, 6, 9, 10 are    */ ps[11] = pi + 4;
+          ps[ 4] = ps[7];  /* calculated below              */ ps[ 7] = pi + 3;
+          ps[ 0] = ps[3];  ps[ 1] = pi;     ps[ 2] = pi + 1;   ps[ 3] = pi + 2;
+          cs[2] = cs[3]; cs[3] = ci + 1;
+          cs[0] = cs[1]; cs[1] = ci;
+          break;
+        case 3:
+          ps[12] = ps[0];  ps[13] = ps[1];   ps[14] = ps[2];   ps[15] = ps[3];
+          ps[ 8] = pi;     /* values for 5, 6, 9, 10 are    */ ps[11] = pi + 7;
+          ps[ 4] = pi + 1; /* calculated below              */ ps[ 7] = pi + 6;
+          ps[ 0] = pi + 2; ps[ 1] = pi + 3;  ps[ 2] = pi + 4;  ps[ 3] = pi + 5;
+          cs[2] = cs[0]; cs[3] = cs[1];
+          cs[0] = ci;    cs[1] = ci + 1;
+          break;
+      }
+      // set p11, p12, p21, p22
+      ps[5] = coords.length;
+      coords.push([
+        (-4 * coords[ps[0]][0] - coords[ps[15]][0] +
+          6 * (coords[ps[4]][0] + coords[ps[1]][0]) -
+          2 * (coords[ps[12]][0] + coords[ps[3]][0]) +
+          3 * (coords[ps[13]][0] + coords[ps[7]][0])) / 9,
+        (-4 * coords[ps[0]][1] - coords[ps[15]][1] +
+          6 * (coords[ps[4]][1] + coords[ps[1]][1]) -
+          2 * (coords[ps[12]][1] + coords[ps[3]][1]) +
+          3 * (coords[ps[13]][1] + coords[ps[7]][1])) / 9
+      ]);
+      ps[6] = coords.length;
+      coords.push([
+        (-4 * coords[ps[3]][0] - coords[ps[12]][0] +
+          6 * (coords[ps[2]][0] + coords[ps[7]][0]) -
+          2 * (coords[ps[0]][0] + coords[ps[15]][0]) +
+          3 * (coords[ps[4]][0] + coords[ps[14]][0])) / 9,
+        (-4 * coords[ps[3]][1] - coords[ps[12]][1] +
+          6 * (coords[ps[2]][1] + coords[ps[7]][1]) -
+          2 * (coords[ps[0]][1] + coords[ps[15]][1]) +
+          3 * (coords[ps[4]][1] + coords[ps[14]][1])) / 9
+      ]);
+      ps[9] = coords.length;
+      coords.push([
+        (-4 * coords[ps[12]][0] - coords[ps[3]][0] +
+          6 * (coords[ps[8]][0] + coords[ps[13]][0]) -
+          2 * (coords[ps[0]][0] + coords[ps[15]][0]) +
+          3 * (coords[ps[11]][0] + coords[ps[1]][0])) / 9,
+        (-4 * coords[ps[12]][1] - coords[ps[3]][1] +
+          6 * (coords[ps[8]][1] + coords[ps[13]][1]) -
+          2 * (coords[ps[0]][1] + coords[ps[15]][1]) +
+          3 * (coords[ps[11]][1] + coords[ps[1]][1])) / 9
+      ]);
+      ps[10] = coords.length;
+      coords.push([
+        (-4 * coords[ps[15]][0] - coords[ps[0]][0] +
+          6 * (coords[ps[11]][0] + coords[ps[14]][0]) -
+          2 * (coords[ps[12]][0] + coords[ps[3]][0]) +
+          3 * (coords[ps[2]][0] + coords[ps[8]][0])) / 9,
+        (-4 * coords[ps[15]][1] - coords[ps[0]][1] +
+          6 * (coords[ps[11]][1] + coords[ps[14]][1]) -
+          2 * (coords[ps[12]][1] + coords[ps[3]][1]) +
+          3 * (coords[ps[2]][1] + coords[ps[8]][1])) / 9
+      ]);
+      mesh.figures.push({
+        type: 'patch',
+        coords: new Int32Array(ps), // making copies of ps and cs
+        colors: new Int32Array(cs)
+      });
+    }
+  }
+
+  function decodeType7Shading(mesh, reader) {
+    var coords = mesh.coords;
+    var colors = mesh.colors;
+    var ps = new Int32Array(16); // p00, p10, ..., p30, p01, ..., p33
+    var cs = new Int32Array(4); // c00, c30, c03, c33
+    while (reader.hasData) {
+      var f = reader.readFlag();
+      assert(0 <= f && f <= 3, 'Unknown type7 flag');
+      var i, ii;
+      var pi = coords.length;
+      for (i = 0, ii = (f !== 0 ? 12 : 16); i < ii; i++) {
+        coords.push(reader.readCoordinate());
+      }
+      var ci = colors.length;
+      for (i = 0, ii = (f !== 0 ? 2 : 4); i < ii; i++) {
+        colors.push(reader.readComponents());
+      }
+      var tmp1, tmp2, tmp3, tmp4;
+      switch (f) {
+        case 0:
+          ps[12] = pi + 3; ps[13] = pi + 4;  ps[14] = pi + 5;  ps[15] = pi + 6;
+          ps[ 8] = pi + 2; ps[ 9] = pi + 13; ps[10] = pi + 14; ps[11] = pi + 7;
+          ps[ 4] = pi + 1; ps[ 5] = pi + 12; ps[ 6] = pi + 15; ps[ 7] = pi + 8;
+          ps[ 0] = pi;     ps[ 1] = pi + 11; ps[ 2] = pi + 10; ps[ 3] = pi + 9;
+          cs[2] = ci + 1; cs[3] = ci + 2;
+          cs[0] = ci;     cs[1] = ci + 3;
+          break;
+        case 1:
+          tmp1 = ps[12]; tmp2 = ps[13]; tmp3 = ps[14]; tmp4 = ps[15];
+          ps[12] = pi + 5; ps[13] = pi + 4;  ps[14] = pi + 3;  ps[15] = pi + 2;
+          ps[ 8] = pi + 6; ps[ 9] = pi + 11; ps[10] = pi + 10; ps[11] = pi + 1;
+          ps[ 4] = pi + 7; ps[ 5] = pi + 8;  ps[ 6] = pi + 9;  ps[ 7] = pi;
+          ps[ 0] = tmp1;   ps[ 1] = tmp2;    ps[ 2] = tmp3;    ps[ 3] = tmp4;
+          tmp1 = cs[2]; tmp2 = cs[3];
+          cs[2] = ci + 1; cs[3] = ci;
+          cs[0] = tmp1;   cs[1] = tmp2;
+          break;
+        case 2:
+          ps[12] = ps[15]; ps[13] = pi + 7; ps[14] = pi + 6;  ps[15] = pi + 5;
+          ps[ 8] = ps[11]; ps[ 9] = pi + 8; ps[10] = pi + 11; ps[11] = pi + 4;
+          ps[ 4] = ps[7];  ps[ 5] = pi + 9; ps[ 6] = pi + 10; ps[ 7] = pi + 3;
+          ps[ 0] = ps[3];  ps[ 1] = pi;     ps[ 2] = pi + 1;  ps[ 3] = pi + 2;
+          cs[2] = cs[3]; cs[3] = ci + 1;
+          cs[0] = cs[1]; cs[1] = ci;
+          break;
+        case 3:
+          ps[12] = ps[0];  ps[13] = ps[1];   ps[14] = ps[2];   ps[15] = ps[3];
+          ps[ 8] = pi;     ps[ 9] = pi + 9;  ps[10] = pi + 8;  ps[11] = pi + 7;
+          ps[ 4] = pi + 1; ps[ 5] = pi + 10; ps[ 6] = pi + 11; ps[ 7] = pi + 6;
+          ps[ 0] = pi + 2; ps[ 1] = pi + 3;  ps[ 2] = pi + 4;  ps[ 3] = pi + 5;
+          cs[2] = cs[0]; cs[3] = cs[1];
+          cs[0] = ci;    cs[1] = ci + 1;
+          break;
+      }
+      mesh.figures.push({
+        type: 'patch',
+        coords: new Int32Array(ps), // making copies of ps and cs
+        colors: new Int32Array(cs)
+      });
+    }
+  }
+
+  function updateBounds(mesh) {
+    var minX = mesh.coords[0][0], minY = mesh.coords[0][1],
+      maxX = minX, maxY = minY;
+    for (var i = 1, ii = mesh.coords.length; i < ii; i++) {
+      var x = mesh.coords[i][0], y = mesh.coords[i][1];
+      minX = minX > x ? x : minX;
+      minY = minY > y ? y : minY;
+      maxX = maxX < x ? x : maxX;
+      maxY = maxY < y ? y : maxY;
+    }
+    mesh.bounds = [minX, minY, maxX, maxY];
+  }
+
+  function Mesh(stream, matrix, xref, res) {
+    assert(isStream(stream), 'Mesh data is not a stream');
+    var dict = stream.dict;
+    this.matrix = matrix;
+    this.shadingType = dict.get('ShadingType');
+    this.type = 'Pattern';
+    this.bbox = dict.get('BBox');
+    var cs = dict.get('ColorSpace', 'CS');
+    cs = ColorSpace.parse(cs, xref, res);
+    this.cs = cs;
+    this.background = dict.has('Background') ?
+      cs.getRgb(dict.get('Background'), 0) : null;
+
+    var fnObj = dict.get('Function');
+    var fn;
+    if (!fnObj) {
+      fn = null;
+    } else if (isArray(fnObj)) {
+      var fnArray = [];
+      for (var j = 0, jj = fnObj.length; j < jj; j++) {
+        var obj = xref.fetchIfRef(fnObj[j]);
+        if (!isPDFFunction(obj)) {
+          error('Invalid function');
+        }
+        fnArray.push(PDFFunction.parse(xref, obj));
+      }
+      fn = function radialAxialColorFunction(arg) {
+        var out = [];
+        for (var i = 0, ii = fnArray.length; i < ii; i++) {
+          out.push(fnArray[i](arg)[0]);
+        }
+        return out;
+      };
+    } else {
+      if (!isPDFFunction(fnObj)) {
+        error('Invalid function');
+      }
+      fn = PDFFunction.parse(xref, fnObj);
+    }
+
+    this.coords = [];
+    this.colors = [];
+    this.figures = [];
+
+    var decodeContext = {
+      bitsPerCoordinate: dict.get('BitsPerCoordinate'),
+      bitsPerComponent: dict.get('BitsPerComponent'),
+      bitsPerFlag: dict.get('BitsPerFlag'),
+      decode: dict.get('Decode'),
+      colorFn: fn,
+      colorSpace: cs,
+      numComps: fn ? 1 : cs.numComps
+    };
+    var reader = new MeshStreamReader(stream, decodeContext);
+
+    var patchMesh = false;
+    switch (this.shadingType) {
+      case PatternType.FREE_FORM_MESH:
+        decodeType4Shading(this, reader);
+        break;
+      case PatternType.LATTICE_FORM_MESH:
+        var verticesPerRow = dict.get('VerticesPerRow') | 0;
+        assert(verticesPerRow >= 2, 'Invalid VerticesPerRow');
+        decodeType5Shading(this, reader, verticesPerRow);
+        break;
+      case PatternType.COONS_PATCH_MESH:
+        decodeType6Shading(this, reader);
+        patchMesh = true;
+        break;
+      case PatternType.TENSOR_PATCH_MESH:
+        decodeType7Shading(this, reader);
+        patchMesh = true;
+        break;
+      default:
+        error('Unsupported mesh type.');
+        break;
+    }
+
+    if (patchMesh) {
+      // dirty bounds calculation for determining, how dense shall be triangles
+      updateBounds(this);
+      for (var i = 0, ii = this.figures.length; i < ii; i++) {
+        buildFigureFromPatch(this, i);
+      }
+    }
+    // calculate bounds
+    updateBounds(this);
+  }
+
+  Mesh.prototype = {
+    getIR: function Mesh_getIR() {
+      var type = this.shadingType;
+      var i, ii, j;
+      var coords = this.coords;
+      var coordsPacked = new Float32Array(coords.length * 2);
+      for (i = 0, j = 0, ii = coords.length; i < ii; i++) {
+        var xy = coords[i];
+        coordsPacked[j++] = xy[0];
+        coordsPacked[j++] = xy[1];
+      }
+      var colors = this.colors;
+      var colorsPacked = new Uint8Array(colors.length * 3);
+      for (i = 0, j = 0, ii = colors.length; i < ii; i++) {
+        var c = colors[i];
+        colorsPacked[j++] = c[0];
+        colorsPacked[j++] = c[1];
+        colorsPacked[j++] = c[2];
+      }
+      var figures = this.figures;
+      var bbox = this.bbox;
+      var bounds = this.bounds;
+      var matrix = this.matrix;
+      var background = this.background;
+
+      return ['Mesh', type, coordsPacked, colorsPacked, figures, bounds,
+        matrix, bbox, background];
+    }
+  };
+
+  return Mesh;
+})();
+
+Shadings.Dummy = (function DummyClosure() {
+  function Dummy() {
+    this.type = 'Pattern';
+  }
+
+  Dummy.prototype = {
+    getIR: function Dummy_getIR() {
+      return ['Dummy'];
+    }
+  };
+  return Dummy;
+})();
+
+function getTilingPatternIR(operatorList, dict, args) {
+  var matrix = dict.get('Matrix');
+  var bbox = dict.get('BBox');
+  var xstep = dict.get('XStep');
+  var ystep = dict.get('YStep');
+  var paintType = dict.get('PaintType');
+  var tilingType = dict.get('TilingType');
+
+  return [
+    'TilingPattern', args, operatorList, matrix, bbox, xstep, ystep,
+    paintType, tilingType
+  ];
+}
+
+
 var PartialEvaluator = (function PartialEvaluatorClosure() {
   function PartialEvaluator(pdfManager, xref, handler, pageIndex,
                             uniquePrefix, idCounters, fontCache) {
     this.state = new EvalState();
     this.stateStack = [];
 
     this.pdfManager = pdfManager;
     this.xref = xref;
@@ -14240,27 +14442,28 @@ var PartialEvaluator = (function Partial
 
       var matrix = xobj.dict.get('Matrix');
       var bbox = xobj.dict.get('BBox');
       var group = xobj.dict.get('Group');
       if (group) {
         var groupOptions = {
           matrix: matrix,
           bbox: bbox,
-          smask: !!smask,
+          smask: smask,
           isolated: false,
           knockout: false
         };
 
         var groupSubtype = group.get('S');
         if (isName(groupSubtype) && groupSubtype.name === 'Transparency') {
           groupOptions.isolated = group.get('I') || false;
           groupOptions.knockout = group.get('K') || false;
-          // There is also a group colorspace, but since we put everything in
-          // RGB I'm not sure we need it.
+          var colorSpace = group.get('CS');
+          groupOptions.colorSpace = colorSpace ?
+            ColorSpace.parseToIR(colorSpace, this.xref, resources) : null;
         }
         operatorList.addOp(OPS.beginGroup, [groupOptions]);
       }
 
       operatorList.addOp(OPS.paintFormXObjectBegin, [matrix, bbox]);
 
       this.getOperatorList(xobj, xobj.dict.get('Resources') || resources,
                            operatorList, state);
@@ -14309,17 +14512,17 @@ var PartialEvaluator = (function Partial
 
       var SMALL_IMAGE_DIMENSIONS = 200;
       // Inlining small images into the queue as RGB data
       if (inline && !softMask && !mask &&
           !(image instanceof JpegStream) &&
           (w + h) < SMALL_IMAGE_DIMENSIONS) {
         var imageObj = new PDFImage(this.xref, resources, image,
                                     inline, null, null);
-        var imgData = imageObj.getImageData();
+        var imgData = imageObj.createImageData();
         operatorList.addOp(OPS.paintInlineImageXObject, [imgData]);
         return;
       }
 
       // If there is no imageMask, create the PDFImage and a lot
       // of image processing can be done here.
       var uniquePrefix = this.uniquePrefix || '';
       var objId = 'img_' + uniquePrefix + (++this.idCounters.obj);
@@ -14332,34 +14535,46 @@ var PartialEvaluator = (function Partial
         operatorList.addOp(OPS.paintJpegXObject, args);
         this.handler.send(
             'obj', [objId, this.pageIndex, 'JpegStream', image.getIR()]);
         return;
       }
 
 
       PDFImage.buildImage(function(imageObj) {
-          var imgData = imageObj.getImageData();
+          var imgData = imageObj.createImageData();
           self.handler.send('obj', [objId, self.pageIndex, 'Image', imgData],
                             null, [imgData.data.buffer]);
         }, self.handler, self.xref, resources, image, inline);
 
       operatorList.addOp(OPS.paintImageXObject, args);
     },
 
+    handleSMask: function PartialEvaluator_handleSmask(smask, resources,
+                                                       operatorList) {
+      var smaskContent = smask.get('G');
+      var smaskOptions = {
+        subtype: smask.get('S').name,
+        backdrop: smask.get('BC')
+      };
+
+      this.buildFormXObject(resources, smaskContent, smaskOptions,
+                            operatorList);
+    },
+
     handleTilingType: function PartialEvaluator_handleTilingType(
                           fn, args, resources, pattern, patternDict,
                           operatorList) {
       // Create an IR of the pattern code.
       var tilingOpList = this.getOperatorList(pattern,
                                   patternDict.get('Resources') || resources);
       // Add the dependencies to the parent operator list so they are resolved
       // before sub operator list is executed synchronously.
       operatorList.addDependencies(tilingOpList.dependencies);
-      operatorList.addOp(fn, TilingPattern.getIR({
+      operatorList.addOp(fn, getTilingPatternIR({
                                fnArray: tilingOpList.fnArray,
                                argsArray: tilingOpList.argsArray
                               }, patternDict, args));
     },
 
     handleSetFont: function PartialEvaluator_handleSetFont(
                       resources, fontArgs, fontRef, operatorList) {
 
@@ -14409,17 +14624,17 @@ var PartialEvaluator = (function Partial
           }
         }
       }
 
       return glyphs;
     },
 
     setGState: function PartialEvaluator_setGState(resources, gState,
-                                                   operatorList) {
+                                                   operatorList, xref) {
 
       var self = this;
       // TODO(mack): This should be rewritten so that this function returns
       // what should be added to the queue during each iteration
       function setGStateForKey(gStateObj, key, value) {
         switch (key) {
           case 'Type':
             break;
@@ -14439,19 +14654,28 @@ var PartialEvaluator = (function Partial
                                                 operatorList);
             operatorList.addDependency(loadedName);
             gStateObj.push([key, [loadedName, value[1]]]);
             break;
           case 'BM':
             gStateObj.push([key, value]);
             break;
           case 'SMask':
-            // We support the default so don't trigger a warning bar.
-            if (!isName(value) || value.name != 'None')
-              UnsupportedManager.notify(UNSUPPORTED_FEATURES.smask);
+            if (isName(value) && value.name === 'None') {
+              gStateObj.push([key, false]);
+              break;
+            }
+            var dict = xref.fetchIfRef(value);
+            if (isDict(dict)) {
+              self.handleSMask(dict, resources, operatorList);
+              gStateObj.push([key, true]);
+            } else {
+              warn('Unsupported SMask type');
+            }
+
             break;
           // Only generate info log messages for the following since
           // they are unlikey to have a big impact on the rendering.
           case 'OP':
           case 'op':
           case 'OPM':
           case 'BG':
           case 'BG2':
@@ -14723,17 +14947,17 @@ var PartialEvaluator = (function Partial
             case OPS.setGState:
               var dictName = args[0];
               var extGState = resources.get('ExtGState');
 
               if (!isDict(extGState) || !extGState.has(dictName.name))
                 break;
 
               var gState = extGState.get(dictName.name);
-              self.setGState(resources, gState, operatorList);
+              self.setGState(resources, gState, operatorList, xref);
               args = [];
               continue;
           } // switch
 
           operatorList.addOp(fn, args);
       }
 
       // some pdf don't close all restores inside object/form
@@ -15031,31 +15255,32 @@ var PartialEvaluator = (function Partial
       var charToUnicode = [];
       if (isName(cmapObj)) {
         var isIdentityMap = cmapObj.name.substr(0, 9) == 'Identity-';
         if (!isIdentityMap)
           error('ToUnicode file cmap translation not implemented');
       } else if (isStream(cmapObj)) {
         var cmap = CMapFactory.create(cmapObj).map;
         // Convert UTF-16BE
-        for (var i in cmap) {
-          var token = cmap[i];
+        // NOTE: cmap can be a sparse array, so use forEach instead of for(;;)
+        //  to iterate over all keys.
+        cmap.forEach(function(token, i) {
           var str = [];
           for (var k = 0; k < token.length; k += 2) {
             var w1 = (token.charCodeAt(k) << 8) | token.charCodeAt(k + 1);
             if ((w1 & 0xF800) !== 0xD800) { // w1 < 0xD800 || w1 > 0xDFFF
               str.push(w1);
               continue;
             }
             k += 2;
             var w2 = (token.charCodeAt(k) << 8) | token.charCodeAt(k + 1);
             str.push(((w1 & 0x3ff) << 10) + (w2 & 0x3ff) + 0x10000);
           }
           cmap[i] = String.fromCharCode.apply(String, str);
-        }
+        });
         return cmap;
       }
       return charToUnicode;
     },
     readCidToGidMap: function PartialEvaluator_readCidToGidMap(cidToGidStream) {
       // Extract the encoding from the CIDToGIDMap
       var glyphsData = cidToGidStream.getBytes();
 
@@ -15440,17 +15665,18 @@ var PartialEvaluator = (function Partial
             data[offset + rowSize + 2] = data[offset + rowSize - 2];
             data[offset + rowSize + 3] = data[offset + rowSize - 1];
             offset -= imgRowSize;
           }
         }
         // replacing queue items
         squash(fnArray, j, count * 4, OPS.paintInlineImageXObjectGroup);
         argsArray.splice(j, count * 4,
-          [{width: imgWidth, height: imgHeight, data: imgData}, map]);
+          [{width: imgWidth, height: imgHeight, kind: 'rgba_32bpp',
+            data: imgData}, map]);
         i = j;
         ii = argsArray.length;
       }
     }
     // grouping paintImageMaskXObject's into paintImageMaskXObjectGroup
     // searching for (save, transform, paintImageMaskXObject, restore)+
     var MIN_IMAGES_IN_MASKS_BLOCK = 10;
     var MAX_IMAGES_IN_MASKS_BLOCK = 100;
@@ -28164,20 +28390,20 @@ var PDFImage = (function PDFImageClosure
           index++;
         }
       }
     },
     getComponents: function PDFImage_getComponents(buffer) {
       var bpc = this.bpc;
 
       // This image doesn't require any extra work.
-      if (bpc === 8)
+      if (bpc === 8) {
         return buffer;
-
-      var bufferLength = buffer.length;
+      }
+
       var width = this.width;
       var height = this.height;
       var numComps = this.numComps;
 
       var length = width * height * numComps;
       var bufferPos = 0;
       var output = bpc <= 8 ? new Uint8Array(length) :
         bpc <= 16 ? new Uint16Array(length) : new Uint32Array(length);
@@ -28314,43 +28540,75 @@ var PDFImage = (function PDFImageClosure
           continue;
         }
         var k = 255 / alpha;
         buffer[i] = clamp((buffer[i] - matteRgb[0]) * k + matteRgb[0]);
         buffer[i + 1] = clamp((buffer[i + 1] - matteRgb[1]) * k + matteRgb[1]);
         buffer[i + 2] = clamp((buffer[i + 2] - matteRgb[2]) * k + matteRgb[2]);
       }
     },
-    fillRgbaBuffer: function PDFImage_fillRgbaBuffer(buffer, width, height) {
+    createImageData: function PDFImage_createImageData() {
+      var drawWidth = this.drawWidth;
+      var drawHeight = this.drawHeight;
+      var imgData = {       // other fields are filled in below
+        width: drawWidth,
+        height: drawHeight,
+      };
+
       var numComps = this.numComps;
       var originalWidth = this.width;
       var originalHeight = this.height;
       var bpc = this.bpc;
 
       // rows start at byte boundary;
       var rowBytes = (originalWidth * numComps * bpc + 7) >> 3;
       var imgArray = this.getImageBytes(originalHeight * rowBytes);
 
       // imgArray can be incomplete (e.g. after CCITT fax encoding)
       var actualHeight = 0 | (imgArray.length / rowBytes *
-                         height / originalHeight);
+                         drawHeight / originalHeight);
+
+      // If it is a 1-bit-per-pixel grayscale (i.e. black-and-white) image
+      // without any complications, we pass a same-sized copy to the main
+      // thread rather than expanding by 32x to RGBA form. This saves *lots* of
+      // memory for many scanned documents. It's also much faster.
+      if (this.colorSpace.name === 'DeviceGray' && bpc === 1 &&
+          !this.smask && !this.mask && !this.needsDecode &&
+          drawWidth === originalWidth && drawHeight === originalHeight) {
+        imgData.kind = 'grayscale_1bpp';
+
+        // We must make a copy of imgArray, otherwise it'll be neutered upon
+        // transfer which will break any code that subsequently reuses it.
+        var newArray = new Uint8Array(imgArray.length);
+        newArray.set(imgArray);
+        imgData.data = newArray;
+        imgData.origLength = imgArray.length;
+        return imgData;
+      }
+
       var comps = this.getComponents(imgArray);
 
+      var rgbaBuf = new Uint8Array(drawWidth * drawHeight * 4);
+
       // Handle opacity here since color key masking needs to be performed on
       // undecoded values.
-      this.fillOpacity(buffer, width, height, actualHeight, comps);
+      this.fillOpacity(rgbaBuf, drawWidth, drawHeight, actualHeight, comps);
 
       if (this.needsDecode) {
         this.decodeBuffer(comps);
       }
 
-      this.colorSpace.fillRgb(buffer, originalWidth, originalHeight, width,
-                              height, actualHeight, bpc, comps);
-
-      this.undoPreblend(buffer, width, actualHeight);
+      this.colorSpace.fillRgb(rgbaBuf, originalWidth, originalHeight, drawWidth,
+                              drawHeight, actualHeight, bpc, comps);
+
+      this.undoPreblend(rgbaBuf, drawWidth, actualHeight);
+
+      imgData.kind = 'rgba_32bpp';
+      imgData.data = rgbaBuf;
+      return imgData;
     },
     fillGrayBuffer: function PDFImage_fillGrayBuffer(buffer) {
       var numComps = this.numComps;
       if (numComps != 1)
         error('Reading gray scale from a color image: ' + numComps);
 
       var width = this.width;
       var height = this.height;
@@ -28365,28 +28623,16 @@ var PDFImage = (function PDFImageClosure
         this.decodeBuffer(comps);
       }
       var length = width * height;
       // we aren't using a colorspace so we need to scale the value
       var scale = 255 / ((1 << bpc) - 1);
       for (var i = 0; i < length; ++i)
         buffer[i] = (scale * comps[i]) | 0;
     },
-    getImageData: function PDFImage_getImageData() {
-      var drawWidth = this.drawWidth;
-      var drawHeight = this.drawHeight;
-      var imgData = {
-        width: drawWidth,
-        height: drawHeight,
-        data: new Uint8Array(drawWidth * drawHeight * 4)
-      };
-      var pixels = imgData.data;
-      this.fillRgbaBuffer(pixels, drawWidth, drawHeight);
-      return imgData;
-    },
     getImageBytes: function PDFImage_getImageBytes(length) {
       this.image.reset();
       return this.image.getBytes(length);
     }
   };
   return PDFImage;
 })();
 
@@ -31644,16 +31890,23 @@ var Parser = (function ParserClosure() {
   return Parser;
 })();
 
 var Lexer = (function LexerClosure() {
   function Lexer(stream, knownCommands) {
     this.stream = stream;
     this.nextChar();
 
+    // While lexing, we build up many strings one char at a time. Using += for
+    // this can result in lots of garbage strings. It's better to build an
+    // array of single-char strings and then join() them together at the end.
+    // And reusing a single array (i.e. |this.strBuf|) over and over for this
+    // purpose uses less memory than using a new array for each string.
+    this.strBuf = [];
+
     // The PDFs might have "glued" commands with other commands, operands or
     // literals, e.g. "q1". The knownCommands is a dictionary of the valid
     // commands and their prefixes. The prefixes are built the following way:
     // if there a command that is a prefix of the other valid command or
     // literal (e.g. 'f' and 'false') the following prefixes must be included,
     // 'fa', 'fal', 'fals'. The prefixes are not needed, if the command has no
     // other commands or literals as a prefix. The knowCommands is optional.
     this.knownCommands = knownCommands;
@@ -31698,154 +31951,173 @@ var Lexer = (function LexerClosure() {
 
   Lexer.prototype = {
     nextChar: function Lexer_nextChar() {
       return (this.currentChar = this.stream.getByte());
     },
     getNumber: function Lexer_getNumber() {
       var floating = false;
       var ch = this.currentChar;
-      var str = String.fromCharCode(ch);
+      var allDigits = ch >= 0x30 && ch <= 0x39;
+      var strBuf = this.strBuf;
+      strBuf.length = 0;
+      strBuf.push(String.fromCharCode(ch));
       while ((ch = this.nextChar()) >= 0) {
-        if (ch === 0x2E && !floating) { // '.'
-          str += '.';
+        if (ch >= 0x30 && ch <= 0x39) { // '0'-'9'
+          strBuf.push(String.fromCharCode(ch));
+        } else if (ch === 0x2E && !floating) { // '.'
+          strBuf.push('.');
           floating = true;
+          allDigits = false;
         } else if (ch === 0x2D) { // '-'
           // ignore minus signs in the middle of numbers to match
           // Adobe's behavior
           warn('Badly formated number');
-        } else if (ch >= 0x30 && ch <= 0x39) { // '0'-'9'
-          str += String.fromCharCode(ch);
+          allDigits = false;
         } else if (ch === 0x45 || ch === 0x65) { // 'E', 'e'
           floating = true;
+          allDigits = false;
         } else {
           // the last character doesn't belong to us
           break;
         }
       }
-      var value = parseFloat(str);
-      if (isNaN(value))
-        error('Invalid floating point number: ' + value);
+      var value;
+      if (allDigits) {
+        value = 0;
+        var charCodeOfZero = 48;    // '0'
+        for (var i = 0, ii = strBuf.length; i < ii; i++) {
+          value = value * 10 + (strBuf[i].charCodeAt(0) - charCodeOfZero);
+        }
+      } else {
+        value = parseFloat(strBuf.join(''));
+        if (isNaN(value)) {
+          error('Invalid floating point number: ' + value);
+        }
+      }
       return value;
     },
     getString: function Lexer_getString() {
       var numParen = 1;
       var done = false;
-      var str = '';
+      var strBuf = this.strBuf;
+      strBuf.length = 0;
 
       var ch = this.nextChar();
       while (true) {
         var charBuffered = false;
         switch (ch | 0) {
           case -1:
             warn('Unterminated string');
             done = true;
             break;
           case 0x28: // '('
             ++numParen;
-            str += '(';
+            strBuf.push('(');
             break;
           case 0x29: // ')'
             if (--numParen === 0) {
               this.nextChar(); // consume strings ')'
               done = true;
             } else {
-              str += ')';
+              strBuf.push(')');
             }
             break;
           case 0x5C: // '\\'
             ch = this.nextChar();
             switch (ch) {
               case -1:
                 warn('Unterminated string');
                 done = true;
                 break;
               case 0x6E: // 'n'
-                str += '\n';
+                strBuf.push('\n');
                 break;
               case 0x72: // 'r'
-                str += '\r';
+                strBuf.push('\r');
                 break;
               case 0x74: // 't'
-                str += '\t';
+                strBuf.push('\t');
                 break;
               case 0x62: // 'b'
-                str += '\b';
+                strBuf.push('\b');
                 break;
               case 0x66: // 'f'
-                str += '\f';
+                strBuf.push('\f');
                 break;
               case 0x5C: // '\'
               case 0x28: // '('
               case 0x29: // ')'
-                str += String.fromCharCode(ch);
+                strBuf.push(String.fromCharCode(ch));
                 break;
               case 0x30: case 0x31: case 0x32: case 0x33: // '0'-'3'
               case 0x34: case 0x35: case 0x36: case 0x37: // '4'-'7'
                 var x = ch & 0x0F;
                 ch = this.nextChar();
                 charBuffered = true;
                 if (ch >= 0x30 && ch <= 0x37) { // '0'-'7'
                   x = (x << 3) + (ch & 0x0F);
                   ch = this.nextChar();
                   if (ch >= 0x30 && ch <= 0x37) {  // '0'-'7'
                     charBuffered = false;
                     x = (x << 3) + (ch & 0x0F);
                   }
                 }
 
-                str += String.fromCharCode(x);
+                strBuf.push(String.fromCharCode(x));
                 break;
               case 0x0A: case 0x0D: // LF, CR
                 break;
               default:
-                str += String.fromCharCode(ch);
+                strBuf.push(String.fromCharCode(ch));
                 break;
             }
             break;
           default:
-            str += String.fromCharCode(ch);
+            strBuf.push(String.fromCharCode(ch));
             break;
         }
         if (done) {
           break;
         }
         if (!charBuffered) {
           ch = this.nextChar();
         }
       }
-      return str;
+      return strBuf.join('');
     },
     getName: function Lexer_getName() {
-      var str = '', ch;
+      var ch;
+      var strBuf = this.strBuf;
+      strBuf.length = 0;
       while ((ch = this.nextChar()) >= 0 && !specialChars[ch]) {
         if (ch === 0x23) { // '#'
           ch = this.nextChar();
           var x = toHexDigit(ch);
           if (x != -1) {
             var x2 = toHexDigit(this.nextChar());
             if (x2 == -1)
               error('Illegal digit in hex char in name: ' + x2);
-            str += String.fromCharCode((x << 4) | x2);
+            strBuf.push(String.fromCharCode((x << 4) | x2));
           } else {
-            str += '#';
-            str += String.fromCharCode(ch);
-          }
-        } else {
-          str += String.fromCharCode(ch);
-        }
-      }
-      if (str.length > 128) {
+            strBuf.push('#', String.fromCharCode(ch));
+          }
+        } else {
+          strBuf.push(String.fromCharCode(ch));
+        }
+      }
+      if (strBuf.length > 128) {
         error('Warning: name token is longer than allowed by the spec: ' +
-              str.length);
-      }
-      return new Name(str);
+              strBuf.length);
+      }
+      return new Name(strBuf.join(''));
     },
     getHexString: function Lexer_getHexString() {
-      var str = '';
+      var strBuf = this.strBuf;
+      strBuf.length = 0;
       var ch = this.currentChar;
       var isFirstHex = true;
       var firstDigit;
       var secondDigit;
       while (true) {
         if (ch < 0) {
           warn('Unterminated hex string');
           break;
@@ -31865,23 +32137,23 @@ var Lexer = (function LexerClosure() {
             }
           } else {
             secondDigit = toHexDigit(ch);
             if (secondDigit === -1) {
               warn('Ignoring invalid character "' + ch + '" in hex string');
               ch = this.nextChar();
               continue;
             }
-            str += String.fromCharCode((firstDigit << 4) | secondDigit);
+            strBuf.push(String.fromCharCode((firstDigit << 4) | secondDigit));
           }
           isFirstHex = !isFirstHex;
           ch = this.nextChar();
         }
       }
-      return str;
+      return strBuf.join('');
     },
     getObj: function Lexer_getObj() {
       // skip whitespace and comments
       var comment = false;
       var ch = this.currentChar;
       while (true) {
         if (ch < 0) {
           return EOF;
@@ -32060,16 +32332,210 @@ var Linearization = (function Linearizat
     }
   };
 
   return Linearization;
 })();
 
 
 
+var PostScriptParser = (function PostScriptParserClosure() {
+  function PostScriptParser(lexer) {
+    this.lexer = lexer;
+    this.operators = [];
+    this.token = null;
+    this.prev = null;
+  }
+  PostScriptParser.prototype = {
+    nextToken: function PostScriptParser_nextToken() {
+      this.prev = this.token;
+      this.token = this.lexer.getToken();
+    },
+    accept: function PostScriptParser_accept(type) {
+      if (this.token.type == type) {
+        this.nextToken();
+        return true;
+      }
+      return false;
+    },
+    expect: function PostScriptParser_expect(type) {
+      if (this.accept(type))
+        return true;
+      error('Unexpected symbol: found ' + this.token.type + ' expected ' +
+        type + '.');
+    },
+    parse: function PostScriptParser_parse() {
+      this.nextToken();
+      this.expect(PostScriptTokenTypes.LBRACE);
+      this.parseBlock();
+      this.expect(PostScriptTokenTypes.RBRACE);
+      return this.operators;
+    },
+    parseBlock: function PostScriptParser_parseBlock() {
+      while (true) {
+        if (this.accept(PostScriptTokenTypes.NUMBER)) {
+          this.operators.push(this.prev.value);
+        } else if (this.accept(PostScriptTokenTypes.OPERATOR)) {
+          this.operators.push(this.prev.value);
+        } else if (this.accept(PostScriptTokenTypes.LBRACE)) {
+          this.parseCondition();
+        } else {
+          return;
+        }
+      }
+    },
+    parseCondition: function PostScriptParser_parseCondition() {
+      // Add two place holders that will be updated later
+      var conditionLocation = this.operators.length;
+      this.operators.push(null, null);
+
+      this.parseBlock();
+      this.expect(PostScriptTokenTypes.RBRACE);
+      if (this.accept(PostScriptTokenTypes.IF)) {
+        // The true block is right after the 'if' so it just falls through on
+        // true else it jumps and skips the true block.
+        this.operators[conditionLocation] = this.operators.length;
+        this.operators[conditionLocation + 1] = 'jz';
+      } else if (this.accept(PostScriptTokenTypes.LBRACE)) {
+        var jumpLocation = this.operators.length;
+        this.operators.push(null, null);
+        var endOfTrue = this.operators.length;
+        this.parseBlock();
+        this.expect(PostScriptTokenTypes.RBRACE);
+        this.expect(PostScriptTokenTypes.IFELSE);
+        // The jump is added at the end of the true block to skip the false
+        // block.
+        this.operators[jumpLocation] = this.operators.length;
+        this.operators[jumpLocation + 1] = 'j';
+
+        this.operators[conditionLocation] = endOfTrue;
+        this.operators[conditionLocation + 1] = 'jz';
+      } else {
+        error('PS Function: error parsing conditional.');
+      }
+    }
+  };
+  return PostScriptParser;
+})();
+
+var PostScriptTokenTypes = {
+  LBRACE: 0,
+  RBRACE: 1,
+  NUMBER: 2,
+  OPERATOR: 3,
+  IF: 4,
+  IFELSE: 5
+};
+
+var PostScriptToken = (function PostScriptTokenClosure() {
+  function PostScriptToken(type, value) {
+    this.type = type;
+    this.value = value;
+  }
+
+  var opCache = {};
+
+  PostScriptToken.getOperator = function PostScriptToken_getOperator(op) {
+    var opValue = opCache[op];
+    if (opValue)
+      return opValue;
+
+    return opCache[op] = new PostScriptToken(PostScriptTokenTypes.OPERATOR, op);
+  };
+
+  PostScriptToken.LBRACE = new PostScriptToken(PostScriptTokenTypes.LBRACE,
+    '{');
+  PostScriptToken.RBRACE = new PostScriptToken(PostScriptTokenTypes.RBRACE,
+    '}');
+  PostScriptToken.IF = new PostScriptToken(PostScriptTokenTypes.IF, 'IF');
+  PostScriptToken.IFELSE = new PostScriptToken(PostScriptTokenTypes.IFELSE,
+    'IFELSE');
+  return PostScriptToken;
+})();
+
+var PostScriptLexer = (function PostScriptLexerClosure() {
+  function PostScriptLexer(stream) {
+    this.stream = stream;
+    this.nextChar();
+  }
+  PostScriptLexer.prototype = {
+    nextChar: function PostScriptLexer_nextChar() {
+      return (this.currentChar = this.stream.getByte());
+    },
+    getToken: function PostScriptLexer_getToken() {
+      var s = '';
+      var comment = false;
+      var ch = this.currentChar;
+
+      // skip comments
+      while (true) {
+        if (ch < 0) {
+          return EOF;
+        }
+
+        if (comment) {
+          if (ch === 0x0A || ch === 0x0D) {
+            comment = false;
+          }
+        } else if (ch == 0x25) { // '%'
+          comment = true;
+        } else if (!Lexer.isSpace(ch)) {
+          break;
+        }
+        ch = this.nextChar();
+      }
+      switch (ch | 0) {
+        case 0x30: case 0x31: case 0x32: case 0x33: case 0x34: // '0'-'4'
+        case 0x35: case 0x36: case 0x37: case 0x38: case 0x39: // '5'-'9'
+        case 0x2B: case 0x2D: case 0x2E: // '+', '-', '.'
+        return new PostScriptToken(PostScriptTokenTypes.NUMBER,
+          this.getNumber());
+        case 0x7B: // '{'
+          this.nextChar();
+          return PostScriptToken.LBRACE;
+        case 0x7D: // '}'
+          this.nextChar();
+          return PostScriptToken.RBRACE;
+      }
+      // operator
+      var str = String.fromCharCode(ch);
+      while ((ch = this.nextChar()) >= 0 && // and 'A'-'Z', 'a'-'z'
+        ((ch >= 0x41 && ch <= 0x5A) || (ch >= 0x61 && ch <= 0x7A))) {
+        str += String.fromCharCode(ch);
+      }
+      switch (str.toLowerCase()) {
+        case 'if':
+          return PostScriptToken.IF;
+        case 'ifelse':
+          return PostScriptToken.IFELSE;
+        default:
+          return PostScriptToken.getOperator(str);
+      }
+    },
+    getNumber: function PostScriptLexer_getNumber() {
+      var ch = this.currentChar;
+      var str = String.fromCharCode(ch);
+      while ((ch = this.nextChar()) >= 0) {
+        if ((ch >= 0x30 && ch <= 0x39) || // '0'-'9'
+          ch === 0x2D || ch === 0x2E) { // '-', '.'
+          str += String.fromCharCode(ch);
+        } else {
+          break;
+        }
+      }
+      var value = parseFloat(str);
+      if (isNaN(value))
+        error('Invalid floating point number: ' + value);
+      return value;
+    }
+  };
+  return PostScriptLexer;
+})();
+
+
 var Stream = (function StreamClosure() {
   function Stream(arrayBuffer, start, length, dict) {
     this.bytes = arrayBuffer instanceof Uint8Array ? arrayBuffer :
       new Uint8Array(arrayBuffer);
     this.start = start || 0;
     this.pos = this.start;
     this.end = (start + length) || this.bytes.length;
     this.dict = dict;
@@ -34592,17 +35058,21 @@ var WorkerMessageHandler = PDFJS.WorkerM
       };
 
       PDFJS.maxImageSize = data.maxImageSize === undefined ?
                            -1 : data.maxImageSize;
       PDFJS.disableFontFace = data.disableFontFace;
       PDFJS.disableCreateObjectURL = data.disableCreateObjectURL;
       PDFJS.verbosity = data.verbosity;
 
-      getPdfManager(data).then(function pdfManagerReady() {
+      getPdfManager(data).then(function () {
+        pdfManager.onLoadedStream().then(function(stream) {
+          handler.send('DataLoaded', { length: stream.bytes.byteLength });
+        });
+      }).then(function pdfManagerReady() {
         loadDocument(false).then(onSuccess, function loadFailure(ex) {
           // Try again with recoveryMode == true
           if (!(ex instanceof XRefParseException)) {
             if (ex instanceof PasswordException) {
               // after password exception prepare to receive a new password
               // to repeat loading
               pdfManager.passwordChangedPromise =
                 new LegacyPromise();
@@ -34659,22 +35129,16 @@ var WorkerMessageHandler = PDFJS.WorkerM
 
     handler.on('GetData', function wphSetupGetData(data, deferred) {
       pdfManager.requestLoadedStream();
       pdfManager.onLoadedStream().then(function(stream) {
         deferred.resolve(stream.bytes);
       });
     });
 
-    handler.on('DataLoaded', function wphSetupDataLoaded(data, deferred) {
-      pdfManager.onLoadedStream().then(function(stream) {
-        deferred.resolve({ length: stream.bytes.byteLength });
-      });
-    });
-
     handler.on('UpdatePassword', function wphSetupUpdatePassword(data) {
       pdfManager.updatePassword(data);
     });
 
     handler.on('GetAnnotationsRequest', function wphSetupGetAnnotations(data) {
       pdfManager.getPage(data.pageIndex).then(function(page) {
         pdfManager.ensure(page, 'getAnnotationsData', []).then(
           function(annotationsData) {
--- a/browser/extensions/pdfjs/content/network.js
+++ b/browser/extensions/pdfjs/content/network.js
@@ -38,17 +38,18 @@
 var NetworkManager = (function NetworkManagerClosure() {
 
   var OK_RESPONSE = 200;
   var PARTIAL_CONTENT_RESPONSE = 206;
 
   function NetworkManager(url, args) {
     this.url = url;
     args = args || {};
-    this.httpHeaders = args.httpHeaders || {};
+    this.isHttp = /^https?:/i.test(url);
+    this.httpHeaders = (this.isHttp && args.httpHeaders) || {};
     this.withCredentials = args.withCredentials || false;
     this.getXhr = args.getXhr ||
       function NetworkManager_getXhr() {
         return new XMLHttpRequest();
       };
 
     this.currXhrId = 0;
     this.pendingRequests = {};
@@ -96,17 +97,17 @@ var NetworkManager = (function NetworkMa
       xhr.withCredentials = this.withCredentials;
       for (var property in this.httpHeaders) {
         var value = this.httpHeaders[property];
         if (typeof value === 'undefined') {
           continue;
         }
         xhr.setRequestHeader(property, value);
       }
-      if ('begin' in args && 'end' in args) {
+      if (this.isHttp && 'begin' in args && 'end' in args) {
         var rangeStr = args.begin + '-' + (args.end - 1);
         xhr.setRequestHeader('Range', 'bytes=' + rangeStr);
         pendingRequest.expectedStatus = 206;
       } else {
         pendingRequest.expectedStatus = 200;
       }
 
       xhr.mozResponseType = xhr.responseType = 'arraybuffer';
@@ -151,17 +152,17 @@ var NetworkManager = (function NetworkMa
         // The XHR request might have been aborted in onHeadersReceived()
         // callback, in which case we should abort request
         return;
       }
 
       delete this.pendingRequests[xhrId];
 
       // success status == 0 can be on ftp, file and other protocols
-      if (xhr.status === 0 && /^https?:/i.test(this.url)) {
+      if (xhr.status === 0 && this.isHttp) {
         if (pendingRequest.onError) {
           pendingRequest.onError(xhr.status);
         }
         return;
       }
       var xhrStatus = xhr.status || OK_RESPONSE;
 
       // From http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.2:
--- a/browser/extensions/pdfjs/content/web/viewer.css
+++ b/browser/extensions/pdfjs/content/web/viewer.css
@@ -30,16 +30,17 @@ body {
   background-image: url(images/texture.png);
 }
 
 body,
 input,
 button,
 select {
   font: message-box;
+  outline: none;
 }
 
 .hidden {
   display: none !important;
 }
 [hidden] {
   display: none !important;
 }
@@ -834,16 +835,17 @@ html[dir="rtl"] .secondaryToolbarButton.
 .secondaryToolbarButton.download::before {
   content: url(images/toolbarButton-download.png);
 }
 
 .toolbarButton.bookmark,
 .secondaryToolbarButton.bookmark {
   -moz-box-sizing: border-box;
   box-sizing: border-box;
+  outline: none;
   padding-top: 4px;
   text-decoration: none;
 }
 .secondaryToolbarButton.bookmark {
   padding-top: 5px;
 }
 
 .bookmark[href='#'] {
@@ -1481,38 +1483,39 @@ html[dir='rtl'] #documentPropertiesConta
   color: black;
 }
 
 #viewer.textLayer-shadow .textLayer > div {
   background-color: rgba(255,255,255, .6);
   color: black;
 }
 
-.grab-to-pan-grab * {
+.grab-to-pan-grab {
   cursor: url("images/grab.cur"), move !important;
   cursor: -moz-grab !important;
   cursor: grab !important;
 }
-.grab-to-pan-grabbing,
-.grab-to-pan-grabbing * {
+.grab-to-pan-grab *:not(input):not(textarea):not(button):not(select):not(:link) {
+  cursor: inherit !important;
+}
+.grab-to-pan-grab:active,
+.grab-to-pan-grabbing {
   cursor: url("images/grabbing.cur"), move !important;
   cursor: -moz-grabbing !important;
   cursor: grabbing !important;
-}
-.grab-to-pan-grab input,
-.grab-to-pan-grab textarea,
-.grab-to-pan-grab button,
-.grab-to-pan-grab button *,
-.grab-to-pan-grab select,
-.grab-to-pan-grab option {
-  cursor: auto !important;
-}
-.grab-to-pan-grab a[href],
-.grab-to-pan-grab a[href] * {
-  cursor: pointer !important;
+
+  position: fixed;
+  background: transparent;
+  display: block;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  overflow: hidden;
+  z-index: 50000; /* should be higher than anything else in PDF.js! */
 }
 
 @page {
   margin: 0;
 }
 
 #printContainer {
   display: none;
--- a/browser/extensions/pdfjs/content/web/viewer.js
+++ b/browser/extensions/pdfjs/content/web/viewer.js
@@ -12,17 +12,17 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 /* globals PDFJS, PDFBug, FirefoxCom, Stats, Cache, PDFFindBar, CustomStyle,
            PDFFindController, ProgressBar, TextLayerBuilder, DownloadManager,
            getFileName, scrollIntoView, getPDFFileNameFromURL, PDFHistory,
-           Preferences, ViewHistory, PageView, ThumbnailView,
+           Preferences, ViewHistory, PageView, ThumbnailView, URL,
            noContextMenuHandler, SecondaryToolbar, PasswordPrompt,
            PresentationMode, HandTool, Promise, DocumentProperties */
 
 'use strict';
 
 var DEFAULT_URL = 'compressed.tracemonkey-pldi-09.pdf';
 var DEFAULT_SCALE = 'auto';
 var DEFAULT_SCALE_DELTA = 1.1;
@@ -766,31 +766,29 @@ var PDFFindController = {
   // Where find algorithm currently is in the document.
   offset: {
     pageIdx: null,
     matchIdx: null
   },
 
   resumePageIdx: null,
 
-  resumeCallback: null,
-
   state: null,
 
   dirtyMatch: false,
 
   findTimeout: null,
 
   pdfPageSource: null,
 
   integratedFind: false,
 
   initialize: function(options) {
     if(typeof PDFFindBar === 'undefined' || PDFFindBar === null) {
-        throw 'PDFFindController cannot be initialized ' +
+      throw 'PDFFindController cannot be initialized ' +
             'without a PDFFindController instance';
     }
 
     this.pdfPageSource = options.pdfPageSource;
     this.integratedFind = options.integratedFind;
 
     var events = [
       'find',
@@ -840,20 +838,18 @@ var PDFFindController = {
         break;
       }
 
       matches.push(matchIdx);
     }
     this.pageMatches[pageIndex] = matches;
     this.updatePage(pageIndex);
     if (this.resumePageIdx === pageIndex) {
-      var callback = this.resumeCallback;
       this.resumePageIdx = null;
-      this.resumeCallback = null;
-      callback();
+      this.nextPageMatch();
     }
   },
 
   extractText: function() {
     if (this.startedTextExtraction) {
       return;
     }
     this.startedTextExtraction = true;
@@ -932,17 +928,16 @@ var PDFFindController = {
 
     if (this.dirtyMatch) {
       // Need to recalculate the matches, reset everything.
       this.dirtyMatch = false;
       this.selected.pageIdx = this.selected.matchIdx = -1;
       this.offset.pageIdx = currentPageIndex;
       this.offset.matchIdx = null;
       this.hadMatch = false;
-      this.resumeCallback = null;
       this.resumePageIdx = null;
       this.pageMatches = [];
       var self = this;
 
       for (var i = 0; i < numPages; i++) {
         // Wipe out any previous highlighted matches.
         this.updatePage(i);
 
@@ -959,17 +954,17 @@ var PDFFindController = {
 
     // If there's no query there's no point in searching.
     if (this.state.query === '') {
       this.updateUIState(FindStates.FIND_FOUND);
       return;
     }
 
     // If we're waiting on a page, we return since we can't do anything else.
-    if (this.resumeCallback) {
+    if (this.resumePageIdx) {
       return;
     }
 
     var offset = this.offset;
     // If there's already a matchIdx that means we are iterating through a
     // page's matches.
     if (offset.matchIdx !== null) {
       var numPageMatches = this.pageMatches[offset.pageIdx].length;
@@ -985,58 +980,59 @@ var PDFFindController = {
       // We went beyond the current page's matches, so we advance to the next
       // page.
       this.advanceOffsetPage(previous);
     }
     // Start searching through the page.
     this.nextPageMatch();
   },
 
+  matchesReady: function(matches) {
+    var offset = this.offset;
+    var numMatches = matches.length;
+    var previous = this.state.findPrevious;
+    if (numMatches) {
+      // There were matches for the page, so initialize the matchIdx.
+      this.hadMatch = true;
+      offset.matchIdx = previous ? numMatches - 1 : 0;
+      this.updateMatch(true);
+      // matches were found
+      return true;
+    } else {
+      // No matches attempt to search the next page.
+      this.advanceOffsetPage(previous);
+      if (offset.wrapped) {
+        offset.matchIdx = null;
+        if (!this.hadMatch) {
+          // No point in wrapping there were no matches.
+          this.updateMatch(false);
+          // while matches were not found, searching for a page 
+          // with matches should nevertheless halt.
+          return true;
+        }
+      }
+      // matches were not found (and searching is not done)
+      return false;
+    }
+  },
+
   nextPageMatch: function() {
-    if (this.resumePageIdx !== null)
+    if (this.resumePageIdx !== null) {
       console.error('There can only be one pending page.');
-
-    var matchesReady = function(matches) {
-      var offset = this.offset;
-      var numMatches = matches.length;
-      var previous = this.state.findPrevious;
-      if (numMatches) {
-        // There were matches for the page, so initialize the matchIdx.
-        this.hadMatch = true;
-        offset.matchIdx = previous ? numMatches - 1 : 0;
-        this.updateMatch(true);
-      } else {
-        // No matches attempt to search the next page.
-        this.advanceOffsetPage(previous);
-        if (offset.wrapped) {
-          offset.matchIdx = null;
-          if (!this.hadMatch) {
-            // No point in wrapping there were no matches.
-            this.updateMatch(false);
-            return;
-          }
-        }
-        // Search the next page.
-        this.nextPageMatch();
+    }
+    do {
+      var pageIdx = this.offset.pageIdx;
+      var matches = this.pageMatches[pageIdx];
+      if (!matches) {
+        // The matches don't exist yet for processing by "matchesReady",
+        // so set a resume point for when they do exist.
+        this.resumePageIdx = pageIdx;
+        break;
       }
-    }.bind(this);
-
-    var pageIdx = this.offset.pageIdx;
-    var pageMatches = this.pageMatches;
-    if (!pageMatches[pageIdx]) {
-      // The matches aren't ready setup a callback so we can be notified,
-      // when they are ready.
-      this.resumeCallback = function() {
-        matchesReady(pageMatches[pageIdx]);
-      };
-      this.resumePageIdx = pageIdx;
-      return;
-    }
-    // The matches are finished already.
-    matchesReady(pageMatches[pageIdx]);
+    } while (!this.matchesReady(matches));
   },
 
   advanceOffsetPage: function(previous) {
     var offset = this.offset;
     var numPages = this.extractTextPromises.length;
     offset.pageIdx = previous ? offset.pageIdx - 1 : offset.pageIdx + 1;
     offset.matchIdx = null;
     if (offset.pageIdx >= numPages || offset.pageIdx < 0) {
@@ -1904,26 +1900,27 @@ var GrabToPan = (function GrabToPanClosu
     // Bind the contexts to ensure that `this` always points to
     // the GrabToPan instance.
     this.activate = this.activate.bind(this);
     this.deactivate = this.deactivate.bind(this);
     this.toggle = this.toggle.bind(this);
     this._onmousedown = this._onmousedown.bind(this);
     this._onmousemove = this._onmousemove.bind(this);
     this._endPan = this._endPan.bind(this);
+
+    // This overlay will be inserted in the document when the mouse moves during
+    // a grab operation, to ensure that the cursor has the desired appearance.
+    var overlay = this.overlay = document.createElement('div');
+    overlay.className = 'grab-to-pan-grabbing';
   }
   GrabToPan.prototype = {
     /**
      * Class name of element which can be grabbed
      */
     CSS_CLASS_GRAB: 'grab-to-pan-grab',
-    /**
-     * Class name of element which is being dragged & panned
-     */
-    CSS_CLASS_GRABBING: 'grab-to-pan-grabbing',
 
     /**
      * Bind a mousedown event to the element to enable grab-detection.
      */
     activate: function GrabToPan_activate() {
       if (!this.active) {
         this.active = true;
         this.element.addEventListener('mousedown', this._onmousedown, true);
@@ -1996,44 +1993,47 @@ var GrabToPan = (function GrabToPanClosu
       this.document.addEventListener('mousemove', this._onmousemove, true);
       this.document.addEventListener('mouseup', this._endPan, true);
       // When a scroll event occurs before a mousemove, assume that the user
       // dragged a scrollbar (necessary for Opera Presto, Safari and IE)
       // (not needed for Chrome/Firefox)
       this.element.addEventListener('scroll', this._endPan, true);
       event.preventDefault();
       event.stopPropagation();
-      this.element.classList.remove(this.CSS_CLASS_GRAB);
       this.document.documentElement.classList.add(this.CSS_CLASS_GRABBING);
     },
 
     /**
      * @private
      */
     _onmousemove: function GrabToPan__onmousemove(event) {
       this.element.removeEventListener('scroll', this._endPan, true);
       if (isLeftMouseReleased(event)) {
-        this.document.removeEventListener('mousemove', this._onmousemove, true);
+        this._endPan();
         return;
       }
       var xDiff = event.clientX - this.clientXStart;
       var yDiff = event.clientY - this.clientYStart;
       this.element.scrollTop = this.scrollTopStart - yDiff;
       this.element.scrollLeft = this.scrollLeftStart - xDiff;
+      if (!this.overlay.parentNode) {
+        document.body.appendChild(this.overlay);
+      }
     },
 
     /**
      * @private
      */
     _endPan: function GrabToPan__endPan() {
       this.element.removeEventListener('scroll', this._endPan, true);
       this.document.removeEventListener('mousemove', this._onmousemove, true);
       this.document.removeEventListener('mouseup', this._endPan, true);
-      this.document.documentElement.classList.remove(this.CSS_CLASS_GRABBING);
-      this.element.classList.add(this.CSS_CLASS_GRAB);
+      if (this.overlay.parentNode) {
+        this.overlay.parentNode.removeChild(this.overlay);
+      }
     }
   };
 
   // Get the correct (vendor-prefixed) name of the matches method.
   var matchesSelector;
   ['webkitM', 'mozM', 'msM', 'oM', 'm'].some(function(prefix) {
     var name = prefix + 'atches';
     if (name in document.documentElement) {
@@ -2181,17 +2181,17 @@ var DocumentProperties = {
 
   getProperties: function documentPropertiesGetProperties() {
     var self = this;
 
     // Get the file name.
     this.fileName = getPDFFileNameFromURL(PDFView.url);
 
     // Get the file size.
-    PDFView.pdfDocument.dataLoaded().then(function(data) {
+    PDFView.pdfDocument.getDownloadInfo().then(function(data) {
       self.setFileSize(data.length);
     });
 
     // Get the other document properties.
     PDFView.pdfDocument.getMetadata().then(function(data) {
       var fields = [
         { field: self.fileNameField, content: self.fileName },
         { field: self.fileSizeField, content: self.fileSize },
@@ -2676,16 +2676,21 @@ var PDFView = {
 
       requestDataRange: function PdfDataRangeTransport_requestDataRange(
                                   begin, end) {
         FirefoxCom.request('requestDataRange', { begin: begin, end: end });
       }
     };
 
     window.addEventListener('message', function windowMessage(e) {
+      if (e.source !== null) {
+        // The message MUST originate from Chrome code.
+        console.warn('Rejected untrusted message from ' + e.origin);
+        return;
+      }
       var args = e.data;
 
       if (typeof args !== 'object' || !('pdfjsLoadAction' in args))
         return;
       switch (args.pdfjsLoadAction) {
         case 'supportsRangedLoading':
           PDFView.open(args.pdfUrl, 0, undefined, pdfDataRangeTransport, {
             length: args.length,
@@ -2984,17 +2989,17 @@ var PDFView = {
 
     PDFFindController.reset();
 
     this.pdfDocument = pdfDocument;
 
     var errorWrapper = document.getElementById('errorWrapper');
     errorWrapper.setAttribute('hidden', 'true');
 
-    pdfDocument.dataLoaded().then(function() {
+    pdfDocument.getDownloadInfo().then(function() {
       PDFView.loadingBar.hide();
       var outerContainer = document.getElementById('outerContainer');
       outerContainer.classList.remove('loadingInProgress');
     });
 
     var thumbsView = document.getElementById('thumbnailView');
     thumbsView.parentNode.scrollTop = 0;
 
@@ -5176,40 +5181,16 @@ window.addEventListener('resize', functi
 });
 
 window.addEventListener('hashchange', function webViewerHashchange(evt) {
   if (PDFHistory.isHashChangeUnlocked) {
     PDFView.setHash(document.location.hash.substring(1));
   }
 });
 
-window.addEventListener('change', function webViewerChange(evt) {
-  var files = evt.target.files;
-  if (!files || files.length === 0)
-    return;
-
-  // Read the local file into a Uint8Array.
-  var fileReader = new FileReader();
-  fileReader.onload = function webViewerChangeFileReaderOnload(evt) {
-    var buffer = evt.target.result;
-    var uint8Array = new Uint8Array(buffer);
-    PDFView.open(uint8Array, 0);
-  };
-
-  var file = files[0];
-  fileReader.readAsArrayBuffer(file);
-  PDFView.setTitleUsingUrl(file.name);
-
-  // URL does not reflect proper document location - hiding some icons.
-  document.getElementById('viewBookmark').setAttribute('hidden', 'true');
-  document.getElementById('secondaryViewBookmark').
-    setAttribute('hidden', 'true');
-  document.getElementById('download').setAttribute('hidden', 'true');
-  document.getElementById('secondaryDownload').setAttribute('hidden', 'true');
-}, true);
 
 function selectScaleOption(value) {
   var options = document.getElementById('scaleSelect').options;
   var predefinedValueFound = false;
   for (var i = 0; i < options.length; i++) {
     var option = options[i];
     if (option.value != value) {
       option.selected = false;
@@ -5289,29 +5270,32 @@ window.addEventListener('pagechange', fu
         scrollIntoView(thumbnail, { top: THUMBNAIL_SCROLL_MARGIN });
       }
     }
   }
   document.getElementById('previous').disabled = (page <= 1);
   document.getElementById('next').disabled = (page >= PDFView.pages.length);
 }, true);
 
-// Firefox specific event, so that we can prevent browser from zooming
-window.addEventListener('DOMMouseScroll', function(evt) {
-  if (evt.ctrlKey) {
+function handleMouseWheel(evt) {
+  var MOUSE_WHEEL_DELTA_FACTOR = 40;
+  var ticks = (evt.type === 'DOMMouseScroll') ? -evt.detail :
+              evt.wheelDelta / MOUSE_WHEEL_DELTA_FACTOR;
+  var direction = (ticks < 0) ? 'zoomOut' : 'zoomIn';
+
+  if (evt.ctrlKey) { // Only zoom the pages, not the entire viewer
     evt.preventDefault();
-
-    var ticks = evt.detail;
-    var direction = (ticks > 0) ? 'zoomOut' : 'zoomIn';
     PDFView[direction](Math.abs(ticks));
   } else if (PresentationMode.active) {
-    var FIREFOX_DELTA_FACTOR = -40;
-    PDFView.mouseScroll(evt.detail * FIREFOX_DELTA_FACTOR);
+    PDFView.mouseScroll(ticks * MOUSE_WHEEL_DELTA_FACTOR);
   }
-}, false);
+}
+
+window.addEventListener('DOMMouseScroll', handleMouseWheel);
+window.addEventListener('mousewheel', handleMouseWheel);
 
 window.addEventListener('click', function click(evt) {
   if (!PresentationMode.active) {
     if (SecondaryToolbar.opened && PDFView.container.contains(evt.target)) {
       SecondaryToolbar.close();
     }
   } else if (evt.button === 0) {
     // Necessary since preventDefault() in 'mousedown' won't stop
--- a/browser/locales/en-US/chrome/browser/aboutPrivateBrowsing.dtd
+++ b/browser/locales/en-US/chrome/browser/aboutPrivateBrowsing.dtd
@@ -8,14 +8,14 @@
 <!ENTITY privatebrowsingpage.perwindow.issueDesc        "&brandShortName; won't remember any history for this window.">
 <!ENTITY privatebrowsingpage.perwindow.issueDesc.normal "You are not currently in a private window.">
 
 <!ENTITY privatebrowsingpage.perwindow.description     "In a Private Browsing window, &brandShortName; won't keep any browser history, search history, download history, web form history, cookies, or temporary internet files.  However, files you download and bookmarks you make will be kept.">
 
 <!ENTITY privatebrowsingpage.openPrivateWindow.label "Open a Private Window">
 <!ENTITY privatebrowsingpage.openPrivateWindow.accesskey "P">
 
-<!-- LOCALIZATION NOTE (privatebrowsingpage.howToStart3): please leave &basePBMenu.label; intact in the translation -->
-<!ENTITY privatebrowsingpage.howToStart3               "To start Private Browsing, you can also select &basePBMenu.label; &gt; &newPrivateWindow.label;.">
+<!-- LOCALIZATION NOTE (privatebrowsingpage.howToStart4): please leave &newPrivateWindow.label; intact in the translation -->
+<!ENTITY privatebrowsingpage.howToStart4               "To start Private Browsing, you can also select &newPrivateWindow.label; from the menu.">
 <!ENTITY privatebrowsingpage.howToStop3                "To stop Private Browsing, you can close this window.">
 
 <!ENTITY privatebrowsingpage.moreInfo                  "While this computer won't have a record of your browsing history, your internet service provider or employer can still track the pages you visit.">
 <!ENTITY privatebrowsingpage.learnMore                 "Learn More">
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -521,14 +521,14 @@ mixedContentBlocked.unblock.accesskey = 
 # LOCALIZATION NOTE - %S is brandShortName
 slowStartup.message = %S seems slow… to… start.
 slowStartup.helpButton.label = Learn How to Speed It Up
 slowStartup.helpButton.accesskey = L
 slowStartup.disableNotificationButton.label = Don't Tell Me Again
 slowStartup.disableNotificationButton.accesskey = A
 
 
-# LOCALIZATION NOTE(tipSection.tip0): %1$S will be replaced with the text defined
-# in tipSection.tip0.hint, %2$S will be replaced with brandShortName, %3$S will
-# be replaced with a hyperlink containing the text defined in tipSection.tip0.learnMore.
-tipSection.tip0 = %1$S: You can customize %2$S to work the way you do. Simply drag any of the above to the menu or toolbar. %3$S about customizing %2$S.
-tipSection.tip0.hint = Hint
-tipSection.tip0.learnMore = Learn more
+# LOCALIZATION NOTE(customizeTips.tip0): %1$S will be replaced with the text defined
+# in customizeTips.tip0.hint, %2$S will be replaced with brandShortName, %3$S will
+# be replaced with a hyperlink containing the text defined in customizeTips.tip0.learnMore.
+customizeTips.tip0 = %1$S: You can customize %2$S to work the way you do. Simply drag any of the above to the menu or toolbar. %3$S about customizing %2$S.
+customizeTips.tip0.hint = Hint
+customizeTips.tip0.learnMore = Learn more
--- a/browser/locales/en-US/chrome/browser/devtools/debugger.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/debugger.properties
@@ -209,16 +209,20 @@ loadingText=Loading\u2026
 # LOCALIZATION NOTE (errorLoadingText): The text that is displayed in the debugger
 # viewer when there is an error loading a file
 errorLoadingText=Error loading source:\n
 
 # LOCALIZATION NOTE (addWatchExpressionText): The text that is displayed in the
 # watch expressions list to add a new item.
 addWatchExpressionText=Add watch expression
 
+# LOCALIZATION NOTE (addWatchExpressionButton): The button that is displayed in the
+# variables view popup.
+addWatchExpressionButton=Watch
+
 # LOCALIZATION NOTE (emptyVariablesText): The text that is displayed in the
 # variables pane when there are no variables to display.
 emptyVariablesText=No variables to display
 
 # LOCALIZATION NOTE (scopeLabel): The text that is displayed in the variables
 # pane as a header for each variable scope (e.g. "Global scope, "With scope",
 # etc.).
 scopeLabel=%S scope
--- a/browser/metro/base/content/bindings/grid.xml
+++ b/browser/metro/base/content/bindings/grid.xml
@@ -25,16 +25,27 @@
       <property name="isArranging" readonly="true" onget="return !!this._scheduledArrangeItemsTimerId"/>
 
       <field name="controller">null</field>
 
       <!-- collection of child items excluding empty tiles -->
       <property name="items" readonly="true" onget="return this.querySelectorAll('richgriditem[value]');"/>
       <property name="itemCount" readonly="true" onget="return this.items.length;"/>
 
+      <method name="isItem">
+        <parameter name="anItem"/>
+        <body>
+          <![CDATA[
+            // only non-empty child nodes are considered items
+            return anItem && anItem.hasAttribute("value") &&
+                   anItem.parentNode == this;
+          ]]>
+        </body>
+      </method>
+
       <!-- nsIDOMXULMultiSelectControlElement (not fully implemented) -->
 
       <method name="clearSelection">
         <body>
           <![CDATA[
             // 'selection' and 'selected' are confusingly overloaded here
             // as richgrid is adopting multi-select behavior, but select/selected are already being
             // used to describe triggering the default action of a tile
@@ -49,16 +60,19 @@
           ]]>
         </body>
       </method>
 
       <method name="toggleItemSelection">
         <parameter name="anItem"/>
         <body>
           <![CDATA[
+            if (!this.isItem(anItem))
+              return;
+
             let wasSelected = anItem.selected;
             if ("single" == this.getAttribute("seltype")) {
               this.clearSelection();
             }
             this._selectedItem = wasSelected ? null : anItem;
             if (wasSelected)
               anItem.removeAttribute("selected");
             else
@@ -67,16 +81,18 @@
           ]]>
         </body>
       </method>
 
       <method name="selectItem">
         <parameter name="anItem"/>
         <body>
           <![CDATA[
+            if (!this.isItem(anItem))
+              return;
             let wasSelected = anItem.selected,
                 isSingleMode = ("single" == this.getAttribute("seltype"));
             if (isSingleMode) {
               this.clearSelection();
             }
             this._selectedItem = anItem;
             if (wasSelected) {
               return;
@@ -103,17 +119,17 @@
         </body>
       </method>
 
       <method name="handleItemClick">
         <parameter name="aItem"/>
         <parameter name="aEvent"/>
         <body>
           <![CDATA[
-            if (!this.isBound)
+            if (!(this.isBound && this.isItem(aItem)))
               return;
 
             if ("single" == this.getAttribute("seltype")) {
               // we'll republish this as a selectionchange event on the grid
               aEvent.stopPropagation();
               this.selectItem(aItem);
             }
 
@@ -123,17 +139,17 @@
         </body>
       </method>
 
       <method name="handleItemContextMenu">
         <parameter name="aItem"/>
         <parameter name="aEvent"/>
         <body>
           <![CDATA[
-            if (!this.isBound || this.noContext)
+            if (!this.isBound || this.noContext || !this.isItem(aItem))
               return;
             // we'll republish this as a selectionchange event on the grid
             aEvent.stopPropagation();
             this.toggleItemSelection(aItem);
           ]]>
         </body>
       </method>
 
@@ -359,17 +375,17 @@
         </body>
       </method>
 
       <method name="removeItem">
         <parameter name="aItem"/>
         <parameter name="aSkipArrange"/>
         <body>
           <![CDATA[
-            if (!aItem || Array.indexOf(this.items, aItem) < 0)
+            if (!this.isItem(aItem))
               return null;
 
             let removal = this.removeChild(aItem);
             // replace the slot if necessary
             if (this.childElementCount < this.minSlots) {
               this.nextSlot();
             }
 
@@ -382,17 +398,17 @@
           ]]>
         </body>
       </method>
 
       <method name="getIndexOfItem">
         <parameter name="anItem"/>
         <body>
           <![CDATA[
-            if (!anItem)
+            if (!this.isItem(anItem))
               return -1;
 
             return Array.indexOf(this.items, anItem);
           ]]>
         </body>
       </method>
 
       <method name="getItemAtIndex">
@@ -786,18 +802,18 @@
         </body>
       </method>
 
       <method name="bendItem">
         <parameter name="aItem"/>
         <parameter name="aEvent"/>
         <body><![CDATA[
           // apply the transform to the contentBox element of the item
-          let bendNode = 'richgriditem' == aItem.nodeName && aItem._contentBox;
-          if (!bendNode)
+          let bendNode = this.isItem(aItem) ? aItem._contentBox : null;
+          if (!bendNode || aItem.hasAttribute("bending"))
             return;
 
           let event = aEvent;
           let rect = bendNode.getBoundingClientRect();
           let angle;
           let x = (event.clientX - rect.left) / rect.width;
           let y = (event.clientY - rect.top) / rect.height;
           let perspective = '450px';
@@ -851,16 +867,17 @@
     </implementation>
     <handlers>
       <!--  item bend effect handlers -->
       <handler event="mousedown" button="0" phase="capturing" action="this.bendItem(event.target, event)"/>
       <handler event="touchstart" action="this.bendItem(event.target, event.touches[0])"/>
       <handler event="mouseup" button="0" action="this.unbendItem(event.target)"/>
       <handler event="mouseout" button="0" action="this.unbendItem(event.target)"/>
       <handler event="touchend" action="this.unbendItem(event.target)"/>
+      <handler event="touchcancel" action="this.unbendItem(event.target)"/>
       <!--  /item bend effect handler -->
 
       <handler event="context-action">
         <![CDATA[
           // context-action is an event fired by the appbar typically
           // which directs us to do something to the selected tiles
           switch (event.action) {
             case "clear":
--- a/browser/metro/base/content/browser-scripts.js
+++ b/browser/metro/base/content/browser-scripts.js
@@ -51,16 +51,22 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
                                   "resource://gre/modules/UITelemetry.jsm");
+
+#ifdef MOZ_UPDATER
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+                                  "resource://gre/modules/AddonManager.jsm");
+#endif
+
 /*
  * Services
  */
 
 XPCOMUtils.defineLazyServiceGetter(this, "StyleSheetSvc",
                                    "@mozilla.org/content/style-sheet-service;1",
                                    "nsIStyleSheetService");
 XPCOMUtils.defineLazyServiceGetter(window, "gHistSvc",
--- a/browser/metro/base/content/flyoutpanels/AboutFlyoutPanel.js
+++ b/browser/metro/base/content/flyoutpanels/AboutFlyoutPanel.js
@@ -69,20 +69,16 @@ let AboutFlyoutPanel = {
         onUnload();
 #endif
         break;
     }
   }
 };
 
 #ifdef MOZ_UPDATER
-Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
-Components.utils.import("resource://gre/modules/DownloadUtils.jsm");
-Components.utils.import("resource://gre/modules/AddonManager.jsm");
-
 function onUnload(aEvent) {
   if (!gAppUpdater) {
     return;
   }
 
   if (gAppUpdater.isChecking)
     gAppUpdater.checker.stopChecking(Components.interfaces.nsIUpdateChecker.CURRENT_CHECK);
   // Safe to call even when there isn't a download in progress.
--- a/browser/metro/base/content/helperui/SelectionHelperUI.js
+++ b/browser/metro/base/content/helperui/SelectionHelperUI.js
@@ -611,17 +611,16 @@ var SelectionHelperUI = {
     window.addEventListener("touchstart", this, false);
 
     Elements.browsers.addEventListener("URLChanged", this, true);
     Elements.browsers.addEventListener("SizeChanged", this, true);
 
     Elements.tabList.addEventListener("TabSelect", this, true);
 
     Elements.navbar.addEventListener("transitionend", this, true);
-    Elements.navbar.addEventListener("MozAppbarDismissing", this, true);
 
     this.overlay.enabled = true;
   },
 
   _shutdown: function _shutdown() {
     messageManager.removeMessageListener("Content:SelectionRange", this);
     messageManager.removeMessageListener("Content:SelectionCopied", this);
     messageManager.removeMessageListener("Content:SelectionFail", this);
@@ -639,17 +638,16 @@ var SelectionHelperUI = {
     window.removeEventListener("touchstart", this, false);
 
     Elements.browsers.removeEventListener("URLChanged", this, true);
     Elements.browsers.removeEventListener("SizeChanged", this, true);
 
     Elements.tabList.removeEventListener("TabSelect", this, true);
 
     Elements.navbar.removeEventListener("transitionend", this, true);
-    Elements.navbar.removeEventListener("MozAppbarDismissing", this, true);
 
     this._shutdownAllMarkers();
 
     this._selectionMarkIds = [];
     this._msgTarget = null;
     this._activeSelectionRect = null;
 
     this.overlay.displayDebugLayer = false;
@@ -910,43 +908,33 @@ var SelectionHelperUI = {
    * display.
    */
   _onDeckOffsetChanged: function _onDeckOffsetChanged(aEvent) {
     // Update the monocle position and display
     this.attachToCaret(null, this._lastPoint.xPos, this._lastPoint.yPos);
   },
 
   /*
-   * Detects when the nav bar hides or shows, so we can enable
-   * selection at the appropriate location once the transition is
-   * complete, or shutdown selection down when the nav bar is hidden.
+   * Detects when the nav bar transitions, so we can enable selection at the
+   * appropriate location once the transition is complete, or shutdown
+   * selection down when the nav bar is hidden.
    */
   _onNavBarTransitionEvent: function _onNavBarTransitionEvent(aEvent) {
+    // Ignore when selection is in content
     if (this.layerMode == kContentLayer) {
       return;
     }
 
-    if (aEvent.propertyName == "bottom" && Elements.navbar.isShowing) {
-      this._sendAsyncMessage("Browser:SelectionUpdate", {});
-      return;
-    }
-    
-    if (aEvent.propertyName == "transform" && Elements.navbar.isShowing) {
+    // After tansitioning up, show the monocles
+    if (Elements.navbar.isShowing) {
+      this._showAfterUpdate = true;
       this._sendAsyncMessage("Browser:SelectionUpdate", {});
-      this._showMonocles(ChromeSelectionHandler.hasSelection);
     }
   },
 
-  _onNavBarDismissEvent: function _onNavBarDismissEvent() {
-    if (!this.isActive || this.layerMode == kContentLayer) {
-      return;
-    }
-    this._hideMonocles();
-  },
-
   _onKeyboardChangedEvent: function _onKeyboardChangedEvent() {
     if (!this.isActive || this.layerMode == kContentLayer) {
       return;
     }
     this._sendAsyncMessage("Browser:SelectionUpdate", {});
   },
 
   /*
@@ -1085,20 +1073,16 @@ var SelectionHelperUI = {
       case "MozDeckOffsetChanged":
         this._onDeckOffsetChanged(aEvent);
         break;
 
       case "transitionend":
         this._onNavBarTransitionEvent(aEvent);
         break;
 
-      case "MozAppbarDismissing":
-        this._onNavBarDismissEvent();
-        break;
-
       case "KeyboardChanged":
         this._onKeyboardChangedEvent();
         break;
     }
   },
 
   receiveMessage: function sh_receiveMessage(aMessage) {
     if (this._debugEvents) Util.dumpLn("SelectionHelperUI:", aMessage.name);
new file mode 100644
--- /dev/null
+++ b/browser/metro/base/tests/mochitest/browser_flyouts.js
@@ -0,0 +1,44 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+gTests.push({
+  desc: "about flyout hides navbar, clears navbar selection, doesn't leak",
+  run: function() {
+    yield showNavBar();
+
+    let edit = document.getElementById("urlbar-edit");
+    edit.value = "http://www.wikipedia.org/";
+
+    sendElementTap(window, edit);
+
+    yield waitForCondition(function () {
+      return SelectionHelperUI.isSelectionUIVisible;
+    });
+    ok(ContextUI.navbarVisible, "nav bar visible");
+
+    let promise = waitForEvent(FlyoutPanelsUI.AboutFlyoutPanel._topmostElement, "transitionend");
+    FlyoutPanelsUI.show('AboutFlyoutPanel');
+    yield promise;
+
+    yield waitForCondition(function () {
+      return !SelectionHelperUI.isSelectionUIVisible;
+    });
+    ok(!ContextUI.navbarVisible, "nav bar hidden");
+
+    promise = waitForEvent(FlyoutPanelsUI.AboutFlyoutPanel._topmostElement, "transitionend");
+    FlyoutPanelsUI.hide('AboutFlyoutPanel');
+    yield promise;
+  }
+});
+
+function test() {
+  if (!isLandscapeMode()) {
+    todo(false, "browser_selection_tests need landscape mode to run.");
+    return;
+  }
+  runTests();
+}
--- a/browser/metro/base/tests/mochitest/browser_form_auto_complete.html
+++ b/browser/metro/base/tests/mochitest/browser_form_auto_complete.html
@@ -1,16 +1,16 @@
 <!DOCTYPE html>
 <html>
 <head>
 <style>
 </style>
 </head>
 <body style="margin: 20px 20px 20px 20px;">
-<form id="form1" method="get" action="" autocomplete="on">
+<form id="form1" method="get" autocomplete="on">
 <datalist id="testdatalist">
    <option value="one">
    <option value="two">
    <option value="three">
    <option value="four">
    <option value="five">
 </datalist> 
 <input autocomplete="on" list="testdatalist" id="textedit1" style="width:200px;" type="text" />
--- a/browser/metro/base/tests/mochitest/browser_form_auto_complete.js
+++ b/browser/metro/base/tests/mochitest/browser_form_auto_complete.js
@@ -55,19 +55,18 @@ gTests.push({
       return !Browser.selectedTab.isLoading();
     });
 
     let tabDocument = Browser.selectedTab.browser.contentWindow.document;
     let form = tabDocument.getElementById("form1");
     let input = tabDocument.getElementById("textedit1");
 
     input.value = "hellothere";
-    form.action = chromeRoot + "browser_form_auto_complete.html";
 
-    loadedPromise = waitForEvent(Browser.selectedTab.browser, "DOMContentLoaded");
+    loadedPromise = waitForObserver("satchel-storage-changed", null, "formhistory-add");
     form.submit();
     yield loadedPromise;
 
     // XXX Solves a problem with events not getting delivered to Content.js
     // immediately after submitting the form.
     yield waitForMs(500);
 
     tabDocument = Browser.selectedTab.browser.contentWindow.document;
--- a/browser/metro/base/tests/mochitest/browser_selection_urlbar.js
+++ b/browser/metro/base/tests/mochitest/browser_selection_urlbar.js
@@ -169,18 +169,69 @@ gTests.push({
 
     let copy = document.getElementById("context-copy");
     ok(!copy.hidden, "copy menu item is visible")
 
     let condition = getClipboardCondition("http://www.wikipedia.org/");
     let promise = waitForCondition(condition);
     sendElementTap(window, copy);
     ok((yield promise), "copy text onto clipboard")
+
+    clearSelection(edit);
+    edit.blur();
   }
 })
 
+gTests.push({
+  desc: "bug 965832 - selection monocles move with the nav bar",
+  run: function() {
+    yield showNavBar();
+
+    let originalUtils = Services.metro;
+    Services.metro = {
+      keyboardHeight: 0,
+      keyboardVisible: false
+    };
+    registerCleanupFunction(function() {
+      Services.metro = originalUtils;
+    });
+
+    let edit = document.getElementById("urlbar-edit");
+    edit.value = "http://www.wikipedia.org/";
+
+    sendElementTap(window, edit);
+    
+    let promise = waitForEvent(window, "MozDeckOffsetChanged");
+    Services.metro.keyboardHeight = 300;
+    Services.metro.keyboardVisible = true;
+    Services.obs.notifyObservers(null, "metro_softkeyboard_shown", null);
+    yield promise;
+
+    yield waitForCondition(function () {
+      return SelectionHelperUI.isSelectionUIVisible;
+    });
+
+    promise = waitForEvent(window, "MozDeckOffsetChanged");
+    Services.metro.keyboardHeight = 0;
+    Services.metro.keyboardVisible = false;
+    Services.obs.notifyObservers(null, "metro_softkeyboard_hidden", null);
+    yield promise;
+
+    yield waitForCondition(function () {
+      return SelectionHelperUI.isSelectionUIVisible;
+    });
+
+    clearSelection(edit);
+    edit.blur();
+
+    yield waitForCondition(function () {
+      return !SelectionHelperUI.isSelectionUIVisible;
+    });
+  }
+});
+
 function test() {
   if (!isLandscapeMode()) {
     todo(false, "browser_selection_tests need landscape mode to run.");
     return;
   }
   runTests();
 }
--- a/browser/metro/base/tests/mochitest/browser_tiles.js
+++ b/browser/metro/base/tests/mochitest/browser_tiles.js
@@ -495,16 +495,18 @@ gTests.push({
   setUp: gridSlotsSetup,
   run: function() {
     let grid = this.grid;
     // grid is initially populated with empty slots matching the minSlots attribute
     is(grid.children.length, 6, "minSlots slots are created");
     is(grid.itemCount, 0, "slots do not count towards itemCount");
     ok(Array.every(grid.children, (node) => node.nodeName == 'richgriditem'), "slots have nodeName richgriditem");
     ok(Array.every(grid.children, isNotBoundByRichGrid_Item), "slots aren't bound by the richgrid-item binding");
+
+    ok(!grid.isItem(grid.children[0]), "slot fails isItem validation");
   },
   tearDown: gridSlotsTearDown
 });
 
 gTests.push({
   desc: "richgrid using slots for items",
   setUp: gridSlotsSetup, // creates grid with minSlots = num. slots = 6
   run: function() {
@@ -643,8 +645,31 @@ gTests.push({
 
     let item1 = grid.removeItem(grid.items[0]);
     is(grid.children.length, 6);
     is(grid.itemCount, 4);
     is(grid.items[0].getAttribute("label"), "item 2", "removeItem removes the node so the nextSibling takes its place");
   },
   tearDown: gridSlotsTearDown
 });
+
+gTests.push({
+  desc: "richgrid empty slot selection",
+  setUp: gridSlotsSetup,
+  run: function() {
+    let grid = this.grid;
+    // leave grid empty, it has 6 slots
+
+    is(grid.itemCount, 0, "Grid setup with 0 items");
+    is(grid.children.length, 6, "Empty grid has the expected number of slots");
+
+    info("slot is initially selected: " + grid.children[0].selected);
+    grid.selectItem(grid.children[0]);
+    info("after selectItem, slot is selected: " + grid.children[0].selected);
+
+    ok(!grid.children[0].selected, "Attempting to select an empty slot has no effect");
+
+    grid.toggleItemSelection(grid.children[0]);
+    ok(!grid.children[0].selected, "Attempting to toggle selection on an empty slot has no effect");
+
+  },
+  tearDown: gridSlotsTearDown
+});
--- a/browser/metro/base/tests/mochitest/head.js
+++ b/browser/metro/base/tests/mochitest/head.js
@@ -514,17 +514,17 @@ function waitForImageLoad(aWindow, aImag
 
 /**
  * Waits a specified number of miliseconds for an observer event.
  *
  * @param aObsEvent the observer event to wait for
  * @param aTimeoutMs the number of miliseconds to wait before giving up
  * @returns a Promise that resolves to true, or to an Error
  */
-function waitForObserver(aObsEvent, aTimeoutMs) {
+function waitForObserver(aObsEvent, aTimeoutMs, aObsData) {
   try {
 
   let deferred = Promise.defer();
   let timeoutMs = aTimeoutMs || kDefaultWait;
   let timerID = 0;
 
   var observeWatcher = {
     onEvent: function () {
@@ -535,17 +535,18 @@ function waitForObserver(aObsEvent, aTim
 
     onError: function () {
       clearTimeout(timerID);
       Services.obs.removeObserver(this, aObsEvent);
       deferred.reject(new Error(aObsEvent + " event timeout"));
     },
 
     observe: function (aSubject, aTopic, aData) {
-      if (aTopic == aObsEvent) {
+      if (aTopic == aObsEvent &&
+        (!aObsData || (aObsData == aData))) {
         this.onEvent();
       }
     },
 
     QueryInterface: function (aIID) {
       if (!aIID.equals(Ci.nsIObserver) &&
           !aIID.equals(Ci.nsISupportsWeakReference) &&
           !aIID.equals(Ci.nsISupports)) {
--- a/browser/metro/base/tests/mochitest/metro.ini
+++ b/browser/metro/base/tests/mochitest/metro.ini
@@ -32,16 +32,17 @@ support-files =
   res/textarea01.html
   res/testEngine.xml
   res/blankpage1.html
   res/blankpage2.html
   res/blankpage3.html
   res/documentindesignmode.html
 
 [browser_apzc_basic.js]
+[browser_flyouts.js]
 [browser_bookmarks.js]
 [browser_canonizeURL.js]
 [browser_circular_progress_indicator.js]
 [browser_colorUtils.js]
 [browser_crashprompt.js]
 [browser_context_menu_tests.js]
 [browser_context_ui.js]
 [browser_downloads.js]
--- a/browser/metro/modules/CrossSlide.jsm
+++ b/browser/metro/modules/CrossSlide.jsm
@@ -35,17 +35,17 @@ let CrossSlidingStateNames = [
 ];
 
 // --------------------------------
 // module helpers
 //
 
 function isSelectable(aElement) {
   // placeholder logic
-  return aElement.nodeName == 'richgriditem';
+  return aElement.nodeName == 'richgriditem' && aElement.hasAttribute("value");
 }
 function withinCone(aLen, aHeight) {
   // check pt falls within 45deg either side of the cross axis
   return aLen > aHeight;
 }
 function getScrollAxisFromElement(aElement) {
   // keeping it simple - just return apparent scroll axis for the document
   let win = aElement.ownerDocument.defaultView;
new file mode 100644
--- /dev/null
+++ b/browser/metro/shell/commandexecutehandler/CommandExecuteHandler.exe.manifest
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this file,
+   - You can obtain one at http://mozilla.org/MPL/2.0/.  -->
+
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
+<assemblyIdentity
+        version="1.0.0.0"
+        processorArchitecture="*"
+        name="Mozilla.FirefoxCEH"
+        type="win32"
+/>
+<description>Firefox Launcher</description>
+<ms_asmv3:trustInfo xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3">
+  <ms_asmv3:security>
+    <ms_asmv3:requestedPrivileges>
+      <ms_asmv3:requestedExecutionLevel level="asInvoker" uiAccess="false" />
+    </ms_asmv3:requestedPrivileges>
+  </ms_asmv3:security>
+</ms_asmv3:trustInfo>
+  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+    <application>
+      <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
+      <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
+    </application>
+  </compatibility>
+</assembly>
new file mode 100644
--- /dev/null
+++ b/browser/metro/shell/commandexecutehandler/CommandExecuteHandler.rc
@@ -0,0 +1,6 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+1 24 "CommandExecuteHandler.exe.manifest"
--- a/browser/metro/shell/commandexecutehandler/Makefile.in
+++ b/browser/metro/shell/commandexecutehandler/Makefile.in
@@ -1,15 +1,16 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 include $(topsrcdir)/config/config.mk
 
 DIST_PROGRAM = CommandExecuteHandler$(BIN_SUFFIX)
+RCINCLUDE = CommandExecuteHandler.rc
 
 # Don't link against mozglue.dll
 MOZ_GLUE_LDFLAGS =
 MOZ_GLUE_PROGRAM_LDFLAGS =
 
 OS_LIBS = \
 	kernel32.lib \
 	user32.lib \
--- a/browser/metro/theme/browser.css
+++ b/browser/metro/theme/browser.css
@@ -49,48 +49,65 @@
 .tabs-scrollbox > .scrollbutton-up[collapsed],
 .tabs-scrollbox > .scrollbutton-down[collapsed],
 #tabs[input="imprecise"] > .tabs-scrollbox > .scrollbutton-up,
 #tabs[input="imprecise"] > .tabs-scrollbox > .scrollbutton-down {
   visibility: hidden !important;
   pointer-events: none;
 }
 
+#tabs > .tabs-scrollbox > .scrollbutton-down:-moz-locale-dir(rtl),
 #tabs > .tabs-scrollbox > .scrollbutton-up {
   list-style-image: url("images/tab-arrows.png") !important;
   -moz-image-region: rect(15px 58px 63px 14px) !important;
   padding-right: 15px;
   width: @tabs_scrollarrow_width@;
 }
+#tabs > .tabs-scrollbox > .scrollbutton-down:hover:-moz-locale-dir(rtl),
 #tabs > .tabs-scrollbox > .scrollbutton-up:hover {
   -moz-image-region: rect(14px 102px 62px 58px) !important;
 }
+#tabs > .tabs-scrollbox > .scrollbutton-down:active:-moz-locale-dir(rtl),
 #tabs > .tabs-scrollbox > .scrollbutton-up:active {
   -moz-image-region: rect(14px 152px 62px 108px) !important;
 }
+#tabs > .tabs-scrollbox > .scrollbutton-down[disabled]:-moz-locale-dir(rtl),
 #tabs > .tabs-scrollbox > .scrollbutton-up[disabled] {
   -moz-image-region: rect(15px 196px 63px 152px) !important;
 }
 
+#tabs > .tabs-scrollbox > .scrollbutton-up:-moz-locale-dir(rtl),
 #tabs > .tabs-scrollbox > .scrollbutton-down {
   list-style-image: url("images/tab-arrows.png") !important;
   -moz-image-region: rect(73px 58px 121px 14px) !important;
   padding-left: 15px;
   width: @tabs_scrollarrow_width@;
 }
+#tabs > .tabs-scrollbox > .scrollbutton-up:hover:-moz-locale-dir(rtl),
 #tabs > .tabs-scrollbox > .scrollbutton-down:hover {
   -moz-image-region: rect(72px 102px 120px 58px) !important;
 }
+#tabs > .tabs-scrollbox > .scrollbutton-up:active:-moz-locale-dir(rtl),
 #tabs > .tabs-scrollbox > .scrollbutton-down:active {
   -moz-image-region: rect(72px 152px 120px 108px) !important;
 }
+#tabs > .tabs-scrollbox > .scrollbutton-up[disabled]:-moz-locale-dir(rtl),
 #tabs > .tabs-scrollbox > .scrollbutton-down[disabled] {
   -moz-image-region: rect(73px 196px 121px 152px) !important;
 }
 
+.tabs-scrollbox > .scrollbutton-up:not([disabled]):not([collapsed]):-moz-locale-dir(rtl)::after {
+  right: calc(@tabs_scrollarrow_width@ + @metro_spacing_normal@);
+}
+
+.tabs-scrollbox > .scrollbutton-down:not([disabled]):not([collapsed]):-moz-locale-dir(rtl)::before {
+  right: auto;
+  left: calc(@tabs_scrollarrow_width@ + @newtab_button_width@);
+}
+
 .tabs-scrollbox > .scrollbutton-up:not([disabled]):not([collapsed])::after {
   content: "";
   visibility: visible;
   display: block;
   background-color: rgb(90, 91, 95);
   position: absolute;
   top: 0;
   left: calc(@tabs_scrollarrow_width@ + @metro_spacing_normal@); /* .scrollbutton-up width + #tabs-container left padding */
--- a/browser/modules/BrowserUITelemetry.jsm
+++ b/browser/modules/BrowserUITelemetry.jsm
@@ -438,16 +438,19 @@ this.BrowserUITelemetry = {
 
     // Determine if the menubar is currently visible. On OS X, the menubar
     // is never shown, despite not having the collapsed attribute set.
     let menuBar = document.getElementById("toolbar-menubar");
     result.menuBarEnabled =
       menuBar && Services.appinfo.OS != "Darwin"
               && menuBar.getAttribute("autohide") != "true";
 
+    // Determine if the titlebar is currently visible.
+    result.titleBarEnabled = !Services.prefs.getBoolPref("browser.tabs.drawInTitlebar");
+
     // Examine all customizable areas and see what default items
     // are present and missing.
     let defaultKept = [];
     let defaultMoved = [];
     let nondefaultAdded = [];
 
     for (let areaID of CustomizableUI.areas) {
       let items = CustomizableUI.getWidgetIdsInArea(areaID);
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -1916,20 +1916,16 @@ chatbox {
 
 #main-window[customize-entered] #tab-view-deck {
   background-image: url("chrome://browser/skin/customizableui/customizeMode-gridTexture.png"),
                     url("chrome://browser/skin/customizableui/background-noise-toolbar.png"),
                     linear-gradient(to bottom, #bcbcbc, #b5b5b5);
   background-attachment: fixed;
 }
 
-#main-window:-moz-any([customize-entering],[customize-entered]) #tab-view-deck {
-  padding: 0 2em 2em;
-}
-
 #main-window[customize-entered] #navigator-toolbox > toolbar:not(#toolbar-menubar):not(#TabsToolbar),
 #main-window[customize-entered] #customization-container {
   border: 3px solid hsla(0,0%,0%,.1);
   border-top-width: 0;
   background-clip: padding-box;
   background-origin: padding-box;
   -moz-border-right-colors: hsla(0,0%,0%,.05) hsla(0,0%,0%,.1) hsla(0,0%,0%,.2);
   -moz-border-bottom-colors: hsla(0,0%,0%,.05) hsla(0,0%,0%,.1) hsla(0,0%,0%,.2);
@@ -1942,16 +1938,21 @@ chatbox {
 
 #main-window[customize-entered] #TabsToolbar {
   -moz-appearance: none;
   background-clip: padding-box;
   border-right: 3px solid transparent;
   border-left: 3px solid transparent;
 }
 
+#main-window[customizing] #TabsToolbar::after {
+  margin-left: 2em;
+  margin-right: 2em;
+}
+
 /* End customization mode */
 
 
 #main-window[privatebrowsingmode=temporary] #TabsToolbar::before {
   display: -moz-box;
   content: "";
   background: url("chrome://browser/skin/privatebrowsing-mask.png") center no-repeat;
   width: 40px;
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -419,29 +419,33 @@ toolbar .toolbarbutton-1:not([type="menu
 /**
  * Draw seperators before toolbar button dropmarkers, as well as between
  * consecutive toolbarbutton-1's within a toolbaritem.
  */
 #nav-bar .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker::before,
 #nav-bar .toolbaritem-combined-buttons > .toolbarbutton-1 + .toolbarbutton-1::before {
   content: "";
   display: -moz-box;
-  position: absolute;
+  position: relative;
   top: calc(50% - 9px);
   width: 1px;
   height: 18px;
   -moz-margin-end: -1px;
   background-image: linear-gradient(hsla(210,54%,20%,.2) 0, hsla(210,54%,20%,.2) 18px);
   background-clip: padding-box;
   background-position: center;
   background-repeat: no-repeat;
   background-size: 1px 18px;
   box-shadow: 0 0 0 1px hsla(0,0%,100%,.2);
 }
 
+#nav-bar .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker {
+  -moz-box-orient: horizontal;
+}
+
 #nav-bar .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon {
   -moz-margin-start: 10px;
 }
 
 @media not all and (min-resolution: 2dppx) {
 %include ../shared/toolbarbuttons.inc.css
 %include ../shared/menupanel.inc.css
 
@@ -2556,16 +2560,20 @@ toolbarbutton.chevron > .toolbarbutton-m
   border: none;
 }
 
 .tabbrowser-tab:not(:-moz-lwtheme) {
   color: #333;
   text-shadow: @loweredShadow@;
 }
 
+.tabbrowser-tab[selected=true]:-moz-lwtheme {
+  text-shadow: inherit;
+}
+
 .tabbrowser-tabs[closebuttons="hidden"] > * > * > * > .tab-close-button:not([pinned]) {
   display: -moz-box;
   visibility: hidden;
 }
 
 .tabs-newtab-button > .toolbarbutton-icon {
   -moz-box-align: center;
   border: solid transparent;
@@ -2599,17 +2607,17 @@ toolbarbutton.chevron > .toolbarbutton-m
   position: relative;
   -moz-appearance: none;
   background-repeat: repeat-x;
 }
 
 /*
  * Draw the bottom border of the tabstrip when core doesn't do it for us:
  */
-#main-window:-moz-any([privatebrowsingmode=temporary],[sizemode="fullscreen"],[customizing],[customize-exiting]) #TabsToolbar::after,
+#main-window:-moz-any([privatebrowsingmode=temporary],[sizemode="fullscreen"],[customize-entered]) #TabsToolbar::after,
 #main-window:not([tabsintitlebar]) #TabsToolbar::after,
 #TabsToolbar:-moz-lwtheme::after {
   content: '';
   /* Because we use placeholders for window controls etc. in the tabstrip,
    * and position those with ordinal attributes, and because our layout code
    * expects :before/:after nodes to come first/last in the frame list,
    * we have to reorder this element to come last, hence the
    * ordinal group value (see bug 853415). */
@@ -4013,18 +4021,19 @@ window > chatbox {
 /* Customization mode */
 
 %include ../shared/customizableui/customizeMode.inc.css
 
 #main-window[customize-entered] #titlebar {
   padding-top: 0;
 }
 
-#main-window:-moz-any([customize-entering],[customize-entered]) #tab-view-deck {
-  padding: 0 2em 2em;
+#main-window[tabsintitlebar][customize-entered] #titlebar-content {
+  margin-bottom: 0px !important;
+  margin-top: 11px !important;
 }
 
 #main-window[customize-entered] #tab-view-deck {
   background-image: url("chrome://browser/skin/customizableui/customizeMode-gridTexture.png"),
                     url("chrome://browser/skin/customizableui/background-noise-toolbar.png"),
                     linear-gradient(to bottom, rgb(233,233,233), rgb(178,178,178) 21px);
   background-attachment: fixed;
 }
@@ -4061,16 +4070,22 @@ window > chatbox {
     -moz-image-region: rect(0, 96px, 48px, 48px);
   }
 
   #customization-titlebar-visibility-button > .button-box > .button-icon {
     width: 24px;
   }
 }
 
+#main-window[customizing] #navigator-toolbox::after,
+#main-window[customize-entered] #TabsToolbar::after {
+  margin-left: 2em;
+  margin-right: 2em;
+}
+
 /* End customization mode */
 
 #main-window[privatebrowsingmode=temporary] {
   background-image: url("chrome://browser/skin/privatebrowsing-mask.png");
   background-position: top right;
   background-repeat: no-repeat;
   background-color: -moz-mac-chrome-active;
 }
@@ -4132,9 +4147,9 @@ window > chatbox {
 #UITourTooltipDescription {
   font-size: 1.1rem;
   line-height: 1.9rem;
 }
 
 #UITourTooltipClose {
   -moz-margin-end: -15px;
   margin-top: -12px;
-}
\ No newline at end of file
+}
--- a/browser/themes/shared/customizableui/customizeMode.inc.css
+++ b/browser/themes/shared/customizableui/customizeMode.inc.css
@@ -1,13 +1,23 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* Customization mode */
+
+#main-window:-moz-any([customize-entering],[customize-entered]) #content-deck {
+  margin: 0 2em 2em;
+}
+
+#main-window:-moz-any([customize-entering],[customize-entered]) #navigator-toolbox > toolbar:not(#TabsToolbar) {
+  margin-left: 2em;
+  margin-right: 2em;
+}
+
 #main-window:-moz-any([customize-entering],[customize-exiting]) #tab-view-deck {
   pointer-events: none;
 }
 
 #nav-bar[customize-entered] > #nav-bar-customization-target {
   margin: 1px 3px;
 }
 
@@ -122,24 +132,31 @@ toolbarpaletteitem[place="panel"] {
 }
 
 toolbarpaletteitem[notransition].panel-customization-placeholder,
 toolbarpaletteitem[notransition][place="palette"],
 toolbarpaletteitem[notransition][place="panel"] {
   transition: none;
 }
 
-toolbarpaletteitem > toolbarbutton > .toolbarbutton-icon {
-  transition: transform .3s cubic-bezier(.6, 2, .75, 1.5);
+toolbarpaletteitem > toolbarbutton > .toolbarbutton-icon,
+toolbarpaletteitem > toolbaritem.panel-wide-item,
+toolbarpaletteitem > toolbarbutton[type="menu-button"] {
+  transition: transform .3s cubic-bezier(.6, 2, .75, 1.5) !important;
 }
 
 toolbarpaletteitem[mousedown] > toolbarbutton > .toolbarbutton-icon {
   transform: scale(1.3);
 }
 
+toolbarpaletteitem[mousedown] > toolbaritem.panel-wide-item,
+toolbarpaletteitem[mousedown] > toolbarbutton[type="menu-button"] {
+  transform: scale(1.1);
+}
+
 /* Override the toolkit styling for items being dragged over. */
 toolbarpaletteitem[place="toolbar"] {
   border-left-width: 0;
   border-right-width: 0;
   margin-right: 0;
   margin-left: 0;
 }
 
--- a/browser/themes/shared/customizableui/panelUIOverlay.inc.css
+++ b/browser/themes/shared/customizableui/panelUIOverlay.inc.css
@@ -532,18 +532,24 @@ toolbarbutton.panel-multiview-anchor {
   color: HighlightText;
 }
 
 toolbarpaletteitem[place="palette"] > #bookmarks-menu-button > .toolbarbutton-menubutton-dropmarker,
 #bookmarks-menu-button[cui-areatype="menu-panel"] > .toolbarbutton-menubutton-dropmarker {
   display: none;
 }
 
+#search-container[cui-areatype="menu-panel"],
+#wrapper-search-container[place="panel"] {
+  width: @menuPanelWidth@;
+}
+
 #search-container[cui-areatype="menu-panel"] {
-  width: @menuPanelWidth@;
+  margin-top: 6px;
+  margin-bottom: 6px;
 }
 
 toolbarpaletteitem[place="palette"] > #search-container {
   min-width: 7em;
   width: 7em;
 }
 
 #edit-controls@inAnyPanel@,
--- a/browser/themes/shared/devtools/common.css
+++ b/browser/themes/shared/devtools/common.css
@@ -22,29 +22,29 @@
 }
 
 /* Splitters */
 .devtools-horizontal-splitter {
   -moz-appearance: none;
   background-image: none;
   background-color: transparent;
   border: 0;
-  border-bottom: 1px solid #aaa;
+  border-bottom: 1px solid rgba(118, 121, 125, .5);
   min-height: 3px;
   height: 3px;
   margin-top: -3px;
   position: relative;
 }
 
 .devtools-side-splitter {
   -moz-appearance: none;
   background-image: none;
   background-color: transparent;
   border: 0;
-  -moz-border-end: 1px solid #aaa;
+  -moz-border-end: 1px solid rgba(118, 121, 125, .5);
   min-width: 3px;
   width: 3px;
   -moz-margin-start: -3px;
   position: relative;
   cursor: e-resize;
 }
 
 .devtools-toolbox-side-iframe {
--- a/browser/themes/shared/devtools/debugger.inc.css
+++ b/browser/themes/shared/devtools/debugger.inc.css
@@ -1,16 +1,15 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* Sources and breakpoints pane */
 
-
 #sources-pane[selectedIndex="0"] + #sources-and-editor-splitter {
   border-color: transparent;
 }
 
 #sources-pane > tabs {
   -moz-border-end: 1px solid;
 }
 
@@ -286,16 +285,32 @@
   margin: 2px;
   background: -moz-image-rect(url(commandline-icon.png), 0, 32, 16, 16);
 }
 
 .dbg-expression-input {
   color: inherit;
 }
 
+.dbg-expression-button {
+  -moz-appearance: none;
+  border: none;
+  background: none;
+  cursor: pointer;
+  text-decoration: underline;
+}
+
+.theme-dark .dbg-expression-button {
+  color: #46afe3; /* Blue highlight color */
+}
+
+.theme-light .dbg-expression-button {
+  color: #0088cc; /* Blue highlight color */
+}
+
 /* Event listeners view */
 
 .dbg-event-listener-type {
   font-weight: 600;
 }
 
 .theme-dark .dbg-event-listener-location {
   color: #b8c8d9; /* Light content text */
@@ -549,41 +564,16 @@
 #step-in {
   list-style-image: url(debugger-step-in.png);
 }
 
 #step-out {
   list-style-image: url(debugger-step-out.png);
 }
 
-#debugger-controls > toolbarbutton,
-#sources-controls > toolbarbutton {
-  margin: 0;
-  box-shadow: none;
-  border-radius: 0;
-  border-width: 0;
-  -moz-border-end-width: 1px;
-  outline-offset: -3px;
-}
-
-#debugger-controls > toolbarbutton:last-of-type,
-#sources-controls > toolbarbutton:last-of-type {
-  -moz-border-end-width: 0;
-}
-
-#debugger-controls,
-#sources-controls {
-  box-shadow: 0 1px 0 hsla(210,16%,76%,.15) inset,
-              0 0 0 1px hsla(210,16%,76%,.15) inset,
-              0 1px 0 hsla(210,16%,76%,.15);
-  border: 1px solid hsla(210,8%,5%,.45);
-  border-radius: 3px;
-  margin: 0 3px;
-}
-
 #instruments-pane-toggle {
   background: none;
   box-shadow: none;
   border: none;
   list-style-image: url(debugger-collapse.png);
   -moz-image-region: rect(0px,16px,16px,0px);
 }
 
--- a/browser/themes/shared/devtools/light-theme.css
+++ b/browser/themes/shared/devtools/light-theme.css
@@ -302,9 +302,17 @@ div.CodeMirror span.eval-text {
   color: black;
   border-bottom: 1px solid #d9e1e8;
 }
 
 .theme-tooltip-panel .devtools-tooltip-simple-text:last-child {
   border-bottom: 0;
 }
 
+.devtools-horizontal-splitter {
+  border-bottom: 1px solid #aaa;
+}
+
+.devtools-side-splitter {
+  -moz-border-end: 1px solid #aaa;
+}
+
 %include toolbars.inc.css
--- a/browser/themes/shared/devtools/profiler.inc.css
+++ b/browser/themes/shared/devtools/profiler.inc.css
@@ -64,38 +64,16 @@
 .theme-dark .selected .profiler-sidebar-item > hbox {
   color: #b6babf;
 }
 
 .theme-light .selected .profiler-sidebar-item > hbox {
   color: #ebeced;
 }
 
-#profiler-controls > toolbarbutton {
-  margin: 0;
-  box-shadow: none;
-  border-radius: 0;
-  border-width: 0;
-  -moz-border-end-width: 1px;
-  outline-offset: -3px;
-}
-
-#profiler-controls > toolbarbutton:last-of-type {
-  -moz-border-end-width: 0;
-}
-
-#profiler-controls {
-  box-shadow: 0 1px 0 hsla(210,16%,76%,.15) inset,
-              0 0 0 1px hsla(210,16%,76%,.15) inset,
-              0 1px 0 hsla(210,16%,76%,.15);
-  border: 1px solid hsla(210,8%,5%,.45);
-  border-radius: 3px;
-  margin: 0 3px;
-}
-
 #profiler-start {
   list-style-image: url("chrome://browser/skin/devtools/profiler-stopwatch.png");
   -moz-image-region: rect(0px,16px,16px,0px);
 }
 
 #profiler-start[checked] {
   -moz-image-region: rect(0px,32px,16px,16px);
 }
--- a/browser/themes/shared/devtools/toolbars.inc.css
+++ b/browser/themes/shared/devtools/toolbars.inc.css
@@ -21,16 +21,38 @@
   min-width: 78px;
   min-height: 22px;
   text-shadow: none;
   border: 1px solid hsla(210,8%,5%,.45);
   border-radius: 3px;
   margin: 0 3px;
 }
 
+.devtools-menulist:-moz-focusring,
+.devtools-toolbarbutton:-moz-focusring {
+  outline: 1px dotted hsla(210,30%,85%,0.7);
+  outline-offset: -4px;
+}
+
+.devtools-toolbarbutton > .toolbarbutton-icon {
+  margin: 0;
+}
+
+.devtools-toolbarbutton:not([label]) {
+  min-width: 32px;
+}
+
+.devtools-toolbarbutton:not([label]) > .toolbarbutton-text {
+  display: none;
+}
+
+.devtools-toolbarbutton > .toolbarbutton-menubutton-button {
+  -moz-box-orient: horizontal;
+}
+
 .theme-dark .devtools-menulist,
 .theme-dark .devtools-toolbarbutton {
   background: linear-gradient(hsla(212,7%,57%,.35), hsla(212,7%,57%,.1)) padding-box;
   color: inherit;
   box-shadow: 0 1px 0 hsla(210,16%,76%,.15) inset, 0 0 0 1px hsla(210,16%,76%,.15) inset, 0 1px 0 hsla(210,16%,76%,.15);
 }
 
 .theme-light .devtools-menulist,
@@ -57,61 +79,39 @@
 .theme-light .devtools-toolbarbutton[type=menu-button] > .toolbarbutton-menubutton-button {
   color: #667380;
 }
 
 .theme-light .devtools-toolbarbutton[type=menu-button][checked=true] > .toolbarbutton-menubutton-button {
   color: #18191a;
 }
 
-.devtools-toolbarbutton > .toolbarbutton-menubutton-button {
-  -moz-box-orient: horizontal;
-}
-
-.devtools-menulist:-moz-focusring,
-.devtools-toolbarbutton:-moz-focusring {
-  outline: 1px dotted hsla(210,30%,85%,0.7);
-  outline-offset: -4px;
-}
-
-.devtools-toolbarbutton > .toolbarbutton-icon {
-  margin: 0;
-}
-
-.devtools-toolbarbutton:not([label]) {
-  min-width: 32px;
-}
-
-.devtools-toolbarbutton:not([label]) > .toolbarbutton-text {
-  display: none;
-}
-
 .theme-dark .devtools-toolbarbutton:not([checked]):hover:active {
   border-color: hsla(210,8%,5%,.6);
   background: linear-gradient(hsla(220,6%,10%,.3), hsla(212,7%,57%,.15) 65%, hsla(212,7%,57%,.3));
   box-shadow: 0 0 3px hsla(210,8%,5%,.25) inset, 0 1px 3px hsla(210,8%,5%,.25) inset, 0 1px 0 hsla(210,16%,76%,.15);
 }
 
 .theme-dark .devtools-menulist[open=true],
 .theme-dark .devtools-toolbarbutton[open=true],
 .theme-dark .devtools-toolbarbutton[checked=true] {
   border-color: hsla(210,8%,5%,.6) !important;
   background: linear-gradient(hsla(220,6%,10%,.6), hsla(210,11%,18%,.45) 75%, hsla(210,11%,30%,.4));
   box-shadow: 0 1px 3px hsla(210,8%,5%,.25) inset, 0 1px 3px hsla(210,8%,5%,.25) inset, 0 1px 0 hsla(210,16%,76%,.15);
 }
 
-.devtools-toolbarbutton[checked=true] {
+.theme-dark .devtools-toolbarbutton[checked=true] {
   color: hsl(208,100%,60%);
 }
 
-.devtools-toolbarbutton[checked=true]:hover {
+.theme-dark .devtools-toolbarbutton[checked=true]:hover {
   background-color: transparent !important;
 }
 
-.devtools-toolbarbutton[checked=true]:hover:active {
+.theme-dark .devtools-toolbarbutton[checked=true]:hover:active {
   background-color: hsla(210,8%,5%,.2) !important;
 }
 
 .devtools-option-toolbarbutton {
   -moz-appearance: none;
   list-style-image: url("chrome://browser/skin/devtools/option-icon.png");
   -moz-image-region: rect(0px 16px 16px 0px);
   background: none;
@@ -161,16 +161,47 @@
 .devtools-toolbarbutton[type=menu] > .toolbarbutton-menu-dropmarker,
 .devtools-toolbarbutton[type=menu-button] > .toolbarbutton-menubutton-dropmarker {
   -moz-appearance: none !important;
   list-style-image: url("chrome://browser/skin/devtools/dropmarker.png");
   -moz-box-align: center;
   padding: 0 3px;
 }
 
+/* Toolbar button groups */
+.theme-light .devtools-toolbarbutton-group > .devtools-toolbarbutton,
+.theme-dark .devtools-toolbarbutton-group > .devtools-toolbarbutton {
+  margin: 0;
+  box-shadow: none;
+  border-radius: 0;
+  border-width: 0;
+  -moz-border-end-width: 1px;
+  outline-offset: -3px;
+}
+
+.devtools-toolbarbutton-group > .devtools-toolbarbutton:last-of-type {
+  -moz-border-end-width: 0;
+}
+
+.devtools-toolbarbutton-group {
+  border-radius: 3px;
+  margin: 0 3px;
+}
+
+.theme-dark .devtools-toolbarbutton-group {
+  box-shadow: 0 1px 0 hsla(210,16%,76%,.15) inset,
+              0 0 0 1px hsla(210,16%,76%,.15) inset,
+              0 1px 0 hsla(210,16%,76%,.15);
+  border: 1px solid hsla(210,8%,5%,.45);
+}
+
+.theme-light .devtools-toolbarbutton-group {
+  border: 1px solid #bbb;
+}
+
 /* Text input */
 
 .devtools-textinput,
 .devtools-searchinput {
   -moz-appearance: none;
   margin: 0 3px;
   border: 1px solid rgb(88, 94, 101);
 %ifdef XP_MACOSX
@@ -268,17 +299,16 @@
   font: inherit;
   margin-bottom: 0;
   padding: 0;
   border-width: 0 0 1px 0;
   border-style: solid;
   overflow: hidden;
 }
 
-
 .devtools-sidebar-tabs > tabs > .tabs-right,
 .devtools-sidebar-tabs > tabs > .tabs-left {
   display: none;
 }
 
 .devtools-sidebar-tabs > tabs > tab {
   -moz-appearance: none;
   /* We want to match the height of a toolbar with a toolbarbutton
@@ -294,16 +324,17 @@
    */
   min-height: 32px;
   text-align: center;
   color: inherit;
   -moz-box-flex: 1;
   border-width: 0;
   position: static;
   -moz-margin-start: -1px;
+  text-shadow: none;
 }
 
 .devtools-sidebar-tabs > tabs > tab:first-of-type {
   -moz-margin-start: -3px;
 }
 
 .devtools-sidebar-tabs > tabs > tab {
   background-size: calc(100% - 1px) 100%, 1px 100%;
--- a/browser/themes/shared/devtools/widgets.inc.css
+++ b/browser/themes/shared/devtools/widgets.inc.css
@@ -66,17 +66,16 @@
   background-position: -7px center;
 }
 
 .scrollbutton-up[disabled] > .toolbarbutton-icon,
 .scrollbutton-down[disabled] > .toolbarbutton-icon {
   opacity: 0.5;
 }
 
-
 /* Draw shadows to indicate there is more content 'behind' scrollbuttons. */
 .scrollbutton-up:-moz-locale-dir(ltr),
 .scrollbutton-down:-moz-locale-dir(rtl) {
   border-right: solid 1px rgba(255, 255, 255, .1);
   border-left: solid 1px transparent;
   box-shadow: 3px 0px 3px -3px #181d20;
 }
 
@@ -111,26 +110,36 @@
 #breadcrumb-separator-before,
 #breadcrumb-separator-after,
 #breadcrumb-separator-normal {
   width: 12px;
   height: 25px;
   overflow: hidden;
 }
 
-#breadcrumb-separator-before,
-#breadcrumb-separator-after:after {
+.theme-dark #breadcrumb-separator-before,
+.theme-dark #breadcrumb-separator-after:after {
   background: #1d4f73; /* Select Highlight Blue */
 }
 
-#breadcrumb-separator-after,
-#breadcrumb-separator-before:after {
+.theme-dark #breadcrumb-separator-after,
+.theme-dark #breadcrumb-separator-before:after {
   background: #343c45; /* Toolbars */
 }
 
+.theme-light #breadcrumb-separator-before,
+.theme-light #breadcrumb-separator-after:after {
+  background: #4c9ed9; /* Select Highlight Blue */
+}
+
+.theme-light #breadcrumb-separator-after,
+.theme-light #breadcrumb-separator-before:after {
+  background: #f0f1f2; /* Toolbars */
+}
+
 /* This chevron arrow cannot be replicated easily in CSS, so we are using
  * a background image for it (still keeping it in a separate element so
  * we can handle RTL support with a CSS transform).
  */
 #breadcrumb-separator-normal {
   background: url(breadcrumbs-divider@2x.png) no-repeat center right;
   background-size: 12px 24px;
 }
@@ -163,19 +172,26 @@
 }
 
 .breadcrumbs-widget-item[checked] + .breadcrumbs-widget-item {
   background: -moz-element(#breadcrumb-separator-after) no-repeat 0 0;
 }
 
 .breadcrumbs-widget-item[checked] {
   background: -moz-element(#breadcrumb-separator-before) no-repeat 0 0;
+}
+
+.theme-dark .breadcrumbs-widget-item[checked] {
   background-color: #1d4f73; /* Select Highlight Blue */
 }
 
+.theme-light .breadcrumbs-widget-item[checked] {
+  background-color: #4c9ed9; /* Select Highlight Blue */
+}
+
 .breadcrumbs-widget-item:first-child {
   background-image: none;
 }
 
 /* RTL support: move the images that were on the left to the right,
  * and move images that were on the right to the left.
  */
 .breadcrumbs-widget-item:-moz-locale-dir(rtl) {
@@ -185,76 +201,63 @@
 .breadcrumbs-widget-item:-moz-locale-dir(rtl),
 .breadcrumbs-widget-item[checked] + .breadcrumbs-widget-item:-moz-locale-dir(rtl) {
   background-position: center right;
 }
 
 #breadcrumb-separator-before:-moz-locale-dir(rtl),
 #breadcrumb-separator-after:-moz-locale-dir(rtl),
 #breadcrumb-separator-normal:-moz-locale-dir(rtl) {
-  transform:  scaleX(-1);
+  transform: scaleX(-1);
 }
 
 #breadcrumb-separator-before:-moz-locale-dir(rtl):after,
 #breadcrumb-separator-after:-moz-locale-dir(rtl):after {
   transform: translateX(-5px) rotate(45deg);
 }
 
-.breadcrumbs-widget-item:not([checked]):hover label {
-  color: white;
-}
-
+.breadcrumbs-widget-item[checked] .breadcrumbs-widget-item-id,
 .breadcrumbs-widget-item[checked] .breadcrumbs-widget-item-tag,
-.breadcrumbs-widget-item[checked] .breadcrumbs-widget-item-id,
 .breadcrumbs-widget-item[checked] .breadcrumbs-widget-item-pseudo-classes,
 .breadcrumbs-widget-item[checked] .breadcrumbs-widget-item-classes {
   color: #f5f7fa; /* Foreground (Text) - Light */
 }
 
-.theme-dark .breadcrumbs-widget-item-id,
-.theme-dark .breadcrumbs-widget-item-classes,
-.theme-dark .breadcrumbs-widget-item[checked] .breadcrumbs-widget-item-classes {
+.theme-dark .breadcrumbs-widget-item,
+.theme-dark .breadcrumbs-widget-item-classes {
+  color: #f5f7fa; /* Foreground (Text) - Light */
+}
+
+.theme-light .breadcrumbs-widget-item,
+.theme-light .breadcrumbs-widget-item-classes {
+  color: #18191a; /* Foreground (Text) - Dark */
+}
+
+.theme-dark .breadcrumbs-widget-item-id {
   color: #b6babf; /* Foreground (Text) - Grey */
 }
 
+.theme-light .breadcrumbs-widget-item-id {
+  color: #585959; /* Foreground (Text) - Grey */
+}
+
 .theme-dark .breadcrumbs-widget-item-pseudo-classes {
   color: #d99b28; /* Light Orange */
 }
 
-.theme-light .breadcrumbs-widget-item[checked] {
-  background: -moz-element(#breadcrumb-separator-before) no-repeat 0 0;
-  background-color: #4c9ed9; /* Select Highlight Blue */
-}
-
-.theme-light .breadcrumbs-widget-item:first-child {
-  background-image: none;
-}
-
-.theme-light #breadcrumb-separator-before,
-.theme-light #breadcrumb-separator-after:after {
-  background: #4c9ed9; /* Select Highlight Blue */
+.theme-light .breadcrumbs-widget-item-pseudo-classes {
+  color: #d97e00; /* Light Orange */
 }
 
-.theme-light #breadcrumb-separator-after,
-.theme-light #breadcrumb-separator-before:after {
-  background: #f0f1f2; /*  Toolbars */
-}
-
-.theme-light .breadcrumbs-widget-item,
-.theme-light .breadcrumbs-widget-item-id,
-.theme-light .breadcrumbs-widget-item-classes {
-  color: #585959; /* Foreground (Text) - Grey */
-}
-
-.theme-light .breadcrumbs-widget-item-pseudo-classes {
-  color: #585959; /* Foreground (Text) - Grey */
+.theme-dark .breadcrumbs-widget-item:not([checked]):hover label {
+  color: white;
 }
 
 .theme-light .breadcrumbs-widget-item:not([checked]):hover label {
-  color: #18191a; /* Foreground (Text) - Dark */
+  color: black;
 }
 
 /* SimpleListWidget */
 
 %filter substitution
 %define slw_selectionGradient linear-gradient(hsl(206,59%,39%), hsl(206,59%,29%))
 %define slw_selectionTextColor #fff
 
--- a/browser/themes/windows/browser-aero.css
+++ b/browser/themes/windows/browser-aero.css
@@ -256,17 +256,17 @@
     position: absolute;
     pointer-events: none;
     top: 50%;
     width: -moz-available;
     z-index: -1;
   }
 
   /* Need to constrain the glass fog to avoid overlapping layers, see bug 886281. */
-  #main-window:not([customizing]) #navigator-toolbox:not(:-moz-lwtheme) {
+  #navigator-toolbox:not(:-moz-lwtheme) {
     overflow: -moz-hidden-unscrollable;
   }
 
   #main-window[sizemode=normal] .tabbrowser-arrowscrollbox > .arrowscrollbox-scrollbox > .scrollbox-innerbox:not(:-moz-lwtheme) {
     position: relative;
   }
 
   /* End Glass Fog */
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -1474,17 +1474,17 @@ toolbarbutton[type="socialmark"] > .tool
 
 /* Tabstrip */
 
 #TabsToolbar {
   min-height: 0;
   padding: 0;
 }
 
-#TabsToolbar:not(:-moz-lwtheme) {
+#main-window:not([customizing]) #TabsToolbar:not(:-moz-lwtheme) {
   background-image: linear-gradient(to top, @toolbarShadowColor@ 2px, rgba(0,0,0,.05) 2px, transparent 50%);
 }
 
 #main-window[tabsintitlebar] #TabsToolbar {
   background-color: transparent;
 }
 
 %ifndef WINDOWS_AERO
@@ -2436,26 +2436,27 @@ chatbox {
 
 %include ../shared/customizableui/customizeMode.inc.css
 
 #main-window[customize-entered] {
   background-image: url("chrome://browser/skin/customizableui/customizeMode-gridTexture.png");
   background-attachment: fixed;
 }
 
-#main-window:-moz-any([customize-entering],[customize-entered]) #tab-view-deck {
-  padding: 0 2em 2em;
-}
-
 #customization-container {
   border-left: 1px solid @toolbarShadowColor@;
   border-right: 1px solid @toolbarShadowColor@;
   background-clip: padding-box;
 }
 
+#main-window[customizing] #navigator-toolbox::after {
+  margin-left: 2em;
+  margin-right: 2em;
+}
+
 /* End customization mode */
 
 #main-window[privatebrowsingmode=temporary] #TabsToolbar::after {
   content: "";
   display: -moz-box;
   width: 40px;
   background: url("chrome://browser/skin/privatebrowsing-indicator.png") no-repeat center center;
 }
--- a/layout/xul/nsMenuPopupFrame.cpp
+++ b/layout/xul/nsMenuPopupFrame.cpp
@@ -91,17 +91,17 @@ nsMenuPopupFrame::nsMenuPopupFrame(nsIPr
   mIsOpenChanged(false),
   mIsContextMenu(false),
   mAdjustOffsetForContextMenu(false),
   mGeneratedChildren(false),
   mMenuCanOverlapOSBar(false),
   mShouldAutoPosition(true),
   mInContentShell(true),
   mIsMenuLocked(false),
-  mIsDragPopup(false),
+  mMouseTransparent(false),
   mHFlip(false),
   mVFlip(false)
 {
   // the preference name is backwards here. True means that the 'top' level is
   // the default, and false means that the 'parent' level is the default.
   if (sDefaultLevelIsTop >= 0)
     return;
   sDefaultLevelIsTop =
@@ -136,22 +136,16 @@ nsMenuPopupFrame::Init(nsIContent*      
   nsCOMPtr<nsIAtom> tag = doc->BindingManager()->ResolveTag(aContent, &namespaceID);
   if (namespaceID == kNameSpaceID_XUL) {
     if (tag == nsGkAtoms::menupopup || tag == nsGkAtoms::popup)
       mPopupType = ePopupTypeMenu;
     else if (tag == nsGkAtoms::tooltip)
       mPopupType = ePopupTypeTooltip;
   }
 
-  if (mPopupType == ePopupTypePanel &&
-      aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
-                            nsGkAtoms::drag, eIgnoreCase)) {
-    mIsDragPopup = true;
-  }
-
   nsCOMPtr<nsIDocShellTreeItem> dsti = PresContext()->GetDocShell();
   if (dsti && dsti->ItemType() == nsIDocShellTreeItem::typeChrome) {
     mInContentShell = false;
   }
 
   // To improve performance, create the widget for the popup only if it is not
   // a leaf. Leaf popups such as menus will create their widgets later when
   // the popup opens.
@@ -238,17 +232,30 @@ nsMenuPopupFrame::CreateWidgetForView(ns
 {
   // Create a widget for ourselves.
   nsWidgetInitData widgetData;
   widgetData.mWindowType = eWindowType_popup;
   widgetData.mBorderStyle = eBorderStyle_default;
   widgetData.clipSiblings = true;
   widgetData.mPopupHint = mPopupType;
   widgetData.mNoAutoHide = IsNoAutoHide();
-  widgetData.mIsDragPopup = mIsDragPopup;
+
+  if (!mInContentShell) {
+    // A drag popup may be used for non-static translucent drag feedback
+    if (mPopupType == ePopupTypePanel &&
+        mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+                              nsGkAtoms::drag, eIgnoreCase)) {
+      widgetData.mIsDragPopup = true;
+    }
+
+    // If mousethrough="always" is set directly on the popup, then the widget
+    // should ignore mouse events, passing them through to the content behind.
+    mMouseTransparent = GetStateBits() & NS_FRAME_MOUSE_THROUGH_ALWAYS;
+    widgetData.mMouseTransparent = mMouseTransparent;
+  }
 
   nsAutoString title;
   if (mContent && widgetData.mNoAutoHide) {
     if (mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::titlebar,
                               nsGkAtoms::normal, eCaseMatters)) {
       widgetData.mBorderStyle = eBorderStyle_title;
 
       mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::label, title);
@@ -1809,29 +1816,16 @@ nsMenuPopupFrame::GetWidget()
 }
 
 void
 nsMenuPopupFrame::AttachedDismissalListener()
 {
   mConsumeRollupEvent = nsIPopupBoxObject::ROLLUP_DEFAULT;
 }
 
-void
-nsMenuPopupFrame::BuildDisplayList(nsDisplayListBuilder*   aBuilder,
-                                   const nsRect&           aDirtyRect,
-                                   const nsDisplayListSet& aLists)
-{
-  // don't pass events to drag popups
-  if (aBuilder->IsForEventDelivery() && mIsDragPopup) {
-    return;
-  }
-
-  nsBoxFrame::BuildDisplayList(aBuilder, aDirtyRect, aLists);
-}
-
 // helpers /////////////////////////////////////////////////////////////
 
 NS_IMETHODIMP 
 nsMenuPopupFrame::AttributeChanged(int32_t aNameSpaceID,
                                    nsIAtom* aAttribute,
                                    int32_t aModType)
 
 {
--- a/layout/xul/nsMenuPopupFrame.h
+++ b/layout/xul/nsMenuPopupFrame.h
@@ -225,17 +225,17 @@ public:
   // reset the current incremental search string, calculated in
   // FindMenuWithShortcut.
   nsMenuFrame* Enter(mozilla::WidgetGUIEvent* aEvent);
 
   nsPopupType PopupType() const { return mPopupType; }
   bool IsMenu() MOZ_OVERRIDE { return mPopupType == ePopupTypeMenu; }
   bool IsOpen() MOZ_OVERRIDE { return mPopupState == ePopupOpen || mPopupState == ePopupOpenAndVisible; }
 
-  bool IsDragPopup() { return mIsDragPopup; }
+  bool IsMouseTransparent() { return mMouseTransparent; }
 
   static nsIContent* GetTriggerContent(nsMenuPopupFrame* aMenuPopupFrame);
   void ClearTriggerContent() { mTriggerContent = nullptr; }
 
   // returns true if the popup is in a content shell, or false for a popup in
   // a chrome shell
   bool IsInContentShell() { return mInContentShell; }
 
@@ -329,20 +329,16 @@ public:
 
   // Return the anchor if there is one.
   nsIContent* GetAnchor() const { return mAnchorContent; }
 
   // Return the screen coordinates of the popup, or (-1, -1) if anchored.
   // This position is in CSS pixels.
   nsIntPoint ScreenPosition() const { return nsIntPoint(mScreenXPos, mScreenYPos); }
 
-  virtual void BuildDisplayList(nsDisplayListBuilder*   aBuilder,
-                                const nsRect&           aDirtyRect,
-                                const nsDisplayListSet& aLists) MOZ_OVERRIDE;
-
   nsIntPoint GetLastClientOffset() const { return mLastClientOffset; }
 
   // Return the alignment of the popup
   int8_t GetAlignmentPosition() const;
 
   // Return the offset applied to the alignment of the popup
   nscoord GetAlignmentOffset() const { return mAlignmentOffset; }
 protected:
@@ -475,17 +471,17 @@ protected:
   // true if we need to offset the popup to ensure it's not under the mouse
   bool mAdjustOffsetForContextMenu;
   bool mGeneratedChildren; // true if the contents have been created
 
   bool mMenuCanOverlapOSBar;    // can we appear over the taskbar/menubar?
   bool mShouldAutoPosition; // Should SetPopupPosition be allowed to auto position popup?
   bool mInContentShell; // True if the popup is in a content shell
   bool mIsMenuLocked; // Should events inside this menu be ignored?
-  bool mIsDragPopup; // True if this is a popup used for drag feedback
+  bool mMouseTransparent; // True if this is a popup is transparent to mouse events
 
   // the flip modes that were used when the popup was opened
   bool mHFlip;
   bool mVFlip;
 
   static int8_t sDefaultLevelIsTop;
 }; // class nsMenuPopupFrame
 
--- a/layout/xul/nsXULPopupManager.cpp
+++ b/layout/xul/nsXULPopupManager.cpp
@@ -1380,31 +1380,31 @@ nsXULPopupManager::GetTopPopup(nsPopupTy
   return nullptr;
 }
 
 void
 nsXULPopupManager::GetVisiblePopups(nsTArray<nsIFrame *>& aPopups)
 {
   aPopups.Clear();
 
+  // Iterate over both lists of popups
   nsMenuChainItem* item = mPopups;
-  while (item) {
-    if (item->Frame()->PopupState() == ePopupOpenAndVisible)
-      aPopups.AppendElement(static_cast<nsIFrame*>(item->Frame()));
-    item = item->GetParent();
-  }
+  for (int32_t list = 0; list < 2; list++) {
+    while (item) {
+      // Skip panels which are not open and visible as well as popups that
+      // are transparent to mouse events.
+      if (item->Frame()->PopupState() == ePopupOpenAndVisible &&
+          !item->Frame()->IsMouseTransparent()) {
+        aPopups.AppendElement(item->Frame());
+      }
 
-  item = mNoHidePanels;
-  while (item) {
-    // skip panels which are not open and visible as well as draggable popups,
-    // as those don't respond to events.
-    if (item->Frame()->PopupState() == ePopupOpenAndVisible && !item->Frame()->IsDragPopup()) {
-      aPopups.AppendElement(static_cast<nsIFrame*>(item->Frame()));
+      item = item->GetParent();
     }
-    item = item->GetParent();
+
+    item = mNoHidePanels;
   }
 }
 
 already_AddRefed<nsIDOMNode>
 nsXULPopupManager::GetLastTriggerNode(nsIDocument* aDocument, bool aIsTooltip)
 {
   if (!aDocument)
     return nullptr;
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -515,16 +515,25 @@ abstract public class BrowserApp extends
                 hideBrowserSearch();
                 hideHomePager();
 
                 // Re-enable doorhanger notifications. They may trigger on the selected tab above.
                 mDoorHangerPopup.enable();
             }
         });
 
+        mBrowserToolbar.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+            @Override
+            public void onFocusChange(View v, boolean hasFocus) {
+                if (isHomePagerVisible()) {
+                    mHomePager.onToolbarFocusChange(hasFocus);
+                }
+            }
+        });
+
         // Intercept key events for gamepad shortcuts
         mBrowserToolbar.setOnKeyListener(this);
 
         if (mTabsPanel != null) {
             mTabsPanel.setTabsLayoutChangeListener(this);
             updateSideBarState();
         }
 
--- a/mobile/android/base/home/BookmarksPanel.java
+++ b/mobile/android/base/home/BookmarksPanel.java
@@ -2,16 +2,17 @@
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.home;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserContract.Bookmarks;
+import org.mozilla.gecko.db.BrowserContract.URLColumns;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.home.BookmarksListAdapter.FolderInfo;
 import org.mozilla.gecko.home.BookmarksListAdapter.OnRefreshFolderListener;
 import org.mozilla.gecko.home.BookmarksListAdapter.RefreshType;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 
 import android.app.Activity;
 import android.content.Context;
@@ -60,16 +61,32 @@ public class BookmarksPanel extends Home
     private CursorLoaderCallbacks mLoaderCallbacks;
 
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
         final View view = inflater.inflate(R.layout.home_bookmarks_panel, container, false);
 
         mList = (BookmarksListView) view.findViewById(R.id.bookmarks_list);
 
+        mList.setContextMenuInfoFactory(new HomeListView.ContextMenuInfoFactory() {
+            @Override
+            public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) {
+                final int type = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks.TYPE));
+                if (type == Bookmarks.TYPE_FOLDER) {
+                    // We don't show a context menu for folders
+                    return null;
+                }
+                final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id);
+                info.url = cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.URL));
+                info.title = cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.TITLE));
+                info.bookmarkId = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID));
+                return info;
+            }
+        });
+
         return view;
     }
 
     @Override
     public void onViewCreated(View view, Bundle savedInstanceState) {
         super.onViewCreated(view, savedInstanceState);
 
         OnUrlOpenListener listener = null;
--- a/mobile/android/base/home/HomeBanner.java
+++ b/mobile/android/base/home/HomeBanner.java
@@ -92,23 +92,23 @@ public class HomeBanner extends LinearLa
         GeckoAppShell.getEventDispatcher().registerEventListener("HomeBanner:Data", this);
     }
 
     @Override
     public void onDetachedFromWindow() {
         GeckoAppShell.getEventDispatcher().unregisterEventListener("HomeBanner:Data", this);
     }
 
-    public void showBanner() {
+    public void show() {
         if (!mDismissed) {
             GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Get", null));
         }
     }
 
-    public void hideBanner() {
+    public void hide() {
         animateDown();
     }
 
     public void setScrollingPages(boolean scrollingPages) {
         mScrollingPages = scrollingPages;
     }
 
     @Override
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/home/HomeContextMenuInfo.java
@@ -0,0 +1,51 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.home;
+
+import org.mozilla.gecko.db.BrowserContract.Combined;
+import org.mozilla.gecko.util.StringUtils;
+
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+
+
+/**
+ * A ContextMenuInfo for HomeListView
+ */
+public class HomeContextMenuInfo extends AdapterContextMenuInfo {
+
+    public String url;
+    public String title;
+    public boolean isFolder = false;
+    public boolean inReadingList = false;
+    public int display = Combined.DISPLAY_NORMAL;
+    public int historyId = -1;
+    public int bookmarkId = -1;
+
+    public HomeContextMenuInfo(View targetView, int position, long id) {
+        super(targetView, position, id);
+    }
+
+    public boolean hasBookmarkId() {
+        return bookmarkId > -1;
+    }
+
+    public boolean hasHistoryId() {
+        return historyId > -1;
+    }
+
+    public boolean isInReadingList() {
+        return inReadingList;
+    }
+
+    public String getDisplayTitle() {
+        if (!TextUtils.isEmpty(title)) {
+            return title;
+        }
+        return StringUtils.stripCommonSubdomains(StringUtils.stripScheme(url, StringUtils.UrlFlags.STRIP_HTTPS));
+    }
+}
\ No newline at end of file
--- a/mobile/android/base/home/HomeFragment.java
+++ b/mobile/android/base/home/HomeFragment.java
@@ -10,17 +10,17 @@ import org.mozilla.gecko.favicons.Favico
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.ReaderModeUtils;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.home.HomeListView.HomeContextMenuInfo;
+import org.mozilla.gecko.home.HomeContextMenuInfo;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.util.UiAsyncTask;
 
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Bitmap;
 import android.os.Bundle;
@@ -84,22 +84,22 @@ abstract class HomeFragment extends Frag
 
         MenuInflater inflater = new MenuInflater(view.getContext());
         inflater.inflate(R.menu.home_contextmenu, menu);
 
         menu.setHeaderTitle(info.getDisplayTitle());
 
         // Hide the "Edit" menuitem if this item isn't a bookmark,
         // or if this is a reading list item.
-        if (info.bookmarkId < 0 || info.inReadingList) {
+        if (!info.hasBookmarkId() || info.isInReadingList()) {
             menu.findItem(R.id.home_edit_bookmark).setVisible(false);
         }
 
         // Hide the "Remove" menuitem if this item doesn't have a bookmark or history ID.
-        if (info.bookmarkId < 0 && info.historyId < 0) {
+        if (!info.hasBookmarkId() && !info.hasHistoryId()) {
             menu.findItem(R.id.home_remove).setVisible(false);
         }
 
         menu.findItem(R.id.home_share).setVisible(!GeckoProfile.get(getActivity()).inGuestMode());
 
         final boolean canOpenInReader = (info.display == Combined.DISPLAY_READER);
         menu.findItem(R.id.home_open_in_reader).setVisible(canOpenInReader);
     }
@@ -147,17 +147,17 @@ abstract class HomeFragment extends Frag
                 Log.e(LOGTAG, "Can't open in new tab because URL is null");
                 return false;
             }
 
             int flags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_BACKGROUND;
             if (item.getItemId() == R.id.home_open_private_tab)
                 flags |= Tabs.LOADURL_PRIVATE;
 
-            final String url = (info.inReadingList ? ReaderModeUtils.getAboutReaderForUrl(info.url) : info.url);
+            final String url = (info.isInReadingList() ? ReaderModeUtils.getAboutReaderForUrl(info.url) : info.url);
             Tabs.getInstance().loadUrl(url, flags);
             Toast.makeText(context, R.string.new_tab_opened, Toast.LENGTH_SHORT).show();
             return true;
         }
 
         if (itemId == R.id.home_edit_bookmark) {
             // UI Dialog associates to the activity context, not the applications'.
             new EditBookmarkDialog(getActivity()).show(info.url);
@@ -167,25 +167,23 @@ abstract class HomeFragment extends Frag
         if (itemId == R.id.home_open_in_reader) {
             final String url = ReaderModeUtils.getAboutReaderForUrl(info.url);
             Tabs.getInstance().loadUrl(url, Tabs.LOADURL_NONE);
             return true;
         }
 
         if (itemId == R.id.home_remove) {
             // Prioritize removing a history entry over a bookmark in the case of a combined item.
-            final int historyId = info.historyId;
-            if (historyId > -1) {
-                new RemoveHistoryTask(context, historyId).execute();
+            if (info.hasHistoryId()) {
+                new RemoveHistoryTask(context, info.historyId).execute();
                 return true;
             }
 
-            final int bookmarkId = info.bookmarkId;
-            if (bookmarkId > -1) {
-                new RemoveBookmarkTask(context, bookmarkId, info.url, info.inReadingList).execute();
+            if (info.hasBookmarkId()) {
+                new RemoveBookmarkTask(context, info.bookmarkId, info.url, info.isInReadingList()).execute();
                 return true;
             }
         }
 
         return false;
     }
 
     @Override
--- a/mobile/android/base/home/HomeListView.java
+++ b/mobile/android/base/home/HomeListView.java
@@ -1,32 +1,25 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.home;
 
 import org.mozilla.gecko.R;
-import org.mozilla.gecko.db.BrowserContract.Bookmarks;
-import org.mozilla.gecko.db.BrowserContract.Combined;
-import org.mozilla.gecko.db.BrowserContract.URLColumns;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
-import org.mozilla.gecko.util.StringUtils;
 
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.database.Cursor;
 import android.graphics.drawable.Drawable;
-import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.ContextMenu.ContextMenuInfo;
-import android.view.MotionEvent;
 import android.view.View;
-import android.widget.AbsListView.LayoutParams;
 import android.widget.AdapterView;
 import android.widget.AdapterView.OnItemLongClickListener;
 import android.widget.ListView;
 
 /**
  * HomeListView is a custom extension of ListView, that packs a HomeContextMenuInfo
  * when any of its rows is long pressed.
  */
@@ -37,16 +30,19 @@ public class HomeListView extends ListVi
     private HomeContextMenuInfo mContextMenuInfo;
 
     // On URL open listener
     private OnUrlOpenListener mUrlOpenListener;
 
     // Top divider
     private boolean mShowTopDivider;
 
+    // ContextMenuInfo maker
+    private ContextMenuInfoFactory mContextMenuInfoFactory;
+
     public HomeListView(Context context) {
         this(context, null);
     }
 
     public HomeListView(Context context, AttributeSet attrs) {
         this(context, attrs, R.attr.homeListViewStyle);
     }
 
@@ -82,18 +78,24 @@ public class HomeListView extends ListVi
 
     @Override
     public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
         Object item = parent.getItemAtPosition(position);
 
         // HomeListView could hold headers too. Add a context menu info only for its children.
         if (item instanceof Cursor) {
             Cursor cursor = (Cursor) item;
-            mContextMenuInfo = new HomeContextMenuInfo(view, position, id, cursor);
+            if (cursor == null || mContextMenuInfoFactory == null) {
+                mContextMenuInfo = null;
+                return false;
+            }
+
+            mContextMenuInfo = mContextMenuInfoFactory.makeInfoForCursor(view, position, id, cursor);
             return showContextMenuForChild(HomeListView.this);
+
         } else {
             mContextMenuInfo = null;
             return false;
         }
     }
 
     @Override
     public ContextMenuInfo getContextMenuInfo() {
@@ -109,97 +111,27 @@ public class HomeListView extends ListVi
                     position--;
                 }
 
                 listener.onItemClick(parent, view, position, id);
             }
         });
     }
 
+    public void setContextMenuInfoFactory(final ContextMenuInfoFactory factory) {
+        mContextMenuInfoFactory = factory;
+    }
+
     public OnUrlOpenListener getOnUrlOpenListener() {
         return mUrlOpenListener;
     }
 
     public void setOnUrlOpenListener(OnUrlOpenListener listener) {
         mUrlOpenListener = listener;
     }
 
-    /**
-     * A ContextMenuInfo for HomeListView that adds details from the cursor.
+    /*
+     * Interface for creating ContextMenuInfo from cursors
      */
-    public static class HomeContextMenuInfo extends AdapterContextMenuInfo {
-
-        public int bookmarkId;
-        public int historyId;
-        public String url;
-        public String title;
-        public int display;
-        public boolean isFolder;
-        public boolean inReadingList;
-
-        /**
-         * This constructor assumes that the cursor was generated from a query
-         * to either the combined view or the bookmarks table.
-         */
-        public HomeContextMenuInfo(View targetView, int position, long id, Cursor cursor) {
-            super(targetView, position, id);
-
-            if (cursor == null) {
-                return;
-            }
-
-            final int typeCol = cursor.getColumnIndex(Bookmarks.TYPE);
-            if (typeCol != -1) {
-                isFolder = (cursor.getInt(typeCol) == Bookmarks.TYPE_FOLDER);
-            } else {
-                isFolder = false;
-            }
-
-            // We don't show a context menu for folders, so return early.
-            if (isFolder) {
-                return;
-            }
-
-            url = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.URL));
-            title = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.TITLE));
-
-            final int bookmarkIdCol = cursor.getColumnIndex(Combined.BOOKMARK_ID);
-            if (bookmarkIdCol == -1) {
-                // If there isn't a bookmark ID column, this must be a bookmarks cursor,
-                // so the regular ID column will correspond to a bookmark ID.
-                bookmarkId = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID));
-            } else if (cursor.isNull(bookmarkIdCol)) {
-                // If this is a combined cursor, we may get a history item without a
-                // bookmark, in which case the bookmarks ID column value will be null.
-                bookmarkId = -1;
-            } else {
-                bookmarkId = cursor.getInt(bookmarkIdCol);
-            }
-
-            final int historyIdCol = cursor.getColumnIndex(Combined.HISTORY_ID);
-            if (historyIdCol != -1) {
-                historyId = cursor.getInt(historyIdCol);
-            } else {
-                historyId = -1;
-            }
-
-            // We only have the parent column in cursors from getBookmarksInFolder.
-            final int parentCol = cursor.getColumnIndex(Bookmarks.PARENT);
-            if (parentCol != -1) {
-                inReadingList = (cursor.getInt(parentCol) == Bookmarks.FIXED_READING_LIST_ID);
-            } else {
-                inReadingList = false;
-            }
-
-            final int displayCol = cursor.getColumnIndex(Combined.DISPLAY);
-            if (displayCol != -1) {
-                display = cursor.getInt(displayCol);
-            } else {
-                display = Combined.DISPLAY_NORMAL;
-            }
-        }
-
-        public String getDisplayTitle() {
-            return TextUtils.isEmpty(title) ?
-                StringUtils.stripCommonSubdomains(StringUtils.stripScheme(url, StringUtils.UrlFlags.STRIP_HTTPS)) : title;
-        }
+    public interface ContextMenuInfoFactory {
+    	public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor);
     }
 }
--- a/mobile/android/base/home/HomePager.java
+++ b/mobile/android/base/home/HomePager.java
@@ -283,19 +283,19 @@ public class HomePager extends ViewPager
     public void setCurrentItem(int item, boolean smoothScroll) {
         super.setCurrentItem(item, smoothScroll);
 
         if (mDecor != null) {
             mDecor.onPageSelected(item);
         }
         if (mHomeBanner != null) {
             if (item == mDefaultPanelIndex) {
-                mHomeBanner.showBanner();
+                mHomeBanner.show();
             } else {
-                mHomeBanner.hideBanner();
+                mHomeBanner.hide();
             }
         }
     }
 
     @Override
     public boolean onInterceptTouchEvent(MotionEvent event) {
         if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
             // Drop the soft keyboard by stealing focus from the URL bar.
@@ -310,16 +310,24 @@ public class HomePager extends ViewPager
         // Get touches to pages, pass to banner, and forward to pages.
         if (mHomeBanner != null) {
             mHomeBanner.handleHomeTouch(event);
         }
 
         return super.dispatchTouchEvent(event);
     }
 
+    public void onToolbarFocusChange(boolean hasFocus) {
+        if (hasFocus) {
+            mHomeBanner.hide();
+        } else if (mDefaultPanelIndex == getCurrentItem() || getAdapter().getCount() == 0) {
+            mHomeBanner.show();
+        }
+    }
+
     private void updateUiFromPanelConfigs(List<PanelConfig> panelConfigs) {
         // We only care about the adapter if HomePager is currently
         // loaded, which means it's visible in the activity.
         if (!mLoaded) {
             return;
         }
 
         if (mDecor != null) {
@@ -391,19 +399,19 @@ public class HomePager extends ViewPager
         @Override
         public void onPageSelected(int position) {
             if (mDecor != null) {
                 mDecor.onPageSelected(position);
             }
 
             if (mHomeBanner != null) {
                 if (position == mDefaultPanelIndex) {
-                    mHomeBanner.showBanner();
+                    mHomeBanner.show();
                 } else {
-                    mHomeBanner.hideBanner();
+                    mHomeBanner.hide();
                 }
             }
         }
 
         @Override
         public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
             if (mDecor != null) {
                 mDecor.onPageScrolled(position, positionOffset, positionOffsetPixels);
--- a/mobile/android/base/home/LastTabsPanel.java
+++ b/mobile/android/base/home/LastTabsPanel.java
@@ -42,17 +42,17 @@ public class LastTabsPanel extends HomeF
 
     // Cursor loader ID for the session parser
     private static final int LOADER_ID_LAST_TABS = 0;
 
     // Adapter for the list of search results
     private LastTabsAdapter mAdapter;
 
     // The view shown by the fragment.
-    private ListView mList;
+    private HomeListView mList;
 
     // The title for this HomeFragment panel.
     private TextView mTitle;
 
     // The button view for restoring tabs from last session.
     private View mRestoreButton;
 
     // Reference to the View to display when there are no results.
@@ -98,32 +98,42 @@ public class LastTabsPanel extends HomeF
 
     @Override
     public void onViewCreated(View view, Bundle savedInstanceState) {
         mTitle = (TextView) view.findViewById(R.id.title);
         if (mTitle != null) {
             mTitle.setText(R.string.home_last_tabs_title);
         }
 
-        mList = (ListView) view.findViewById(R.id.list);
+        mList = (HomeListView) view.findViewById(R.id.list);
         mList.setTag(HomePager.LIST_TAG_LAST_TABS);
 
         mList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
             @Override
             public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                 final Cursor c = mAdapter.getCursor();
                 if (c == null || !c.moveToPosition(position)) {
                     return;
                 }
 
                 final String url = c.getString(c.getColumnIndexOrThrow(Combined.URL));
                 mNewTabsListener.onNewTabs(new String[] { url });
             }
         });
 
+        mList.setContextMenuInfoFactory(new HomeListView.ContextMenuInfoFactory() {
+            @Override
+            public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) {
+                final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id);
+                info.url = cursor.getString(cursor.getColumnIndexOrThrow(Combined.URL));
+                info.title = cursor.getString(cursor.getColumnIndexOrThrow(Combined.TITLE));
+                return info;
+            }
+        });
+
         registerForContextMenu(mList);
 
         mRestoreButton = view.findViewById(R.id.open_all_tabs_button);
         mRestoreButton.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                 openAllTabs();
             }
--- a/mobile/android/base/home/MostRecentPanel.java
+++ b/mobile/android/base/home/MostRecentPanel.java
@@ -1,17 +1,19 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.home;
 
 import org.mozilla.gecko.R;
+import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.BrowserContract.Bookmarks;
 import org.mozilla.gecko.db.BrowserDB.URLColumns;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 import org.mozilla.gecko.home.TwoLinePageRow;
 
 import android.app.Activity;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.database.Cursor;
@@ -41,17 +43,17 @@ public class MostRecentPanel extends Hom
 
     // Cursor loader ID for history query
     private static final int LOADER_ID_HISTORY = 0;
 
     // Adapter for the list of search results
     private MostRecentAdapter mAdapter;
 
     // The view shown by the fragment.
-    private ListView mList;
+    private HomeListView mList;
 
     // Reference to the View to display when there are no results.
     private View mEmptyView;
 
     // Callbacks used for the search and favicon cursor loaders
     private CursorLoaderCallbacks mCursorLoaderCallbacks;
 
     // On URL open listener
@@ -85,31 +87,50 @@ public class MostRecentPanel extends Hom
 
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
         return inflater.inflate(R.layout.home_most_recent_panel, container, false);
     }
 
     @Override
     public void onViewCreated(View view, Bundle savedInstanceState) {
-        mList = (ListView) view.findViewById(R.id.list);
+        mList = (HomeListView) view.findViewById(R.id.list);
         mList.setTag(HomePager.LIST_TAG_MOST_RECENT);
 
         mList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
             @Override
             public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                 position -= mAdapter.getMostRecentSectionsCountBefore(position);
                 final Cursor c = mAdapter.getCursor(position);
                 final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL));
 
                 // This item is a TwoLinePageRow, so we allow switch-to-tab.
                 mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB));
             }
         });
 
+        mList.setContextMenuInfoFactory(new HomeListView.ContextMenuInfoFactory() {
+            @Override
+            public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) {
+                final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id);
+                info.url = cursor.getString(cursor.getColumnIndexOrThrow(Combined.URL));
+                info.title = cursor.getString(cursor.getColumnIndexOrThrow(Combined.TITLE));
+                info.display = cursor.getInt(cursor.getColumnIndexOrThrow(Combined.DISPLAY));
+                info.historyId = cursor.getInt(cursor.getColumnIndexOrThrow(Combined.HISTORY_ID));
+                final int bookmarkIdCol = cursor.getColumnIndexOrThrow(Combined.BOOKMARK_ID);
+                if (cursor.isNull(bookmarkIdCol)) {
+                    // If this is a combined cursor, we may get a history item without a
+                    // bookmark, in which case the bookmarks ID column value will be null.
+                    info.bookmarkId =  -1;
+                } else {
+                    info.bookmarkId = cursor.getInt(bookmarkIdCol);
+                }
+                return info;
+            }
+        });
         registerForContextMenu(mList);
     }
 
     @Override
     public void onDestroyView() {
         super.onDestroyView();
         mList = null;
         mEmptyView = null;
--- a/mobile/android/base/home/PanelGridItemView.java
+++ b/mobile/android/base/home/PanelGridItemView.java
@@ -32,21 +32,20 @@ public class PanelGridItemView extends F
 
     private final ImageView mThumbnailView;
 
     public PanelGridItemView(Context context) {
         this(context, null);
     }
 
     public PanelGridItemView(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
+        this(context, attrs, R.attr.panelGridItemViewStyle);
     }
 
     public PanelGridItemView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
 
         LayoutInflater.from(context).inflate(R.layout.panel_grid_item_view, this);
         mThumbnailView = (ImageView) findViewById(R.id.image);
-        mThumbnailView.setBackgroundColor(Color.rgb(255, 148, 0));
     }
 
     public void updateFromCursor(Cursor cursor) { }
 }
--- a/mobile/android/base/home/PanelGridView.java
+++ b/mobile/android/base/home/PanelGridView.java
@@ -18,17 +18,17 @@ import android.view.ViewGroup;
 import android.widget.GridView;
 
 public class PanelGridView extends GridView implements DatasetBacked {
     private static final String LOGTAG = "GeckoPanelGridView";
 
     private final PanelGridViewAdapter mAdapter;
 
     public PanelGridView(Context context, ViewConfig viewConfig) {
-        super(context, null, R.attr.homeGridViewStyle);
+        super(context, null, R.attr.panelGridViewStyle);
         mAdapter = new PanelGridViewAdapter(context);
         setAdapter(mAdapter);
         setNumColumns(AUTO_FIT);
     }
 
     @Override
     public void setDataset(Cursor cursor) {
         mAdapter.swapCursor(cursor);
--- a/mobile/android/base/home/ReadingListPanel.java
+++ b/mobile/android/base/home/ReadingListPanel.java
@@ -40,17 +40,17 @@ import java.util.EnumSet;
 public class ReadingListPanel extends HomeFragment {
     // Cursor loader ID for reading list
     private static final int LOADER_ID_READING_LIST = 0;
 
     // Adapter for the list of reading list items
     private ReadingListAdapter mAdapter;
 
     // The view shown by the fragment
-    private ListView mList;
+    private HomeListView mList;
 
     // Reference to the View to display when there are no results.
     private View mEmptyView;
 
     // Reference to top view.
     private View mTopView;
 
     // Callbacks used for the reading list and favicon cursor loaders
@@ -84,17 +84,17 @@ public class ReadingListPanel extends Ho
     }
 
     @Override
     public void onViewCreated(View view, Bundle savedInstanceState) {
         super.onViewCreated(view, savedInstanceState);
 
         mTopView = view;
 
-        mList = (ListView) view.findViewById(R.id.list);
+        mList = (HomeListView) view.findViewById(R.id.list);
         mList.setTag(HomePager.LIST_TAG_READING_LIST);
 
         mList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
             @Override
             public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                 final Cursor c = mAdapter.getCursor();
                 if (c == null || !c.moveToPosition(position)) {
                     return;
@@ -103,16 +103,27 @@ public class ReadingListPanel extends Ho
                 String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL));
                 url = ReaderModeUtils.getAboutReaderForUrl(url);
 
                 // This item is a TwoLinePageRow, so we allow switch-to-tab.
                 mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB));
             }
         });
 
+        mList.setContextMenuInfoFactory(new HomeListView.ContextMenuInfoFactory() {
+            @Override
+            public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) {
+                final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id);
+                info.url = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.URL));
+                info.title = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.TITLE));
+                info.bookmarkId = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID));
+                info.inReadingList = true;
+                return info;
+            }
+        });
         registerForContextMenu(mList);
     }
 
     @Override
     public void onDestroyView() {
         super.onDestroyView();
         mList = null;
         mTopView = null;
--- a/mobile/android/base/home/TopSitesPanel.java
+++ b/mobile/android/base/home/TopSitesPanel.java
@@ -13,17 +13,16 @@ import org.mozilla.gecko.animation.Prope
 import org.mozilla.gecko.animation.ViewHelper;
 import org.mozilla.gecko.db.BrowserContract.Thumbnails;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.BrowserDB.URLColumns;
 import org.mozilla.gecko.db.BrowserDB.TopSitesCursorWrapper;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.gfx.BitmapUtils;
-import org.mozilla.gecko.home.HomeListView.HomeContextMenuInfo;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 import org.mozilla.gecko.home.PinSiteDialog.OnSiteSelectedListener;
 import org.mozilla.gecko.home.TopSitesGridView.OnEditPinnedSiteListener;
 import org.mozilla.gecko.home.TopSitesGridView.TopSitesGridContextMenuInfo;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.app.Activity;
 import android.content.ContentResolver;
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -216,16 +216,17 @@ gbjar.sources += [
     'home/FadedTextView.java',
     'home/FramePanelLayout.java',
     'home/HistoryPanel.java',
     'home/HomeAdapter.java',
     'home/HomeBanner.java',
     'home/HomeConfig.java',
     'home/HomeConfigLoader.java',
     'home/HomeConfigPrefsBackend.java',
+    'home/HomeContextMenuInfo.java',
     'home/HomeFragment.java',
     'home/HomeListView.java',
     'home/HomePager.java',
     'home/HomePagerTabStrip.java',
     'home/LastTabsPanel.java',
     'home/MostRecentPanel.java',
     'home/MultiTypeCursorAdapter.java',
     'home/PanelGridItemView.java',
@@ -357,16 +358,17 @@ gbjar.sources += [
     'widget/DateTimePicker.java',
     'widget/Divider.java',
     'widget/DoorHanger.java',
     'widget/FaviconView.java',
     'widget/FlowLayout.java',
     'widget/GeckoActionProvider.java',
     'widget/GeckoPopupMenu.java',
     'widget/IconTabWidget.java',
+    'widget/SquaredImageView.java',
     'widget/TabRow.java',
     'widget/ThumbnailView.java',
     'widget/TwoWayView.java',
     'ZoomConstraints.java',
 ]
 gbjar.sources += [ thirdparty_source_dir + f for f in [
     'com/googlecode/eyesfree/braille/selfbraille/ISelfBrailleService.java',
     'com/googlecode/eyesfree/braille/selfbraille/SelfBrailleClient.java',
--- a/mobile/android/base/resources/layout/panel_grid_item_view.xml
+++ b/mobile/android/base/resources/layout/panel_grid_item_view.xml
@@ -1,14 +1,12 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <merge xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:gecko="http://schemas.android.com/apk/res-auto">
 
-    <ImageView android:id="@+id/image"
-               android:layout_width="fill_parent"
-               android:layout_height="80dp"
-               android:layout_marginRight="5dp" />
+    <org.mozilla.gecko.widget.SquaredImageView android:id="@+id/image"
+               style="@style/Widget.PanelGridItemImageView" />
 
 </merge>
--- a/mobile/android/base/resources/values-v11/themes.xml
+++ b/mobile/android/base/resources/values-v11/themes.xml
@@ -42,17 +42,18 @@
         <item name="menuItemActionBarStyle">@style/Widget.MenuItemActionBar</item>
         <item name="menuItemActionViewStyle">@style/Widget.MenuItemActionView</item>
         <item name="menuItemDefaultStyle">@style/Widget.MenuItemDefault</item>
         <item name="menuItemSecondaryActionBarStyle">@style/Widget.MenuItemSecondaryActionBar</item>
         <item name="menuItemShareActionButtonStyle">@style/Widget.MenuItemSecondaryActionBar</item>
         <item name="bookmarksListViewStyle">@style/Widget.BookmarksListView</item>
         <item name="topSitesGridItemViewStyle">@style/Widget.TopSitesGridItemView</item>
         <item name="topSitesGridViewStyle">@style/Widget.TopSitesGridView</item>
-        <item name="homeGridViewStyle">@style/Widget.HomeGridView</item>
+        <item name="panelGridViewStyle">@style/Widget.PanelGridView</item>
+        <item name="panelGridItemViewStyle">@style/Widget.PanelGridItemView</item>
         <item name="topSitesThumbnailViewStyle">@style/Widget.TopSitesThumbnailView</item>
         <item name="homeListViewStyle">@style/Widget.HomeListView</item>
         <item name="geckoMenuListViewStyle">@style/Widget.GeckoMenuListView</item>
         <item name="menuItemActionModeStyle">@style/GeckoActionBar.Button</item>
         <item name="android:actionModeStyle">@style/GeckoActionBar</item>
         <item name="android:actionButtonStyle">@style/GeckoActionBar.Button</item>
         <item name="android:actionModeCutDrawable">@drawable/ab_cut</item>
         <item name="android:actionModeCopyDrawable">@drawable/ab_copy</item>
--- a/mobile/android/base/resources/values-xlarge-v11/dimens.xml
+++ b/mobile/android/base/resources/values-xlarge-v11/dimens.xml
@@ -7,10 +7,11 @@
 
     <dimen name="browser_toolbar_height">56dp</dimen>
     <dimen name="remote_tab_child_row_height">56dp</dimen>
     <dimen name="remote_tab_group_row_height">34dp</dimen>
     <dimen name="tabs_counter_size">26sp</dimen>
     <dimen name="tabs_panel_indicator_width">60dp</dimen>
     <dimen name="tabs_panel_list_padding">8dip</dimen>
     <dimen name="history_tab_widget_width">270dp</dimen>
+    <dimen name="panel_grid_view_column_width">250dp</dimen>
 
 </resources>
--- a/mobile/android/base/resources/values/attrs.xml
+++ b/mobile/android/base/resources/values/attrs.xml
@@ -33,16 +33,22 @@
         <attr name="bookmarksListViewStyle" format="reference" />
 
         <!-- Default style for the TopSitesGridItemView -->
         <attr name="topSitesGridItemViewStyle" format="reference" />
 
         <!-- Default style for the HomeGridView -->
         <attr name="homeGridViewStyle" format="reference" />
 
+        <!-- Style for the PanelGridView -->
+        <attr name="panelGridViewStyle" format="reference" />
+
+        <!-- Default style for the PanelGridItemView -->
+        <attr name="panelGridItemViewStyle" format="reference" />
+
         <!-- Default style for the TopSitesGridView -->
         <attr name="topSitesGridViewStyle" format="reference" />
 
         <!-- Default style for the TopSitesThumbnailView -->
         <attr name="topSitesThumbnailViewStyle" format="reference" />
 
         <!-- Default style for the HomeListView -->
         <attr name="homeListViewStyle" format="reference" />
--- a/mobile/android/base/resources/values/colors.xml
+++ b/mobile/android/base/resources/values/colors.xml
@@ -85,10 +85,11 @@
   <color name="url_bar_urltext">#A6A6A6</color>
   <color name="url_bar_domaintext">#000</color>
   <color name="url_bar_domaintext_private">#FFF</color>
   <color name="url_bar_blockedtext">#b14646</color>
   <color name="url_bar_shadow">#12000000</color>
 
   <color name="home_last_tab_bar_bg">#FFF5F7F9</color>
 
+  <color name="panel_grid_item_image_background">#D1D9E1</color>
 </resources>
 
--- a/mobile/android/base/resources/values/dimens.xml
+++ b/mobile/android/base/resources/values/dimens.xml
@@ -96,9 +96,12 @@
     <dimen name="page_action_button_width">32dp</dimen>
 
     <!-- Banner -->
     <dimen name="home_banner_height">72dp</dimen>
 
     <!-- Icon Grid -->
     <dimen name="icongrid_columnwidth">128dp</dimen>
     <dimen name="icongrid_padding">16dp</dimen>
+
+    <!-- PanelGridView dimensions -->
+    <dimen name="panel_grid_view_column_width">180dp</dimen>
 </resources>
--- a/mobile/android/base/resources/values/styles.xml
+++ b/mobile/android/base/resources/values/styles.xml
@@ -140,16 +140,39 @@
 
     <style name="Widget.TopSitesGridItemView">
       <item name="android:layout_width">fill_parent</item>
       <item name="android:layout_height">fill_parent</item>
       <item name="android:padding">5dip</item>
       <item name="android:orientation">vertical</item>
     </style>
 
+    <style name="Widget.PanelGridView" parent="Widget.GridView">
+        <item name="android:layout_width">fill_parent</item>
+        <item name="android:layout_height">fill_parent</item>
+        <item name="android:paddingTop">0dp</item>
+        <item name="android:stretchMode">columnWidth</item>
+        <item name="android:columnWidth">@dimen/panel_grid_view_column_width</item>
+        <item name="android:horizontalSpacing">2dp</item>
+        <item name="android:verticalSpacing">2dp</item>
+    </style>
+
+    <style name="Widget.PanelGridItemView">
+      <item name="android:layout_height">wrap_content</item>
+      <item name="android:layout_width">wrap_content</item>
+    </style>
+
+    <style name="Widget.PanelGridItemImageView">
+      <item name="android:layout_height">@dimen/panel_grid_view_column_width</item>
+      <item name="android:layout_width">fill_parent</item>
+      <item name="android:scaleType">centerCrop</item>
+      <item name="android:adjustViewBounds">true</item>
+      <item name="android:background">@color/panel_grid_item_image_background</item>
+    </style>
+
     <style name="Widget.BookmarkItemView" parent="Widget.TwoLineRow"/>
 
     <style name="Widget.BookmarksListView" parent="Widget.HomeListView"/>
 
     <style name="Widget.TopSitesThumbnailView">
       <item name="android:padding">0dip</item>
       <item name="android:scaleType">centerCrop</item>
     </style>
--- a/mobile/android/base/resources/values/themes.xml
+++ b/mobile/android/base/resources/values/themes.xml
@@ -75,17 +75,18 @@
         <item name="android:dropDownItemStyle">@style/Widget.DropDownItem</item>
         <item name="android:editTextStyle">@style/Widget.EditText</item>
         <item name="android:gridViewStyle">@style/Widget.GridView</item>
         <item name="android:textViewStyle">@style/Widget.TextView</item>
         <item name="android:spinnerStyle">@style/Widget.Spinner</item>
         <item name="bookmarksListViewStyle">@style/Widget.BookmarksListView</item>
         <item name="topSitesGridItemViewStyle">@style/Widget.TopSitesGridItemView</item>
         <item name="topSitesGridViewStyle">@style/Widget.TopSitesGridView</item>
-        <item name="homeGridViewStyle">@style/Widget.HomeGridView</item>
+        <item name="panelGridViewStyle">@style/Widget.PanelGridView</item>
+        <item name="panelGridItemViewStyle">@style/Widget.PanelGridItemView</item>
         <item name="topSitesThumbnailViewStyle">@style/Widget.TopSitesThumbnailView</item>
         <item name="homeListViewStyle">@style/Widget.HomeListView</item>
         <item name="geckoMenuListViewStyle">@style/Widget.GeckoMenuListView</item>
         <item name="menuItemDefaultStyle">@style/Widget.MenuItemDefault</item>
         <item name="menuItemActionBarStyle">@style/Widget.MenuItemActionBar</item>
         <item name="menuItemActionModeStyle">@style/GeckoActionBar.Button</item>
     </style>
 
--- a/mobile/android/base/toolbar/BrowserToolbar.java
+++ b/mobile/android/base/toolbar/BrowserToolbar.java
@@ -137,16 +137,17 @@ public class BrowserToolbar extends Geck
     private List<View> mFocusOrder;
 
     private OnActivateListener mActivateListener;
     private OnCommitListener mCommitListener;
     private OnDismissListener mDismissListener;
     private OnFilterListener mFilterListener;
     private OnStartEditingListener mStartEditingListener;
     private OnStopEditingListener mStopEditingListener;
+    private OnFocusChangeListener mFocusChangeListener;
 
     final private BrowserApp mActivity;
     private boolean mHasSoftMenuButton;
 
     private UIMode mUIMode;
     private boolean mAnimatingEntry;
 
     private int mUrlBarViewOffset;
@@ -310,16 +311,19 @@ public class BrowserToolbar extends Geck
                 setContentDescription(contentDescription);
             }
         });
 
         mUrlEditLayout.setOnFocusChangeListener(new View.OnFocusChangeListener() {
             @Override
             public void onFocusChange(View v, boolean hasFocus) {
                 setSelected(hasFocus);
+                if (mFocusChangeListener != null) {
+                    mFocusChangeListener.onFocusChange(v, hasFocus);
+                }
             }
         });
 
         mTabs.setOnClickListener(new Button.OnClickListener() {
             @Override
             public void onClick(View v) {
                 toggleTabs();
             }
@@ -788,16 +792,20 @@ public class BrowserToolbar extends Geck
     public void setOnStartEditingListener(OnStartEditingListener listener) {
         mStartEditingListener = listener;
     }
 
     public void setOnStopEditingListener(OnStopEditingListener listener) {
         mStopEditingListener = listener;
     }
 
+    public void setOnFocusChangeListener(OnFocusChangeListener listener) {
+        mFocusChangeListener = listener;
+    }
+
     private void showUrlEditLayout() {
         setUrlEditLayoutVisibility(true, null);
     }
 
     private void showUrlEditLayout(PropertyAnimator animator) {
         setUrlEditLayoutVisibility(true, animator);
     }
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/widget/SquaredImageView.java
@@ -0,0 +1,21 @@
+package org.mozilla.gecko.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+final class SquaredImageView extends ImageView {
+    public SquaredImageView(Context context) {
+        super(context);
+    }
+
+    public SquaredImageView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth());
+    }
+}
\ No newline at end of file
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -90,27 +90,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   let [name, script] = aScript;
   XPCOMUtils.defineLazyGetter(window, name, function() {
     let sandbox = {};
     Services.scriptloader.loadSubScript(script, sandbox);
     return sandbox[name];
   });
 });
 
-// Lazily-loaded browser scripts that use observer notifcations:
-var LazyNotificationGetter = {
-  observers: [],
-  shutdown: function lng_shutdown() {
-    this.observers.forEach(function(o) {
-      Services.obs.removeObserver(o, o.notification);
-    });
-    this.observers = [];
-  }
-};
-
 [
 #ifdef MOZ_WEBRTC
   ["WebrtcUI", ["getUserMedia:request", "recording-device-events"], "chrome://browser/content/WebrtcUI.js"],
 #endif
   ["MemoryObserver", ["memory-pressure", "Memory:Dump"], "chrome://browser/content/MemoryObserver.js"],
   ["ConsoleAPI", ["console-api-log-event"], "chrome://browser/content/ConsoleAPI.js"],
   ["FindHelper", ["FindInPage:Find", "FindInPage:Prev", "FindInPage:Next", "FindInPage:Closed", "Tab:Selected"], "chrome://browser/content/FindHelper.js"],
   ["PermissionsHelper", ["Permissions:Get", "Permissions:Clear"], "chrome://browser/content/PermissionsHelper.js"],
@@ -120,40 +109,32 @@ var LazyNotificationGetter = {
 ].forEach(function (aScript) {
   let [name, notifications, script] = aScript;
   XPCOMUtils.defineLazyGetter(window, name, function() {
     let sandbox = {};
     Services.scriptloader.loadSubScript(script, sandbox);
     return sandbox[name];
   });
   notifications.forEach(function (aNotification) {
-    let o = {
-      notification: aNotification,
-      observe: function(s, t, d) {
-        window[name].observe(s, t, d);
-      }
-    };
-    Services.obs.addObserver(o, aNotification, false);
-    LazyNotificationGetter.observers.push(o);
+    Services.obs.addObserver(function(s, t, d) {
+        window[name].observe(s, t, d)
+    }, aNotification, false);
   });
 });
 
 // Lazily-loaded JS modules that use observer notifications
 [
   ["Home", ["HomePanels:Get"], "resource://gre/modules/Home.jsm"],
 ].forEach(module => {
   let [name, notifications, resource] = module;
   XPCOMUtils.defineLazyModuleGetter(this, name, resource);
   notifications.forEach(notification => {
-    let o = {
-      notification: notification,
-      observe: (s, t, d) => this[name].observe(s, t, d)
-    };
-    Services.obs.addObserver(o, notification, false);
-    LazyNotificationGetter.observers.push(o);
+    Services.obs.addObserver((s,t,d) => {
+      this[name].observe(s,t,d)
+    }, notification, false);
   });
 });
 
 XPCOMUtils.defineLazyServiceGetter(this, "Haptic",
   "@mozilla.org/widget/hapticfeedback;1", "nsIHapticFeedback");
 
 XPCOMUtils.defineLazyServiceGetter(this, "DOMUtils",
   "@mozilla.org/inspector/dom-utils;1", "inIDOMUtils");
--- a/services/common/Makefile.in
+++ b/services/common/Makefile.in
@@ -1,13 +1,14 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 modules := \
+  hawk.js \
   storageservice.js \
   stringbundle.js \
   tokenserverclient.js \
   utils.js \
   $(NULL)
 
 pp_modules := \
   async.js \
new file mode 100644
--- /dev/null
+++ b/services/common/hawk.js
@@ -0,0 +1,201 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/*
+ * HAWK is an HTTP authentication scheme using a message authentication code
+ * (MAC) algorithm to provide partial HTTP request cryptographic verification.
+ *
+ * For details, see: https://github.com/hueniverse/hawk
+ *
+ * With HAWK, it is essential that the clocks on clients and server not have an
+ * absolute delta of greater than one minute, as the HAWK protocol uses
+ * timestamps to reduce the possibility of replay attacks.  However, it is
+ * likely that some clients' clocks will be more than a little off, especially
+ * in mobile devices, which would break HAWK-based services (like sync and
+ * firefox accounts) for those clients.
+ *
+ * This library provides a stateful HAWK client that calculates (roughly) the
+ * clock delta on the client vs the server.  The library provides an interface
+ * for deriving HAWK credentials and making HAWK-authenticated REST requests to
+ * a single remote server.  Therefore, callers who want to interact with
+ * multiple HAWK services should instantiate one HawkClient per service.
+ */
+
+this.EXPORTED_SYMBOLS = ["HawkClient"];
+
+const {interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/FxAccountsCommon.js");
+Cu.import("resource://services-common/utils.js");
+Cu.import("resource://services-crypto/utils.js");
+Cu.import("resource://services-common/rest.js");
+Cu.import("resource://gre/modules/Promise.jsm");
+
+/*
+ * A general purpose client for making HAWK authenticated requests to a single
+ * host.  Keeps track of the clock offset between the client and the host for
+ * computation of the timestamp in the HAWK Authorization header.
+ *
+ * Clients should create one HawkClient object per each server they wish to
+ * interact with.
+ *
+ * @param host
+ *        The url of the host
+ */
+function HawkClient(host) {
+  this.host = host;
+
+  // Clock offset in milliseconds between our client's clock and the date
+  // reported in responses from our host.
+  this._localtimeOffsetMsec = 0;
+}
+
+HawkClient.prototype = {
+
+  /*
+   * Construct an error message for a response.  Private.
+   *
+   * @param restResponse
+   *        A RESTResponse object from a RESTRequest
+   *
+   * @param errorString
+   *        A string describing the error
+   */
+  _constructError: function(restResponse, errorString) {
+    return {
+      error: errorString,
+      message: restResponse.statusText,
+      code: restResponse.status,
+      errno: restResponse.status
+    };
+  },
+
+  /*
+   *
+   * Update clock offset by determining difference from date gives in the (RFC
+   * 1123) Date header of a server response.  Because HAWK tolerates a window
+   * of one minute of clock skew (so two minutes total since the skew can be
+   * positive or negative), the simple method of calculating offset here is
+   * probably good enough.  We keep the value in milliseconds to make life
+   * easier, even though the value will not have millisecond accuracy.
+   *
+   * @param dateString
+   *        An RFC 1123 date string (e.g., "Mon, 13 Jan 2014 21:45:06 GMT")
+   *
+   * For HAWK clock skew and replay protection, see
+   * https://github.com/hueniverse/hawk#replay-protection
+   */
+  _updateClockOffset: function(dateString) {
+    try {
+      let serverDateMsec = Date.parse(dateString);
+      this._localtimeOffsetMsec = serverDateMsec - this.now();
+      log.debug("Clock offset vs " + this.host + ": " + this._localtimeOffsetMsec);
+    } catch(err) {
+      log.warn("Bad date header in server response: " + dateString);
+    }
+  },
+
+  /*
+   * Get the current clock offset in milliseconds.
+   *
+   * The offset is the number of milliseconds that must be added to the client
+   * clock to make it equal to the server clock.  For example, if the client is
+   * five minutes ahead of the server, the localtimeOffsetMsec will be -300000.
+   */
+  get localtimeOffsetMsec() {
+    return this._localtimeOffsetMsec;
+  },
+
+  /*
+   * return current time in milliseconds
+   */
+  now: function() {
+    return Date.now();
+  },
+
+  /* A general method for sending raw RESTRequest calls authorized using HAWK
+   *
+   * @param path
+   *        API endpoint path
+   * @param method
+   *        The HTTP request method
+   * @param credentials
+   *        Hawk credentials
+   * @param payloadObj
+   *        An object that can be encodable as JSON as the payload of the
+   *        request
+   * @return Promise
+   *        Returns a promise that resolves to the text response of the API call,
+   *        or is rejected with an error.  If the server response can be parsed
+   *        as JSON and contains an 'error' property, the promise will be
+   *        rejected with this JSON-parsed response.
+   */
+  request: function(path, method, credentials=null, payloadObj={}, retryOK=true) {
+    method = method.toLowerCase();
+
+    let deferred = Promise.defer();
+    let uri = this.host + path;
+    let self = this;
+
+    function onComplete(error) {
+      let restResponse = this.response;
+      let status = restResponse.status;
+
+      log.debug("(Response) code: " + status +
+                " - Status text: " + restResponse.statusText,
+                " - Response text: " + restResponse.body);
+
+      if (error) {
+        // When things really blow up, reconstruct an error object that follows
+        // the general format of the server on error responses.
+        return deferred.reject(self._constructError(restResponse, error));
+      }
+
+      self._updateClockOffset(restResponse.headers["date"]);
+
+      if (status === 401 && retryOK) {
+        // Retry once if we were rejected due to a bad timestamp.
+        // Clock offset is adjusted already in the top of this function.
+        log.debug("Received 401 for " + path + ": retrying");
+        return deferred.resolve(
+            self.request(path, method, credentials, payloadObj, false));
+      }
+
+      // If the server returned a json error message, use it in the rejection
+      // of the promise.
+      //
+      // In the case of a 401, in which we are probably being rejected for a
+      // bad timestamp, retry exactly once, during which time clock offset will
+      // be adjusted.
+
+      let jsonResponse = {};
+      try {
+        jsonResponse = JSON.parse(restResponse.body);
+      } catch(notJSON) {}
+
+      let okResponse = (200 <= status && status < 300);
+      if (!okResponse || jsonResponse.error) {
+        if (jsonResponse.error) {
+          return deferred.reject(jsonResponse);
+        }
+        return deferred.reject(self._constructError(restResponse, "Request failed"));
+      }
+      // It's up to the caller to know how to decode the response.
+      // We just return the raw text.
+      deferred.resolve(this.response.body);
+    };
+
+    let extra = {
+      now: this.now(),
+      localtimeOffsetMsec: this.localtimeOffsetMsec,
+    };
+
+    let request = new HAWKAuthenticatedRESTRequest(uri, credentials, extra);
+    request[method](payloadObj, onComplete);
+
+    return deferred.promise;
+  }
+}
--- a/services/common/rest.js
+++ b/services/common/rest.js
@@ -4,17 +4,18 @@
 
 #ifndef MERGED_COMPARTMENT
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 this.EXPORTED_SYMBOLS = [
   "RESTRequest",
   "RESTResponse",
-  "TokenAuthenticatedRESTRequest"
+  "TokenAuthenticatedRESTRequest",
+  "HAWKAuthenticatedRESTRequest",
 ];
 
 #endif
 
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Log.jsm");
@@ -142,16 +143,21 @@ RESTRequest.prototype = {
 
   NOT_SENT:    0,
   SENT:        1,
   IN_PROGRESS: 2,
   COMPLETED:   4,
   ABORTED:     8,
 
   /**
+   * HTTP status text of response
+   */
+  statusText: null,
+
+  /**
    * Request timeout (in seconds, though decimal values can be used for
    * up to millisecond granularity.)
    *
    * 0 for no timeout.
    */
   timeout: null,
 
   /**
@@ -607,35 +613,49 @@ RESTResponse.prototype = {
   request: null,
 
   /**
    * HTTP status code
    */
   get status() {
     let status;
     try {
-      let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel);
-      status = channel.responseStatus;
+      status = this.request.channel.responseStatus;
     } catch (ex) {
       this._log.debug("Caught exception fetching HTTP status code:" +
                       CommonUtils.exceptionStr(ex));
       return null;
     }
     delete this.status;
     return this.status = status;
   },
 
   /**
+   * HTTP status text
+   */
+  get statusText() {
+    let statusText;
+    try {
+      statusText = this.request.channel.responseStatusText;
+    } catch (ex) {
+      this._log.debug("Caught exception fetching HTTP status text:" +
+                      CommonUtils.exceptionStr(ex));
+      return null;
+    }
+    delete this.statusText;
+    return this.statusText = statusText;
+  },
+
+  /**
    * Boolean flag that indicates whether the HTTP status code is 2xx or not.
    */
   get success() {
     let success;
     try {
-      let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel);
-      success = channel.requestSucceeded;
+      success = this.request.channel.requestSucceeded;
     } catch (ex) {
       this._log.debug("Caught exception fetching HTTP success flag:" +
                       CommonUtils.exceptionStr(ex));
       return null;
     }
     delete this.success;
     return this.success = success;
   },
@@ -699,8 +719,65 @@ TokenAuthenticatedRESTRequest.prototype 
 
     this.setHeader("Authorization", sig.getHeader());
 
     return RESTRequest.prototype.dispatch.call(
       this, method, data, onComplete, onProgress
     );
   },
 };
+
+/**
+ * Single-use HAWK-authenticated HTTP requests to RESTish resources.
+ *
+ * @param uri
+ *        (String) URI for the RESTRequest constructor
+ *
+ * @param credentials
+ *        (Object) Optional credentials for computing HAWK authentication
+ *        header.
+ *
+ * @param payloadObj
+ *        (Object) Optional object to be converted to JSON payload
+ *
+ * @param extra
+ *        (Object) Optional extra params for HAWK header computation.
+ *        Valid properties are:
+ *
+ *          now:                 <current time in milliseconds>,
+ *          localtimeOffsetMsec: <local clock offset vs server>
+ *
+ * extra.localtimeOffsetMsec is the value in milliseconds that must be added to
+ * the local clock to make it agree with the server's clock.  For instance, if
+ * the local clock is two minutes ahead of the server, the time offset in
+ * milliseconds will be -120000.
+ */
+this.HAWKAuthenticatedRESTRequest =
+ function HawkAuthenticatedRESTRequest(uri, credentials, extra={}) {
+  RESTRequest.call(this, uri);
+
+  this.credentials = credentials;
+  this.now = extra.now || Date.now();
+  this.localtimeOffsetMsec = extra.localtimeOffsetMsec || 0;
+  this._log.trace("local time, offset: " + this.now + ", " + (this.localtimeOffsetMsec));
+};
+HAWKAuthenticatedRESTRequest.prototype = {
+  __proto__: RESTRequest.prototype,
+
+  dispatch: function dispatch(method, data, onComplete, onProgress) {
+    if (this.credentials) {
+      let options = {
+        now: this.now,
+        localtimeOffsetMsec: this.localtimeOffsetMsec,
+        credentials: this.credentials,
+        payload: data && JSON.stringify(data) || "",
+        contentType: "application/json; charset=utf-8",
+      };
+      let header = CryptoUtils.computeHAWK(this.uri, method, options);
+      this.setHeader("Authorization", header.field);
+      this._log.trace("hawk auth header: " + header.field);
+    }
+
+    return RESTRequest.prototype.dispatch.call(
+      this, method, data, onComplete, onProgress
+    );
+  }
+};
new file mode 100644
--- /dev/null
+++ b/services/common/tests/unit/test_hawk.js
@@ -0,0 +1,485 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://services-common/hawk.js");
+
+const SECOND_MS = 1000;
+const MINUTE_MS = SECOND_MS * 60;
+const HOUR_MS = MINUTE_MS * 60;
+
+const TEST_CREDS = {
+  id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
+  key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
+  algorithm: "sha256"
+};
+
+initTestLogging("Trace");
+
+add_task(function test_now() {
+  let client = new HawkClient("https://example.com");
+
+  do_check_true(client.now() - Date.now() < SECOND_MS);
+  run_next_test();
+});
+
+add_task(function test_updateClockOffset() {
+  let client = new HawkClient("https://example.com");
+
+  let now = new Date();
+  let serverDate = now.toUTCString();
+
+  // Client's clock is off
+  client.now = () => { return now.valueOf() + HOUR_MS; }
+
+  client._updateClockOffset(serverDate);
+
+  // Check that they're close; there will likely be a one-second rounding
+  // error, so checking strict equality will likely fail.
+  //
+  // localtimeOffsetMsec is how many milliseconds to add to the local clock so
+  // that it agrees with the server.  We are one hour ahead of the server, so
+  // our offset should be -1 hour.
+  do_check_true(Math.abs(client.localtimeOffsetMsec + HOUR_MS) <= SECOND_MS);
+
+  run_next_test();
+});
+
+add_task(function test_authenticated_get_request() {
+  let message = "{\"msg\": \"Great Success!\"}";
+  let method = "GET";
+
+  let server = httpd_setup({"/foo": (request, response) => {
+      do_check_true(request.hasHeader("Authorization"));
+
+      response.setStatusLine(request.httpVersion, 200, "OK");
+      response.bodyOutputStream.write(message, message.length);
+    }
+  });
+
+  let client = new HawkClient(server.baseURI);
+
+  let response = yield client.request("/foo", method, TEST_CREDS);
+  let result = JSON.parse(response);
+
+  do_check_eq("Great Success!", result.msg);
+
+  yield deferredStop(server);
+});
+
+add_task(function test_authenticated_post_request() {
+  let method = "POST";
+
+  let server = httpd_setup({"/foo": (request, response) => {
+      do_check_true(request.hasHeader("Authorization"));
+
+      response.setStatusLine(request.httpVersion, 200, "OK");
+      response.setHeader("Content-Type", "application/json");
+      response.bodyOutputStream.writeFrom(request.bodyInputStream, request.bodyInputStream.available());
+    }
+  });
+
+  let client = new HawkClient(server.baseURI);
+
+  let response = yield client.request("/foo", method, TEST_CREDS, {foo: "bar"});
+  let result = JSON.parse(response);
+
+  do_check_eq("bar", result.foo);
+
+  yield deferredStop(server);
+});
+
+add_task(function test_credentials_optional() {
+  let method = "GET";
+  let server = httpd_setup({
+    "/foo": (request, response) => {
+      do_check_false(request.hasHeader("Authorization"));
+
+      let message = JSON.stringify({msg: "you're in the friend zone"});
+      response.setStatusLine(request.httpVersion, 200, "OK");
+      response.setHeader("Content-Type", "application/json");
+      response.bodyOutputStream.write(message, message.length);
+    }
+  });
+
+  let client = new HawkClient(server.baseURI);
+  let result = yield client.request("/foo", method); // credentials undefined
+  do_check_eq(JSON.parse(result).msg, "you're in the friend zone");
+  yield deferredStop(server);
+});
+
+add_task(function test_server_error() {
+  let message = "Ohai!";
+  let method = "GET";
+
+  let server = httpd_setup({"/foo": (request, response) => {
+      response.setStatusLine(request.httpVersion, 418, "I am a Teapot");
+      response.bodyOutputStream.write(message, message.length);
+    }
+  });
+
+  let client = new HawkClient(server.baseURI);
+
+  try {
+    yield client.request("/foo", method, TEST_CREDS);
+  } catch(err) {
+    do_check_eq(418, err.code);
+    do_check_eq("I am a Teapot", err.message);
+  }
+
+  yield deferredStop(server);
+});
+
+add_task(function test_server_error_json() {
+  let message = JSON.stringify({error: "Cannot get ye flask."});
+  let method = "GET";
+
+  let server = httpd_setup({"/foo": (request, response) => {
+      response.setStatusLine(request.httpVersion, 400, "What wouldst thou deau?");
+      response.bodyOutputStream.write(message, message.length);
+    }
+  });
+
+  let client = new HawkClient(server.baseURI);
+
+  try {
+    yield client.request("/foo", method, TEST_CREDS);
+  } catch(err) {
+    do_check_eq("Cannot get ye flask.", err.error);
+  }
+
+  yield deferredStop(server);
+});
+
+add_task(function test_offset_after_request() {
+  let message = "Ohai!";
+  let method = "GET";
+
+  let server = httpd_setup({"/foo": (request, response) => {
+      response.setStatusLine(request.httpVersion, 200, "OK");
+      response.bodyOutputStream.write(message, message.length);
+    }
+  });
+
+  let client = new HawkClient(server.baseURI);
+  let now = Date.now();
+  client.now = () => { return now + HOUR_MS; };
+
+  do_check_eq(client.localtimeOffsetMsec, 0);
+
+  let response = yield client.request("/foo", method, TEST_CREDS);
+  // Should be about an hour off
+  do_check_true(Math.abs(client.localtimeOffsetMsec + HOUR_MS) < SECOND_MS);
+
+  yield deferredStop(server);
+});
+
+add_task(function test_offset_in_hawk_header() {
+  let message = "Ohai!";
+  let method = "GET";
+
+  let server = httpd_setup({
+    "/first": function(request, response) {
+      response.setStatusLine(request.httpVersion, 200, "OK");
+      response.bodyOutputStream.write(message, message.length);
+    },
+
+    "/second": function(request, response) {
+      // We see a better date now in the ts component of the header
+      let delta = getTimestampDelta(request.getHeader("Authorization"));
+      let message = "Delta: " + delta;
+
+      // We're now within HAWK's one-minute window.
+      // I hope this isn't a recipe for intermittent oranges ...
+      if (delta < MINUTE_MS) {
+        response.setStatusLine(request.httpVersion, 200, "OK");
+      } else {
+        response.setStatusLine(request.httpVersion, 400, "Delta: " + delta);
+      }
+      response.bodyOutputStream.write(message, message.length);
+    }
+  });
+
+  let client = new HawkClient(server.baseURI);
+  function getOffset() {
+    return client.localtimeOffsetMsec;
+  }
+
+  client.now = () => {
+    return Date.now() + 12 * HOUR_MS;
+  };
+
+  // We begin with no offset
+  do_check_eq(client.localtimeOffsetMsec, 0);
+  yield client.request("/first", method, TEST_CREDS);
+
+  // After the first server response, our offset is updated to -12 hours.
+  // We should be safely in the window, now.
+  do_check_true(Math.abs(client.localtimeOffsetMsec + 12 * HOUR_MS) < MINUTE_MS);
+  yield client.request("/second", method, TEST_CREDS);
+
+  yield deferredStop(server);
+});
+
+add_task(function test_2xx_success() {
+  // Just to ensure that we're not biased toward 200 OK for success
+  let credentials = {
+    id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
+    key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
+    algorithm: "sha256"
+  };
+  let method = "GET";
+
+  let server = httpd_setup({"/foo": (request, response) => {
+      response.setStatusLine(request.httpVersion, 202, "Accepted");
+    }
+  });
+
+  let client = new HawkClient(server.baseURI);
+
+  let response = yield client.request("/foo", method, credentials);
+
+  // Shouldn't be any content in a 202
+  do_check_eq(response, "");
+
+  yield deferredStop(server);
+
+});
+
+add_task(function test_retry_request_on_fail() {
+  let attempts = 0;
+  let credentials = {
+    id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
+    key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
+    algorithm: "sha256"
+  };
+  let method = "GET";
+
+  let server = httpd_setup({
+    "/maybe": function(request, response) {
+      // This path should be hit exactly twice; once with a bad timestamp, and
+      // again when the client retries the request with a corrected timestamp.
+      attempts += 1;
+      do_check_true(attempts <= 2);
+
+      let delta = getTimestampDelta(request.getHeader("Authorization"));
+
+      // First time through, we should have a bad timestamp
+      if (attempts === 1) {
+        do_check_true(delta > MINUTE_MS);
+        let message = "never!!!";
+        response.setStatusLine(request.httpVersion, 401, "Unauthorized");
+        return response.bodyOutputStream.write(message, message.length);
+      }
+
+      // Second time through, timestamp should be corrected by client
+      do_check_true(delta < MINUTE_MS);
+      let message = "i love you!!!";
+      response.setStatusLine(request.httpVersion, 200, "OK");
+      response.bodyOutputStream.write(message, message.length);
+    }
+  });
+
+  let client = new HawkClient(server.baseURI);
+  function getOffset() {
+    return client.localtimeOffsetMsec;
+  }
+
+  client.now = () => {
+    return Date.now() + 12 * HOUR_MS;
+  };
+
+  // We begin with no offset
+  do_check_eq(client.localtimeOffsetMsec, 0);
+
+  // Request will have bad timestamp; client will retry once
+  let response = yield client.request("/maybe", method, credentials);
+  do_check_eq(response, "i love you!!!");
+
+  yield deferredStop(server);
+});
+
+add_task(function test_multiple_401_retry_once() {
+  // Like test_retry_request_on_fail, but always return a 401
+  // and ensure that the client only retries once.
+  let attempts = 0;
+  let credentials = {
+    id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
+    key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
+    algorithm: "sha256"
+  };
+  let method = "GET";
+
+  let server = httpd_setup({
+    "/maybe": function(request, response) {
+      // This path should be hit exactly twice; once with a bad timestamp, and
+      // again when the client retries the request with a corrected timestamp.
+      attempts += 1;
+
+      do_check_true(attempts <= 2);
+
+      let message = "never!!!";
+      response.setStatusLine(request.httpVersion, 401, "Unauthorized");
+      response.bodyOutputStream.write(message, message.length);
+    }
+  });
+
+  let client = new HawkClient(server.baseURI);
+  function getOffset() {
+    return client.localtimeOffsetMsec;
+  }
+
+  client.now = () => {
+    return Date.now() - 12 * HOUR_MS;
+  };
+
+  // We begin with no offset
+  do_check_eq(client.localtimeOffsetMsec, 0);
+
+  // Request will have bad timestamp; client will retry once
+  try {
+    yield client.request("/maybe", method, credentials);
+  } catch (err) {
+    do_check_eq(err.code, 401);
+  }
+  do_check_eq(attempts, 2);
+
+  yield deferredStop(server);
+});
+
+add_task(function test_500_no_retry() {
+  // If we get a 500 error, the client should not retry (as it would with a
+  // 401)
+  let attempts = 0;
+  let credentials = {
+    id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
+    key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
+    algorithm: "sha256"
+  };
+  let method = "GET";
+
+  let server = httpd_setup({
+    "/no-shutup": function() {
+      attempts += 1;
+      let message = "Cannot get ye flask.";
+      response.setStatusLine(request.httpVersion, 500, "Internal server error");
+      response.bodyOutputStream.write(message, message.length);
+    }
+  });
+
+  let client = new HawkClient(server.baseURI);
+  function getOffset() {
+    return client.localtimeOffsetMsec;
+  }
+
+  // Throw off the clock so the HawkClient would want to retry the request if
+  // it could
+  client.now = () => {
+    return Date.now() - 12 * HOUR_MS;
+  };
+
+  // Request will 500; no retries
+  try {
+    yield client.request("/no-shutup", method, credentials);
+  } catch(err) {
+    do_check_eq(err.code, 500);
+  }
+  do_check_eq(attempts, 1);
+
+  yield deferredStop(server);
+
+});
+
+add_task(function test_401_then_500() {
+  // Like test_multiple_401_retry_once, but return a 500 to the
+  // second request, ensuring that the promise is properly rejected
+  // in client.request.
+  let attempts = 0;
+  let credentials = {
+    id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
+    key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
+    algorithm: "sha256"
+  };
+  let method = "GET";
+
+  let server = httpd_setup({
+    "/maybe": function(request, response) {
+      // This path should be hit exactly twice; once with a bad timestamp, and
+      // again when the client retries the request with a corrected timestamp.
+      attempts += 1;
+      do_check_true(attempts <= 2);
+
+      let delta = getTimestampDelta(request.getHeader("Authorization"));
+
+      // First time through, we should have a bad timestamp
+      // Client will retry
+      if (attempts === 1) {
+        do_check_true(delta > MINUTE_MS);
+        let message = "never!!!";
+        response.setStatusLine(request.httpVersion, 401, "Unauthorized");
+        return response.bodyOutputStream.write(message, message.length);
+      }
+
+      // Second time through, timestamp should be corrected by client
+      // And fail on the client
+      do_check_true(delta < MINUTE_MS);
+      let message = "Cannot get ye flask.";
+      response.setStatusLine(request.httpVersion, 500, "Internal server error");
+      response.bodyOutputStream.write(message, message.length);
+    }
+  });
+
+  let client = new HawkClient(server.baseURI);
+  function getOffset() {
+    return client.localtimeOffsetMsec;
+  }
+
+  client.now = () => {
+    return Date.now() - 12 * HOUR_MS;
+  };
+
+  // We begin with no offset
+  do_check_eq(client.localtimeOffsetMsec, 0);
+
+  // Request will have bad timestamp; client will retry once
+  try {
+    yield client.request("/maybe", method, credentials);
+  } catch(err) {
+    do_check_eq(err.code, 500);
+  }
+  do_check_eq(attempts, 2);
+
+  yield deferredStop(server);
+});
+
+add_task(function throw_if_not_json_body() {
+  do_test_pending();
+  let client = new HawkClient("https://example.com");
+  try {
+    yield client.request("/bogus", "GET", {}, "I am not json");
+  } catch(err) {
+    do_check_true(!!err.message);
+    do_test_finished();
+  }
+});
+
+// End of tests.
+// Utility functions follow
+
+function getTimestampDelta(authHeader, now=Date.now()) {
+  let tsMS = new Date(
+      parseInt(/ts="(\d+)"/.exec(authHeader)[1], 10) * SECOND_MS);
+  return Math.abs(tsMS - now);
+}
+
+function deferredStop(server) {
+  let deferred = Promise.defer();
+  server.stop(deferred.resolve);
+  return deferred.promise;
+}
+
+function run_test() {
+  initTestLogging("Trace");
+  run_next_test();
+}
+
--- a/services/common/tests/unit/test_restrequest.js
+++ b/services/common/tests/unit/test_restrequest.js
@@ -826,8 +826,67 @@ add_test(function test_not_sending_cooki
   let res = new RESTRequest(server.baseURI + "/test");
   res.get(function (error) {
     do_check_null(error);
     do_check_true(this.response.success);
     do_check_eq("COOKIE!", this.response.body);
     server.stop(run_next_test);
   });
 });
+
+add_test(function test_hawk_authenticated_request() {
+  do_test_pending();
+
+  let onProgressCalled = false;
+  let postData = {your: "data"};
+
+  // An arbitrary date - Feb 2, 1971.  It ends in a bunch of zeroes to make our
+  // computation with the hawk timestamp easier, since hawk throws away the
+  // millisecond values.
+  let then = 34329600000;
+
+  let clockSkew = 120000;
+  let timeOffset = -1 * clockSkew;
+  let localTime = then + clockSkew;
+
+  let credentials = {
+    id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
+    key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
+    algorithm: "sha256"
+  };
+
+  let server = httpd_setup({
+    "/elysium": function(request, response) {
+      do_check_true(request.hasHeader("Authorization"));
+
+      // check that the header timestamp is our arbitrary system date, not
+      // today's date.  Note that hawk header timestamps are in seconds, not
+      // milliseconds.
+      let authorization = request.getHeader("Authorization");
+      let tsMS = parseInt(/ts="(\d+)"/.exec(authorization)[1], 10) * 1000;
+      do_check_eq(tsMS, then);
+
+      let message = "yay";
+      response.setStatusLine(request.httpVersion, 200, "OK");
+      response.bodyOutputStream.write(message, message.length);
+    }
+  });
+
+  function onProgress() {
+    onProgressCalled = true;
+  }
+
+  function onComplete(error) {
+    do_check_eq(200, this.response.status);
+    do_check_eq(this.response.body, "yay");
+    do_check_true(onProgressCalled);
+    do_test_finished();
+    server.stop(run_next_test);
+  }
+
+  let url = server.baseURI + "/elysium";
+  let extra = {
+    now: localTime,
+    localtimeOffsetMsec: timeOffset
+  };
+  let request = new HAWKAuthenticatedRESTRequest(url, credentials, extra);
+  request.post(postData, onComplete, onProgress);
+});
--- a/services/common/tests/unit/xpcshell.ini
+++ b/services/common/tests/unit/xpcshell.ini
@@ -20,16 +20,17 @@ firefox-appdir = browser
 [test_utils_stackTrace.js]
 [test_utils_utf8.js]
 [test_utils_uuid.js]
 
 [test_async_chain.js]
 [test_async_querySpinningly.js]
 [test_bagheera_server.js]
 [test_bagheera_client.js]
+[test_hawk.js]
 [test_observers.js]
 [test_restrequest.js]
 [test_tokenauthenticatedrequest.js]
 
 # Storage service APIs
 [test_storageservice_bso.js]
 [test_storageservice_client.js]
 
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -63,16 +63,36 @@ InternalMethods = function(mock) {
       filename: DEFAULT_STORAGE_FILENAME,
       baseDir: OS.Constants.Path.profileDir,
     });
   }
 }
 InternalMethods.prototype = {
 
   /**
+   * Return the current time in milliseconds as an integer.  Allows tests to
+   * manipulate the date to simulate certificate expiration.
+   */
+  now: function() {
+    return this.fxAccountsClient.now();
+  },
+
+  /**
+   * Return clock offset in milliseconds, as reported by the fxAccountsClient.
+   * This can be overridden for testing.
+   *