From: Tim Düsterhus Date: Fri, 29 Mar 2019 16:22:23 +0000 (+0100) Subject: Preserve file type if image contains transparent pixels X-Git-Tag: 5.2.0_Alpha_1~124^2~2 X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=c4eb14f5cd1cf499b7d5eaf944b6c200c0d6cd10;p=GitHub%2FWoltLab%2FWCF.git Preserve file type if image contains transparent pixels --- diff --git a/wcfsetup/install/files/js/WCF.Attachment.js b/wcfsetup/install/files/js/WCF.Attachment.js index 8abb95b879..34cf0c4720 100644 --- a/wcfsetup/install/files/js/WCF.Attachment.js +++ b/wcfsetup/install/files/js/WCF.Attachment.js @@ -296,9 +296,10 @@ if (COMPILER_TARGET_DEFAULT) { require([ 'WoltLabSuite/Core/FileUtil', - 'WoltLabSuite/Core/ImageResizer', + 'WoltLabSuite/Core/Image/ImageUtil', + 'WoltLabSuite/Core/Image/Resizer', 'WoltLabSuite/Core/Ajax/Status' - ], (function (FileUtil, ImageResizer, AjaxStatus) { + ], (function (FileUtil, ImageUtil, ImageResizer, AjaxStatus) { AjaxStatus.show(); var files = []; @@ -360,7 +361,7 @@ if (COMPILER_TARGET_DEFAULT) { var fileType = this._options.autoScale.fileType; - if (this._options.autoScale.fileType === 'keep') { + if (this._options.autoScale.fileType === 'keep' || ImageUtil.containsTransparentPixels(resizedImage)) { fileType = file.type; } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/ExifUtil.js b/wcfsetup/install/files/js/WoltLabSuite/Core/ExifUtil.js deleted file mode 100644 index 8db5931941..0000000000 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/ExifUtil.js +++ /dev/null @@ -1,204 +0,0 @@ -/** - * Provides helper functions for Exif metadata handling. - * - * @author Maximilian Mader - * @copyright 2001-2018 WoltLab GmbH - * @license GNU Lesser General Public License - * @module WoltLabSuite/Core/ExifUtil - */ -define([], function() { - "use strict"; - - var _tagNames = { - 'SOI': 0xD8, // Start of image - 'APP0': 0xE0, // JFIF tag - 'APP1': 0xE1, // EXIF / XMP - 'APP2': 0xE2, // General purpose tag - 'APP3': 0xE3, // General purpose tag - 'APP4': 0xE4, // General purpose tag - 'APP5': 0xE5, // General purpose tag - 'APP6': 0xE6, // General purpose tag - 'APP7': 0xE7, // General purpose tag - 'APP8': 0xE8, // General purpose tag - 'APP9': 0xE9, // General purpose tag - 'APP10': 0xEA, // General purpose tag - 'APP11': 0xEB, // General purpose tag - 'APP12': 0xEC, // General purpose tag - 'APP13': 0xED, // General purpose tag - 'APP14': 0xEE, // Often used to store copyright information - 'COM': 0xFE, // Comments - }; - - // Known sequence signatures - var _signatureEXIF = 'Exif'; - var _signatureXMP = 'http://ns.adobe.com/xap/1.0/'; - - return { - /** - * Extracts the EXIF / XMP sections of a JPEG blob. - * - * @param blob {Blob} JPEG blob - * @returns {Promise} Promise resolving with the EXIF / XMP sections - */ - getExifBytesFromJpeg: function (blob) { - return new Promise(function (resolve, reject) { - if (!(blob instanceof Blob) && !(blob instanceof File)) { - return reject(new TypeError('The argument must be a Blob or a File')); - } - - var reader = new FileReader(); - - reader.addEventListener('error', function () { - reader.abort(); - reject(reader.error); - }); - - reader.addEventListener('load', function() { - var buffer = reader.result; - var bytes = new Uint8Array(buffer); - var exif = new Uint8Array(); - - if (bytes[0] !== 0xFF && bytes[1] !== _tagNames.SOI) { - return reject(new Error('Not a JPEG')); - } - - for (var i = 2; i < bytes.length;) { - // each sequence starts with 0xFF - if (bytes[i] !== 0xFF) break; - - var length = 2 + ((bytes[i + 2] << 8) | bytes[i + 3]); - - // Check if the next byte indicates an EXIF sequence - if (bytes[i + 1] === _tagNames.APP1) { - var signature = ''; - for (var j = i + 4; bytes[j] !== 0 && j < bytes.length; j++) { - signature += String.fromCharCode(bytes[j]); - } - - // Only copy Exif and XMP data - if (signature === _signatureEXIF || signature === _signatureXMP) { - // append the found EXIF sequence, usually only a single EXIF (APP1) sequence should be defined - var sequence = Array.prototype.slice.call(bytes, i, length + i); // IE11 does not have slice in the Uint8Array prototype - var concat = new Uint8Array(exif.length + sequence.length); - concat.set(exif); - concat.set(sequence, exif.length); - exif = concat; - } - } - - i += length - } - - // No EXIF data found - resolve(exif); - }); - - reader.readAsArrayBuffer(blob); - }); - }, - - /** - * Removes all EXIF and XMP sections of a JPEG blob. - * - * @param blob {Blob} JPEG blob - * @returns {Promise} Promise resolving with the altered JPEG blob - */ - removeExifData: function (blob) { - return new Promise(function (resolve, reject) { - if (!(blob instanceof Blob) && !(blob instanceof File)) { - return reject(new TypeError('The argument must be a Blob or a File')); - } - - var reader = new FileReader(); - - reader.addEventListener('error', function () { - reader.abort(); - reject(reader.error); - }); - - reader.addEventListener('load', function () { - var buffer = reader.result; - var bytes = new Uint8Array(buffer); - - if (bytes[0] !== 0xFF && bytes[1] !== _tagNames.SOI) { - return reject(new Error('Not a JPEG')); - } - - for (var i = 2; i < bytes.length;) { - // each sequence starts with 0xFF - if (bytes[i] !== 0xFF) break; - - var length = 2 + ((bytes[i + 2] << 8) | bytes[i + 3]); - - // Check if the next byte indicates an EXIF sequence - if (bytes[i + 1] === _tagNames.APP1) { - var signature = ''; - for (var j = i + 4; bytes[j] !== 0 && j < bytes.length; j++) { - signature += String.fromCharCode(bytes[j]); - } - - // Only remove Exif and XMP data - if (signature === _signatureEXIF || signature === _signatureXMP) { - var start = Array.prototype.slice.call(bytes, 0, i); - var end = Array.prototype.slice.call(bytes, i + length); - bytes = new Uint8Array(start.length + end.length); - bytes.set(start, 0); - bytes.set(end, start.length); - } - } - else { - i += length; - } - } - - resolve(new Blob([bytes], {type: blob.type})); - }); - - reader.readAsArrayBuffer(blob); - }); - }, - - /** - * Overrides the APP1 (EXIF / XMP) sections of a JPEG blob with the given data. - * - * @param blob {Blob} JPEG blob - * @param exif {Uint8Array} APP1 sections - * @returns {Promise} Promise resolving with the altered JPEG blob - */ - setExifData: function (blob, exif) { - return this.removeExifData(blob).then(function (blob) { - return new Promise(function (resolve) { - var reader = new FileReader(); - - reader.addEventListener('error', function () { - reader.abort(); - reject(reader.error); - }); - - reader.addEventListener('load', function () { - var buffer = reader.result; - var bytes = new Uint8Array(buffer); - var offset = 2; - - // check if the second tag is the JFIF tag - if (bytes[2] === 0xFF && bytes[3] === _tagNames.APP0) { - offset += 2 + ((bytes[4] << 8) | bytes[5]); - } - - var start = Array.prototype.slice.call(bytes, 0, offset); - var end = Array.prototype.slice.call(bytes, offset); - - bytes = new Uint8Array(start.length + exif.length + end.length); - bytes.set(start); - bytes.set(exif, offset); - bytes.set(end, offset + exif.length); - - resolve(new Blob([bytes], {type: blob.type})); - }); - - reader.readAsArrayBuffer(blob); - }); - }); - } - }; -}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Image/ExifUtil.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Image/ExifUtil.js new file mode 100644 index 0000000000..618446cc7d --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Image/ExifUtil.js @@ -0,0 +1,204 @@ +/** + * Provides helper functions for Exif metadata handling. + * + * @author Maximilian Mader + * @copyright 2001-2018 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Image/ExifUtil + */ +define([], function() { + "use strict"; + + var _tagNames = { + 'SOI': 0xD8, // Start of image + 'APP0': 0xE0, // JFIF tag + 'APP1': 0xE1, // EXIF / XMP + 'APP2': 0xE2, // General purpose tag + 'APP3': 0xE3, // General purpose tag + 'APP4': 0xE4, // General purpose tag + 'APP5': 0xE5, // General purpose tag + 'APP6': 0xE6, // General purpose tag + 'APP7': 0xE7, // General purpose tag + 'APP8': 0xE8, // General purpose tag + 'APP9': 0xE9, // General purpose tag + 'APP10': 0xEA, // General purpose tag + 'APP11': 0xEB, // General purpose tag + 'APP12': 0xEC, // General purpose tag + 'APP13': 0xED, // General purpose tag + 'APP14': 0xEE, // Often used to store copyright information + 'COM': 0xFE, // Comments + }; + + // Known sequence signatures + var _signatureEXIF = 'Exif'; + var _signatureXMP = 'http://ns.adobe.com/xap/1.0/'; + + return { + /** + * Extracts the EXIF / XMP sections of a JPEG blob. + * + * @param blob {Blob} JPEG blob + * @returns {Promise} Promise resolving with the EXIF / XMP sections + */ + getExifBytesFromJpeg: function (blob) { + return new Promise(function (resolve, reject) { + if (!(blob instanceof Blob) && !(blob instanceof File)) { + return reject(new TypeError('The argument must be a Blob or a File')); + } + + var reader = new FileReader(); + + reader.addEventListener('error', function () { + reader.abort(); + reject(reader.error); + }); + + reader.addEventListener('load', function() { + var buffer = reader.result; + var bytes = new Uint8Array(buffer); + var exif = new Uint8Array(); + + if (bytes[0] !== 0xFF && bytes[1] !== _tagNames.SOI) { + return reject(new Error('Not a JPEG')); + } + + for (var i = 2; i < bytes.length;) { + // each sequence starts with 0xFF + if (bytes[i] !== 0xFF) break; + + var length = 2 + ((bytes[i + 2] << 8) | bytes[i + 3]); + + // Check if the next byte indicates an EXIF sequence + if (bytes[i + 1] === _tagNames.APP1) { + var signature = ''; + for (var j = i + 4; bytes[j] !== 0 && j < bytes.length; j++) { + signature += String.fromCharCode(bytes[j]); + } + + // Only copy Exif and XMP data + if (signature === _signatureEXIF || signature === _signatureXMP) { + // append the found EXIF sequence, usually only a single EXIF (APP1) sequence should be defined + var sequence = Array.prototype.slice.call(bytes, i, length + i); // IE11 does not have slice in the Uint8Array prototype + var concat = new Uint8Array(exif.length + sequence.length); + concat.set(exif); + concat.set(sequence, exif.length); + exif = concat; + } + } + + i += length + } + + // No EXIF data found + resolve(exif); + }); + + reader.readAsArrayBuffer(blob); + }); + }, + + /** + * Removes all EXIF and XMP sections of a JPEG blob. + * + * @param blob {Blob} JPEG blob + * @returns {Promise} Promise resolving with the altered JPEG blob + */ + removeExifData: function (blob) { + return new Promise(function (resolve, reject) { + if (!(blob instanceof Blob) && !(blob instanceof File)) { + return reject(new TypeError('The argument must be a Blob or a File')); + } + + var reader = new FileReader(); + + reader.addEventListener('error', function () { + reader.abort(); + reject(reader.error); + }); + + reader.addEventListener('load', function () { + var buffer = reader.result; + var bytes = new Uint8Array(buffer); + + if (bytes[0] !== 0xFF && bytes[1] !== _tagNames.SOI) { + return reject(new Error('Not a JPEG')); + } + + for (var i = 2; i < bytes.length;) { + // each sequence starts with 0xFF + if (bytes[i] !== 0xFF) break; + + var length = 2 + ((bytes[i + 2] << 8) | bytes[i + 3]); + + // Check if the next byte indicates an EXIF sequence + if (bytes[i + 1] === _tagNames.APP1) { + var signature = ''; + for (var j = i + 4; bytes[j] !== 0 && j < bytes.length; j++) { + signature += String.fromCharCode(bytes[j]); + } + + // Only remove Exif and XMP data + if (signature === _signatureEXIF || signature === _signatureXMP) { + var start = Array.prototype.slice.call(bytes, 0, i); + var end = Array.prototype.slice.call(bytes, i + length); + bytes = new Uint8Array(start.length + end.length); + bytes.set(start, 0); + bytes.set(end, start.length); + } + } + else { + i += length; + } + } + + resolve(new Blob([bytes], {type: blob.type})); + }); + + reader.readAsArrayBuffer(blob); + }); + }, + + /** + * Overrides the APP1 (EXIF / XMP) sections of a JPEG blob with the given data. + * + * @param blob {Blob} JPEG blob + * @param exif {Uint8Array} APP1 sections + * @returns {Promise} Promise resolving with the altered JPEG blob + */ + setExifData: function (blob, exif) { + return this.removeExifData(blob).then(function (blob) { + return new Promise(function (resolve) { + var reader = new FileReader(); + + reader.addEventListener('error', function () { + reader.abort(); + reject(reader.error); + }); + + reader.addEventListener('load', function () { + var buffer = reader.result; + var bytes = new Uint8Array(buffer); + var offset = 2; + + // check if the second tag is the JFIF tag + if (bytes[2] === 0xFF && bytes[3] === _tagNames.APP0) { + offset += 2 + ((bytes[4] << 8) | bytes[5]); + } + + var start = Array.prototype.slice.call(bytes, 0, offset); + var end = Array.prototype.slice.call(bytes, offset); + + bytes = new Uint8Array(start.length + exif.length + end.length); + bytes.set(start); + bytes.set(exif, offset); + bytes.set(end, offset + exif.length); + + resolve(new Blob([bytes], {type: blob.type})); + }); + + reader.readAsArrayBuffer(blob); + }); + }); + } + }; +}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Image/ImageUtil.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Image/ImageUtil.js new file mode 100644 index 0000000000..cd558c4be5 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Image/ImageUtil.js @@ -0,0 +1,29 @@ +/** + * Provides helper functions for Image metadata handling. + * + * @author Tim Duesterhus + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Image/ImageUtil + */ +define([], function() { + "use strict"; + + return { + /** + * Returns whether the given canvas contains transparent pixels. + * + * @param image {Canvas} Canvas to check + * @returns {bool} + */ + containsTransparentPixels: function (canvas) { + var imageData = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height); + + for (var i = 3, max = imageData.data.length; i < max; i += 4) { + if (imageData.data[i] !== 255) return true; + } + + return false; + } + }; +}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Image/Resizer.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Image/Resizer.js new file mode 100644 index 0000000000..f427f6159e --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Image/Resizer.js @@ -0,0 +1,206 @@ +/** + * This module allows resizing and conversion of HTMLImageElements to Blob and File objects + * + * @author Maximilian Mader + * @copyright 2001-2018 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Image/Resizer + */ +define([ + 'WoltLabSuite/Core/FileUtil', + 'WoltLabSuite/Core/Image/ExifUtil', + 'Pica' +], function(FileUtil, ExifUtil, Pica) { + "use strict"; + + var pica = new Pica({features: ['js', 'wasm', 'ww']}); + + /** + * @constructor + */ + function ImageResizer() { } + ImageResizer.prototype = { + maxWidth: 800, + maxHeight: 600, + quality: 0.8, + fileType: 'image/jpeg', + + /** + * Sets the default maximum width for this instance + * + * @param {Number} value the new default maximum width + * @returns {ImageResizer} this ImageResizer instance + */ + setMaxWidth: function (value) { + if (value == null) value = ImageResizer.prototype.maxWidth; + + this.maxWidth = value; + return this; + }, + + /** + * Sets the default maximum height for this instance + * + * @param {Number} value the new default maximum height + * @returns {ImageResizer} this ImageResizer instance + */ + setMaxHeight: function (value) { + if (value == null) value = ImageResizer.prototype.maxHeight; + + this.maxHeight = value; + return this; + }, + + /** + * Sets the default quality for this instance + * + * @param {Number} value the new default quality + * @returns {ImageResizer} this ImageResizer instance + */ + setQuality: function (value) { + if (value == null) value = ImageResizer.prototype.quality; + + this.quality = value; + return this; + }, + + /** + * Sets the default file type for this instance + * + * @param {Number} value the new default file type + * @returns {ImageResizer} this ImageResizer instance + */ + setFileType: function (value) { + if (value == null) value = ImageResizer.prototype.fileType; + + this.fileType = value; + return this; + }, + + /** + * Converts the given object of exif data and image data into a File. + * + * @param {Object{exif: Uint8Array|undefined, image: Canvas} data object containing exif data and image data + * @param {String} fileName the name of the returned file + * @param {String} [fileType] the type of the returned image + * @param {Number} [quality] quality setting, currently only effective for "image/jpeg" + * @returns {Promise} the File object + */ + saveFile: function (data, fileName, fileType, quality) { + fileType = fileType || this.fileType; + quality = quality || this.quality; + + var basename = fileName.match(/(.+)(\..+?)$/); + + return pica.toBlob(data.image, fileType, quality) + .then(function (blob) { + if (fileType === 'image/jpeg' && typeof data.exif !== 'undefined') { + return ExifUtil.setExifData(blob, data.exif); + } + + return blob; + }) + .then(function (blob) { + return FileUtil.blobToFile(blob, basename[1] + '_autoscaled'); + }); + }, + + /** + * Loads the given file into an image object and parses Exif information. + * + * @param {File} file the file to load + * @returns {Promise} resulting image data + */ + loadFile: function (file) { + var exif = undefined; + if (file.type === 'image/jpeg') { + // Extract EXIF data + exif = ExifUtil.getExifBytesFromJpeg(file); + } + + var loader = new Promise(function (resolve, reject) { + var reader = new FileReader(); + var image = new Image(); + + reader.addEventListener('load', function () { + image.src = reader.result; + }); + + reader.addEventListener('error', function () { + reader.abort(); + reject(reader.error); + }); + + image.addEventListener('error', reject); + + image.addEventListener('load', function () { + resolve(image); + }); + + reader.readAsDataURL(file); + }); + + return Promise.all([ exif, loader ]) + .then(function (result) { + return { exif: result[0], image: result[1] }; + }); + }, + + /** + * Downscales an image given as File object. + * + * @param {Image} image the image to resize + * @param {Number} [maxWidth] maximum width + * @param {Number} [maxHeight] maximum height + * @param {Number} [quality] quality in percent + * @param {boolean} [force] whether to force scaling even if unneeded (thus re-encoding with a possibly smaller file size) + * @param {Promise} cancelPromise a Promise used to cancel pica's operation when it resolves + * @returns {Promise} a Promise resolving with the resized image as a {Canvas} or undefined if no resizing happened + */ + resize: function (image, maxWidth, maxHeight, quality, force, cancelPromise) { + maxWidth = maxWidth || this.maxWidth; + maxHeight = maxHeight || this.maxHeight; + quality = quality || this.quality; + force = force || false; + + var canvas = document.createElement('canvas'); + + // Prevent upscaling + var newWidth = Math.min(maxWidth, image.width); + var newHeight = Math.min(maxHeight, image.height); + + if (image.width <= newWidth && image.height <= newHeight && !force) { + return Promise.resolve(undefined); + } + + // Keep image ratio + if (newWidth >= newHeight) { + canvas.width = newWidth; + canvas.height = newWidth * (image.height / image.width); + } + else { + canvas.width = newHeight * (image.width / image.height); + canvas.height = newHeight; + } + + // Map to Pica's quality + var resizeQuality = 1; + if (quality >= 0.8) { + resizeQuality = 3; + } + else if (quality >= 0.4) { + resizeQuality = 2; + } + + var options = { + quality: resizeQuality, + cancelToken: cancelPromise, + alpha: true + }; + + return pica.resize(image, canvas, options); + } + }; + + return ImageResizer; +}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/ImageResizer.js b/wcfsetup/install/files/js/WoltLabSuite/Core/ImageResizer.js deleted file mode 100644 index 3765407e3f..0000000000 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/ImageResizer.js +++ /dev/null @@ -1,205 +0,0 @@ -/** - * This module allows resizing and conversion of HTMLImageElements to Blob and File objects - * - * @author Maximilian Mader - * @copyright 2001-2018 WoltLab GmbH - * @license GNU Lesser General Public License - * @module WoltLabSuite/Core/ImageResizer - */ -define([ - 'WoltLabSuite/Core/FileUtil', - 'WoltLabSuite/Core/ExifUtil', - 'Pica' -], function(FileUtil, ExifUtil, Pica) { - "use strict"; - - var pica = new Pica({features: ['js', 'wasm', 'ww']}); - - /** - * @constructor - */ - function ImageResizer() { } - ImageResizer.prototype = { - maxWidth: 800, - maxHeight: 600, - quality: 0.8, - fileType: 'image/jpeg', - - /** - * Sets the default maximum width for this instance - * - * @param {Number} value the new default maximum width - * @returns {ImageResizer} this ImageResizer instance - */ - setMaxWidth: function (value) { - if (value == null) value = ImageResizer.prototype.maxWidth; - - this.maxWidth = value; - return this; - }, - - /** - * Sets the default maximum height for this instance - * - * @param {Number} value the new default maximum height - * @returns {ImageResizer} this ImageResizer instance - */ - setMaxHeight: function (value) { - if (value == null) value = ImageResizer.prototype.maxHeight; - - this.maxHeight = value; - return this; - }, - - /** - * Sets the default quality for this instance - * - * @param {Number} value the new default quality - * @returns {ImageResizer} this ImageResizer instance - */ - setQuality: function (value) { - if (value == null) value = ImageResizer.prototype.quality; - - this.quality = value; - return this; - }, - - /** - * Sets the default file type for this instance - * - * @param {Number} value the new default file type - * @returns {ImageResizer} this ImageResizer instance - */ - setFileType: function (value) { - if (value == null) value = ImageResizer.prototype.fileType; - - this.fileType = value; - return this; - }, - - /** - * Converts the given object of exif data and image data into a File. - * - * @param {Object{exif: Uint8Array|undefined, image: Canvas} data object containing exif data and image data - * @param {String} fileName the name of the returned file - * @param {String} [fileType] the type of the returned image - * @param {Number} [quality] quality setting, currently only effective for "image/jpeg" - * @returns {Promise} the File object - */ - saveFile: function (data, fileName, fileType, quality) { - fileType = fileType || this.fileType; - quality = quality || this.quality; - - var basename = fileName.match(/(.+)(\..+?)$/); - - return pica.toBlob(data.image, fileType, quality) - .then(function (blob) { - if (fileType === 'image/jpeg' && typeof data.exif !== 'undefined') { - return ExifUtil.setExifData(blob, data.exif); - } - - return blob; - }) - .then(function (blob) { - return FileUtil.blobToFile(blob, basename[1] + '_autoscaled'); - }); - }, - - /** - * Loads the given file into an image object and parses Exif information. - * - * @param {File} file the file to load - * @returns {Promise} resulting image data - */ - loadFile: function (file) { - var exif = undefined; - if (file.type === 'image/jpeg') { - // Extract EXIF data - exif = ExifUtil.getExifBytesFromJpeg(file); - } - - var loader = new Promise(function (resolve, reject) { - var reader = new FileReader(); - var image = new Image(); - - reader.addEventListener('load', function () { - image.src = reader.result; - }); - - reader.addEventListener('error', function () { - reader.abort(); - reject(reader.error); - }); - - image.addEventListener('error', reject); - - image.addEventListener('load', function () { - resolve(image); - }); - - reader.readAsDataURL(file); - }); - - return Promise.all([ exif, loader ]) - .then(function (result) { - return { exif: result[0], image: result[1] }; - }); - }, - - /** - * Downscales an image given as File object. - * - * @param {Image} image the image to resize - * @param {Number} [maxWidth] maximum width - * @param {Number} [maxHeight] maximum height - * @param {Number} [quality] quality in percent - * @param {boolean} [force] whether to force scaling even if unneeded (thus re-encoding with a possibly smaller file size) - * @param {Promise} cancelPromise a Promise used to cancel pica's operation when it resolves - * @returns {Promise} a Promise resolving with the resized image as a {Canvas} or undefined if no resizing happened - */ - resize: function (image, maxWidth, maxHeight, quality, force, cancelPromise) { - maxWidth = maxWidth || this.maxWidth; - maxHeight = maxHeight || this.maxHeight; - quality = quality || this.quality; - force = force || false; - - var canvas = document.createElement('canvas'); - - // Prevent upscaling - var newWidth = Math.min(maxWidth, image.width); - var newHeight = Math.min(maxHeight, image.height); - - if (image.width <= newWidth && image.height <= newHeight && !force) { - return Promise.resolve(undefined); - } - - // Keep image ratio - if (newWidth >= newHeight) { - canvas.width = newWidth; - canvas.height = newWidth * (image.height / image.width); - } - else { - canvas.width = newHeight * (image.width / image.height); - canvas.height = newHeight; - } - - // Map to Pica's quality - var resizeQuality = 1; - if (quality >= 0.8) { - resizeQuality = 3; - } - else if (quality >= 0.4) { - resizeQuality = 2; - } - - var options = { - quality: resizeQuality, - cancelToken: cancelPromise - }; - - return pica.resize(image, canvas, options); - } - }; - - return ImageResizer; -}); diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index a1421fcdc0..30d47dd5e7 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -1392,7 +1392,7 @@ Das Fehlerprotokoll enthält {$data[count]} neue Einträge. Die ersten drei, in - + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index 0ba9e5857a..3f3eb676e6 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -1369,7 +1369,7 @@ This protocol file contains {$data[count]} new entries. The first three error me - +