merge because ttaubert; a=#fx-team
authorRob Campbell <rcampbell@mozilla.com>
Thu, 19 Jan 2012 11:06:34 -0400
changeset 86131 94bcd5b46e1cb9a245b58e8006cd0fce7aa9c64a
parent 86130 f96d6dd51d6c3538e6d9e8cab1af927647ec379c (current diff)
parent 86126 f38769f877d646dd1608c300f84d96638b2cc738 (diff)
child 86132 a89c7779fed4e8bb265ca07c1a9fc0555bcf18d2
push id805
push userakeybl@mozilla.com
push dateWed, 01 Feb 2012 18:17:35 +0000
treeherdermozilla-aurora@6fb3bf232436 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone12.0a1
merge because ttaubert; a=#fx-team
new file mode 100644
--- /dev/null
+++ b/browser/base/content/browser-thumbnails.js
@@ -0,0 +1,119 @@
+#ifdef 0
+/* 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/. */
+#endif
+
+/**
+ * Keeps thumbnails of open web pages up-to-date.
+ */
+let gBrowserThumbnails = {
+  _captureDelayMS: 2000,
+
+  /**
+   * Map of capture() timeouts assigned to their browsers.
+   */
+  _timeouts: null,
+
+  /**
+   * Cache for the PageThumbs module.
+   */
+  _pageThumbs: null,
+
+  /**
+   * List of tab events we want to listen for.
+   */
+  _tabEvents: ["TabClose", "TabSelect"],
+
+  init: function Thumbnails_init() {
+    gBrowser.addTabsProgressListener(this);
+
+    this._tabEvents.forEach(function (aEvent) {
+      gBrowser.tabContainer.addEventListener(aEvent, this, false);
+    }, this);
+
+    this._timeouts = new WeakMap();
+
+    XPCOMUtils.defineLazyModuleGetter(this, "_pageThumbs",
+      "resource:///modules/PageThumbs.jsm", "PageThumbs");
+  },
+
+  uninit: function Thumbnails_uninit() {
+    gBrowser.removeTabsProgressListener(this);
+
+    this._tabEvents.forEach(function (aEvent) {
+      gBrowser.tabContainer.removeEventListener(aEvent, this, false);
+    }, this);
+
+    this._timeouts = null;
+    this._pageThumbs = null;
+  },
+
+  handleEvent: function Thumbnails_handleEvent(aEvent) {
+    switch (aEvent.type) {
+      case "TabSelect":
+        this._delayedCapture(aEvent.target.linkedBrowser);
+        break;
+      case "TabClose": {
+        let browser = aEvent.target.linkedBrowser;
+        if (this._timeouts.has(browser)) {
+          clearTimeout(this._timeouts.get(browser));
+          this._timeouts.delete(browser);
+        }
+        break;
+      }
+    }
+  },
+
+  /**
+   * State change progress listener for all tabs.
+   */
+  onStateChange: function Thumbnails_onStateChange(aBrowser, aWebProgress,
+                                                   aRequest, aStateFlags, aStatus) {
+    if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+        aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK)
+      this._delayedCapture(aBrowser);
+  },
+
+  _capture: function Thumbnails_capture(aBrowser) {
+    if (this._shouldCapture(aBrowser)) {
+      let canvas = this._pageThumbs.capture(aBrowser.contentWindow);
+      this._pageThumbs.store(aBrowser.currentURI.spec, canvas);
+    }
+  },
+
+  _delayedCapture: function Thumbnails_delayedCapture(aBrowser) {
+    if (this._timeouts.has(aBrowser))
+      clearTimeout(this._timeouts.get(aBrowser));
+
+    let timeout = setTimeout(function () {
+      this._timeouts.delete(aBrowser);
+      this._capture(aBrowser);
+    }.bind(this), this._captureDelayMS);
+
+    this._timeouts.set(aBrowser, timeout);
+  },
+
+  _shouldCapture: function Thumbnails_shouldCapture(aBrowser) {
+    // There's no point in taking screenshot of loading pages.
+    if (aBrowser.docShell.busyFlags != Ci.nsIDocShell.BUSY_FLAGS_NONE)
+      return false;
+
+    // Don't take screenshots of about: pages.
+    if (aBrowser.currentURI.schemeIs("about"))
+      return false;
+
+    let channel = aBrowser.docShell.currentDocumentChannel;
+
+    try {
+      // If the channel is a nsIHttpChannel get its http status code.
+      let httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
+
+      // Continue only if we have a 2xx status code.
+      return Math.floor(httpChannel.responseStatus / 100) == 2;
+    } catch (e) {
+      // Not a http channel, we just assume a success status code.
+      return true;
+    }
+  }
+};
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -191,16 +191,17 @@ let gInitialPages = [
   "about:privatebrowsing",
   "about:sessionrestore"
 ];
 
 #include browser-fullZoom.js
 #include browser-places.js
 #include browser-tabPreviews.js
 #include browser-tabview.js
+#include browser-thumbnails.js
 
 #ifdef MOZ_SERVICES_SYNC
 #include browser-syncui.js
 #endif
 
 XPCOMUtils.defineLazyGetter(this, "Win7Features", function () {
 #ifdef XP_WIN
   const WINTASKBAR_CONTRACTID = "@mozilla.org/windows-taskbar;1";
@@ -1694,16 +1695,17 @@ function delayedStartup(isLoadingBlank, 
   if (document.mozFullScreen)
     onMozFullScreenChange();
 
 #ifdef MOZ_SERVICES_SYNC
   // initialize the sync UI
   gSyncUI.init();
 #endif
 
+  gBrowserThumbnails.init();
   TabView.init();
 
   setUrlAndSearchBarWidthForConditionalForwardButton();
   window.addEventListener("resize", function resizeHandler(event) {
     if (event.target == window)
       setUrlAndSearchBarWidthForConditionalForwardButton();
   });
 
@@ -1815,16 +1817,17 @@ function BrowserShutdown() {
   } else {
     if (Win7Features)
       Win7Features.onCloseWindow();
 
     gPrefService.removeObserver(ctrlTab.prefName, ctrlTab);
     gPrefService.removeObserver(allTabs.prefName, allTabs);
     ctrlTab.uninit();
     TabView.uninit();
+    gBrowserThumbnails.uninit();
 
     try {
       FullZoom.destroy();
     }
     catch(ex) {
       Components.utils.reportError(ex);
     }
 
--- a/browser/components/Makefile.in
+++ b/browser/components/Makefile.in
@@ -66,16 +66,17 @@ PARALLEL_DIRS = \
   places \
   preferences \
   privatebrowsing \
   search \
   sessionstore \
   shell \
   sidebar \
   tabview \
+  thumbnails \
   migration \
   $(NULL)
 
 ifdef MOZ_SAFE_BROWSING
 PARALLEL_DIRS += safebrowsing
 endif
 
 TEST_DIRS += test
new file mode 100644
--- /dev/null
+++ b/browser/components/thumbnails/BrowserPageThumbs.manifest
@@ -0,0 +1,2 @@
+component {5a4ae9b5-f475-48ae-9dce-0b4c1d347884} PageThumbsProtocol.js
+contract @mozilla.org/network/protocol;1?name=moz-page-thumb {5a4ae9b5-f475-48ae-9dce-0b4c1d347884}
new file mode 100644
--- /dev/null
+++ b/browser/components/thumbnails/Makefile.in
@@ -0,0 +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/.
+
+DEPTH		= ../../..
+topsrcdir	= @top_srcdir@
+srcdir		= @srcdir@
+VPATH		= @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+EXTRA_COMPONENTS = \
+  BrowserPageThumbs.manifest \
+  PageThumbsProtocol.js \
+  $(NULL)
+
+ifdef ENABLE_TESTS
+	DIRS += test
+endif
+
+include $(topsrcdir)/config/rules.mk
+
+XPIDL_FLAGS += -I$(topsrcdir)/browser/components/
new file mode 100644
--- /dev/null
+++ b/browser/components/thumbnails/PageThumbsProtocol.js
@@ -0,0 +1,448 @@
+/* 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/. */
+
+/**
+ * PageThumbsProtocol.js
+ *
+ * This file implements the moz-page-thumb:// protocol and the corresponding
+ * channel delivering cached thumbnails.
+ *
+ * URL structure:
+ *
+ * moz-page-thumb://thumbnail?url=http%3A%2F%2Fwww.mozilla.org%2F
+ *
+ * This URL requests an image for 'http://www.mozilla.org/'.
+ */
+
+"use strict";
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Cr = Components.results;
+const Ci = Components.interfaces;
+
+Cu.import("resource:///modules/PageThumbs.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+  "resource://gre/modules/NetUtil.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+  "resource://gre/modules/Services.jsm");
+
+/**
+ * Implements the thumbnail protocol handler responsible for moz-page-thumb: URIs.
+ */
+function Protocol() {
+}
+
+Protocol.prototype = {
+  /**
+   * The scheme used by this protocol.
+   */
+  get scheme() PageThumbs.scheme,
+
+  /**
+   * The default port for this protocol (we don't support ports).
+   */
+  get defaultPort() -1,
+
+  /**
+   * The flags specific to this protocol implementation.
+   */
+  get protocolFlags() {
+    return Ci.nsIProtocolHandler.URI_IS_UI_RESOURCE |
+           Ci.nsIProtocolHandler.URI_NORELATIVE |
+           Ci.nsIProtocolHandler.URI_NOAUTH;
+  },
+
+  /**
+   * Creates a new URI object that is suitable for loading by this protocol.
+   * @param aSpec The URI string in UTF8 encoding.
+   * @param aOriginCharset The charset of the document from which the URI originated.
+   * @return The newly created URI.
+   */
+  newURI: function Proto_newURI(aSpec, aOriginCharset) {
+    let uri = Cc["@mozilla.org/network/simple-uri;1"].createInstance(Ci.nsIURI);
+    uri.spec = aSpec;
+    return uri;
+  },
+
+  /**
+   * Constructs a new channel from the given URI for this protocol handler.
+   * @param aURI The URI for which to construct a channel.
+   * @return The newly created channel.
+   */
+  newChannel: function Proto_newChannel(aURI) {
+    return new Channel(aURI);
+  },
+
+  /**
+   * Decides whether to allow a blacklisted port.
+   * @return Always false, we'll never allow ports.
+   */
+  allowPort: function () false,
+
+  classID: Components.ID("{5a4ae9b5-f475-48ae-9dce-0b4c1d347884}"),
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIProtocolHandler])
+};
+
+let NSGetFactory = XPCOMUtils.generateNSGetFactory([Protocol]);
+
+/**
+ * A channel implementation responsible for delivering cached thumbnails.
+ */
+function Channel(aURI) {
+  this._uri = aURI;
+
+  // nsIChannel
+  this.originalURI = aURI;
+
+  // nsIHttpChannel
+  this._responseHeaders = {"content-type": PageThumbs.contentType};
+}
+
+Channel.prototype = {
+  /**
+   * Tracks if the channel has been opened, yet.
+   */
+  _wasOpened: false,
+
+  /**
+   * Opens this channel asynchronously.
+   * @param aListener The listener that receives the channel data when available.
+   * @param aContext A custom context passed to the listener's methods.
+   */
+  asyncOpen: function Channel_asyncOpen(aListener, aContext) {
+    if (this._wasOpened)
+      throw Cr.NS_ERROR_ALREADY_OPENED;
+
+    if (this.canceled)
+      return;
+
+    this._listener = aListener;
+    this._context = aContext;
+
+    this._isPending = true;
+    this._wasOpened = true;
+
+    // Try to read the data from the thumbnail cache.
+    this._readCache(function (aData) {
+      // Update response if there's no data.
+      if (!aData) {
+        this._responseStatus = 404;
+        this._responseText = "Not Found";
+      }
+
+      this._startRequest();
+
+      if (!this.canceled) {
+        this._addToLoadGroup();
+
+        if (aData)
+          this._serveData(aData);
+
+        if (!this.canceled)
+          this._stopRequest();
+      }
+    }.bind(this));
+  },
+
+  /**
+   * Reads a data stream from the cache entry.
+   * @param aCallback The callback the data is passed to.
+   */
+  _readCache: function Channel_readCache(aCallback) {
+    let {url} = parseURI(this._uri);
+
+    // Return early if there's no valid URL given.
+    if (!url) {
+      aCallback(null);
+      return;
+    }
+
+    // Try to get a cache entry.
+    PageThumbsCache.getReadEntry(url, function (aEntry) {
+      let inputStream = aEntry && aEntry.openInputStream(0);
+
+      function closeEntryAndFinish(aData) {
+        if (aEntry) {
+          aEntry.close();
+        }
+        aCallback(aData);
+      }
+
+      // Check if we have a valid entry and if it has any data.
+      if (!inputStream || !inputStream.available()) {
+        closeEntryAndFinish();
+        return;
+      }
+
+      try {
+        // Read the cache entry's data.
+        NetUtil.asyncFetch(inputStream, function (aData, aStatus) {
+          // We might have been canceled while waiting.
+          if (this.canceled)
+            return;
+
+          // Check if we have a valid data stream.
+          if (!Components.isSuccessCode(aStatus) || !aData.available())
+            aData = null;
+
+          closeEntryAndFinish(aData);
+        }.bind(this));
+      } catch (e) {
+        closeEntryAndFinish();
+      }
+    }.bind(this));
+  },
+
+  /**
+   * Calls onStartRequest on the channel listener.
+   */
+  _startRequest: function Channel_startRequest() {
+    try {
+      this._listener.onStartRequest(this, this._context);
+    } catch (e) {
+      // The listener might throw if the request has been canceled.
+      this.cancel(Cr.NS_BINDING_ABORTED);
+    }
+  },
+
+  /**
+   * Calls onDataAvailable on the channel listener and passes the data stream.
+   * @param aData The data to be delivered.
+   */
+  _serveData: function Channel_serveData(aData) {
+    try {
+      let available = aData.available();
+      this._listener.onDataAvailable(this, this._context, aData, 0, available);
+    } catch (e) {
+      // The listener might throw if the request has been canceled.
+      this.cancel(Cr.NS_BINDING_ABORTED);
+    }
+  },
+
+  /**
+   * Calls onStopRequest on the channel listener.
+   */
+  _stopRequest: function Channel_stopRequest() {
+    try {
+      this._listener.onStopRequest(this, this._context, this.status);
+    } catch (e) {
+      // This might throw but is generally ignored.
+    }
+
+    // The request has finished, clean up after ourselves.
+    this._cleanup();
+  },
+
+  /**
+   * Adds this request to the load group, if any.
+   */
+  _addToLoadGroup: function Channel_addToLoadGroup() {
+    if (this.loadGroup)
+      this.loadGroup.addRequest(this, this._context);
+  },
+
+  /**
+   * Removes this request from its load group, if any.
+   */
+  _removeFromLoadGroup: function Channel_removeFromLoadGroup() {
+    if (!this.loadGroup)
+      return;
+
+    try {
+      this.loadGroup.removeRequest(this, this._context, this.status);
+    } catch (e) {
+      // This might throw but is ignored.
+    }
+  },
+
+  /**
+   * Cleans up the channel when the request has finished.
+   */
+  _cleanup: function Channel_cleanup() {
+    this._removeFromLoadGroup();
+    this.loadGroup = null;
+
+    this._isPending = false;
+
+    delete this._listener;
+    delete this._context;
+  },
+
+  /* :::::::: nsIChannel ::::::::::::::: */
+
+  contentType: PageThumbs.contentType,
+  contentLength: -1,
+  owner: null,
+  contentCharset: null,
+  notificationCallbacks: null,
+
+  get URI() this._uri,
+  get securityInfo() null,
+
+  /**
+   * Opens this channel synchronously. Not supported.
+   */
+  open: function Channel_open() {
+    // Synchronous data delivery is not implemented.
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  },
+
+  /* :::::::: nsIHttpChannel ::::::::::::::: */
+
+  redirectionLimit: 10,
+  requestMethod: "GET",
+  allowPipelining: true,
+  referrer: null,
+
+  get requestSucceeded() true,
+
+  _responseStatus: 200,
+  get responseStatus() this._responseStatus,
+
+  _responseText: "OK",
+  get responseStatusText() this._responseText,
+
+  /**
+   * Checks if the server sent the equivalent of a "Cache-control: no-cache"
+   * response header.
+   * @return Always false.
+   */
+  isNoCacheResponse: function () false,
+
+  /**
+   * Checks if the server sent the equivalent of a "Cache-control: no-cache"
+   * response header.
+   * @return Always false.
+   */
+  isNoStoreResponse: function () false,
+
+  /**
+   * Returns the value of a particular request header. Not implemented.
+   */
+  getRequestHeader: function Channel_getRequestHeader() {
+    throw Cr.NS_ERROR_NOT_AVAILABLE;
+  },
+
+  /**
+   * This method is called to set the value of a particular request header.
+   * Not implemented.
+   */
+  setRequestHeader: function Channel_setRequestHeader() {
+    if (this._wasOpened)
+      throw Cr.NS_ERROR_IN_PROGRESS;
+  },
+
+  /**
+   * Call this method to visit all request headers. Not implemented.
+   */
+  visitRequestHeaders: function () {},
+
+  /**
+   * Gets the value of a particular response header.
+   * @param aHeader The case-insensitive name of the response header to query.
+   * @return The header value.
+   */
+  getResponseHeader: function Channel_getResponseHeader(aHeader) {
+    let name = aHeader.toLowerCase();
+    if (name in this._responseHeaders)
+      return this._responseHeaders[name];
+
+    throw Cr.NS_ERROR_NOT_AVAILABLE;
+  },
+
+  /**
+   * This method is called to set the value of a particular response header.
+   * @param aHeader The case-insensitive name of the response header to query.
+   * @param aValue The response header value to set.
+   */
+  setResponseHeader: function Channel_setResponseHeader(aHeader, aValue, aMerge) {
+    let name = aHeader.toLowerCase();
+    if (!aValue && !aMerge)
+      delete this._responseHeaders[name];
+    else
+      this._responseHeaders[name] = aValue;
+  },
+
+  /**
+   * Call this method to visit all response headers.
+   * @param aVisitor The header visitor.
+   */
+  visitResponseHeaders: function Channel_visitResponseHeaders(aVisitor) {
+    for (let name in this._responseHeaders) {
+      let value = this._responseHeaders[name];
+
+      try {
+        aVisitor.visitHeader(name, value);
+      } catch (e) {
+        // The visitor can throw to stop the iteration.
+        return;
+      }
+    }
+  },
+
+  /* :::::::: nsIRequest ::::::::::::::: */
+
+  loadFlags: Ci.nsIRequest.LOAD_NORMAL,
+  loadGroup: null,
+
+  get name() this._uri.spec,
+
+  _status: Cr.NS_OK,
+  get status() this._status,
+
+  _isPending: false,
+  isPending: function () this._isPending,
+
+  resume: function () {},
+  suspend: function () {},
+
+  /**
+   * Cancels this request.
+   * @param aStatus The reason for cancelling.
+   */
+  cancel: function Channel_cancel(aStatus) {
+    if (this.canceled)
+      return;
+
+    this._isCanceled = true;
+    this._status = aStatus;
+
+    this._cleanup();
+  },
+
+  /* :::::::: nsIHttpChannelInternal ::::::::::::::: */
+
+  documentURI: null,
+
+  _isCanceled: false,
+  get canceled() this._isCanceled,
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannel,
+                                         Ci.nsIHttpChannel,
+                                         Ci.nsIHttpChannelInternal,
+                                         Ci.nsIRequest])
+};
+
+/**
+ * Parses a given URI and extracts all parameters relevant to this protocol.
+ * @param aURI The URI to parse.
+ * @return The parsed parameters.
+ */
+function parseURI(aURI) {
+  let {scheme, staticHost} = PageThumbs;
+  let re = new RegExp("^" + scheme + "://" + staticHost + ".*?\\?");
+  let query = aURI.spec.replace(re, "");
+  let params = {};
+
+  query.split("&").forEach(function (aParam) {
+    let [key, value] = aParam.split("=").map(decodeURIComponent);
+    params[key.toLowerCase()] = value;
+  });
+
+  return params;
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/thumbnails/test/Makefile.in
@@ -0,0 +1,21 @@
+# 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/.
+
+DEPTH		= ../../../..
+topsrcdir	= @top_srcdir@
+srcdir		= @srcdir@
+VPATH		= @srcdir@
+relativesrcdir  = browser/components/thumbnails/test
+
+include $(DEPTH)/config/autoconf.mk
+include $(topsrcdir)/config/rules.mk
+
+_BROWSER_FILES = \
+	browser_thumbnails_cache.js \
+	browser_thumbnails_capture.js \
+	head.js \
+	$(NULL)
+
+libs::	$(_BROWSER_FILES)
+	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/browser/components/thumbnails/test/browser_thumbnails_cache.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * These tests ensure that saving a thumbnail to the cache works. They also
+ * retrieve the thumbnail and display it using an <img> element to compare
+ * its pixel colors.
+ */
+function runTests() {
+  // Create a new tab with a red background.
+  yield addTab("data:text/html,<body bgcolor=ff0000></body>");
+  let cw = gBrowser.selectedTab.linkedBrowser.contentWindow;
+
+  // Capture a thumbnail for the tab.
+  let canvas = PageThumbs.capture(cw);
+
+  // Store the tab into the thumbnail cache.
+  yield PageThumbs.store("key", canvas, next);
+
+  let {width, height} = canvas;
+  let thumb = PageThumbs.getThumbnailURL("key", width, height);
+
+  // Create a new tab with an image displaying the previously stored thumbnail.
+  yield addTab("data:text/html,<img src='" + thumb + "'/>" + 
+               "<canvas width=" + width + " height=" + height + "/>");
+
+  cw = gBrowser.selectedTab.linkedBrowser.contentWindow;
+  let [img, canvas] = cw.document.querySelectorAll("img, canvas");
+
+  // Draw the image to a canvas and compare the pixel color values.
+  let ctx = canvas.getContext("2d");
+  ctx.drawImage(img, 0, 0, width, height);
+  checkCanvasColor(ctx, 255, 0, 0, "we have a red image and canvas");
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/thumbnails/test/browser_thumbnails_capture.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * These tests ensure that capturing a site's screenshot to a canvas actually
+ * works.
+ */
+function runTests() {
+  // Create a tab with a red background.
+  yield addTab("data:text/html,<body bgcolor=ff0000></body>");
+  checkCurrentThumbnailColor(255, 0, 0, "we have a red thumbnail");
+
+  // Load a page with a green background.
+  yield navigateTo("data:text/html,<body bgcolor=00ff00></body>");
+  checkCurrentThumbnailColor(0, 255, 0, "we have a green thumbnail");
+
+  // Load a page with a blue background.
+  yield navigateTo("data:text/html,<body bgcolor=0000ff></body>");
+  checkCurrentThumbnailColor(0, 0, 255, "we have a blue thumbnail");
+}
+
+/**
+ * Captures a thumbnail of the currently selected tab and checks the color of
+ * the resulting canvas.
+ * @param aRed The red component's intensity.
+ * @param aGreen The green component's intensity.
+ * @param aBlue The blue component's intensity.
+ * @param aMessage The info message to print when checking the pixel color.
+ */
+function checkCurrentThumbnailColor(aRed, aGreen, aBlue, aMessage) {
+  let tab = gBrowser.selectedTab;
+  let cw = tab.linkedBrowser.contentWindow;
+
+  let canvas = PageThumbs.capture(cw);
+  let ctx = canvas.getContext("2d");
+
+  checkCanvasColor(ctx, aRed, aGreen, aBlue, aMessage);
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/thumbnails/test/head.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource:///modules/PageThumbs.jsm");
+
+registerCleanupFunction(function () {
+  while (gBrowser.tabs.length > 1)
+    gBrowser.removeTab(gBrowser.tabs[1]);
+});
+
+/**
+ * Provide the default test function to start our test runner.
+ */
+function test() {
+  TestRunner.run();
+}
+
+/**
+ * The test runner that controls the execution flow of our tests.
+ */
+let TestRunner = {
+  /**
+   * Starts the test runner.
+   */
+  run: function () {
+    waitForExplicitFinish();
+
+    this._iter = runTests();
+    this.next();
+  },
+
+  /**
+   * Runs the next available test or finishes if there's no test left.
+   */
+  next: function () {
+    try {
+      TestRunner._iter.next();
+    } catch (e if e instanceof StopIteration) {
+      finish();
+    }
+  }
+};
+
+/**
+ * Continues the current test execution.
+ */
+function next() {
+  TestRunner.next();
+}
+
+/**
+ * Creates a new tab with the given URI.
+ * @param aURI The URI that's loaded in the tab.
+ */
+function addTab(aURI) {
+  let tab = gBrowser.selectedTab = gBrowser.addTab(aURI);
+  whenBrowserLoaded(tab.linkedBrowser);
+}
+
+/**
+ * Loads a new URI into the currently selected tab.
+ * @param aURI The URI to load.
+ */
+function navigateTo(aURI) {
+  let browser = gBrowser.selectedTab.linkedBrowser;
+  whenBrowserLoaded(browser);
+  browser.loadURI(aURI);
+}
+
+/**
+ * Continues the current test execution when a load event for the given browser
+ * has been received
+ * @param aBrowser The browser to listen on.
+ */
+function whenBrowserLoaded(aBrowser) {
+  aBrowser.addEventListener("load", function onLoad() {
+    aBrowser.removeEventListener("load", onLoad, true);
+    executeSoon(next);
+  }, true);
+}
+
+/**
+ * Checks the top-left pixel of a given canvas' 2d context for a given color.
+ * @param aContext The 2D context of a canvas.
+ * @param aRed The red component's intensity.
+ * @param aGreen The green component's intensity.
+ * @param aBlue The blue component's intensity.
+ * @param aMessage The info message to print when comparing the pixel color.
+ */
+function checkCanvasColor(aContext, aRed, aGreen, aBlue, aMessage) {
+  let [r, g, b] = aContext.getImageData(0, 0, 1, 1).data;
+  ok(r == aRed && g == aGreen && b == aBlue, aMessage);
+}
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -279,16 +279,17 @@
 @BINPATH@/components/fuelApplication.js
 @BINPATH@/components/WebContentConverter.js
 @BINPATH@/components/BrowserComponents.manifest
 @BINPATH@/components/nsBrowserContentHandler.js
 @BINPATH@/components/nsBrowserGlue.js
 @BINPATH@/components/nsSetDefaultBrowser.manifest
 @BINPATH@/components/nsSetDefaultBrowser.js
 @BINPATH@/components/BrowserPlaces.manifest
+@BINPATH@/components/BrowserPageThumbs.manifest
 @BINPATH@/components/nsPrivateBrowsingService.manifest
 @BINPATH@/components/nsPrivateBrowsingService.js
 @BINPATH@/components/toolkitsearch.manifest
 @BINPATH@/components/nsSearchService.js
 @BINPATH@/components/nsSearchSuggestions.js
 @BINPATH@/components/passwordmgr.manifest
 @BINPATH@/components/nsLoginInfo.js
 @BINPATH@/components/nsLoginManager.js
@@ -342,16 +343,17 @@
 @BINPATH@/components/toolkitplaces.manifest
 @BINPATH@/components/nsLivemarkService.js
 @BINPATH@/components/nsTaggingService.js
 @BINPATH@/components/nsPlacesAutoComplete.manifest
 @BINPATH@/components/nsPlacesAutoComplete.js
 @BINPATH@/components/nsPlacesExpiration.js
 @BINPATH@/components/PlacesProtocolHandler.js
 @BINPATH@/components/PlacesCategoriesStarter.js
+@BINPATH@/components/PageThumbsProtocol.js
 @BINPATH@/components/nsDefaultCLH.manifest
 @BINPATH@/components/nsDefaultCLH.js
 @BINPATH@/components/nsContentPrefService.manifest
 @BINPATH@/components/nsContentPrefService.js
 @BINPATH@/components/nsContentDispatchChooser.manifest
 @BINPATH@/components/nsContentDispatchChooser.js
 @BINPATH@/components/nsHandlerService.manifest
 @BINPATH@/components/nsHandlerService.js
--- a/browser/modules/Makefile.in
+++ b/browser/modules/Makefile.in
@@ -47,16 +47,17 @@ include $(topsrcdir)/config/config.mk
 ifdef ENABLE_TESTS
 DIRS += test
 endif
 
 EXTRA_JS_MODULES = \
 	openLocationLastURL.jsm \
 	NetworkPrioritizer.jsm \
 	offlineAppCache.jsm \
+	PageThumbs.jsm \
 	$(NULL)
 
 ifeq ($(MOZ_WIDGET_TOOLKIT),windows) 
 EXTRA_JS_MODULES += \
 	WindowsPreviewPerTab.jsm \
 	WindowsJumpLists.jsm \
 	$(NULL)
 endif
new file mode 100644
--- /dev/null
+++ b/browser/modules/PageThumbs.jsm
@@ -0,0 +1,265 @@
+/* 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";
+
+let EXPORTED_SYMBOLS = ["PageThumbs", "PageThumbsCache"];
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
+
+/**
+ * The default width for page thumbnails.
+ *
+ * Hint: This is the default value because the 'New Tab Page' is the only
+ *       client for now.
+ */
+const THUMBNAIL_WIDTH = 201;
+
+/**
+ * The default height for page thumbnails.
+ *
+ * Hint: This is the default value because the 'New Tab Page' is the only
+ *       client for now.
+ */
+const THUMBNAIL_HEIGHT = 127;
+
+/**
+ * The default background color for page thumbnails.
+ */
+const THUMBNAIL_BG_COLOR = "#fff";
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+  "resource://gre/modules/NetUtil.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+  "resource://gre/modules/Services.jsm");
+
+/**
+ * Singleton providing functionality for capturing web page thumbnails and for
+ * accessing them if already cached.
+ */
+let PageThumbs = {
+  /**
+   * The scheme to use for thumbnail urls.
+   */
+  get scheme() "moz-page-thumb",
+
+  /**
+   * The static host to use for thumbnail urls.
+   */
+  get staticHost() "thumbnail",
+
+  /**
+   * The thumbnails' image type.
+   */
+  get contentType() "image/png",
+
+  /**
+   * Gets the thumbnail image's url for a given web page's url.
+   * @param aUrl The web page's url that is depicted in the thumbnail.
+   * @return The thumbnail image's url.
+   */
+  getThumbnailURL: function PageThumbs_getThumbnailURL(aUrl) {
+    return this.scheme + "://" + this.staticHost +
+           "?url=" + encodeURIComponent(aUrl);
+  },
+
+  /**
+   * Creates a canvas containing a thumbnail depicting the given window.
+   * @param aWindow The DOM window to capture a thumbnail from.
+   * @return The newly created canvas containing the image data.
+   */
+  capture: function PageThumbs_capture(aWindow) {
+    let [sx, sy, sw, sh, scale] = this._determineCropRectangle(aWindow);
+
+    let canvas = this._createCanvas();
+    let ctx = canvas.getContext("2d");
+
+    // Scale the canvas accordingly.
+    ctx.scale(scale, scale);
+
+    try {
+      // Draw the window contents to the canvas.
+      ctx.drawWindow(aWindow, sx, sy, sw, sh, THUMBNAIL_BG_COLOR,
+                     ctx.DRAWWINDOW_DO_NOT_FLUSH);
+    } catch (e) {
+      // We couldn't draw to the canvas for some reason.
+    }
+
+    return canvas;
+  },
+
+  /**
+   * Stores the image data contained in the given canvas to the underlying
+   * storage.
+   * @param aKey The key to use for the storage.
+   * @param aCanvas The canvas containing the thumbnail's image data.
+   * @param aCallback The function to be called when the canvas data has been
+   *                  stored (optional).
+   */
+  store: function PageThumbs_store(aKey, aCanvas, aCallback) {
+    let self = this;
+
+    function finish(aSuccessful) {
+      if (aCallback)
+        aCallback(aSuccessful);
+    }
+
+    // Get a writeable cache entry.
+    PageThumbsCache.getWriteEntry(aKey, function (aEntry) {
+      if (!aEntry) {
+        finish(false);
+        return;
+      }
+
+      // Extract image data from the canvas.
+      self._readImageData(aCanvas, function (aData) {
+        let outputStream = aEntry.openOutputStream(0);
+
+        // Write the image data to the cache entry.
+        NetUtil.asyncCopy(aData, outputStream, function (aResult) {
+          let success = Components.isSuccessCode(aResult);
+          if (success)
+            aEntry.markValid();
+
+          aEntry.close();
+          finish(success);
+        });
+      });
+    });
+  },
+
+  /**
+   * Reads the image data from a given canvas and passes it to the callback.
+   * @param aCanvas The canvas to read the image data from.
+   * @param aCallback The function that the image data is passed to.
+   */
+  _readImageData: function PageThumbs_readImageData(aCanvas, aCallback) {
+    let dataUri = aCanvas.toDataURL(PageThumbs.contentType, "");
+    let uri = Services.io.newURI(dataUri, "UTF8", null);
+
+    NetUtil.asyncFetch(uri, function (aData, aResult) {
+      if (Components.isSuccessCode(aResult) && aData && aData.available())
+        aCallback(aData);
+    });
+  },
+
+  /**
+   * Determines the crop rectangle for a given content window.
+   * @param aWindow The content window.
+   * @return An array containing x, y, width, heigh and the scale of the crop
+   *         rectangle.
+   */
+  _determineCropRectangle: function PageThumbs_determineCropRectangle(aWindow) {
+    let sx = 0;
+    let sy = 0;
+    let sw = aWindow.innerWidth;
+    let sh = aWindow.innerHeight;
+
+    let scale = Math.max(THUMBNAIL_WIDTH / sw, THUMBNAIL_HEIGHT / sh);
+    let scaledWidth = sw * scale;
+    let scaledHeight = sh * scale;
+
+    if (scaledHeight > THUMBNAIL_HEIGHT) {
+      sy = Math.floor(Math.abs((scaledHeight - THUMBNAIL_HEIGHT) / 2) / scale);
+      sh -= 2 * sy;
+    }
+
+    if (scaledWidth > THUMBNAIL_WIDTH) {
+      sx = Math.floor(Math.abs((scaledWidth - THUMBNAIL_WIDTH) / 2) / scale);
+      sw -= 2 * sx;
+    }
+
+    return [sx, sy, sw, sh, scale];
+  },
+
+  /**
+   * Creates a new hidden canvas element.
+   * @return The newly created canvas.
+   */
+  _createCanvas: function PageThumbs_createCanvas() {
+    let doc = Services.appShell.hiddenDOMWindow.document;
+    let canvas = doc.createElementNS(HTML_NAMESPACE, "canvas");
+    canvas.mozOpaque = true;
+    canvas.width = THUMBNAIL_WIDTH;
+    canvas.height = THUMBNAIL_HEIGHT;
+    return canvas;
+  }
+};
+
+/**
+ * A singleton handling the storage of page thumbnails.
+ */
+let PageThumbsCache = {
+  /**
+   * Calls the given callback with a cache entry opened for reading.
+   * @param aKey The key identifying the desired cache entry.
+   * @param aCallback The callback that is called when the cache entry is ready.
+   */
+  getReadEntry: function Cache_getReadEntry(aKey, aCallback) {
+    // Try to open the desired cache entry.
+    this._openCacheEntry(aKey, Ci.nsICache.ACCESS_READ, aCallback);
+  },
+
+  /**
+   * Calls the given callback with a cache entry opened for writing.
+   * @param aKey The key identifying the desired cache entry.
+   * @param aCallback The callback that is called when the cache entry is ready.
+   */
+  getWriteEntry: function Cache_getWriteEntry(aKey, aCallback) {
+    // Try to open the desired cache entry.
+    this._openCacheEntry(aKey, Ci.nsICache.ACCESS_WRITE, aCallback);
+  },
+
+  /**
+   * Opens the cache entry identified by the given key.
+   * @param aKey The key identifying the desired cache entry.
+   * @param aAccess The desired access mode (see nsICache.ACCESS_* constants).
+   * @param aCallback The function to be called when the cache entry was opened.
+   */
+  _openCacheEntry: function Cache_openCacheEntry(aKey, aAccess, aCallback) {
+    function onCacheEntryAvailable(aEntry, aAccessGranted, aStatus) {
+      let validAccess = aAccess == aAccessGranted;
+      let validStatus = Components.isSuccessCode(aStatus);
+
+      // Check if a valid entry was passed and if the
+      // access we requested was actually granted.
+      if (aEntry && !(validAccess && validStatus)) {
+        aEntry.close();
+        aEntry = null;
+      }
+
+      aCallback(aEntry);
+    }
+
+    let listener = this._createCacheListener(onCacheEntryAvailable);
+    this._cacheSession.asyncOpenCacheEntry(aKey, aAccess, listener);
+  },
+
+  /**
+   * Returns a cache listener implementing the nsICacheListener interface.
+   * @param aCallback The callback to be called when the cache entry is available.
+   * @return The new cache listener.
+   */
+  _createCacheListener: function Cache_createCacheListener(aCallback) {
+    return {
+      onCacheEntryAvailable: aCallback,
+      QueryInterface: XPCOMUtils.generateQI([Ci.nsICacheListener])
+    };
+  }
+};
+
+/**
+ * Define a lazy getter for the cache session.
+ */
+XPCOMUtils.defineLazyGetter(PageThumbsCache, "_cacheSession", function () {
+  return Services.cache.createSession(PageThumbs.scheme,
+                                     Ci.nsICache.STORE_ON_DISK, true);
+});
--- a/toolkit/content/Services.jsm
+++ b/toolkit/content/Services.jsm
@@ -58,16 +58,18 @@ XPCOMUtils.defineLazyGetter(Services, "a
 
 XPCOMUtils.defineLazyGetter(Services, "dirsvc", function () {
   return Cc["@mozilla.org/file/directory_service;1"]
            .getService(Ci.nsIDirectoryService)
            .QueryInterface(Ci.nsIProperties);
 });
 
 let initTable = [
+  ["appShell", "@mozilla.org/appshell/appShellService;1", "nsIAppShellService"],
+  ["cache", "@mozilla.org/network/cache-service;1", "nsICacheService"],
   ["console", "@mozilla.org/consoleservice;1", "nsIConsoleService"],
   ["contentPrefs", "@mozilla.org/content-pref/service;1", "nsIContentPrefService"],
   ["cookies", "@mozilla.org/cookiemanager;1", "nsICookieManager2"],
   ["droppedLinkHandler", "@mozilla.org/content/dropped-link-handler;1", "nsIDroppedLinkHandler"],
   ["eTLD", "@mozilla.org/network/effective-tld-service;1", "nsIEffectiveTLDService"],
   ["io", "@mozilla.org/network/io-service;1", "nsIIOService2"],
   ["locale", "@mozilla.org/intl/nslocaleservice;1", "nsILocaleService"],
   ["logins", "@mozilla.org/login-manager;1", "nsILoginManager"],