toolkit/components/thumbnails/PageThumbUtils.jsm
author Mike Hommey <mh+mozilla@glandium.org>
Sat, 23 Jan 2016 11:40:24 -0800
changeset 281396 1378b39a9085890ce142e6933ebd50b6537f4ec5
parent 279631 49de4e1f40e6f0173e51b2e9f53b308341d5d2ac
child 283076 cb217f7271c24838782ff4f772af29a7ba9f53e8
permissions -rw-r--r--
Bug 1242074 - Avoid going back and forth between FasterMake and RecursiveMake; r=gps FasterMake needs some RecursiveMake install manifests to have been processed before doing its work, so we can actually end up processing them twice because of the going back and forth from FasterMake in the hybrid build system. Set the dependency at the RecursiveMake level when doing an hybrid build.

/* 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");

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.mozImageSmoothingEnabled = 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) {
    // 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;
  }
};