mirror of
https://github.com/processwire/processwire.git
synced 2025-08-09 16:26:59 +02:00
Add client-side image resize support to InputfieldImage
This commit is contained in:
@@ -102,6 +102,9 @@
|
||||
font-size: 1.3em;
|
||||
cursor: move;
|
||||
text-shadow: 0px 0px 7px rgba(62, 185, 152, 0.7); }
|
||||
.gridImage__resize.pw-resizing {
|
||||
text-shadow: none;
|
||||
background: rgba(0, 0, 0, 0.7); }
|
||||
.gridImage__progress {
|
||||
/* Reset the default appearance */
|
||||
-webkit-appearance: none;
|
||||
|
@@ -1,3 +1,10 @@
|
||||
|
||||
/*****************************************************************************************************************
|
||||
* ProcessWire InputfieldImage
|
||||
*
|
||||
* Copyright 2017 by ProcessWire
|
||||
*
|
||||
*/
|
||||
function InputfieldImage($) {
|
||||
|
||||
// When uploading a file in place: .gridItem that file(s) will be placed before
|
||||
@@ -1107,6 +1114,7 @@ function InputfieldImage($) {
|
||||
var gridSize = $fileList.data("gridsize");
|
||||
var doneTimer = null; // for AjaxUploadDone event
|
||||
var maxFiles = parseInt($this.find('.InputfieldImageMaxFiles').val());
|
||||
var resizeSettings = getClientResizeSettings($inputfield);
|
||||
|
||||
setupDropzone($this);
|
||||
if(maxFiles != 1) setupDropInPlace($fileList);
|
||||
@@ -1334,9 +1342,10 @@ function InputfieldImage($) {
|
||||
* Upload file
|
||||
*
|
||||
* @param file
|
||||
* @param extension (optional)
|
||||
*
|
||||
*/
|
||||
function uploadFile(file) {
|
||||
function uploadFile(file, extension) {
|
||||
|
||||
var labels = ProcessWire.config.InputfieldImage.labels;
|
||||
var filesizeStr = parseInt(file.size / 1024, 10) + ' kB';
|
||||
@@ -1420,12 +1429,15 @@ function InputfieldImage($) {
|
||||
xhr = new XMLHttpRequest();
|
||||
|
||||
// Update progress bar
|
||||
xhr.upload.addEventListener("progress", function(evt) {
|
||||
if(!evt.lengthComputable) return;
|
||||
function updateProgress(evt) {
|
||||
if(typeof evt != "undefined") {
|
||||
if(!evt.lengthComputable) return;
|
||||
$progressBar.attr("value", parseInt((evt.loaded / evt.total) * 100));
|
||||
}
|
||||
$('body').addClass('pw-uploading');
|
||||
$progressBar.attr("value", parseInt((evt.loaded / evt.total) * 100));
|
||||
$spinner.css('display', 'block');
|
||||
}, false);
|
||||
}
|
||||
xhr.upload.addEventListener("progress", updateProgress, false);
|
||||
|
||||
// File uploaded: called for each file
|
||||
xhr.addEventListener("load", function() {
|
||||
@@ -1535,32 +1547,53 @@ function InputfieldImage($) {
|
||||
} else if($inputfield.find(".InputfieldImageEdit:visible").length) {
|
||||
$inputfield.find(".InputfieldImageEdit__close").click();
|
||||
}
|
||||
|
||||
// Here we go
|
||||
xhr.open("POST", postUrl, true);
|
||||
xhr.setRequestHeader("X-FILENAME", encodeURIComponent(file.name));
|
||||
xhr.setRequestHeader("X-FIELDNAME", fieldName);
|
||||
xhr.setRequestHeader("Content-Type", "application/octet-stream"); // fix issue 96-Pete
|
||||
xhr.setRequestHeader("X-" + postTokenName, postTokenValue);
|
||||
xhr.setRequestHeader("X-REQUESTED-WITH", 'XMLHttpRequest');
|
||||
xhr.send(file);
|
||||
|
||||
|
||||
// Present file info and append it to the list of files
|
||||
if(uploadReplace.item) {
|
||||
uploadReplace.item.replaceWith($progressItem);
|
||||
uploadReplace.item = $progressItem;
|
||||
} else if($uploadBeforeItem && $uploadBeforeItem.length) {
|
||||
$uploadBeforeItem.before($progressItem);
|
||||
$uploadBeforeItem.before($progressItem);
|
||||
} else {
|
||||
$fileList.append($progressItem);
|
||||
}
|
||||
updateGrid();
|
||||
$inputfield.trigger('change');
|
||||
var numFiles = $inputfield.find('.InputfieldFileItem').length;
|
||||
if(numFiles == 1) {
|
||||
$inputfield.removeClass('InputfieldFileEmpty').removeClass('InputfieldFileMultiple').addClass('InputfieldFileSingle');
|
||||
} else if(numFiles > 1) {
|
||||
$inputfield.removeClass('InputfieldFileEmpty').removeClass('InputfieldFileSingle').addClass('InputfieldFileMultiple');
|
||||
|
||||
// Here we go
|
||||
function sendUpload(file, imageData) {
|
||||
xhr.open("POST", postUrl, true);
|
||||
xhr.setRequestHeader("X-FILENAME", encodeURIComponent(file.name));
|
||||
xhr.setRequestHeader("X-FIELDNAME", fieldName);
|
||||
xhr.setRequestHeader("Content-Type", "application/octet-stream"); // fix issue 96-Pete
|
||||
xhr.setRequestHeader("X-" + postTokenName, postTokenValue);
|
||||
xhr.setRequestHeader("X-REQUESTED-WITH", 'XMLHttpRequest');
|
||||
if(typeof imageData != "undefined" && imageData != false) {
|
||||
xhr.send(imageData);
|
||||
} else {
|
||||
xhr.send(file);
|
||||
}
|
||||
|
||||
updateGrid();
|
||||
$inputfield.trigger('change');
|
||||
var numFiles = $inputfield.find('.InputfieldFileItem').length;
|
||||
if(numFiles == 1) {
|
||||
$inputfield.removeClass('InputfieldFileEmpty').removeClass('InputfieldFileMultiple').addClass('InputfieldFileSingle');
|
||||
} else if(numFiles > 1) {
|
||||
$inputfield.removeClass('InputfieldFileEmpty').removeClass('InputfieldFileSingle').addClass('InputfieldFileMultiple');
|
||||
}
|
||||
}
|
||||
|
||||
updateProgress();
|
||||
|
||||
if(resizeSettings.maxWidth > 0 || resizeSettings.maxHeight > 0 || resizeSettings.maxSize > 0) {
|
||||
var resizer = new PWImageResizer(resizeSettings);
|
||||
$spinner.addClass('pw-resizing');
|
||||
resizer.resize(file, function(imageData) {
|
||||
$spinner.removeClass('pw-resizing');
|
||||
// resize completed, start upload
|
||||
sendUpload(file, imageData);
|
||||
});
|
||||
} else {
|
||||
sendUpload(file);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1575,7 +1608,7 @@ function InputfieldImage($) {
|
||||
var toKilobyte = function(i) {
|
||||
return parseInt(i / 1024, 10);
|
||||
};
|
||||
|
||||
|
||||
if(typeof files === "undefined") {
|
||||
fileList.innerHTML = "No support for the File API in this web browser";
|
||||
return;
|
||||
@@ -1600,7 +1633,7 @@ function InputfieldImage($) {
|
||||
$errorParent.append(errorItem(message, files[i].name));
|
||||
|
||||
} else {
|
||||
uploadFile(files[i]);
|
||||
uploadFile(files[i], extension);
|
||||
}
|
||||
|
||||
if(maxFiles == 1) break;
|
||||
@@ -1615,6 +1648,7 @@ function InputfieldImage($) {
|
||||
}, false);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Setup dropzone within an .InputfieldImageEdit panel so one can drag/drop new photo into existing image enlargement
|
||||
@@ -1648,6 +1682,30 @@ function InputfieldImage($) {
|
||||
|
||||
} // initUploadHTML5
|
||||
|
||||
function getClientResizeSettings($inputfield) {
|
||||
|
||||
var settings = {
|
||||
maxWidth: 0,
|
||||
maxHeight: 0,
|
||||
maxSize: 0,
|
||||
quality: 1.0,
|
||||
autoRotate: true,
|
||||
debug: ProcessWire.config.debug
|
||||
};
|
||||
|
||||
var data = $inputfield.attr('data-resize');
|
||||
|
||||
if(typeof data != "undefined" && data.length) {
|
||||
data = data.split(';');
|
||||
settings.maxWidth = parseInt(data[0]);
|
||||
settings.maxHeight = parseInt(data[1]);
|
||||
settings.maxSize = parseFloat(data[2]);
|
||||
settings.quality = parseFloat(data[3]);
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize InputfieldImage
|
||||
*
|
||||
|
File diff suppressed because one or more lines are too long
@@ -11,6 +11,7 @@
|
||||
* @property string $extensions Space separated list of allowed image extensions (default="JPG JPEG GIF PNG")
|
||||
* @property int|string $maxWidth Max width for uploaded images, larger will be sized down (default='')
|
||||
* @property int|string $maxHeight Max height for uploaded images, larger will be sized down (default='')
|
||||
* @property float $maxSize Maximum number of megapixels for client-side resize, i.e. 1.7 is ~1600x1000, alt. to maxWidth/maxHeight (default=0).
|
||||
* @property bool|int $maxReject Reject images that exceed max allowed size? (default=false)
|
||||
* @property int|string $minWidth Min width for uploaded images, smaller will be refused (default='')
|
||||
* @property int|string $minHeight Min height for uploaded images, smaller will be refused (default='')
|
||||
@@ -18,6 +19,8 @@
|
||||
* @property string $itemClass Space separated CSS classes for items rendered by this Inputfield. Generally you should append rather than replace.
|
||||
* @property int|bool $useImageEditor Whether or not the modal image editor is allowed for this field (default=true)
|
||||
* @property int $adminThumbScale for backwards compatibility only
|
||||
* @property int|bool $resizeServer Resize to max width/height at server? 1=Server-only, 0=Use client-side resize when possible (default=0).
|
||||
* @property int $clientQuality Quality setting to use for client-side resize. 60=60%, 90=90%, etc. (default=90).
|
||||
*
|
||||
* The following properties default values are pulled from $config->adminThumbOptions and can be overridden
|
||||
* by setting directly to an instance of this Inputfield:
|
||||
@@ -47,7 +50,7 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
|
||||
return array(
|
||||
'title' => __('Images', __FILE__), // Module Title
|
||||
'summary' => __('One or more image uploads (sortable)', __FILE__), // Module Summary
|
||||
'version' => 119,
|
||||
'version' => 120,
|
||||
'permanent' => true,
|
||||
);
|
||||
}
|
||||
@@ -87,9 +90,12 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
|
||||
$this->set('extensions', 'JPG JPEG GIF PNG');
|
||||
$this->set('maxWidth', '');
|
||||
$this->set('maxHeight', '');
|
||||
$this->set('maxSize', 0.0);
|
||||
$this->set('maxReject', 0);
|
||||
$this->set('minWidth', '');
|
||||
$this->set('minHeight', '');
|
||||
$this->set('resizeServer', 0); // 0=allow client resize, 1=resize at server only
|
||||
$this->set('clientQuality', 90);
|
||||
$this->set('dimensionsByAspectRatio', 0);
|
||||
$this->set('itemClass', 'gridImage ui-widget');
|
||||
|
||||
@@ -152,18 +158,27 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
|
||||
$this->renderValueMode = true;
|
||||
$this->addClass('InputfieldRenderValueMode', 'wrapClass');
|
||||
}
|
||||
|
||||
|
||||
$config = $this->wire('config');
|
||||
$modules = $this->wire('modules');
|
||||
$jqueryCore = $modules->get('JqueryCore');
|
||||
$jqueryCore->use('simulate');
|
||||
$jqueryCore->use('cookie');
|
||||
$modules->loadModuleFileAssets('InputfieldFile');
|
||||
$modules->getInstall("JqueryMagnific");
|
||||
|
||||
|
||||
$this->wire('config')->js('InputfieldImage', array(
|
||||
'labels' => $this->labels
|
||||
|
||||
$config->js('InputfieldImage', array(
|
||||
'labels' => $this->labels,
|
||||
));
|
||||
|
||||
// client side image resize
|
||||
if(!$this->resizeServer && ($this->maxWidth || $this->maxHeight || $this->maxSize)) {
|
||||
$thisURL = $config->urls->InputfieldImage;
|
||||
$jsExt = $config->debug ? "js" : "min.js";
|
||||
$config->scripts->add($thisURL . "exif.$jsExt");
|
||||
$config->scripts->add($thisURL . "PWImageResizer.$jsExt");
|
||||
$this->wrapAttr('data-resize', "$this->maxWidth;$this->maxHeight;$this->maxSize;" . (float) ($this->clientQuality / 100));
|
||||
}
|
||||
|
||||
if(!$renderValueMode && $this->value instanceof Pageimages) {
|
||||
$process = $this->wire('process');
|
||||
@@ -773,6 +788,7 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
|
||||
$f->label = $this->_('Default image grid mode');
|
||||
$f->description = $this->_('In the admin, the list of images will appear in this mode by default. The user can change it at any time by clicking the icons in the top right corner of the field.');
|
||||
$f->notes = $this->_('If you have recently used this images field, you will have to clear your cookies before seeing any changes to this setting.');
|
||||
$f->icon = 'photo';
|
||||
$f->addOption('grid', '[i.fa.fa-th][/i] ' . $this->_('Square grid images'));
|
||||
$f->addOption('left', '[i.fa.fa-tasks][/i] ' . $this->_('Proportional grid images'));
|
||||
$f->addOption('list', '[i.fa.fa-th-list][/i] ' . $this->_('Vertical list (verbose)'));
|
||||
@@ -781,9 +797,11 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
|
||||
|
||||
/** @var InputfieldFieldset $fieldset */
|
||||
$fieldset = $this->modules->get('InputfieldFieldset');
|
||||
$fieldset->label = $this->_("Max Image Dimensions");
|
||||
$fieldset->collapsed = $this->maxWidth || $this->maxHeight ? Inputfield::collapsedNo : Inputfield::collapsedYes;
|
||||
$fieldset->label = $this->_("Maximum image dimensions");
|
||||
$fieldset->icon = 'expand';
|
||||
//$fieldset->collapsed = $this->maxWidth || $this->maxHeight ? Inputfield::collapsedNo : Inputfield::collapsedYes;
|
||||
$fieldset->description = $this->_("Optionally enter the max width and/or height of uploaded images. If specified, images will be resized at upload time when they exceed either the max width or height. The resize is performed at upload time, and thus does not affect any images in the system, or images added via the API."); // Max image dimensions description
|
||||
$fieldset->description .= ' ' . $this->_('Applies to JPG, PNG and GIF images.');
|
||||
|
||||
$description = $this->_("Enter the value in number of pixels or leave blank for no limit."); // Min/Max width/height description
|
||||
/** @var InputfieldInteger $field */
|
||||
@@ -791,16 +809,60 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
|
||||
$field->attr('name', 'maxWidth');
|
||||
$field->attr('value', $this->maxWidth ? (int) $this->maxWidth : '');
|
||||
$field->label = $this->_("Max width for uploaded images");
|
||||
$field->icon = 'arrows-h';
|
||||
$field->description = $description;
|
||||
$field->columnWidth = 50;
|
||||
$field->appendMarkup = ' px';
|
||||
$fieldset->add($field);
|
||||
|
||||
$field = $this->modules->get("InputfieldInteger");
|
||||
$field->attr('name', 'maxHeight');
|
||||
$field->attr('value', $this->maxHeight ? (int) $this->maxHeight : '');
|
||||
$field->label = $this->_("Max height for uploaded images");
|
||||
$field->icon = 'arrows-v';
|
||||
$field->description = $description;
|
||||
$field->columnWidth = 50;
|
||||
$field->appendMarkup = ' px';
|
||||
$fieldset->add($field);
|
||||
|
||||
/** @var InputfieldRadios $field */
|
||||
$field = $this->modules->get('InputfieldRadios');
|
||||
$field->attr('name', 'resizeServer');
|
||||
$field->label = $this->_('How to resize to max dimensions');
|
||||
$field->description = $this->_('Using client-side resize enables you to reduce the file size and dimensions before uploading.');
|
||||
$field->notes = $this->_('When using client-side resize, please specify both max width *and* max height in the fields above, or max megapixels in the field below.');
|
||||
$field->icon = 'object-group';
|
||||
$field->addOption(0, $this->_('Use client-side resize when possible'));
|
||||
$field->addOption(1, $this->_('Use only server-side resize'));
|
||||
$field->attr('value', (int) $this->resizeServer);
|
||||
$fieldset->add($field);
|
||||
|
||||
$field = $this->modules->get('InputfieldFloat');
|
||||
$field->attr('name', 'maxSize');
|
||||
$field->label = $this->_('Max megapixels for uploaded images');
|
||||
$field->description = $this->_('This can be used as an alternative to max width/height. Specify a floating point value.');
|
||||
$field->description .= ' ' . $this->_('Applicable to client-side resize only.');
|
||||
$field->notes = $this->_('A good value for websites is 1.7 which is roughly 1600x1000 pixels, where 1600 and 1200 can be either width or height.');
|
||||
$field->notes .= ' ' . $this->_('Other examples:') . ' 0.2=516x387, 2.0=1633x1225, 3.0=2000x1500, 12.0=4000x3000';
|
||||
$field->icon = 'camera';
|
||||
$field->attr('value', (float) $this->maxSize > 0 ? (float) $this->maxSize : '');
|
||||
$field->showIf = 'resizeServer=0';
|
||||
//$field->collapsed = Inputfield::collapsedBlank;
|
||||
$field->columnWidth = 50;
|
||||
$fieldset->add($field);
|
||||
|
||||
$field = $this->modules->get('InputfieldInteger');
|
||||
$field->attr('name', 'clientQuality');
|
||||
$field->label = $this->_('Client-side resize quality percent for JPEGs');
|
||||
$field->description = $this->_('Specify a number between 10 (lowest quality/smallest file size) and 100 (highest quality/largest file size). Default is 90.');
|
||||
$field->icon = 'signal';
|
||||
$field->min = 10;
|
||||
$field->max = 100;
|
||||
$field->attr('size', 4);
|
||||
$field->attr('value', (int) $this->clientQuality);
|
||||
$field->showIf = 'resizeServer=0';
|
||||
$field->columnWidth = 50;
|
||||
$field->appendMarkup = ' %';
|
||||
$fieldset->add($field);
|
||||
|
||||
// maxReject option comes from @JanRomero PR #1051
|
||||
@@ -811,7 +873,9 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
|
||||
$field->attr('checked', ((int) $this->maxReject) ? 'checked' : '');
|
||||
$field->label = $this->_('Refuse images exceeding max dimensions?');
|
||||
$field->showIf = 'maxWidth|maxHeight>0';
|
||||
$field->description = $this->_('If checked, images that exceed max width/height will be refused rather than resized.');
|
||||
$field->icon = 'ban';
|
||||
$field->description = $this->_('If checked, images that exceed max width/height (that cannot be resized client-side) will be refused rather than resized.');
|
||||
if(!$this->maxReject) $field->collapsed = Inputfield::collapsedYes;
|
||||
$fieldset->add($field);
|
||||
|
||||
$inputfields->add($fieldset);
|
||||
@@ -819,7 +883,8 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
|
||||
// min image dimensions
|
||||
/** @var InputfieldFieldset $fieldset */
|
||||
$fieldset = $this->modules->get('InputfieldFieldset');
|
||||
$fieldset->label = $this->_("Min Image Dimensions");
|
||||
$fieldset->label = $this->_("Minimum image dimensions");
|
||||
$fieldset->icon = 'compress';
|
||||
$fieldset->collapsed = $this->minWidth || $this->minHeight ? Inputfield::collapsedNo : Inputfield::collapsedYes;
|
||||
$fieldset->description = $this->_("Optionally enter the minimum width and/or height of uploaded images. If specified, images that don't meet these minimums will be refused."); // Max image dimensions description
|
||||
|
||||
@@ -830,6 +895,8 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
|
||||
$field->label = $this->_("Min width for uploaded images");
|
||||
$field->description = $description;
|
||||
$field->columnWidth = 50;
|
||||
$field->icon = 'arrows-h';
|
||||
$field->appendMarkup = ' px';
|
||||
$fieldset->add($field);
|
||||
|
||||
$field = $this->modules->get("InputfieldInteger");
|
||||
@@ -838,6 +905,8 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
|
||||
$field->label = $this->_("Min height for uploaded images");
|
||||
$field->description = $description;
|
||||
$field->columnWidth = 50;
|
||||
$field->icon = 'arrows-v';
|
||||
$field->appendMarkup = ' px';
|
||||
$fieldset->add($field);
|
||||
|
||||
$inputfields->add($fieldset);
|
||||
@@ -849,6 +918,9 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
|
||||
$field->label = $this->_("Swap min/max dimensions for portrait images?");
|
||||
$field->showIf = 'minWidth|minHeight|maxWidth|maxHeight>0';
|
||||
$field->description = $this->_('If checked, minimum width/height and maximum width/height dimensions will be swapped for portrait images to accommodate for the different aspect ratio.');
|
||||
$field->description .= ' ' . $this->_('Applies to server-side resizes only.');
|
||||
$field->collapsed = $this->dimensionsByAspectRatio ? Inputfield::collapsedNo : Inputfield::collapsedYes;
|
||||
$field->icon = 'exchange';
|
||||
$inputfields->add($field);
|
||||
|
||||
return $inputfields;
|
||||
|
@@ -85,6 +85,7 @@ $itemPadding: 0.4em;
|
||||
padding: $itemPadding;
|
||||
vertical-align: top;
|
||||
border: 1px dashed transparent; // to have the placeholder item the same size
|
||||
|
||||
|
||||
&__overflow {
|
||||
width: 100%;
|
||||
@@ -105,6 +106,7 @@ $itemPadding: 0.4em;
|
||||
-ms-transform: translate3d(-50%, -50%, 0);
|
||||
transform: translate3d(-50%, -50%, 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&.gridImagePlaceholder {
|
||||
@@ -160,6 +162,10 @@ $itemPadding: 0.4em;
|
||||
font-size: 1.3em;
|
||||
cursor: move;
|
||||
text-shadow: 0px 0px 7px rgba(#3eb998, .7);
|
||||
&.pw-resizing {
|
||||
text-shadow: none;
|
||||
background: rgba(0,0,0,0.7);
|
||||
}
|
||||
}
|
||||
|
||||
&__progress {
|
||||
@@ -834,4 +840,4 @@ $itemPadding: 0.4em;
|
||||
}
|
||||
} // .gridImage
|
||||
} // .InputfieldImageNarrow
|
||||
} // .InputfieldImageEditll
|
||||
} // .InputfieldImageEditAll
|
||||
|
478
wire/modules/Inputfield/InputfieldImage/PWImageResizer.js
Normal file
478
wire/modules/Inputfield/InputfieldImage/PWImageResizer.js
Normal file
@@ -0,0 +1,478 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Requires exif.js (https://github.com/exif-js/exif-js) for JPEG autoRotate functions.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* - `autoRotate` (bool): Correct image orientation when EXIF data suggests it should be? (default=true).
|
||||
* Note: autoRotate is not applied if it is determined that image needs no resize.
|
||||
*
|
||||
* - `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;
|
||||
}
|
||||
|
||||
if(contentType == 'image/jpeg' && This.config.autoRotate) {
|
||||
// jpeg with autoRotate
|
||||
This.consoleLog('detecting JPEG image orientation...');
|
||||
|
||||
if((typeof EXIF.getData === "function") && (typeof EXIF.getTag === "function")) {
|
||||
EXIF.getData(img, function() {
|
||||
var orientation = EXIF.getTag(this, "Orientation");
|
||||
This.consoleLog('image orientation from EXIF tag: ' + orientation);
|
||||
This.scaleImage(img, orientation, completionCallback);
|
||||
});
|
||||
} else {
|
||||
This.consoleLog("can't read EXIF data, the Exif.js library not found");
|
||||
This.scaleImage(img, 0, completionCallback);
|
||||
}
|
||||
|
||||
} else {
|
||||
// png or gif (or jpeg with autoRotate==false)
|
||||
This.scaleImage(img, 0, 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 orientation Orientation number from Exif.js or 0 bypass
|
||||
* @param completionCallback Function to call upon completion
|
||||
*
|
||||
*/
|
||||
PWImageResizer.prototype.scaleImage = function(img, orientation, 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;
|
||||
|
||||
if(typeof orientation === 'undefined') orientation = 1;
|
||||
|
||||
if(orientation) {
|
||||
if(orientation > 4) {
|
||||
canvas.width = height;
|
||||
canvas.style.width = styleHeight;
|
||||
canvas.height = width;
|
||||
canvas.style.height = styleWidth;
|
||||
}
|
||||
switch(orientation) {
|
||||
case 2:
|
||||
ctx.translate(width, 0);
|
||||
ctx.scale(-1, 1);
|
||||
break;
|
||||
case 3:
|
||||
ctx.translate(width, height);
|
||||
ctx.rotate(Math.PI);
|
||||
break;
|
||||
case 4:
|
||||
ctx.translate(0, height);
|
||||
ctx.scale(1, -1);
|
||||
break;
|
||||
case 5:
|
||||
ctx.rotate(0.5 * Math.PI);
|
||||
ctx.scale(1, -1);
|
||||
break;
|
||||
case 6:
|
||||
ctx.rotate(0.5 * Math.PI);
|
||||
ctx.translate(0, -height);
|
||||
break;
|
||||
case 7:
|
||||
ctx.rotate(0.5 * Math.PI);
|
||||
ctx.translate(width, -height);
|
||||
ctx.scale(-1, 1);
|
||||
break;
|
||||
case 8:
|
||||
ctx.rotate(-0.5 * Math.PI);
|
||||
ctx.translate(-width, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
this.config.autoRotate = true;
|
||||
if(typeof customConfig.autoRotate === 'boolean')
|
||||
this.config.autoRotate = customConfig.autoRotate;
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
||||
|
1
wire/modules/Inputfield/InputfieldImage/PWImageResizer.min.js
vendored
Normal file
1
wire/modules/Inputfield/InputfieldImage/PWImageResizer.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1068
wire/modules/Inputfield/InputfieldImage/exif.js
Normal file
1068
wire/modules/Inputfield/InputfieldImage/exif.js
Normal file
File diff suppressed because it is too large
Load Diff
1
wire/modules/Inputfield/InputfieldImage/exif.min.js
vendored
Normal file
1
wire/modules/Inputfield/InputfieldImage/exif.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user