Bug 1239119 - PreviewProvider module produces thumbnail data URIs given a url r?ursula
MozReview-Commit-ID: KD6taVtzJCy
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/PreviewProvider.jsm
@@ -0,0 +1,49 @@
+/* global XPCOMUtils, BackgroundPageThumbs, FileUtils, PageThumbsStorage, Task, MIMEService */
+/* exported PreviewProvider */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["PreviewProvider"];
+
+const {utils: Cu} = Components;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/PageThumbs.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+const {OS} = Cu.import("resource://gre/modules/osfile.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "BackgroundPageThumbs",
+ "resource://gre/modules/BackgroundPageThumbs.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "MIMEService",
+ "@mozilla.org/mime;1", "nsIMIMEService");
+
+let PreviewProvider = {
+ /**
+ * Returns a thumbnail as a data URI for a url, creating it if necessary
+ *
+ * @param {String} url
+ * a url to obtain a thumbnail for
+ * @return {Promise} A Promise that resolves with a base64 encoded thumbnail
+ */
+ getThumbnail: Task.async(function* PreviewProvider_getThumbnail(url) {
+ try {
+ yield BackgroundPageThumbs.captureIfMissing(url);
+ let imgPath = PageThumbsStorage.getFilePathForURL(url);
+
+ // OS.File object used to easily read off-thread
+ let file = yield OS.File.open(imgPath, {read: true, existing: true});
+
+ // nsIFile object needed for MIMEService
+ let nsFile = FileUtils.File(imgPath);
+
+ let contentType = MIMEService.getTypeFromFile(nsFile);
+ let bytes = yield file.read();
+ let encodedData = btoa(String.fromCharCode.apply(null, bytes));
+ file.close();
+ return `data:${contentType};base64,${encodedData}`;
+ } catch (err) {
+ Cu.reportError(`PreviewProvider_getThumbnail error: ${err}`);
+ throw err;
+ }
+ })
+};
--- a/browser/components/newtab/moz.build
+++ b/browser/components/newtab/moz.build
@@ -11,17 +11,18 @@ XPCSHELL_TESTS_MANIFESTS += [
]
EXTRA_JS_MODULES += [
'NewTabMessages.jsm',
'NewTabPrefsProvider.jsm',
'NewTabRemoteResources.jsm',
'NewTabURL.jsm',
'NewTabWebChannel.jsm',
- 'PlacesProvider.jsm'
+ 'PlacesProvider.jsm',
+ 'PreviewProvider.jsm'
]
XPIDL_SOURCES += [
'nsIAboutNewTabService.idl',
]
XPIDL_MODULE = 'browser-newtab'
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/tests/browser/blue_page.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body style="background-color: blue">
+</body>
+</html>
--- a/browser/components/newtab/tests/browser/browser.ini
+++ b/browser/components/newtab/tests/browser/browser.ini
@@ -1,10 +1,12 @@
[DEFAULT]
support-files =
dummy_page.html
newtabwebchannel_basic.html
newtabmessages_prefs.html
+ blue_page.html
+[browser_PreviewProvider.js]
[browser_remotenewtab_pageloads.js]
[browser_newtab_overrides.js]
[browser_newtabmessages.js]
[browser_newtabwebchannel.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/tests/browser/browser_PreviewProvider.js
@@ -0,0 +1,90 @@
+/* globals XPCOMUtils, Services, PreviewProvider, registerCleanupFunction */
+"use strict";
+
+let Cu = Components.utils;
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PreviewProvider",
+ "resource:///modules/PreviewProvider.jsm");
+
+var oldEnabledPref = Services.prefs.getBoolPref("browser.pagethumbnails.capturing_disabled");
+Services.prefs.setBoolPref("browser.pagethumbnails.capturing_disabled", false);
+
+registerCleanupFunction(function() {
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeTab(gBrowser.tabs[1]);
+ }
+ Services.prefs.setBoolPref("browser.pagethumbnails.capturing_disabled", oldEnabledPref);
+});
+
+const TEST_URL = "https://example.com/browser/browser/components/newtab/tests/browser/blue_page.html";
+
+function pixelsForDataURI(dataURI, options) {
+ return new Promise(resolve => {
+ if (!options) {
+ options = {};
+ }
+ let {width, height} = options;
+ if (!width) {
+ width = 100;
+ }
+ if (!height) {
+ height = 100;
+ }
+
+ let htmlns = "http://www.w3.org/1999/xhtml";
+ let img = document.createElementNS(htmlns, "img");
+ img.setAttribute("src", dataURI);
+
+ img.addEventListener("load", function onLoad() {
+ img.removeEventListener("load", onLoad, true);
+ let canvas = document.createElementNS(htmlns, "canvas");
+ canvas.setAttribute("width", width);
+ canvas.setAttribute("height", height);
+ let ctx = canvas.getContext("2d");
+ ctx.drawImage(img, 0, 0, width, height);
+ let result = ctx.getImageData(0, 0, width, height).data;
+ resolve(result);
+ });
+ });
+}
+
+function* chunk_four(listData) {
+ let index = 0;
+ while (index < listData.length) {
+ yield listData.slice(index, index + 5);
+ index += 4;
+ }
+}
+
+add_task(function* open_page() {
+ let dataURI = yield PreviewProvider.getThumbnail(TEST_URL);
+ let pixels = yield pixelsForDataURI(dataURI, {width: 10, height: 10});
+ let rgbCount = {r: 0, g: 0, b: 0, a: 0};
+ for (let [r, g, b, a] of chunk_four(pixels)) {
+ if (r === 255) {
+ rgbCount.r += 1;
+ }
+ if (g === 255) {
+ rgbCount.g += 1;
+ }
+ if (b === 255) {
+ rgbCount.b += 1;
+ }
+ if (a === 255) {
+ rgbCount.a += 1;
+ }
+ }
+ Assert.equal(`${rgbCount.r},${rgbCount.g},${rgbCount.b},${rgbCount.a}`,
+ "0,0,100,100", "there should be 100 blue-only pixels at full opacity");
+});
+
+add_task(function* invalid_url() {
+ try {
+ yield PreviewProvider.getThumbnail("invalid:URL");
+ } catch (err) {
+ Assert.ok(true, "URL Failed");
+ }
+});