summaryrefslogtreecommitdiff
path: root/toolkit/components/thumbnails/PageThumbUtils.jsm
blob: fb5d67ddb5aaa2ff1cbbf66126f5d399608d67be (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
/* 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);

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);

#ifdef XP_MACOSX
      /** *
       * 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 (!aWindow) {
        scale = 2;
      }
#endif

      /** *
       * 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;
    }
  },
};