summaryrefslogtreecommitdiffstats
path: root/browser/extensions/screenshots/build/thumbnailGenerator.js
blob: c80ccb6bacf323cb46976d737dae934e2af92cf5 (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
/* 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/. */

this.thumbnailGenerator = (function () {
  let exports = {}; // This is used in webextension/background/takeshot.js,
  // server/src/pages/shot/controller.js, and
  // server/scr/pages/shotindex/view.js. It is used in a browser
  // environment.

  // Resize down 1/2 at a time produces better image quality.
  // Not quite as good as using a third-party filter (which will be
  // slower), but good enough.
  const maxResizeScaleFactor = 0.5;

  // The shot will be scaled or cropped down to 210px on x, and cropped or
  // scaled down to a maximum of 280px on y.
  // x: 210
  // y: <= 280
  const maxThumbnailWidth = 210;
  const maxThumbnailHeight = 280;

  /**
   * @param {int} imageHeight Height in pixels of the original image.
   * @param {int} imageWidth Width in pixels of the original image.
   * @returns {width, height, scaledX, scaledY}
   */
  function getThumbnailDimensions(imageWidth, imageHeight) {
    const displayAspectRatio = 3 / 4;
    const imageAspectRatio = imageWidth / imageHeight;
    let thumbnailImageWidth, thumbnailImageHeight;
    let scaledX, scaledY;

    if (imageAspectRatio > displayAspectRatio) {
      // "Landscape" mode
      // Scale on y, crop on x
      const yScaleFactor =
        imageHeight > maxThumbnailHeight
          ? maxThumbnailHeight / imageHeight
          : 1.0;
      thumbnailImageHeight = scaledY = Math.round(imageHeight * yScaleFactor);
      scaledX = Math.round(imageWidth * yScaleFactor);
      thumbnailImageWidth = Math.min(scaledX, maxThumbnailWidth);
    } else {
      // "Portrait" mode
      // Scale on x, crop on y
      const xScaleFactor =
        imageWidth > maxThumbnailWidth ? maxThumbnailWidth / imageWidth : 1.0;
      thumbnailImageWidth = scaledX = Math.round(imageWidth * xScaleFactor);
      scaledY = Math.round(imageHeight * xScaleFactor);
      // The CSS could widen the image, in which case we crop more off of y.
      thumbnailImageHeight = Math.min(
        scaledY,
        maxThumbnailHeight,
        maxThumbnailHeight / (maxThumbnailWidth / imageWidth)
      );
    }

    return {
      width: thumbnailImageWidth,
      height: thumbnailImageHeight,
      scaledX,
      scaledY,
    };
  }

  /**
   * @param {dataUrl} String Data URL of the original image.
   * @param {int} imageHeight Height in pixels of the original image.
   * @param {int} imageWidth Width in pixels of the original image.
   * @param {String} urlOrBlob 'blob' for a blob, otherwise data url.
   * @returns A promise that resolves to the data URL or blob of the thumbnail image, or null.
   */
  function createThumbnail(dataUrl, imageWidth, imageHeight, urlOrBlob) {
    // There's cost associated with generating, transmitting, and storing
    // thumbnails, so we'll opt out if the image size is below a certain threshold
    const thumbnailThresholdFactor = 1.2;
    const thumbnailWidthThreshold =
      maxThumbnailWidth * thumbnailThresholdFactor;
    const thumbnailHeightThreshold =
      maxThumbnailHeight * thumbnailThresholdFactor;

    if (
      imageWidth <= thumbnailWidthThreshold &&
      imageHeight <= thumbnailHeightThreshold
    ) {
      // Do not create a thumbnail.
      return Promise.resolve(null);
    }

    const thumbnailDimensions = getThumbnailDimensions(imageWidth, imageHeight);

    return new Promise((resolve, reject) => {
      const thumbnailImage = new Image();
      let srcWidth = imageWidth;
      let srcHeight = imageHeight;
      let destWidth, destHeight;

      thumbnailImage.onload = function () {
        destWidth = Math.round(srcWidth * maxResizeScaleFactor);
        destHeight = Math.round(srcHeight * maxResizeScaleFactor);
        if (
          destWidth <= thumbnailDimensions.scaledX ||
          destHeight <= thumbnailDimensions.scaledY
        ) {
          srcWidth = Math.round(
            srcWidth * (thumbnailDimensions.width / thumbnailDimensions.scaledX)
          );
          srcHeight = Math.round(
            srcHeight *
              (thumbnailDimensions.height / thumbnailDimensions.scaledY)
          );
          destWidth = thumbnailDimensions.width;
          destHeight = thumbnailDimensions.height;
        }

        const thumbnailCanvas = document.createElement("canvas");
        thumbnailCanvas.width = destWidth;
        thumbnailCanvas.height = destHeight;
        const ctx = thumbnailCanvas.getContext("2d");
        ctx.imageSmoothingEnabled = false;

        ctx.drawImage(
          thumbnailImage,
          0,
          0,
          srcWidth,
          srcHeight,
          0,
          0,
          destWidth,
          destHeight
        );

        if (
          thumbnailCanvas.width <= thumbnailDimensions.width ||
          thumbnailCanvas.height <= thumbnailDimensions.height
        ) {
          if (urlOrBlob === "blob") {
            thumbnailCanvas.toBlob(blob => {
              resolve(blob);
            });
          } else {
            resolve(thumbnailCanvas.toDataURL("image/png"));
          }
          return;
        }

        srcWidth = destWidth;
        srcHeight = destHeight;
        thumbnailImage.src = thumbnailCanvas.toDataURL();
      };
      thumbnailImage.src = dataUrl;
    });
  }

  function createThumbnailUrl(shot) {
    const image = shot.getClip(shot.clipNames()[0]).image;
    if (!image.url) {
      return Promise.resolve(null);
    }
    return createThumbnail(
      image.url,
      image.dimensions.x,
      image.dimensions.y,
      "dataurl"
    );
  }

  function createThumbnailBlobFromPromise(shot, blobToUrlPromise) {
    return blobToUrlPromise.then(dataUrl => {
      const image = shot.getClip(shot.clipNames()[0]).image;
      return createThumbnail(
        dataUrl,
        image.dimensions.x,
        image.dimensions.y,
        "blob"
      );
    });
  }

  if (typeof exports !== "undefined") {
    exports.getThumbnailDimensions = getThumbnailDimensions;
    exports.createThumbnailUrl = createThumbnailUrl;
    exports.createThumbnailBlobFromPromise = createThumbnailBlobFromPromise;
  }

  return exports;
})();
null;