mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-25 13:51:41 +00:00
Bug 1197361. Optimize page thumbnails based on screen size. r=ttaubert
This commit is contained in:
parent
f4030f94ef
commit
7c8b086a65
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user