Bug 1197361. Optimize page thumbnails based on screen size. r=ttaubert

This commit is contained in:
Mason Chang 2015-09-25 11:27:16 -07:00
parent f4030f94ef
commit 7c8b086a65
9 changed files with 203 additions and 64 deletions

View File

@ -1942,3 +1942,9 @@ pref("dom.serviceWorkers.interception.enabled", true);
// Enable Push API.
pref("dom.push.enabled", true);
// These are the thumbnail width/height set in about:newtab.
// If you change this, ENSURE IT IS THE SAME SIZE SET
// by about:newtab. These values are in CSS pixels.
pref("toolkit.pageThumbs.minWidth", 280);
pref("toolkit.pageThumbs.minHeight", 190);

View File

@ -122,6 +122,12 @@ input[type=button] {
pointer-events: none;
}
/*
* If you change the sizes here, make sure you
* change the preferences:
* toolkit.pageThumbs.minWidth
* toolkit.pageThumbs.minHeight
*/
/* CELLS */
.newtab-cell,
.newtab-intro-cell,

View File

@ -130,6 +130,12 @@
overflow: hidden;
}
/***
* If you change the sizes here, change them in newTab.css
* and the preference values:
* toolkit.pageThumbs.minWidth
* toolkit.pageThumbs.minHeight
*/
/* THUMBNAILS */
.newtab-thumbnail {
background-origin: padding-box;

View File

@ -5063,3 +5063,7 @@ pref("media.useAudioChannelAPI", false);
pref("dom.requestcontext.enabled", false);
pref("dom.mozKillSwitch.enabled", false);
pref("toolkit.pageThumbs.screenSizeDivisor", 7);
pref("toolkit.pageThumbs.minWidth", 0);
pref("toolkit.pageThumbs.minHeight", 0);

View File

@ -14,6 +14,7 @@ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Promise.jsm", this);
Cu.import("resource://gre/modules/AppConstants.jsm");
this.PageThumbUtils = {
// The default background color for page thumbnails.
@ -27,38 +28,186 @@ this.PageThumbUtils = {
*
* @param aWindow (optional) The document of this window will be used to
* create the canvas. If not given, the hidden window will be used.
* @param aWidth (optional) width of the canvas to create
* @param aHeight (optional) height of the canvas to create
* @return The newly created canvas.
*/
createCanvas: function (aWindow) {
createCanvas: function (aWindow, aWidth = 0, aHeight = 0) {
let doc = (aWindow || Services.appShell.hiddenDOMWindow).document;
let canvas = doc.createElementNS(this.HTML_NAMESPACE, "canvas");
canvas.mozOpaque = true;
canvas.mozImageSmoothingEnabled = true;
let [thumbnailWidth, thumbnailHeight] = this.getThumbnailSize();
canvas.width = thumbnailWidth;
canvas.height = thumbnailHeight;
let [thumbnailWidth, thumbnailHeight] = this.getThumbnailSize(aWindow);
canvas.width = aWidth ? aWidth : thumbnailWidth;
canvas.height = aHeight ? aHeight : thumbnailHeight;
return canvas;
},
/**
* Calculates a preferred initial thumbnail size based on current desktop
* dimensions. The resulting dims will generally be about 1/3 the
* size of the desktop. (jimm: why??)
* Calculates a preferred initial thumbnail size based based on newtab.css
* sizes or a preference for other applications. The sizes should be the same
* as set for the tile sizes in newtab.
*
* @param aWindow (optional) aWindow that is used to calculate the scaling size.
* @return The calculated thumbnail size or a default if unable to calculate.
*/
getThumbnailSize: function () {
getThumbnailSize: function (aWindow = null) {
if (!this._thumbnailWidth || !this._thumbnailHeight) {
let screenManager = Cc["@mozilla.org/gfx/screenmanager;1"]
.getService(Ci.nsIScreenManager);
let left = {}, top = {}, width = {}, height = {};
screenManager.primaryScreen.GetRectDisplayPix(left, top, width, height);
this._thumbnailWidth = Math.round(width.value / 3);
this._thumbnailHeight = Math.round(height.value / 3);
let left = {}, top = {}, screenWidth = {}, screenHeight = {};
screenManager.primaryScreen.GetRectDisplayPix(left, top, screenWidth, screenHeight);
/***
* The system default scale might be different than
* what is reported by the window. For example,
* retina displays have 1:1 system scales, but 2:1 window
* scale as 1 pixel system wide == 2 device pixels.
* To get the best image quality, query both and take the highest one.
*/
let systemScale = screenManager.systemDefaultScale;
let windowScale = aWindow ? aWindow.devicePixelRatio : systemScale;
let scale = Math.max(systemScale, windowScale);
/***
* On retina displays, we can sometimes go down this path
* without a window object. In those cases, force 2x scaling
* as the system scale doesn't represent the 2x scaling
* on OS X.
*/
if (AppConstants.platform == "macosx" && !aWindow) {
scale = 2;
}
/***
* THESE VALUES ARE DEFINED IN newtab.css and hard coded.
* If you change these values from the prefs,
* ALSO CHANGE THEM IN newtab.css
*/
let prefWidth = Services.prefs.getIntPref("toolkit.pageThumbs.minWidth");
let prefHeight = Services.prefs.getIntPref("toolkit.pageThumbs.minHeight");
let divisor = Services.prefs.getIntPref("toolkit.pageThumbs.screenSizeDivisor");
prefWidth *= scale;
prefHeight *= scale;
this._thumbnailWidth = Math.max(Math.round(screenWidth.value / divisor), prefWidth);;
this._thumbnailHeight = Math.max(Math.round(screenHeight.value / divisor), prefHeight);
}
return [this._thumbnailWidth, this._thumbnailHeight];
},
/***
* Given a browser window, return the size of the content
* minus the scroll bars.
*/
getContentSize: function(aWindow) {
let utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
// aWindow may be a cpow, add exposed props security values.
let sbWidth = {}, sbHeight = {};
try {
utils.getScrollbarSize(false, sbWidth, sbHeight);
} catch (e) {
// This might fail if the window does not have a presShell.
Cu.reportError("Unable to get scrollbar size in determineCropSize.");
sbWidth.value = sbHeight.value = 0;
}
// Even in RTL mode, scrollbars are always on the right.
// So there's no need to determine a left offset.
let width = aWindow.innerWidth - sbWidth.value;
let height = aWindow.innerHeight - sbHeight.value;
return [width, height];
},
/***
* Given a browser window, this creates a snapshot of the content
* and returns a canvas with the resulting snapshot of the content
* at the thumbnail size. It has to do this through a two step process:
*
* 1) Render the content at the window size to a canvas that is 2x the thumbnail size
* 2) Downscale the canvas from (1) down to the thumbnail size
*
* This is because the thumbnail size is too small to render at directly,
* causing pages to believe the browser is a small resolution. Also,
* at that resolution, graphical artifacts / text become very jagged.
* It's actually better to the eye to have small blurry text than sharp
* jagged pixels to represent text.
*
* @params aWindow - the window to create a snapshot of.
* @params aDestCanvas (optional) a destination canvas to draw the final snapshot to.
* @return Canvas with a scaled thumbnail of the window.
*/
createSnapshotThumbnail: function(aWindow, aDestCanvas = null) {
if (Cu.isCrossProcessWrapper(aWindow)) {
throw new Error('Do not pass cpows here.');
}
let [contentWidth, contentHeight] = this.getContentSize(aWindow);
let [thumbnailWidth, thumbnailHeight] = this.getThumbnailSize(aWindow);
let intermediateWidth = thumbnailWidth * 2;
let intermediateHeight = thumbnailHeight * 2;
let skipDownscale = false;
let snapshotCanvas = undefined;
// Our intermediate thumbnail is bigger than content,
// which can happen on hiDPI devices like a retina macbook pro.
// In those cases, just render at the final size.
if ((intermediateWidth >= contentWidth) ||
(intermediateHeight >= contentHeight)) {
intermediateWidth = thumbnailWidth;
intermediateHeight = thumbnailHeight;
skipDownscale = true;
snapshotCanvas = aDestCanvas;
}
// If we've been given a large preallocated canvas, so
// just render once into the destination canvas.
if (aDestCanvas &&
((aDestCanvas.width >= intermediateWidth) ||
(aDestCanvas.height >= intermediateHeight))) {
intermediateWidth = aDestCanvas.width;
intermediateHeight = aDestCanvas.height;
skipDownscale = true;
snapshotCanvas = aDestCanvas;
}
if (!snapshotCanvas) {
snapshotCanvas = this.createCanvas(aWindow, intermediateWidth, intermediateHeight);
}
// This is step 1.
// Also by default, canvas does not draw the scrollbars, so no need to
// remove the scrollbar sizes.
let scale = Math.min(Math.max(intermediateWidth / contentWidth,
intermediateHeight / contentHeight), 1);
let snapshotCtx = snapshotCanvas.getContext("2d");
snapshotCtx.save();
snapshotCtx.scale(scale, scale);
snapshotCtx.drawWindow(aWindow, 0, 0, contentWidth, contentHeight,
PageThumbUtils.THUMBNAIL_BG_COLOR,
snapshotCtx.DRAWWINDOW_DO_NOT_FLUSH);
snapshotCtx.restore();
if (skipDownscale) {
return snapshotCanvas;
}
// Part 2: Assumes that the snapshot is 2x the thumbnail size
let finalCanvas = aDestCanvas || this.createCanvas(aWindow, thumbnailWidth, thumbnailHeight);
let finalCtx = finalCanvas.getContext("2d");
finalCtx.save();
finalCtx.scale(0.5, 0.5);
finalCtx.drawImage(snapshotCanvas, 0, 0);
finalCtx.restore();
return finalCanvas;
},
/**
* Determine a good thumbnail crop size and scale for a given content
* window.

View File

@ -182,7 +182,7 @@ this.PageThumbs = {
let deferred = Promise.defer();
let canvas = this.createCanvas();
let canvas = this.createCanvas(aBrowser.contentWindow);
this.captureToCanvas(aBrowser, canvas, () => {
canvas.toBlob(blob => {
deferred.resolve(blob, this.contentType);
@ -221,7 +221,7 @@ this.PageThumbs = {
* transitory as it is based on current navigation state and the type of
* content being displayed.
*
* @param aBrowser The target browser
* @param aBrowser The target browser
* @param aCallback(aResult) A callback invoked once security checks have
* completed. aResult is a boolean indicating the combined result of the
* security checks performed.
@ -264,24 +264,7 @@ this.PageThumbs = {
return;
}
// Generate in-process content thumbnail
let [width, height, scale] =
PageThumbUtils.determineCropSize(aBrowser.contentWindow, aCanvas);
let ctx = aCanvas.getContext("2d");
// Scale the canvas accordingly.
ctx.save();
ctx.scale(scale, scale);
try {
// Draw the window contents to the canvas.
ctx.drawWindow(aBrowser.contentWindow, 0, 0, width, height,
PageThumbUtils.THUMBNAIL_BG_COLOR,
ctx.DRAWWINDOW_DO_NOT_FLUSH);
} catch (e) {
// We couldn't draw to the canvas for some reason.
}
ctx.restore();
aCanvas = PageThumbUtils.createSnapshotThumbnail(aBrowser.contentWindow, aCanvas);
if (aCallback) {
aCallback(aCanvas);

View File

@ -129,20 +129,10 @@ const backgroundPageThumbsContent = {
let canvasDrawDate = new Date();
let canvas = PageThumbUtils.createCanvas(content);
let [sw, sh, scale] = PageThumbUtils.determineCropSize(content, canvas);
let ctx = canvas.getContext("2d");
ctx.save();
ctx.scale(scale, scale);
ctx.drawWindow(content, 0, 0, sw, sh,
PageThumbUtils.THUMBNAIL_BG_COLOR,
ctx.DRAWWINDOW_DO_NOT_FLUSH);
ctx.restore();
let finalCanvas = PageThumbUtils.createSnapshotThumbnail(content);
capture.canvasDrawTime = new Date() - canvasDrawDate;
canvas.toBlob(blob => {
finalCanvas.toBlob(blob => {
capture.imageBlob = new Blob([blob]);
// Load about:blank to finish the capture and wait for onStateChange.
this._loadAboutBlank();

View File

@ -4,6 +4,13 @@
const URL = "http://mochi.test:8888/browser/toolkit/components/thumbnails/" +
"test/background_red_scroll.html";
function isRedThumbnailFuzz(r, g, b, expectedR, expectedB, expectedG, aFuzz)
{
return (Math.abs(r - expectedR) <= aFuzz) &&
(Math.abs(r - expectedR) <= aFuzz) &&
(Math.abs(r - expectedR) <= aFuzz);
}
// Test for black borders caused by scrollbars.
function runTests() {
// Create a tab with a page with a red background and scrollbars.
@ -14,7 +21,9 @@ function runTests() {
yield whenFileExists(URL);
yield retrieveImageDataForURL(URL, function (aData) {
let [r, g, b] = [].slice.call(aData, -4);
is("" + [r,g,b], "255,0,0", "we have a red thumbnail");
let fuzz = 2; // Windows 8 x64 blends with the scrollbar a bit.
var message = "Expected red thumbnail rgb(255, 0, 0), got " + r + "," + g + "," + b;
ok(isRedThumbnailFuzz(r, g, b, 255, 0, 0, fuzz), message);
next();
});
}

View File

@ -462,26 +462,12 @@ addMessageListener("UpdateCharacterSet", function (aMessage) {
* Remote thumbnail request handler for PageThumbs thumbnails.
*/
addMessageListener("Browser:Thumbnail:Request", function (aMessage) {
let thumbnail = content.document.createElementNS(PageThumbUtils.HTML_NAMESPACE,
"canvas");
thumbnail.mozOpaque = true;
thumbnail.mozImageSmoothingEnabled = true;
let snapshotWidth = aMessage.data.canvasWidth;
let snapshotHeight = aMessage.data.canvasHeight;
let canvas = PageThumbUtils.createCanvas(content, snapshotWidth, snapshotHeight);
let snapshot = PageThumbUtils.createSnapshotThumbnail(content, canvas);
thumbnail.width = aMessage.data.canvasWidth;
thumbnail.height = aMessage.data.canvasHeight;
let [width, height, scale] =
PageThumbUtils.determineCropSize(content, thumbnail);
let ctx = thumbnail.getContext("2d");
ctx.save();
ctx.scale(scale, scale);
ctx.drawWindow(content, 0, 0, width, height,
aMessage.data.background,
ctx.DRAWWINDOW_DO_NOT_FLUSH);
ctx.restore();
thumbnail.toBlob(function (aBlob) {
snapshot.toBlob(function (aBlob) {
sendAsyncMessage("Browser:Thumbnail:Response", {
thumbnail: aBlob,
id: aMessage.data.id