Source code

Revision control

Copy as Markdown

Other Tools

/* 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 => {
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;