diff options
Diffstat (limited to 'toolkit/components/thumbnails/PageThumbUtils.jsm')
-rw-r--r-- | toolkit/components/thumbnails/PageThumbUtils.jsm | 354 |
1 files changed, 354 insertions, 0 deletions
diff --git a/toolkit/components/thumbnails/PageThumbUtils.jsm b/toolkit/components/thumbnails/PageThumbUtils.jsm new file mode 100644 index 000000000..dda3a81b3 --- /dev/null +++ b/toolkit/components/thumbnails/PageThumbUtils.jsm @@ -0,0 +1,354 @@ +/* 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/. */ + +/* + * Common thumbnailing routines used by various consumers, including + * PageThumbs and backgroundPageThumbsContent. + */ + +this.EXPORTED_SYMBOLS = ["PageThumbUtils"]; + +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"); + +XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils", + "resource://gre/modules/BrowserUtils.jsm"); + +this.PageThumbUtils = { + // The default background color for page thumbnails. + THUMBNAIL_BG_COLOR: "#fff", + // The namespace for thumbnail canvas elements. + HTML_NAMESPACE: "http://www.w3.org/1999/xhtml", + + /** + * Creates a new canvas element in the context of aWindow, or if aWindow + * is undefined, in the context of hiddenDOMWindow. + * + * @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, aWidth = 0, aHeight = 0) { + let doc = (aWindow || Services.appShell.hiddenDOMWindow).document; + let canvas = doc.createElementNS(this.HTML_NAMESPACE, "canvas"); + canvas.mozOpaque = true; + canvas.imageSmoothingEnabled = true; + 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 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 (aWindow = null) { + if (!this._thumbnailWidth || !this._thumbnailHeight) { + let screenManager = Cc["@mozilla.org/gfx/screenmanager;1"] + .getService(Ci.nsIScreenManager); + 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 destination canvas to draw the final + * snapshot to. Can be null. + * @param aArgs (optional) Additional named parameters: + * fullScale - request that a non-downscaled image be returned. + * @return Canvas with a scaled thumbnail of the window. + */ + createSnapshotThumbnail: function(aWindow, aDestCanvas, aArgs) { + if (Cu.isCrossProcessWrapper(aWindow)) { + throw new Error('Do not pass cpows here.'); + } + let fullScale = aArgs ? aArgs.fullScale : false; + let [contentWidth, contentHeight] = this.getContentSize(aWindow); + let [thumbnailWidth, thumbnailHeight] = aDestCanvas ? + [aDestCanvas.width, aDestCanvas.height] : + this.getThumbnailSize(aWindow); + + // If the caller wants a fullscale image, set the desired thumbnail dims + // to the dims of content and (if provided) size the incoming canvas to + // support our results. + if (fullScale) { + thumbnailWidth = contentWidth; + thumbnailHeight = contentHeight; + if (aDestCanvas) { + aDestCanvas.width = contentWidth; + aDestCanvas.height = contentHeight; + } + } + + let intermediateWidth = thumbnailWidth * 2; + let intermediateHeight = thumbnailHeight * 2; + let skipDownscale = false; + + // If the intermediate thumbnail is larger than content dims (hiDPI + // devices can experience this) or a full preview is requested render + // at the final thumbnail size. + if ((intermediateWidth >= contentWidth || + intermediateHeight >= contentHeight) || fullScale) { + intermediateWidth = thumbnailWidth; + intermediateHeight = thumbnailHeight; + skipDownscale = true; + } + + // Create an intermediate surface + let snapshotCanvas = this.createCanvas(aWindow, intermediateWidth, + intermediateHeight); + + // Step 1: capture the image at the intermediate dims. For thumbnails + // this is twice the thumbnail size, for fullScale images this is at + // content dims. + // 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(); + + // Part 2: Downscale from our intermediate dims to the final thumbnail + // dims and copy the result to aDestCanvas. If the caller didn't + // provide a target canvas, create a new canvas and return it. + let finalCanvas = aDestCanvas || + this.createCanvas(aWindow, thumbnailWidth, thumbnailHeight); + + let finalCtx = finalCanvas.getContext("2d"); + finalCtx.save(); + if (!skipDownscale) { + 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. + * + * @param aWindow The content window. + * @param aCanvas The target canvas. + * @return An array containing width, height and scale. + */ + determineCropSize: function (aWindow, aCanvas) { + if (Cu.isCrossProcessWrapper(aWindow)) { + throw new Error('Do not pass cpows here.'); + } + 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; + + let {width: thumbnailWidth, height: thumbnailHeight} = aCanvas; + let scale = Math.min(Math.max(thumbnailWidth / width, thumbnailHeight / height), 1); + let scaledWidth = width * scale; + let scaledHeight = height * scale; + + if (scaledHeight > thumbnailHeight) + height -= Math.floor(Math.abs(scaledHeight - thumbnailHeight) * scale); + + if (scaledWidth > thumbnailWidth) + width -= Math.floor(Math.abs(scaledWidth - thumbnailWidth) * scale); + + return [width, height, scale]; + }, + + shouldStoreContentThumbnail: function (aDocument, aDocShell) { + if (BrowserUtils.isToolbarVisible(aDocShell, "findbar")) { + return false; + } + + // FIXME Bug 720575 - Don't capture thumbnails for SVG or XML documents as + // that currently regresses Talos SVG tests. + if (aDocument instanceof Ci.nsIDOMXMLDocument) { + return false; + } + + let webNav = aDocShell.QueryInterface(Ci.nsIWebNavigation); + + // Don't take screenshots of about: pages. + if (webNav.currentURI.schemeIs("about")) { + return false; + } + + // There's no point in taking screenshot of loading pages. + if (aDocShell.busyFlags != Ci.nsIDocShell.BUSY_FLAGS_NONE) { + return false; + } + + let channel = aDocShell.currentDocumentChannel; + + // No valid document channel. We shouldn't take a screenshot. + if (!channel) { + return false; + } + + // Don't take screenshots of internally redirecting about: pages. + // This includes error pages. + let uri = channel.originalURI; + if (uri.schemeIs("about")) { + return false; + } + + let httpChannel; + try { + httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); + } catch (e) { /* Not an HTTP channel. */ } + + if (httpChannel) { + // Continue only if we have a 2xx status code. + try { + if (Math.floor(httpChannel.responseStatus / 100) != 2) { + return false; + } + } catch (e) { + // Can't get response information from the httpChannel + // because mResponseHead is not available. + return false; + } + + // Cache-Control: no-store. + if (httpChannel.isNoStoreResponse()) { + return false; + } + + // Don't capture HTTPS pages unless the user explicitly enabled it. + if (uri.schemeIs("https") && + !Services.prefs.getBoolPref("browser.cache.disk_cache_ssl")) { + return false; + } + } // httpChannel + return true; + }, + + /** + * Given a channel, returns true if it should be considered an "error + * response", false otherwise. + */ + isChannelErrorResponse: function(channel) { + // No valid document channel sounds like an error to me! + if (!channel) + return true; + if (!(channel instanceof Ci.nsIHttpChannel)) + // it might be FTP etc, so assume it's ok. + return false; + try { + return !channel.requestSucceeded; + } catch (_) { + // not being able to determine success is surely failure! + return true; + } + }, +}; |