439 lines
16 KiB
JavaScript
439 lines
16 KiB
JavaScript
|
/**
|
||
|
* PWImageResizer: Client-side resizing of images (JPG, PNG, GIF)
|
||
|
*
|
||
|
* Code based on ImageUploader (c) Ross Turner (https://github.com/rossturner/HTML5-ImageUploader).
|
||
|
* Adapted for ProcessWire by Ryan as a resizer-only libary with different behavior and some fixes.
|
||
|
* Updates by Robin S. (https://github.com/toutouwai) to replace exif.js with piexif.js library.
|
||
|
*
|
||
|
* Requires piexif.js (https://github.com/hMatoba/piexifjs) to retain EXIF data in resized JPG.
|
||
|
*
|
||
|
* Config settings:
|
||
|
*
|
||
|
* - `maxWidth` (int): An integer in pixels for the maximum width allowed for uploaded images, selected images
|
||
|
* with a greater width than this value will be scaled down before upload. (default=0)
|
||
|
* Note: if no maxWidth is specified and maxHeight is, then maxHeight is also used for maxWidth.
|
||
|
* If neither maxWidth or maxHeight are specified, then 1024 is used for both.
|
||
|
*
|
||
|
* - `maxHeight` (int): An integer in pixels for the maximum height allowed for uploaded images, selected images
|
||
|
* with a greater height than this value will be scaled down before upload. (default=0)
|
||
|
* Note: if no maxHeight is specified and maxWidth is, then maxWidth is also used for maxHeight.
|
||
|
* If neither maxWidth or maxHeight are specified, then 1024 is used for both.
|
||
|
*
|
||
|
* - `maxSize` (float): A float value in megapixels (MP) for the maximum overall size of the image allowed for
|
||
|
* uploaded images, selected images with a greater size than this value will be scaled down before upload.
|
||
|
* The size of the image is calculated by the formula size = width * height / 1000000, where width and height
|
||
|
* are the dimensions of the image in pixels. If the value is null or is not specified, then maximum size
|
||
|
* restriction is not applied. Default value: null. For websites it's good to set this value around 1.7:
|
||
|
* for landscape images taken by standard photo cameras (Canon, Nikon, etc.), this value will lead to
|
||
|
* scaling down the original photo to size about 1600 x 1000 px, which is sufficient for displaying the
|
||
|
* scaled image on large screen monitors.
|
||
|
*
|
||
|
* - `scaleRatio` (float): Allows scaling down to a specified fraction of the original size.
|
||
|
* (Example: a value of 0.5 will reduce the size by half.) Accepts a decimal value between 0 and 1.
|
||
|
*
|
||
|
* - `quality` (float): A float between 0.1 and 1.0 for the image quality to use in the resulting image data,
|
||
|
* around 0.9 is recommended. Default value: 1.0. Applies to JPEG images only.
|
||
|
*
|
||
|
* - `debug` (bool): Output verbose debugging messages to javascript console.
|
||
|
*
|
||
|
*
|
||
|
* Example usage:
|
||
|
*
|
||
|
* // note: “file” variable is File object from “input[type=file].files” array
|
||
|
* var resizer = new PWImageResizer({
|
||
|
* maxWidth: 1600,
|
||
|
* maxHeight: 1200,
|
||
|
* quality: 0.9
|
||
|
* });
|
||
|
* resizer.resize(file, function(imageData) {
|
||
|
* if(imageData == false) {
|
||
|
* // no resize necessary, you can just upload file as-is
|
||
|
* } else {
|
||
|
* // upload the given resized imageData rather than file
|
||
|
* }
|
||
|
* });
|
||
|
*
|
||
|
*
|
||
|
* LICENSE (from original ImageUploader files by Ross Turner):
|
||
|
*
|
||
|
* Copyright (c) 2012 Ross Turner and contributors (https://github.com/zsinj)
|
||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
|
* of this software and associated documentation files (the "Software"), to deal
|
||
|
* in the Software without restriction, including without limitation the rights
|
||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
|
* copies of the Software, and to permit persons to whom the Software is
|
||
|
* furnished to do so, subject to the following conditions:
|
||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||
|
* SOFTWARE.
|
||
|
*
|
||
|
*/
|
||
|
var PWImageResizer = function(config) {
|
||
|
this.setConfig(config);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Primary public API to PWImageResizer
|
||
|
*
|
||
|
* @param file File to resize (single “File” object item from an “input[type=file].files” array)
|
||
|
* @param completionCallback Callback function upon completion, receives single ImageData argument.
|
||
|
* Receives populated ImageData when resize was necessary and completed.
|
||
|
* Receives boolean false for ImageData when no resize is necessary.
|
||
|
*
|
||
|
*/
|
||
|
PWImageResizer.prototype.resize = function(file, completionCallback) {
|
||
|
var img = document.createElement('img');
|
||
|
|
||
|
this.currentFile = file;
|
||
|
|
||
|
var reader = new FileReader();
|
||
|
var This = this;
|
||
|
var contentType = file.type.toString();
|
||
|
|
||
|
reader.onload = function(e) {
|
||
|
img.src = e.target.result;
|
||
|
|
||
|
img.onload = function() {
|
||
|
if(!This.needsResize(img, contentType)) {
|
||
|
// early exit when no resize necessary
|
||
|
// return false to callback, indicating that no resize is needed
|
||
|
completionCallback(false);
|
||
|
return;
|
||
|
}
|
||
|
This.scaleImage(img, completionCallback);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
reader.readAsDataURL(file);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Return whether or not image needs client-side resize performed
|
||
|
*
|
||
|
* This function not part of the original ImageUploader library.
|
||
|
*
|
||
|
* @param img The <img> element
|
||
|
* @param contentType Content-type of the image, i.e. "image/jpeg", "image/png", "image/gif"
|
||
|
* @returns {boolean}
|
||
|
*
|
||
|
*/
|
||
|
PWImageResizer.prototype.needsResize = function(img, contentType) {
|
||
|
var needsResize = false;
|
||
|
var why = 'n/a';
|
||
|
|
||
|
if(contentType != 'image/jpeg' && contentType != 'image/png' && contentType != 'image/gif') {
|
||
|
// content-type is not a supported image format
|
||
|
why = 'unsupported image content-type: ' + contentType;
|
||
|
|
||
|
} else if(this.config.scaleRatio > 0) {
|
||
|
// always proceed when scaleRatio is used
|
||
|
needsResize = true;
|
||
|
why = 'scaleRatio specified';
|
||
|
|
||
|
} else if(this.config.maxWidth > 0 || this.config.maxHeight > 0) {
|
||
|
// check dimensions
|
||
|
if(this.config.maxWidth > 0 && img.width > this.config.maxWidth) needsResize = true;
|
||
|
if(this.config.maxHeight > 0 && img.height > this.config.maxHeight) needsResize = true;
|
||
|
why = needsResize ? 'dimensions exceed max allowed' : 'dimensions do not require resize';
|
||
|
}
|
||
|
|
||
|
if(!needsResize && this.config.maxSize > 0) {
|
||
|
// check max allowed megapixels
|
||
|
if(this.config.maxSize < (img.width * img.height) / 1000000) needsResize = true;
|
||
|
why = (needsResize ? 'megapixels exceeds ' : 'megapixels below ') + this.config.maxSize;
|
||
|
}
|
||
|
|
||
|
if(this.config.debug) {
|
||
|
this.consoleLog('needsResize=' + (needsResize ? 'Yes' : 'No') + ' (' + why + ')');
|
||
|
}
|
||
|
|
||
|
return needsResize;
|
||
|
};
|
||
|
|
||
|
PWImageResizer.prototype.drawImage = function(context, img, x, y, width, height, deg, flip, flop, center) {
|
||
|
context.save();
|
||
|
|
||
|
if(typeof width === "undefined") width = img.width;
|
||
|
if(typeof height === "undefined") height = img.height;
|
||
|
if(typeof center === "undefined") center = false;
|
||
|
|
||
|
// Set rotation point to center of image, instead of top/left
|
||
|
if(center) {
|
||
|
x -= width/2;
|
||
|
y -= height/2;
|
||
|
}
|
||
|
|
||
|
// Set the origin to the center of the image
|
||
|
context.translate(x + width/2, y + height/2);
|
||
|
|
||
|
// Rotate the canvas around the origin
|
||
|
var rad = 2 * Math.PI - deg * Math.PI / 180;
|
||
|
context.rotate(rad);
|
||
|
|
||
|
// Flip/flop the canvas
|
||
|
if(flip) flipScale = -1; else flipScale = 1;
|
||
|
if(flop) flopScale = -1; else flopScale = 1;
|
||
|
context.scale(flipScale, flopScale);
|
||
|
|
||
|
// Draw the image
|
||
|
context.drawImage(img, -width/2, -height/2, width, height);
|
||
|
|
||
|
context.restore();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Scale an image
|
||
|
*
|
||
|
* @param img The <img> element
|
||
|
* @param completionCallback Function to call upon completion
|
||
|
*
|
||
|
*/
|
||
|
PWImageResizer.prototype.scaleImage = function(img, completionCallback) {
|
||
|
var canvas = document.createElement('canvas');
|
||
|
|
||
|
canvas.width = img.width;
|
||
|
canvas.height = img.height;
|
||
|
|
||
|
var ctx = canvas.getContext('2d');
|
||
|
ctx.save();
|
||
|
|
||
|
// Good explanation of EXIF orientation is here:
|
||
|
// http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/
|
||
|
|
||
|
var width = canvas.width;
|
||
|
var styleWidth = canvas.style.width;
|
||
|
var height = canvas.height;
|
||
|
var styleHeight = canvas.style.height;
|
||
|
|
||
|
ctx.drawImage(img, 0, 0);
|
||
|
ctx.restore();
|
||
|
|
||
|
//Lets find the max available width for scaled image
|
||
|
var ratio = canvas.width / canvas.height;
|
||
|
var mWidth = 0;
|
||
|
var resizeType = '';
|
||
|
|
||
|
if(this.config.maxWidth > 0 || this.config.maxHeight > 0) {
|
||
|
mWidth = Math.min(this.config.maxWidth, ratio * this.config.maxHeight);
|
||
|
resizeType = 'max width/height of ' + this.config.maxWidth + 'x' + this.config.maxHeight;
|
||
|
}
|
||
|
|
||
|
if(this.config.maxSize > 0 && (this.config.maxSize < (canvas.width * canvas.height) / 1000000)) {
|
||
|
var mSize = Math.floor(Math.sqrt(this.config.maxSize * ratio) * 1000);
|
||
|
mWidth = mWidth > 0 ? Math.min(mWidth, mSize) : mSize;
|
||
|
if(mSize === mWidth) resizeType = 'max megapixels of ' + this.config.maxSize;
|
||
|
}
|
||
|
|
||
|
if(this.config.scaleRatio) {
|
||
|
var mScale = Math.floor(this.config.scaleRatio * canvas.width);
|
||
|
mWidth = mWidth > 0 ? Math.min(mWidth, mScale) : mScale;
|
||
|
if(mScale == mWidth) resizeType = 'scale ratio of ' + this.config.scaleRatio;
|
||
|
}
|
||
|
|
||
|
if(mWidth <= 0) {
|
||
|
// mWidth = 1;
|
||
|
this.consoleLog('image size is too small to resize');
|
||
|
completionCallback(false);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if(this.config.debug) {
|
||
|
this.consoleLog('original image size: ' + canvas.width + 'x' + canvas.height + ' px');
|
||
|
this.consoleLog('scaled image size: ' + mWidth + 'x' + Math.floor(mWidth / ratio) + ' px via ' + resizeType);
|
||
|
}
|
||
|
|
||
|
while(canvas.width >= (2 * mWidth)) {
|
||
|
canvas = this.getHalfScaleCanvas(canvas);
|
||
|
}
|
||
|
|
||
|
if(canvas.width > mWidth) {
|
||
|
canvas = this.scaleCanvasWithAlgorithm(canvas, mWidth);
|
||
|
}
|
||
|
|
||
|
var quality = this.config.quality;
|
||
|
if(this.currentFile.type != 'image/jpeg') quality = 1.0;
|
||
|
var imageData = canvas.toDataURL(this.currentFile.type, quality);
|
||
|
|
||
|
if(typeof this.config.onScale === 'function') {
|
||
|
this.config.onScale(imageData);
|
||
|
}
|
||
|
|
||
|
/** Added by @Toutouwai for change to piexif **/
|
||
|
if(this.currentFile.type === 'image/jpeg') {
|
||
|
try {
|
||
|
var exifObj = piexif.load(img.src);
|
||
|
// Detect if there is a high likelihood this image has already been rotated according to its EXIF orientation
|
||
|
var orientation = exifObj['0th'][piexif.ImageIFD.Orientation];
|
||
|
if(orientation > 4 && img.height > img.width) {
|
||
|
// Set EXIF orientation to normal
|
||
|
exifObj['0th'][piexif.ImageIFD.Orientation] = 1;
|
||
|
}
|
||
|
try {
|
||
|
var exifStr = piexif.dump(exifObj);
|
||
|
try {
|
||
|
imageData = piexif.insert(exifStr, imageData);
|
||
|
} catch(error) {
|
||
|
console.error(error);
|
||
|
}
|
||
|
} catch(error) {
|
||
|
console.error(error);
|
||
|
}
|
||
|
} catch(error) {
|
||
|
console.error(error);
|
||
|
}
|
||
|
}
|
||
|
/** End added by @Toutouwai for piexif **/
|
||
|
|
||
|
completionCallback(this.imageDataToBlob(imageData));
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Convert base64 canvas image data to a BLOB
|
||
|
*
|
||
|
* This base64 decodes data so that it can be sent to the server as regular file data, rather than
|
||
|
* data that needs base64 decoding at the server side.
|
||
|
*
|
||
|
* Source: http://stackoverflow.com/questions/23945494/use-html5-to-resize-an-image-before-upload
|
||
|
* (This function is not part of the original ImageUploader library)
|
||
|
*
|
||
|
*/
|
||
|
PWImageResizer.prototype.imageDataToBlob = function(imageData) {
|
||
|
var base64Marker = ';base64,';
|
||
|
|
||
|
if(imageData.indexOf(base64Marker) == -1) {
|
||
|
var parts = imageData.split(',');
|
||
|
var contentType = parts[0].split(':')[1];
|
||
|
var raw = parts[1];
|
||
|
return new Blob([raw], { type: contentType });
|
||
|
}
|
||
|
|
||
|
var parts = imageData.split(base64Marker);
|
||
|
var contentType = parts[0].split(':')[1];
|
||
|
var raw = window.atob(parts[1]);
|
||
|
var rawLength = raw.length;
|
||
|
|
||
|
var uInt8Array = new Uint8Array(rawLength);
|
||
|
|
||
|
for (var i = 0; i < rawLength; ++i) {
|
||
|
uInt8Array[i] = raw.charCodeAt(i);
|
||
|
}
|
||
|
|
||
|
return new Blob([uInt8Array], { type: contentType });
|
||
|
};
|
||
|
|
||
|
PWImageResizer.prototype.scaleCanvasWithAlgorithm = function(canvas, maxWidth) {
|
||
|
var scaledCanvas = document.createElement('canvas');
|
||
|
var scale = maxWidth / canvas.width;
|
||
|
|
||
|
scaledCanvas.width = canvas.width * scale;
|
||
|
scaledCanvas.height = canvas.height * scale;
|
||
|
|
||
|
var srcImgData = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height);
|
||
|
var destImgData = scaledCanvas.getContext('2d').createImageData(scaledCanvas.width, scaledCanvas.height);
|
||
|
|
||
|
this.applyBilinearInterpolation(srcImgData, destImgData, scale);
|
||
|
|
||
|
scaledCanvas.getContext('2d').putImageData(destImgData, 0, 0);
|
||
|
|
||
|
return scaledCanvas;
|
||
|
};
|
||
|
|
||
|
PWImageResizer.prototype.getHalfScaleCanvas = function(canvas) {
|
||
|
var halfCanvas = document.createElement('canvas');
|
||
|
|
||
|
halfCanvas.width = canvas.width / 2;
|
||
|
halfCanvas.height = canvas.height / 2;
|
||
|
|
||
|
halfCanvas.getContext('2d').drawImage(canvas, 0, 0, halfCanvas.width, halfCanvas.height);
|
||
|
|
||
|
return halfCanvas;
|
||
|
};
|
||
|
|
||
|
PWImageResizer.prototype.applyBilinearInterpolation = function(srcCanvasData, destCanvasData, scale) {
|
||
|
function inner(f00, f10, f01, f11, x, y) {
|
||
|
var un_x = 1.0 - x;
|
||
|
var un_y = 1.0 - y;
|
||
|
return (f00 * un_x * un_y + f10 * x * un_y + f01 * un_x * y + f11 * x * y);
|
||
|
}
|
||
|
var i, j;
|
||
|
var iyv, iy0, iy1, ixv, ix0, ix1;
|
||
|
var idxD, idxS00, idxS10, idxS01, idxS11;
|
||
|
var dx, dy;
|
||
|
var r, g, b, a;
|
||
|
for (i = 0; i < destCanvasData.height; ++i) {
|
||
|
iyv = i / scale;
|
||
|
iy0 = Math.floor(iyv);
|
||
|
// Math.ceil can go over bounds
|
||
|
iy1 = (Math.ceil(iyv) > (srcCanvasData.height - 1) ? (srcCanvasData.height - 1) : Math.ceil(iyv));
|
||
|
for (j = 0; j < destCanvasData.width; ++j) {
|
||
|
ixv = j / scale;
|
||
|
ix0 = Math.floor(ixv);
|
||
|
// Math.ceil can go over bounds
|
||
|
ix1 = (Math.ceil(ixv) > (srcCanvasData.width - 1) ? (srcCanvasData.width - 1) : Math.ceil(ixv));
|
||
|
idxD = (j + destCanvasData.width * i) * 4;
|
||
|
// matrix to vector indices
|
||
|
idxS00 = (ix0 + srcCanvasData.width * iy0) * 4;
|
||
|
idxS10 = (ix1 + srcCanvasData.width * iy0) * 4;
|
||
|
idxS01 = (ix0 + srcCanvasData.width * iy1) * 4;
|
||
|
idxS11 = (ix1 + srcCanvasData.width * iy1) * 4;
|
||
|
// overall coordinates to unit square
|
||
|
dx = ixv - ix0;
|
||
|
dy = iyv - iy0;
|
||
|
// I let the r, g, b, a on purpose for debugging
|
||
|
r = inner(srcCanvasData.data[idxS00], srcCanvasData.data[idxS10], srcCanvasData.data[idxS01], srcCanvasData.data[idxS11], dx, dy);
|
||
|
destCanvasData.data[idxD] = r;
|
||
|
|
||
|
g = inner(srcCanvasData.data[idxS00 + 1], srcCanvasData.data[idxS10 + 1], srcCanvasData.data[idxS01 + 1], srcCanvasData.data[idxS11 + 1], dx, dy);
|
||
|
destCanvasData.data[idxD + 1] = g;
|
||
|
|
||
|
b = inner(srcCanvasData.data[idxS00 + 2], srcCanvasData.data[idxS10 + 2], srcCanvasData.data[idxS01 + 2], srcCanvasData.data[idxS11 + 2], dx, dy);
|
||
|
destCanvasData.data[idxD + 2] = b;
|
||
|
|
||
|
a = inner(srcCanvasData.data[idxS00 + 3], srcCanvasData.data[idxS10 + 3], srcCanvasData.data[idxS01 + 3], srcCanvasData.data[idxS11 + 3], dx, dy);
|
||
|
destCanvasData.data[idxD + 3] = a;
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
PWImageResizer.prototype.setConfig = function(customConfig) {
|
||
|
this.config = customConfig;
|
||
|
this.config.debug = this.config.debug || false;
|
||
|
|
||
|
if(typeof customConfig.quality == "undefined") customConfig.quality = 1.0;
|
||
|
if(customConfig.quality < 0.1) customConfig.quality = 0.1;
|
||
|
if(customConfig.quality > 1.0) customConfig.quality = 1.0;
|
||
|
this.config.quality = customConfig.quality;
|
||
|
|
||
|
if((!this.config.maxWidth) || (this.config.maxWidth < 0)) {
|
||
|
this.config.maxWidth = 0;
|
||
|
}
|
||
|
if((!this.config.maxHeight) || (this.config.maxHeight < 0)) {
|
||
|
this.config.maxHeight = 0;
|
||
|
}
|
||
|
if((!this.config.maxSize) || (this.config.maxSize < 0)) {
|
||
|
this.config.maxSize = null;
|
||
|
}
|
||
|
if((!this.config.scaleRatio) || (this.config.scaleRatio <= 0) || (this.config.scaleRatio >= 1)) {
|
||
|
this.config.scaleRatio = null;
|
||
|
}
|
||
|
|
||
|
// ensure both dimensions are provided (ryan)
|
||
|
if(this.config.maxWidth && !this.config.maxHeight) {
|
||
|
this.config.maxHeight = this.config.maxWidth;
|
||
|
} else if(this.config.maxHeight && !this.config.maxWidth) {
|
||
|
this.config.maxWidth = this.config.maxHeight;
|
||
|
} else if(!this.config.maxWidth && !this.config.maxHeight) {
|
||
|
// use default settings (0=disabled)
|
||
|
}
|
||
|
};
|
||
|
|
||
|
PWImageResizer.prototype.consoleLog = function(msg) {
|
||
|
if(this.config.debug) console.log('PWImageResizer: ' + msg);
|
||
|
};
|
||
|
|
||
|
|