diff options
Diffstat (limited to 'browser/extensions/screenshots/build/thumbnailGenerator.js')
-rw-r--r-- | browser/extensions/screenshots/build/thumbnailGenerator.js | 190 |
1 files changed, 190 insertions, 0 deletions
diff --git a/browser/extensions/screenshots/build/thumbnailGenerator.js b/browser/extensions/screenshots/build/thumbnailGenerator.js new file mode 100644 index 0000000000..d66de42bfd --- /dev/null +++ b/browser/extensions/screenshots/build/thumbnailGenerator.js @@ -0,0 +1,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; |