diff --git a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.css b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.css index 56a42dce..099a2e9e 100755 --- a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.css +++ b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.css @@ -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; diff --git a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.js b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.js index 2e72f529..da09abea 100755 --- a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.js +++ b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.js @@ -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 * diff --git a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.min.js b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.min.js index a063f24d..10edc97c 100644 --- a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.min.js +++ b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.min.js @@ -1 +1 @@ -function InputfieldImage(u){var k=null;var b={file:"",item:null,edit:null};var E={type:"image",closeOnContentClick:true,closeBtnInside:true};var c=null;var q=[];function r(){var L=window.File&&window.FileList&&window.FileReader;var K=u(".InputfieldAllowAjaxUpload").length>0;var M=u("#PageIDIndicator").length>0;return(L&&(M||K))}function x(M,K,L){K||(K=250);var N,O;return function(){var R=L||this;var Q=+new Date(),P=arguments;if(N&&Q .gridImage",start:function(P,O){var N=D(L.closest(".Inputfield"),"size");O.placeholder.append(u("
").css({display:"block",height:N+"px",width:N+"px"}));M=window.setTimeout(function(){F(L,null)},100);L.addClass("InputfieldImageSorting")},stop:function(P,N){var O=u(this);if(M!==null){N.item.find(".InputfieldImageEdit__edit").click();clearTimeout(M)}O.children("li").each(function(R){var Q=u(this).find(".InputfieldFileSort");if(Q.val()!=R){Q.val(R).change()}});L.removeClass("InputfieldImageSorting")},cancel:".InputfieldImageEdit"};L.sortable(K)}function o(L){var K=u.extend(true,{},E);K.callbacks={elementParse:function(M){var N=u(M.el).attr("data-original");if(typeof N=="undefined"||!N){N=u(M.el).attr("src")}M.src=N}};K.gallery={enabled:true};L.find("img").magnificPopup(K)}function s(L){var K=u.extend(true,{},E);K.callbacks={elementParse:function(M){M.src=u(M.el).attr("src")}};K.gallery={enabled:false};L.find("img").magnificPopup(K)}function B(K){return K.find(".InputfieldImageEdit--active")}function t(K){return u("#"+K.find(".InputfieldImageEdit__edit").attr("data-current"))}function C(M){var K=M.is(":checked");var L=M.parents(".gridImages").find(".gridImage__deletebox");if(K){L.prop("checked","checked").change()}else{L.removeAttr("checked").change()}}function I(L){if(typeof L=="undefined"){var K=u(".gridImages")}else{var K=L.find(".gridImages")}K.each(function(){var M=u(this),N=B(M);if(N.length){i(t(N),N)}})}function v(Q){var M=[];var P=[];var O=0,K=0;var N;if(typeof Q=="undefined"){N=u(".InputfieldImage.Inputfield")}else{N=Q}N.removeClass("InputfieldImageNarrow");N.each(function(){var S=u(this);var T=S.width();if(T<1){return}if(T<=500){M[O]=S;O++}});for(var R=0;R=P){N.css("max-height","100%").css("max-width","none");N.attr("height",L).removeAttr("width")}else{if(P>K){N.css("max-height","none").css("max-width","100%");N.attr("width",L).removeAttr("height")}else{N.css("max-height","100%").css("max-width","none");N.removeAttr("width").attr("height",L)}}}var K=N.width();if(K){M.css({width:(O?K+"px":L+"px"),height:L+"px"})}else{var Q=M.attr("data-tries");if(!Q){Q=0}if(typeof Q=="undefined"){Q=0}Q=parseInt(Q);if(Q>3){M.css({width:L+"px",height:L+"px"})}else{q.push(M);M.attr("data-tries",Q+1)}}}function z(L){if(L.find(".InputfieldImageListToggle").length){return}var O=u("").append("");var Q=u("").append("");var K=u("").append("");var P="InputfieldImageListToggle--active";var N="";var M=function(V){var U=u(this);var T=U.closest(".Inputfield");var R=U.attr("href");var S;U.parent().children("."+P).removeClass(P);U.addClass(P);if(R=="list"){if(!T.hasClass("InputfieldImageEditAll")){T.find(".InputfieldImageEdit--active .InputfieldImageEdit__close").click();T.addClass("InputfieldImageEditAll")}S=D(T,"listSize");l(T,S);e(T,"mode","list")}else{if(R=="left"){T.removeClass("InputfieldImageEditAll");S=D(T,"size");j(T,S,true);e(T,"mode","left");I()}else{if(R=="grid"){T.removeClass("InputfieldImageEditAll");S=D(T,"size");j(T,S,false);e(T,"mode","grid")}}}A(T.find(".gridImages"));U.blur();return false};O.click(M);Q.click(M);K.click(M);if(L.hasClass("InputfieldImage")){L.find(".InputfieldHeader").append(O).append(Q).append(K);N=D(L,"mode")}else{u(".InputfieldImage .InputfieldHeader",L).append(O).append(Q).append(K)}if(N=="list"){O.click()}else{if(N=="left"){Q.click()}else{}}}function y(P){var M=P.children(".InputfieldHeader");if(M.children(".InputfieldImageSizeSlider").length){return}var O=P.find(".gridImages");var L=O.attr("data-gridsize");var N=L/2;var K=L*2;var Q=u('');M.append(Q);Q.slider({min:N,max:K,value:D(P,"size"),range:"min",slide:function(T,V){var U=V.value;var W=15;var X=Math.floor(L/W);var R=U-N;var S=Math.floor(W+(R/X));if(P.hasClass("InputfieldImageEditAll")){e(P,"size",U);l(P,S)}else{e(P,"listSize",S);j(P,U)}},start:function(R,S){if(P.find(".InputfieldImageEdit:visible").length){P.find(".InputfieldImageEdit__close").click()}},stop:function(R,S){I(P)}})}function e(L,O,N){var M=D(L);var P=L.attr("id");var K=P?P.replace("wrap_Inputfield_",""):"";if(!K.length||typeof N=="undefined"){return}if(M[K][O]==N){return}M[K][O]=N;u.cookie("InputfieldImage",M);c=M}function D(L,O){if(c&&typeof O=="undefined"){return c}var P=L.attr("id");var K=P?P.replace("wrap_Inputfield_",""):"na";var N=c?c:u.cookie("InputfieldImage");var M=null;if(!N){var N={}}if(typeof N[K]=="undefined"){N[K]={}}if(typeof N[K].size=="undefined"){N[K].size=parseInt(L.find(".gridImages").attr("data-size"))}if(typeof N[K].listSize=="undefined"){N[K].listSize=23}if(typeof N[K].mode=="undefined"){N[K].mode=L.find(".gridImages").attr("data-gridMode")}if(c==null){c=N}if(typeof O=="undefined"){M=N}else{if(O===true){M=N[K]}else{if(typeof N[K][O]!="undefined"){M=N[K][O]}}}return M}function a(O){if(O.hasClass("InputfieldStateCollapsed")){return}var P=parseInt(O.find(".InputfieldImageMaxFiles").val());var N=O.find(".gridImages");var M=D(O,"size");var Q=D(O,"mode");var L=Q=="left"?true:false;if(!M){M=N.attr("data-gridsize")}M=parseInt(M);j(O,M,L);if(O.hasClass("InputfieldImageEditAll")||Q=="list"){var K=D(O,"listSize");l(O,K)}if(!O.hasClass("InputfieldImageInit")){O.addClass("InputfieldImageInit");if(O.hasClass("InputfieldRenderValueMode")){return o(O)}else{if(P==1){O.addClass("InputfieldImageMax1");s(O)}else{A(N)}}z(O);y(O)}v(O)}function H(){u("body").addClass("ie-no-drop");u(".InputfieldImage.InputfieldFileMultiple").each(function(){var L=u(this),N=parseInt(L.find(".InputfieldFileMaxFiles").val()),K=L.find(".gridImages"),M=L.find(".InputfieldImageUpload");M.on("change","input[type=file]",function(){var R=u(this),P=R.parent(".InputMask");if(R.val().length>1){P.addClass("ui-state-disabled")}else{P.removeClass("ui-state-disabled")}if(R.next("input.InputfieldFile").length>0){return}var O=K.children("li").length+M.find("input[type=file]").length+1;if(N>0&&O>=N){return}M.find(".InputMask").not(":last").each(function(){var S=u(this);if(S.find("input[type=file]").val()<1){S.remove()}});var Q=P.clone().removeClass("ui-state-disabled");Q.children("input[type=file]").val("");Q.insertAfter(P)})})}function J(M){var L;if(M.length>0){L=M.find(".InputfieldImageUpload")}else{L=u(".InputfieldImageUpload")}L.each(function(P){var Q=u(this);var O=Q.closest(".InputfieldContent");if(Q.hasClass("InputfieldImageInitUpload")){return}N(O,P);Q.addClass("InputfieldImageInitUpload")});function N(X,ai){var W=X.parents("form");var O=X.closest(".InputfieldRepeaterItem");var S=O.length?O.attr("data-editUrl"):W.attr("action");S+=(S.indexOf("?")>-1?"&":"?")+"InputfieldFileAjax=1";var am=W.find("input._post_token");var V=am.attr("name");var aa=am.val();var Z=X.find(".InputfieldImageErrors").first();var R=X.find(".InputfieldImageUpload").data("fieldname");R=R.slice(0,-2);var ah=X.closest(".Inputfield.InputfieldImage");var al=X.find(".InputfieldImageUpload").data("extensions").toLowerCase();var ag=X.find(".InputfieldImageUpload").data("maxfilesize");var Y=X.find("input[type=file]").get(0);var Q=X.find(".gridImages");var aj=Q.get(0);var ac=Q.data("gridsize");var ad=null;var ab=parseInt(X.find(".InputfieldImageMaxFiles").val());ak(X);if(ab!=1){af(Q)}Q.children().addClass("InputfieldFileItemExisting");function U(ao,an){if(typeof an!=="undefined"){ao=""+an+": "+ao}return"
  • "+ao+"
  • "}function P(ao){var an=new String(ao).substring(ao.lastIndexOf("/")+1);if(an.lastIndexOf(".")!=-1){an=an.substring(0,an.lastIndexOf("."))}return an}function ak(ao){if(ao.hasClass("InputfieldImageDropzoneInit")){return}var ar=ao.get(0);var aq=ao.closest(".Inputfield");function an(){if(aq.hasClass("pw-drag-in-file")){return}ao.addClass("ui-state-hover");aq.addClass("pw-drag-in-file")}function ap(){if(!aq.hasClass("pw-drag-in-file")){return}ao.removeClass("ui-state-hover");aq.removeClass("pw-drag-in-file")}ar.addEventListener("dragleave",function(){ap()},false);ar.addEventListener("dragenter",function(){an()},false);ar.addEventListener("dragover",function(at){if(!ao.is("ui-state-hover")){an()}at.preventDefault();at.stopPropagation();return false},false);ar.addEventListener("drop",function(at){ae(at.dataTransfer.files);ap();at.preventDefault();at.stopPropagation();return false},false);ao.addClass("InputfieldImageDropzoneInit")}function af(aw){var aA=null;var ay=false;var ao=null;var an=aw.closest(".Inputfield");function at(){an.addClass("pw-drag-in-file")}function az(){an.removeClass("pw-drag-in-file")}function ar(aC){var aG=aC.offset();var aD=aC.width();var aB=aC.height();var aF=aG.left+aD/2;var aE=aG.top+aB/2;return{clientX:aF,clientY:aE}}function av(){return aw.find(".InputfieldImageEdit--active").length>0}function au(aC){if(av()){return}aC.preventDefault();aC.stopPropagation();at();ay=false;if(aA==null){var aB=aw.attr("data-size")+"px";var aD=u("
    ").addClass("gridImage__overflow");if(aw.closest(".InputfieldImageEditAll").length){aD.css({width:"100%",height:aB})}else{aD.css({width:aB,height:aB})}aA=u("
  • ").addClass("ImageOuter gridImage gridImagePlaceholder").append(aD);aw.append(aA)}var aE=ar(aA);aA.simulate("mousedown",aE)}function ax(aB){if(av()){return}aB.preventDefault();aB.stopPropagation();at();ay=false;if(aA==null){return}var aC={clientX:aB.originalEvent.clientX,clientY:aB.originalEvent.clientY};aA.simulate("mousemove",aC)}function aq(aB){if(av()){return}aB.preventDefault();aB.stopPropagation();if(aA==null){return false}ay=true;if(ao){clearTimeout(ao)}ao=setTimeout(function(){if(!ay||aA==null){return}aA.remove();aA=null;az()},1000)}function ap(aB){if(av()){return}az();ay=false;var aC={clientX:aB.clientX,clientY:aB.clientY};aA.simulate("mouseup",aC);k=aA.next(".gridImage");aA.remove();aA=null}if(aw.length&&!aw.hasClass("gridImagesInitDropInPlace")){aw.on("dragenter",au);aw.on("dragover",ax);aw.on("dragleave",aq);aw.on("drop",ap);aw.addClass("gridImagesInitDropInPlace")}}function T(aI){var aG=ProcessWire.config.InputfieldImage.labels;var au=parseInt(aI.size/1024,10)+" kB";var aH='
    '+aG.dimensions+''+aG.na+"
    "+aG.filesize+""+au+"
    "+aG.variations+"0
    ";var aK=u('
  • '),aD=u(aH),aw=u('
    '),an=u('
    '),aA=u("
    "),aC=u(""),aF=u(' '),aE=u('
    '),ao,ay,aJ,az=URL.createObjectURL(aI),ap=ah.find(".gridImages"),ar=ab==1,aB=D(ah,"size"),at=D(ah,"listSize"),aq=ah.hasClass("InputfieldImageEditAll"),ax=u('');aw.append(ax);aA.find(".gridImage__inner").append(aF);aA.find(".gridImage__inner").append(aE.css("display","none"));aA.find(".gridImage__inner").append(aC);an.append(u('

    '+aI.name+'

    '+au+""));if(aq){aw.css("width",at+"%");an.css("width",(100-at)+"%")}else{aw.css({width:aB+"px",height:aB+"px"})}aK.append(aD).append(aw).append(aA).append(an);ax.attr({src:az,"data-original":az});img=new Image();img.addEventListener("load",function(){aD.find(".dimensions").html(this.width+" × "+this.height);var aL=Math.min(this.width,this.height)/aB;ax.attr({width:this.width/aL,height:this.height/aL})},false);img.src=az;ay=new XMLHttpRequest();ay.upload.addEventListener("progress",function(aL){if(!aL.lengthComputable){return}u("body").addClass("pw-uploading");aC.attr("value",parseInt((aL.loaded/aL.total)*100));aE.css("display","block")},false);ay.addEventListener("load",function(){ay.getAllResponseHeaders();var aM=u.parseJSON(ay.responseText),aP=aM.length>1;if(aM.error!==undefined){aM=[aM]}for(var aR=0;aR1){ah.removeClass("InputfieldFileEmpty").removeClass("InputfieldFileSingle").addClass("InputfieldFileMultiple")}}}function ae(ar){var ao=function(aw){return parseInt(aw/1024,10)};if(typeof ar==="undefined"){aj.innerHTML="No support for the File API in this web browser";return}for(var ap=0,an=ar.length;apag&&ag>2000000){var au=ao(ar[ap].size),at=ao(ag);aq="Filesize "+au+" kb is too big. Maximum allowed is "+at+" kb";Z.append(U(aq,ar[ap].name))}else{T(ar[ap])}}if(ab==1){break}}}Y.addEventListener("change",function(an){ae(this.files);an.preventDefault();an.stopPropagation();this.value=""},false)}function K(){var O=".InputfieldImageEdit__imagewrapper img";u(document).on("dragenter",O,function(){var R=u(this);if(R.closest(".InputfieldImageMax1").length){return}var S=R.attr("src");var P=R.closest(".InputfieldImageEdit");var Q=R.closest(".InputfieldImageEdit__imagewrapper");Q.addClass("InputfieldImageEdit__replace");b.file=new String(S).substring(S.lastIndexOf("/")+1);b.item=u("#"+P.attr("data-for"));b.edit=P}).on("dragleave",O,function(){var Q=u(this);if(Q.closest(".InputfieldImageMax1").length){return}var P=Q.closest(".InputfieldImageEdit__imagewrapper");P.removeClass("InputfieldImageEdit__replace");b.file="";b.item=null;b.edit=null})}K()}function G(){u(".InputfieldImage.Inputfield").each(function(){a(u(this))});w();if(r()){J("")}else{H()}u(document).on("reloaded",".InputfieldImage",function(){var K=u(this);a(K);J(K)}).on("wiretabclick",function(M,L,K){L.find(".InputfieldImage").each(function(){a(u(this))})}).on("opened",".InputfieldImage",function(){a(u(this))})}G()}jQuery(document).ready(function(a){InputfieldImage(a)}); \ No newline at end of file +function InputfieldImage(v){var k=null;var b={file:"",item:null,edit:null};var F={type:"image",closeOnContentClick:true,closeBtnInside:true};var c=null;var r=[];function s(){var M=window.File&&window.FileList&&window.FileReader;var L=v(".InputfieldAllowAjaxUpload").length>0;var N=v("#PageIDIndicator").length>0;return(M&&(N||L))}function y(N,L,M){L||(L=250);var O,P;return function(){var S=M||this;var R=+new Date(),Q=arguments;if(O&&R .gridImage",start:function(Q,P){var O=E(M.closest(".Inputfield"),"size");P.placeholder.append(v("
    ").css({display:"block",height:O+"px",width:O+"px"}));N=window.setTimeout(function(){G(M,null)},100);M.addClass("InputfieldImageSorting")},stop:function(Q,O){var P=v(this);if(N!==null){O.item.find(".InputfieldImageEdit__edit").click();clearTimeout(N)}P.children("li").each(function(S){var R=v(this).find(".InputfieldFileSort");if(R.val()!=S){R.val(S).change()}});M.removeClass("InputfieldImageSorting")},cancel:".InputfieldImageEdit"};M.sortable(L)}function p(M){var L=v.extend(true,{},F);L.callbacks={elementParse:function(N){var O=v(N.el).attr("data-original");if(typeof O=="undefined"||!O){O=v(N.el).attr("src")}N.src=O}};L.gallery={enabled:true};M.find("img").magnificPopup(L)}function t(M){var L=v.extend(true,{},F);L.callbacks={elementParse:function(N){N.src=v(N.el).attr("src")}};L.gallery={enabled:false};M.find("img").magnificPopup(L)}function C(L){return L.find(".InputfieldImageEdit--active")}function u(L){return v("#"+L.find(".InputfieldImageEdit__edit").attr("data-current"))}function D(N){var L=N.is(":checked");var M=N.parents(".gridImages").find(".gridImage__deletebox");if(L){M.prop("checked","checked").change()}else{M.removeAttr("checked").change()}}function J(M){if(typeof M=="undefined"){var L=v(".gridImages")}else{var L=M.find(".gridImages")}L.each(function(){var N=v(this),O=C(N);if(O.length){i(u(O),O)}})}function w(R){var N=[];var Q=[];var P=0,L=0;var O;if(typeof R=="undefined"){O=v(".InputfieldImage.Inputfield")}else{O=R}O.removeClass("InputfieldImageNarrow");O.each(function(){var T=v(this);var U=T.width();if(U<1){return}if(U<=500){N[P]=T;P++}});for(var S=0;S=Q){O.css("max-height","100%").css("max-width","none");O.attr("height",M).removeAttr("width")}else{if(Q>L){O.css("max-height","none").css("max-width","100%");O.attr("width",M).removeAttr("height")}else{O.css("max-height","100%").css("max-width","none");O.removeAttr("width").attr("height",M)}}}var L=O.width();if(L){N.css({width:(P?L+"px":M+"px"),height:M+"px"})}else{var R=N.attr("data-tries");if(!R){R=0}if(typeof R=="undefined"){R=0}R=parseInt(R);if(R>3){N.css({width:M+"px",height:M+"px"})}else{r.push(N);N.attr("data-tries",R+1)}}}function A(M){if(M.find(".InputfieldImageListToggle").length){return}var P=v("").append("");var R=v("").append("");var L=v("").append("");var Q="InputfieldImageListToggle--active";var O="";var N=function(W){var V=v(this);var U=V.closest(".Inputfield");var S=V.attr("href");var T;V.parent().children("."+Q).removeClass(Q);V.addClass(Q);if(S=="list"){if(!U.hasClass("InputfieldImageEditAll")){U.find(".InputfieldImageEdit--active .InputfieldImageEdit__close").click();U.addClass("InputfieldImageEditAll")}T=E(U,"listSize");l(U,T);e(U,"mode","list")}else{if(S=="left"){U.removeClass("InputfieldImageEditAll");T=E(U,"size");j(U,T,true);e(U,"mode","left");J()}else{if(S=="grid"){U.removeClass("InputfieldImageEditAll");T=E(U,"size");j(U,T,false);e(U,"mode","grid")}}}B(U.find(".gridImages"));V.blur();return false};P.click(N);R.click(N);L.click(N);if(M.hasClass("InputfieldImage")){M.find(".InputfieldHeader").append(P).append(R).append(L);O=E(M,"mode")}else{v(".InputfieldImage .InputfieldHeader",M).append(P).append(R).append(L)}if(O=="list"){P.click()}else{if(O=="left"){R.click()}else{}}}function z(Q){var N=Q.children(".InputfieldHeader");if(N.children(".InputfieldImageSizeSlider").length){return}var P=Q.find(".gridImages");var M=P.attr("data-gridsize");var O=M/2;var L=M*2;var R=v('');N.append(R);R.slider({min:O,max:L,value:E(Q,"size"),range:"min",slide:function(U,W){var V=W.value;var X=15;var Y=Math.floor(M/X);var S=V-O;var T=Math.floor(X+(S/Y));if(Q.hasClass("InputfieldImageEditAll")){e(Q,"size",V);l(Q,T)}else{e(Q,"listSize",T);j(Q,V)}},start:function(S,T){if(Q.find(".InputfieldImageEdit:visible").length){Q.find(".InputfieldImageEdit__close").click()}},stop:function(S,T){J(Q)}})}function e(M,P,O){var N=E(M);var Q=M.attr("id");var L=Q?Q.replace("wrap_Inputfield_",""):"";if(!L.length||typeof O=="undefined"){return}if(N[L][P]==O){return}N[L][P]=O;v.cookie("InputfieldImage",N);c=N}function E(M,P){if(c&&typeof P=="undefined"){return c}var Q=M.attr("id");var L=Q?Q.replace("wrap_Inputfield_",""):"na";var O=c?c:v.cookie("InputfieldImage");var N=null;if(!O){var O={}}if(typeof O[L]=="undefined"){O[L]={}}if(typeof O[L].size=="undefined"){O[L].size=parseInt(M.find(".gridImages").attr("data-size"))}if(typeof O[L].listSize=="undefined"){O[L].listSize=23}if(typeof O[L].mode=="undefined"){O[L].mode=M.find(".gridImages").attr("data-gridMode")}if(c==null){c=O}if(typeof P=="undefined"){N=O}else{if(P===true){N=O[L]}else{if(typeof O[L][P]!="undefined"){N=O[L][P]}}}return N}function a(P){if(P.hasClass("InputfieldStateCollapsed")){return}var Q=parseInt(P.find(".InputfieldImageMaxFiles").val());var O=P.find(".gridImages");var N=E(P,"size");var R=E(P,"mode");var M=R=="left"?true:false;if(!N){N=O.attr("data-gridsize")}N=parseInt(N);j(P,N,M);if(P.hasClass("InputfieldImageEditAll")||R=="list"){var L=E(P,"listSize");l(P,L)}if(!P.hasClass("InputfieldImageInit")){P.addClass("InputfieldImageInit");if(P.hasClass("InputfieldRenderValueMode")){return p(P)}else{if(Q==1){P.addClass("InputfieldImageMax1");t(P)}else{B(O)}}A(P);z(P)}w(P)}function I(){v("body").addClass("ie-no-drop");v(".InputfieldImage.InputfieldFileMultiple").each(function(){var M=v(this),O=parseInt(M.find(".InputfieldFileMaxFiles").val()),L=M.find(".gridImages"),N=M.find(".InputfieldImageUpload");N.on("change","input[type=file]",function(){var S=v(this),Q=S.parent(".InputMask");if(S.val().length>1){Q.addClass("ui-state-disabled")}else{Q.removeClass("ui-state-disabled")}if(S.next("input.InputfieldFile").length>0){return}var P=L.children("li").length+N.find("input[type=file]").length+1;if(O>0&&P>=O){return}N.find(".InputMask").not(":last").each(function(){var T=v(this);if(T.find("input[type=file]").val()<1){T.remove()}});var R=Q.clone().removeClass("ui-state-disabled");R.children("input[type=file]").val("");R.insertAfter(Q)})})}function K(N){var M;if(N.length>0){M=N.find(".InputfieldImageUpload")}else{M=v(".InputfieldImageUpload")}M.each(function(Q){var R=v(this);var P=R.closest(".InputfieldContent");if(R.hasClass("InputfieldImageInitUpload")){return}O(P,Q);R.addClass("InputfieldImageInitUpload")});function O(Y,aj){var X=Y.parents("form");var P=Y.closest(".InputfieldRepeaterItem");var T=P.length?P.attr("data-editUrl"):X.attr("action");T+=(T.indexOf("?")>-1?"&":"?")+"InputfieldFileAjax=1";var ao=X.find("input._post_token");var W=ao.attr("name");var ab=ao.val();var aa=Y.find(".InputfieldImageErrors").first();var S=Y.find(".InputfieldImageUpload").data("fieldname");S=S.slice(0,-2);var ai=Y.closest(".Inputfield.InputfieldImage");var an=Y.find(".InputfieldImageUpload").data("extensions").toLowerCase();var ah=Y.find(".InputfieldImageUpload").data("maxfilesize");var Z=Y.find("input[type=file]").get(0);var R=Y.find(".gridImages");var ak=R.get(0);var ad=R.data("gridsize");var ae=null;var ac=parseInt(Y.find(".InputfieldImageMaxFiles").val());var am=n(ai);al(Y);if(ac!=1){ag(R)}R.children().addClass("InputfieldFileItemExisting");function V(aq,ap){if(typeof ap!=="undefined"){aq=""+ap+": "+aq}return"
  • "+aq+"
  • "}function Q(aq){var ap=new String(aq).substring(aq.lastIndexOf("/")+1);if(ap.lastIndexOf(".")!=-1){ap=ap.substring(0,ap.lastIndexOf("."))}return ap}function al(aq){if(aq.hasClass("InputfieldImageDropzoneInit")){return}var au=aq.get(0);var at=aq.closest(".Inputfield");function ap(){if(at.hasClass("pw-drag-in-file")){return}aq.addClass("ui-state-hover");at.addClass("pw-drag-in-file")}function ar(){if(!at.hasClass("pw-drag-in-file")){return}aq.removeClass("ui-state-hover");at.removeClass("pw-drag-in-file")}au.addEventListener("dragleave",function(){ar()},false);au.addEventListener("dragenter",function(){ap()},false);au.addEventListener("dragover",function(av){if(!aq.is("ui-state-hover")){ap()}av.preventDefault();av.stopPropagation();return false},false);au.addEventListener("drop",function(av){af(av.dataTransfer.files);ar();av.preventDefault();av.stopPropagation();return false},false);aq.addClass("InputfieldImageDropzoneInit")}function ag(ay){var aC=null;var aA=false;var aq=null;var ap=ay.closest(".Inputfield");function av(){ap.addClass("pw-drag-in-file")}function aB(){ap.removeClass("pw-drag-in-file")}function au(aE){var aI=aE.offset();var aF=aE.width();var aD=aE.height();var aH=aI.left+aF/2;var aG=aI.top+aD/2;return{clientX:aH,clientY:aG}}function ax(){return ay.find(".InputfieldImageEdit--active").length>0}function aw(aE){if(ax()){return}aE.preventDefault();aE.stopPropagation();av();aA=false;if(aC==null){var aD=ay.attr("data-size")+"px";var aF=v("
    ").addClass("gridImage__overflow");if(ay.closest(".InputfieldImageEditAll").length){aF.css({width:"100%",height:aD})}else{aF.css({width:aD,height:aD})}aC=v("
  • ").addClass("ImageOuter gridImage gridImagePlaceholder").append(aF);ay.append(aC)}var aG=au(aC);aC.simulate("mousedown",aG)}function az(aD){if(ax()){return}aD.preventDefault();aD.stopPropagation();av();aA=false;if(aC==null){return}var aE={clientX:aD.originalEvent.clientX,clientY:aD.originalEvent.clientY};aC.simulate("mousemove",aE)}function at(aD){if(ax()){return}aD.preventDefault();aD.stopPropagation();if(aC==null){return false}aA=true;if(aq){clearTimeout(aq)}aq=setTimeout(function(){if(!aA||aC==null){return}aC.remove();aC=null;aB()},1000)}function ar(aD){if(ax()){return}aB();aA=false;var aE={clientX:aD.clientX,clientY:aD.clientY};aC.simulate("mouseup",aE);k=aC.next(".gridImage");aC.remove();aC=null}if(ay.length&&!ay.hasClass("gridImagesInitDropInPlace")){ay.on("dragenter",aw);ay.on("dragover",az);ay.on("dragleave",at);ay.on("drop",ar);ay.addClass("gridImagesInitDropInPlace")}}function U(aN,aB){var aK=ProcessWire.config.InputfieldImage.labels;var aw=parseInt(aN.size/1024,10)+" kB";var aM='
    '+aK.dimensions+''+aK.na+"
    "+aK.filesize+""+aw+"
    "+aK.variations+"0
    ";var aP=v('
  • '),aH=v(aM),ax=v('
    '),ap=v('
    '),aE=v("
    "),aG=v(""),aJ=v(' '),aI=v('
    '),aq,az,aO,aC=URL.createObjectURL(aN),ar=ai.find(".gridImages"),au=ac==1,aF=E(ai,"size"),av=E(ai,"listSize"),at=ai.hasClass("InputfieldImageEditAll"),ay=v('');ax.append(ay);aE.find(".gridImage__inner").append(aJ);aE.find(".gridImage__inner").append(aI.css("display","none"));aE.find(".gridImage__inner").append(aG);ap.append(v('

    '+aN.name+'

    '+aw+""));if(at){ax.css("width",av+"%");ap.css("width",(100-av)+"%")}else{ax.css({width:aF+"px",height:aF+"px"})}aP.append(aH).append(ax).append(aE).append(ap);ay.attr({src:aC,"data-original":aC});img=new Image();img.addEventListener("load",function(){aH.find(".dimensions").html(this.width+" × "+this.height);var aQ=Math.min(this.width,this.height)/aF;ay.attr({width:this.width/aQ,height:this.height/aQ})},false);img.src=aC;az=new XMLHttpRequest();function aA(aQ){if(typeof aQ!="undefined"){if(!aQ.lengthComputable){return}aG.attr("value",parseInt((aQ.loaded/aQ.total)*100))}v("body").addClass("pw-uploading");aI.css("display","block")}az.upload.addEventListener("progress",aA,false);az.addEventListener("load",function(){az.getAllResponseHeaders();var aR=v.parseJSON(az.responseText),aU=aR.length>1;if(aR.error!==undefined){aR=[aR]}for(var aW=0;aW1){ai.removeClass("InputfieldFileEmpty").removeClass("InputfieldFileSingle").addClass("InputfieldFileMultiple")}}}aA();if(am.maxWidth>0||am.maxHeight>0||am.maxSize>0){var aD=new PWImageResizer(am);aI.addClass("pw-resizing");aD.resize(aN,function(aQ){aI.removeClass("pw-resizing");aL(aN,aQ)})}else{aL(aN)}}function af(au){var aq=function(ay){return parseInt(ay/1024,10)};if(typeof au==="undefined"){ak.innerHTML="No support for the File API in this web browser";return}for(var ar=0,ap=au.length;arah&&ah>2000000){var aw=aq(au[ar].size),av=aq(ah);at="Filesize "+aw+" kb is too big. Maximum allowed is "+av+" kb";aa.append(V(at,au[ar].name))}else{U(au[ar],ax)}}if(ac==1){break}}}Z.addEventListener("change",function(ap){af(this.files);ap.preventDefault();ap.stopPropagation();this.value=""},false)}function L(){var P=".InputfieldImageEdit__imagewrapper img";v(document).on("dragenter",P,function(){var S=v(this);if(S.closest(".InputfieldImageMax1").length){return}var T=S.attr("src");var Q=S.closest(".InputfieldImageEdit");var R=S.closest(".InputfieldImageEdit__imagewrapper");R.addClass("InputfieldImageEdit__replace");b.file=new String(T).substring(T.lastIndexOf("/")+1);b.item=v("#"+Q.attr("data-for"));b.edit=Q}).on("dragleave",P,function(){var R=v(this);if(R.closest(".InputfieldImageMax1").length){return}var Q=R.closest(".InputfieldImageEdit__imagewrapper");Q.removeClass("InputfieldImageEdit__replace");b.file="";b.item=null;b.edit=null})}L()}function n(M){var L={maxWidth:0,maxHeight:0,maxSize:0,quality:1,autoRotate:true,debug:ProcessWire.config.debug};var N=M.attr("data-resize");if(typeof N!="undefined"&&N.length){N=N.split(";");L.maxWidth=parseInt(N[0]);L.maxHeight=parseInt(N[1]);L.maxSize=parseFloat(N[2]);L.quality=parseFloat(N[3])}return L}function H(){v(".InputfieldImage.Inputfield").each(function(){a(v(this))});x();if(s()){K("")}else{I()}v(document).on("reloaded",".InputfieldImage",function(){var L=v(this);a(L);K(L)}).on("wiretabclick",function(N,M,L){M.find(".InputfieldImage").each(function(){a(v(this))})}).on("opened",".InputfieldImage",function(){a(v(this))})}H()}jQuery(document).ready(function(a){InputfieldImage(a)}); \ No newline at end of file diff --git a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.module b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.module index da381978..e9ef7e63 100755 --- a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.module +++ b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.module @@ -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; diff --git a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.scss b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.scss index 63dabeee..d74b9cc8 100755 --- a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.scss +++ b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.scss @@ -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 diff --git a/wire/modules/Inputfield/InputfieldImage/PWImageResizer.js b/wire/modules/Inputfield/InputfieldImage/PWImageResizer.js new file mode 100644 index 00000000..1fe96e65 --- /dev/null +++ b/wire/modules/Inputfield/InputfieldImage/PWImageResizer.js @@ -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 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 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); +}; + + diff --git a/wire/modules/Inputfield/InputfieldImage/PWImageResizer.min.js b/wire/modules/Inputfield/InputfieldImage/PWImageResizer.min.js new file mode 100644 index 00000000..61a0d7f1 --- /dev/null +++ b/wire/modules/Inputfield/InputfieldImage/PWImageResizer.min.js @@ -0,0 +1 @@ +var PWImageResizer=function(a){this.setConfig(a)};PWImageResizer.prototype.resize=function(c,d){var b=document.createElement("img");this.currentFile=c;var a=new FileReader();var e=this;var f=c.type.toString();a.onload=function(g){b.src=g.target.result;b.onload=function(){if(!e.needsResize(b,f)){d(false);return}if(f=="image/jpeg"&&e.config.autoRotate){e.consoleLog("detecting JPEG image orientation...");if((typeof EXIF.getData==="function")&&(typeof EXIF.getTag==="function")){EXIF.getData(b,function(){var h=EXIF.getTag(this,"Orientation");e.consoleLog("image orientation from EXIF tag: "+h);e.scaleImage(b,h,d)})}else{e.consoleLog("can't read EXIF data, the Exif.js library not found");e.scaleImage(b,0,d)}}else{e.scaleImage(b,0,d)}}};a.readAsDataURL(c)};PWImageResizer.prototype.needsResize=function(a,d){var c=false;var b="n/a";if(d!="image/jpeg"&&d!="image/png"&&d!="image/gif"){b="unsupported image content-type: "+d}else{if(this.config.scaleRatio>0){c=true;b="scaleRatio specified"}else{if(this.config.maxWidth>0||this.config.maxHeight>0){if(this.config.maxWidth>0&&a.width>this.config.maxWidth){c=true}if(this.config.maxHeight>0&&a.height>this.config.maxHeight){c=true}b=c?"dimensions exceed max allowed":"dimensions do not require resize"}}}if(!c&&this.config.maxSize>0){if(this.config.maxSize<(a.width*a.height)/1000000){c=true}b=(c?"megapixels exceeds ":"megapixels below ")+this.config.maxSize}if(this.config.debug){this.consoleLog("needsResize="+(c?"Yes":"No")+" ("+b+")")}return c};PWImageResizer.prototype.drawImage=function(d,f,j,i,c,k,b,g,e,a){d.save();if(typeof c==="undefined"){c=f.width}if(typeof k==="undefined"){k=f.height}if(typeof a==="undefined"){a=false}if(a){j-=c/2;i-=k/2}d.translate(j+c/2,i+k/2);var h=2*Math.PI-b*Math.PI/180;d.rotate(h);if(g){flipScale=-1}else{flipScale=1}if(e){flopScale=-1}else{flopScale=1}d.scale(flipScale,flopScale);d.drawImage(f,-c/2,-k/2,c,k);d.restore()};PWImageResizer.prototype.scaleImage=function(i,c,l){var d=document.createElement("canvas");d.width=i.width;d.height=i.height;var p=d.getContext("2d");p.save();var b=d.width;var f=d.style.width;var o=d.height;var e=d.style.height;if(typeof c==="undefined"){c=1}if(c){if(c>4){d.width=o;d.style.width=e;d.height=b;d.style.height=f}switch(c){case 2:p.translate(b,0);p.scale(-1,1);break;case 3:p.translate(b,o);p.rotate(Math.PI);break;case 4:p.translate(0,o);p.scale(1,-1);break;case 5:p.rotate(0.5*Math.PI);p.scale(1,-1);break;case 6:p.rotate(0.5*Math.PI);p.translate(0,-o);break;case 7:p.rotate(0.5*Math.PI);p.translate(b,-o);p.scale(-1,1);break;case 8:p.rotate(-0.5*Math.PI);p.translate(-b,0);break}}p.drawImage(i,0,0);p.restore();var k=d.width/d.height;var g=0;var h="";if(this.config.maxWidth>0||this.config.maxHeight>0){g=Math.min(this.config.maxWidth,k*this.config.maxHeight);h="max width/height of "+this.config.maxWidth+"x"+this.config.maxHeight}if(this.config.maxSize>0&&(this.config.maxSize<(d.width*d.height)/1000000)){var n=Math.floor(Math.sqrt(this.config.maxSize*k)*1000);g=g>0?Math.min(g,n):n;if(n===g){h="max megapixels of "+this.config.maxSize}}if(this.config.scaleRatio){var j=Math.floor(this.config.scaleRatio*d.width);g=g>0?Math.min(g,j):j;if(j==g){h="scale ratio of "+this.config.scaleRatio}}if(g<=0){this.consoleLog("image size is too small to resize");l(false);return}if(this.config.debug){this.consoleLog("original image size: "+d.width+"x"+d.height+" px");this.consoleLog("scaled image size: "+g+"x"+Math.floor(g/k)+" px via "+h)}while(d.width>=(2*g)){d=this.getHalfScaleCanvas(d)}if(d.width>g){d=this.scaleCanvasWithAlgorithm(d,g)}var m=this.config.quality;if(this.currentFile.type!="image/jpeg"){m=1}var a=d.toDataURL(this.currentFile.type,m);if(typeof this.config.onScale==="function"){this.config.onScale(a)}l(this.imageDataToBlob(a))};PWImageResizer.prototype.imageDataToBlob=function(h){var f=";base64,";if(h.indexOf(f)==-1){var e=h.split(",");var g=e[0].split(":")[1];var a=e[1];return new Blob([a],{type:g})}var e=h.split(f);var g=e[0].split(":")[1];var a=window.atob(e[1]);var d=a.length;var c=new Uint8Array(d);for(var b=0;b(y.height-1)?(y.height-1):Math.ceil(p));for(o=0;o(y.width-1)?(y.width-1):Math.ceil(f));e=(o+u.width*q)*4;n=(w+y.width*d)*4;C=(t+y.width*d)*4;m=(w+y.width*c)*4;B=(t+y.width*c)*4;k=f-w;h=p-d;l=z(y.data[n],y.data[C],y.data[m],y.data[B],k,h);u.data[e]=l;s=z(y.data[n+1],y.data[C+1],y.data[m+1],y.data[B+1],k,h);u.data[e+1]=s;v=z(y.data[n+2],y.data[C+2],y.data[m+2],y.data[B+2],k,h);u.data[e+2]=v;x=z(y.data[n+3],y.data[C+3],y.data[m+3],y.data[B+3],k,h);u.data[e+3]=x}}};PWImageResizer.prototype.setConfig=function(a){this.config=a;this.config.debug=this.config.debug||false;if(typeof a.quality=="undefined"){a.quality=1}if(a.quality<0.1){a.quality=0.1}if(a.quality>1){a.quality=1}this.config.quality=a.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 a.autoRotate==="boolean"){this.config.autoRotate=a.autoRotate}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){}}}};PWImageResizer.prototype.consoleLog=function(a){if(this.config.debug){console.log("PWImageResizer: "+a)}}; \ No newline at end of file diff --git a/wire/modules/Inputfield/InputfieldImage/exif.js b/wire/modules/Inputfield/InputfieldImage/exif.js new file mode 100644 index 00000000..f09b72a8 --- /dev/null +++ b/wire/modules/Inputfield/InputfieldImage/exif.js @@ -0,0 +1,1068 @@ +/* + + Javascript library for reading EXIF image meta data + https://github.com/exif-js/exif-js + + The MIT License (MIT) + + Copyright (c) 2008 Jacob Seidelin + + 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 above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + 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. + + */ + +(function() { + + var debug = false; + + var root = this; + + var EXIF = function(obj) { + if (obj instanceof EXIF) return obj; + if (!(this instanceof EXIF)) return new EXIF(obj); + this.EXIFwrapped = obj; + }; + + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = EXIF; + } + exports.EXIF = EXIF; + } else { + root.EXIF = EXIF; + } + + var ExifTags = EXIF.Tags = { + + // version tags + 0x9000 : "ExifVersion", // EXIF version + 0xA000 : "FlashpixVersion", // Flashpix format version + + // colorspace tags + 0xA001 : "ColorSpace", // Color space information tag + + // image configuration + 0xA002 : "PixelXDimension", // Valid width of meaningful image + 0xA003 : "PixelYDimension", // Valid height of meaningful image + 0x9101 : "ComponentsConfiguration", // Information about channels + 0x9102 : "CompressedBitsPerPixel", // Compressed bits per pixel + + // user information + 0x927C : "MakerNote", // Any desired information written by the manufacturer + 0x9286 : "UserComment", // Comments by user + + // related file + 0xA004 : "RelatedSoundFile", // Name of related sound file + + // date and time + 0x9003 : "DateTimeOriginal", // Date and time when the original image was generated + 0x9004 : "DateTimeDigitized", // Date and time when the image was stored digitally + 0x9290 : "SubsecTime", // Fractions of seconds for DateTime + 0x9291 : "SubsecTimeOriginal", // Fractions of seconds for DateTimeOriginal + 0x9292 : "SubsecTimeDigitized", // Fractions of seconds for DateTimeDigitized + + // picture-taking conditions + 0x829A : "ExposureTime", // Exposure time (in seconds) + 0x829D : "FNumber", // F number + 0x8822 : "ExposureProgram", // Exposure program + 0x8824 : "SpectralSensitivity", // Spectral sensitivity + 0x8827 : "ISOSpeedRatings", // ISO speed rating + 0x8828 : "OECF", // Optoelectric conversion factor + 0x9201 : "ShutterSpeedValue", // Shutter speed + 0x9202 : "ApertureValue", // Lens aperture + 0x9203 : "BrightnessValue", // Value of brightness + 0x9204 : "ExposureBias", // Exposure bias + 0x9205 : "MaxApertureValue", // Smallest F number of lens + 0x9206 : "SubjectDistance", // Distance to subject in meters + 0x9207 : "MeteringMode", // Metering mode + 0x9208 : "LightSource", // Kind of light source + 0x9209 : "Flash", // Flash status + 0x9214 : "SubjectArea", // Location and area of main subject + 0x920A : "FocalLength", // Focal length of the lens in mm + 0xA20B : "FlashEnergy", // Strobe energy in BCPS + 0xA20C : "SpatialFrequencyResponse", // + 0xA20E : "FocalPlaneXResolution", // Number of pixels in width direction per FocalPlaneResolutionUnit + 0xA20F : "FocalPlaneYResolution", // Number of pixels in height direction per FocalPlaneResolutionUnit + 0xA210 : "FocalPlaneResolutionUnit", // Unit for measuring FocalPlaneXResolution and FocalPlaneYResolution + 0xA214 : "SubjectLocation", // Location of subject in image + 0xA215 : "ExposureIndex", // Exposure index selected on camera + 0xA217 : "SensingMethod", // Image sensor type + 0xA300 : "FileSource", // Image source (3 == DSC) + 0xA301 : "SceneType", // Scene type (1 == directly photographed) + 0xA302 : "CFAPattern", // Color filter array geometric pattern + 0xA401 : "CustomRendered", // Special processing + 0xA402 : "ExposureMode", // Exposure mode + 0xA403 : "WhiteBalance", // 1 = auto white balance, 2 = manual + 0xA404 : "DigitalZoomRation", // Digital zoom ratio + 0xA405 : "FocalLengthIn35mmFilm", // Equivalent foacl length assuming 35mm film camera (in mm) + 0xA406 : "SceneCaptureType", // Type of scene + 0xA407 : "GainControl", // Degree of overall image gain adjustment + 0xA408 : "Contrast", // Direction of contrast processing applied by camera + 0xA409 : "Saturation", // Direction of saturation processing applied by camera + 0xA40A : "Sharpness", // Direction of sharpness processing applied by camera + 0xA40B : "DeviceSettingDescription", // + 0xA40C : "SubjectDistanceRange", // Distance to subject + + // other tags + 0xA005 : "InteroperabilityIFDPointer", + 0xA420 : "ImageUniqueID" // Identifier assigned uniquely to each image + }; + + var TiffTags = EXIF.TiffTags = { + 0x0100 : "ImageWidth", + 0x0101 : "ImageHeight", + 0x8769 : "ExifIFDPointer", + 0x8825 : "GPSInfoIFDPointer", + 0xA005 : "InteroperabilityIFDPointer", + 0x0102 : "BitsPerSample", + 0x0103 : "Compression", + 0x0106 : "PhotometricInterpretation", + 0x0112 : "Orientation", + 0x0115 : "SamplesPerPixel", + 0x011C : "PlanarConfiguration", + 0x0212 : "YCbCrSubSampling", + 0x0213 : "YCbCrPositioning", + 0x011A : "XResolution", + 0x011B : "YResolution", + 0x0128 : "ResolutionUnit", + 0x0111 : "StripOffsets", + 0x0116 : "RowsPerStrip", + 0x0117 : "StripByteCounts", + 0x0201 : "JPEGInterchangeFormat", + 0x0202 : "JPEGInterchangeFormatLength", + 0x012D : "TransferFunction", + 0x013E : "WhitePoint", + 0x013F : "PrimaryChromaticities", + 0x0211 : "YCbCrCoefficients", + 0x0214 : "ReferenceBlackWhite", + 0x0132 : "DateTime", + 0x010E : "ImageDescription", + 0x010F : "Make", + 0x0110 : "Model", + 0x0131 : "Software", + 0x013B : "Artist", + 0x8298 : "Copyright" + }; + + var GPSTags = EXIF.GPSTags = { + 0x0000 : "GPSVersionID", + 0x0001 : "GPSLatitudeRef", + 0x0002 : "GPSLatitude", + 0x0003 : "GPSLongitudeRef", + 0x0004 : "GPSLongitude", + 0x0005 : "GPSAltitudeRef", + 0x0006 : "GPSAltitude", + 0x0007 : "GPSTimeStamp", + 0x0008 : "GPSSatellites", + 0x0009 : "GPSStatus", + 0x000A : "GPSMeasureMode", + 0x000B : "GPSDOP", + 0x000C : "GPSSpeedRef", + 0x000D : "GPSSpeed", + 0x000E : "GPSTrackRef", + 0x000F : "GPSTrack", + 0x0010 : "GPSImgDirectionRef", + 0x0011 : "GPSImgDirection", + 0x0012 : "GPSMapDatum", + 0x0013 : "GPSDestLatitudeRef", + 0x0014 : "GPSDestLatitude", + 0x0015 : "GPSDestLongitudeRef", + 0x0016 : "GPSDestLongitude", + 0x0017 : "GPSDestBearingRef", + 0x0018 : "GPSDestBearing", + 0x0019 : "GPSDestDistanceRef", + 0x001A : "GPSDestDistance", + 0x001B : "GPSProcessingMethod", + 0x001C : "GPSAreaInformation", + 0x001D : "GPSDateStamp", + 0x001E : "GPSDifferential" + }; + + // EXIF 2.3 Spec + var IFD1Tags = EXIF.IFD1Tags = { + 0x0100: "ImageWidth", + 0x0101: "ImageHeight", + 0x0102: "BitsPerSample", + 0x0103: "Compression", + 0x0106: "PhotometricInterpretation", + 0x0111: "StripOffsets", + 0x0112: "Orientation", + 0x0115: "SamplesPerPixel", + 0x0116: "RowsPerStrip", + 0x0117: "StripByteCounts", + 0x011A: "XResolution", + 0x011B: "YResolution", + 0x011C: "PlanarConfiguration", + 0x0128: "ResolutionUnit", + 0x0201: "JpegIFOffset", // When image format is JPEG, this value show offset to JPEG data stored.(aka "ThumbnailOffset" or "JPEGInterchangeFormat") + 0x0202: "JpegIFByteCount", // When image format is JPEG, this value shows data size of JPEG image (aka "ThumbnailLength" or "JPEGInterchangeFormatLength") + 0x0211: "YCbCrCoefficients", + 0x0212: "YCbCrSubSampling", + 0x0213: "YCbCrPositioning", + 0x0214: "ReferenceBlackWhite" + }; + + var StringValues = EXIF.StringValues = { + ExposureProgram : { + 0 : "Not defined", + 1 : "Manual", + 2 : "Normal program", + 3 : "Aperture priority", + 4 : "Shutter priority", + 5 : "Creative program", + 6 : "Action program", + 7 : "Portrait mode", + 8 : "Landscape mode" + }, + MeteringMode : { + 0 : "Unknown", + 1 : "Average", + 2 : "CenterWeightedAverage", + 3 : "Spot", + 4 : "MultiSpot", + 5 : "Pattern", + 6 : "Partial", + 255 : "Other" + }, + LightSource : { + 0 : "Unknown", + 1 : "Daylight", + 2 : "Fluorescent", + 3 : "Tungsten (incandescent light)", + 4 : "Flash", + 9 : "Fine weather", + 10 : "Cloudy weather", + 11 : "Shade", + 12 : "Daylight fluorescent (D 5700 - 7100K)", + 13 : "Day white fluorescent (N 4600 - 5400K)", + 14 : "Cool white fluorescent (W 3900 - 4500K)", + 15 : "White fluorescent (WW 3200 - 3700K)", + 17 : "Standard light A", + 18 : "Standard light B", + 19 : "Standard light C", + 20 : "D55", + 21 : "D65", + 22 : "D75", + 23 : "D50", + 24 : "ISO studio tungsten", + 255 : "Other" + }, + Flash : { + 0x0000 : "Flash did not fire", + 0x0001 : "Flash fired", + 0x0005 : "Strobe return light not detected", + 0x0007 : "Strobe return light detected", + 0x0009 : "Flash fired, compulsory flash mode", + 0x000D : "Flash fired, compulsory flash mode, return light not detected", + 0x000F : "Flash fired, compulsory flash mode, return light detected", + 0x0010 : "Flash did not fire, compulsory flash mode", + 0x0018 : "Flash did not fire, auto mode", + 0x0019 : "Flash fired, auto mode", + 0x001D : "Flash fired, auto mode, return light not detected", + 0x001F : "Flash fired, auto mode, return light detected", + 0x0020 : "No flash function", + 0x0041 : "Flash fired, red-eye reduction mode", + 0x0045 : "Flash fired, red-eye reduction mode, return light not detected", + 0x0047 : "Flash fired, red-eye reduction mode, return light detected", + 0x0049 : "Flash fired, compulsory flash mode, red-eye reduction mode", + 0x004D : "Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected", + 0x004F : "Flash fired, compulsory flash mode, red-eye reduction mode, return light detected", + 0x0059 : "Flash fired, auto mode, red-eye reduction mode", + 0x005D : "Flash fired, auto mode, return light not detected, red-eye reduction mode", + 0x005F : "Flash fired, auto mode, return light detected, red-eye reduction mode" + }, + SensingMethod : { + 1 : "Not defined", + 2 : "One-chip color area sensor", + 3 : "Two-chip color area sensor", + 4 : "Three-chip color area sensor", + 5 : "Color sequential area sensor", + 7 : "Trilinear sensor", + 8 : "Color sequential linear sensor" + }, + SceneCaptureType : { + 0 : "Standard", + 1 : "Landscape", + 2 : "Portrait", + 3 : "Night scene" + }, + SceneType : { + 1 : "Directly photographed" + }, + CustomRendered : { + 0 : "Normal process", + 1 : "Custom process" + }, + WhiteBalance : { + 0 : "Auto white balance", + 1 : "Manual white balance" + }, + GainControl : { + 0 : "None", + 1 : "Low gain up", + 2 : "High gain up", + 3 : "Low gain down", + 4 : "High gain down" + }, + Contrast : { + 0 : "Normal", + 1 : "Soft", + 2 : "Hard" + }, + Saturation : { + 0 : "Normal", + 1 : "Low saturation", + 2 : "High saturation" + }, + Sharpness : { + 0 : "Normal", + 1 : "Soft", + 2 : "Hard" + }, + SubjectDistanceRange : { + 0 : "Unknown", + 1 : "Macro", + 2 : "Close view", + 3 : "Distant view" + }, + FileSource : { + 3 : "DSC" + }, + + Components : { + 0 : "", + 1 : "Y", + 2 : "Cb", + 3 : "Cr", + 4 : "R", + 5 : "G", + 6 : "B" + } + }; + + function addEvent(element, event, handler) { + if (element.addEventListener) { + element.addEventListener(event, handler, false); + } else if (element.attachEvent) { + element.attachEvent("on" + event, handler); + } + } + + function imageHasData(img) { + return !!(img.exifdata); + } + + + function base64ToArrayBuffer(base64, contentType) { + contentType = contentType || base64.match(/^data\:([^\;]+)\;base64,/mi)[1] || ''; // e.g. 'data:image/jpeg;base64,...' => 'image/jpeg' + base64 = base64.replace(/^data\:([^\;]+)\;base64,/gmi, ''); + var binary = atob(base64); + var len = binary.length; + var buffer = new ArrayBuffer(len); + var view = new Uint8Array(buffer); + for (var i = 0; i < len; i++) { + view[i] = binary.charCodeAt(i); + } + return buffer; + } + + function objectURLToBlob(url, callback) { + var http = new XMLHttpRequest(); + http.open("GET", url, true); + http.responseType = "blob"; + http.onload = function(e) { + if (this.status == 200 || this.status === 0) { + callback(this.response); + } + }; + http.send(); + } + + function getImageData(img, callback) { + function handleBinaryFile(binFile) { + var data = findEXIFinJPEG(binFile); + var iptcdata = findIPTCinJPEG(binFile); + var xmpdata= findXMPinJPEG(binFile); + img.exifdata = data || {}; + img.iptcdata = iptcdata || {}; + img.xmpdata = xmpdata || {}; + if (callback) { + callback.call(img); + } + } + + if (img.src) { + if (/^data\:/i.test(img.src)) { // Data URI + var arrayBuffer = base64ToArrayBuffer(img.src); + handleBinaryFile(arrayBuffer); + + } else if (/^blob\:/i.test(img.src)) { // Object URL + var fileReader = new FileReader(); + fileReader.onload = function(e) { + handleBinaryFile(e.target.result); + }; + objectURLToBlob(img.src, function (blob) { + fileReader.readAsArrayBuffer(blob); + }); + } else { + var http = new XMLHttpRequest(); + http.onload = function() { + if (this.status == 200 || this.status === 0) { + handleBinaryFile(http.response); + } else { + throw "Could not load image"; + } + http = null; + }; + http.open("GET", img.src, true); + http.responseType = "arraybuffer"; + http.send(null); + } + } else if (self.FileReader && (img instanceof self.Blob || img instanceof self.File)) { + var fileReader = new FileReader(); + fileReader.onload = function(e) { + if (debug) console.log("Got file of length " + e.target.result.byteLength); + handleBinaryFile(e.target.result); + }; + + fileReader.readAsArrayBuffer(img); + } + } + + function findEXIFinJPEG(file) { + var dataView = new DataView(file); + + if (debug) console.log("Got file of length " + file.byteLength); + if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) { + if (debug) console.log("Not a valid JPEG"); + return false; // not a valid jpeg + } + + var offset = 2, + length = file.byteLength, + marker; + + while (offset < length) { + if (dataView.getUint8(offset) != 0xFF) { + if (debug) console.log("Not a valid marker at offset " + offset + ", found: " + dataView.getUint8(offset)); + return false; // not a valid marker, something is wrong + } + + marker = dataView.getUint8(offset + 1); + if (debug) console.log(marker); + + // we could implement handling for other markers here, + // but we're only looking for 0xFFE1 for EXIF data + + if (marker == 225) { + if (debug) console.log("Found 0xFFE1 marker"); + + return readEXIFData(dataView, offset + 4, dataView.getUint16(offset + 2) - 2); + + // offset += 2 + file.getShortAt(offset+2, true); + + } else { + offset += 2 + dataView.getUint16(offset+2); + } + + } + + } + + function findIPTCinJPEG(file) { + var dataView = new DataView(file); + + if (debug) console.log("Got file of length " + file.byteLength); + if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) { + if (debug) console.log("Not a valid JPEG"); + return false; // not a valid jpeg + } + + var offset = 2, + length = file.byteLength; + + + var isFieldSegmentStart = function(dataView, offset){ + return ( + dataView.getUint8(offset) === 0x38 && + dataView.getUint8(offset+1) === 0x42 && + dataView.getUint8(offset+2) === 0x49 && + dataView.getUint8(offset+3) === 0x4D && + dataView.getUint8(offset+4) === 0x04 && + dataView.getUint8(offset+5) === 0x04 + ); + }; + + while (offset < length) { + + if ( isFieldSegmentStart(dataView, offset )){ + + // Get the length of the name header (which is padded to an even number of bytes) + var nameHeaderLength = dataView.getUint8(offset+7); + if(nameHeaderLength % 2 !== 0) nameHeaderLength += 1; + // Check for pre photoshop 6 format + if(nameHeaderLength === 0) { + // Always 4 + nameHeaderLength = 4; + } + + var startOffset = offset + 8 + nameHeaderLength; + var sectionLength = dataView.getUint16(offset + 6 + nameHeaderLength); + + return readIPTCData(file, startOffset, sectionLength); + + break; + + } + + + // Not the marker, continue searching + offset++; + + } + + } + var IptcFieldMap = { + 0x78 : 'caption', + 0x6E : 'credit', + 0x19 : 'keywords', + 0x37 : 'dateCreated', + 0x50 : 'byline', + 0x55 : 'bylineTitle', + 0x7A : 'captionWriter', + 0x69 : 'headline', + 0x74 : 'copyright', + 0x0F : 'category' + }; + function readIPTCData(file, startOffset, sectionLength){ + var dataView = new DataView(file); + var data = {}; + var fieldValue, fieldName, dataSize, segmentType, segmentSize; + var segmentStartPos = startOffset; + while(segmentStartPos < startOffset+sectionLength) { + if(dataView.getUint8(segmentStartPos) === 0x1C && dataView.getUint8(segmentStartPos+1) === 0x02){ + segmentType = dataView.getUint8(segmentStartPos+2); + if(segmentType in IptcFieldMap) { + dataSize = dataView.getInt16(segmentStartPos+3); + segmentSize = dataSize + 5; + fieldName = IptcFieldMap[segmentType]; + fieldValue = getStringFromDB(dataView, segmentStartPos+5, dataSize); + // Check if we already stored a value with this name + if(data.hasOwnProperty(fieldName)) { + // Value already stored with this name, create multivalue field + if(data[fieldName] instanceof Array) { + data[fieldName].push(fieldValue); + } + else { + data[fieldName] = [data[fieldName], fieldValue]; + } + } + else { + data[fieldName] = fieldValue; + } + } + + } + segmentStartPos++; + } + return data; + } + + + + function readTags(file, tiffStart, dirStart, strings, bigEnd) { + var entries = file.getUint16(dirStart, !bigEnd), + tags = {}, + entryOffset, tag, + i; + + for (i=0;i 4 ? valueOffset : (entryOffset + 8); + vals = []; + for (n=0;n 4 ? valueOffset : (entryOffset + 8); + return getStringFromDB(file, offset, numValues-1); + + case 3: // short, 16 bit int + if (numValues == 1) { + return file.getUint16(entryOffset + 8, !bigEnd); + } else { + offset = numValues > 2 ? valueOffset : (entryOffset + 8); + vals = []; + for (n=0;n dataView.byteLength) { // this should not happen + // console.log('******** IFD1Offset is outside the bounds of the DataView ********'); + return {}; + } + // console.log('******* thumbnail IFD offset (IFD1) is: %s', IFD1OffsetPointer); + + var thumbTags = readTags(dataView, tiffStart, tiffStart + IFD1OffsetPointer, IFD1Tags, bigEnd) + + // EXIF 2.3 specification for JPEG format thumbnail + + // If the value of Compression(0x0103) Tag in IFD1 is '6', thumbnail image format is JPEG. + // Most of Exif image uses JPEG format for thumbnail. In that case, you can get offset of thumbnail + // by JpegIFOffset(0x0201) Tag in IFD1, size of thumbnail by JpegIFByteCount(0x0202) Tag. + // Data format is ordinary JPEG format, starts from 0xFFD8 and ends by 0xFFD9. It seems that + // JPEG format and 160x120pixels of size are recommended thumbnail format for Exif2.1 or later. + + if (thumbTags['Compression']) { + // console.log('Thumbnail image found!'); + + switch (thumbTags['Compression']) { + case 6: + // console.log('Thumbnail image format is JPEG'); + if (thumbTags.JpegIFOffset && thumbTags.JpegIFByteCount) { + // extract the thumbnail + var tOffset = tiffStart + thumbTags.JpegIFOffset; + var tLength = thumbTags.JpegIFByteCount; + thumbTags['blob'] = new Blob([new Uint8Array(dataView.buffer, tOffset, tLength)], { + type: 'image/jpeg' + }); + } + break; + + case 1: + console.log("Thumbnail image format is TIFF, which is not implemented."); + break; + default: + console.log("Unknown thumbnail image format '%s'", thumbTags['Compression']); + } + } + else if (thumbTags['PhotometricInterpretation'] == 2) { + console.log("Thumbnail image format is RGB, which is not implemented."); + } + return thumbTags; + } + + function getStringFromDB(buffer, start, length) { + var outstr = ""; + for (n = start; n < start+length; n++) { + outstr += String.fromCharCode(buffer.getUint8(n)); + } + return outstr; + } + + function readEXIFData(file, start) { + if (getStringFromDB(file, start, 4) != "Exif") { + if (debug) console.log("Not valid EXIF data! " + getStringFromDB(file, start, 4)); + return false; + } + + var bigEnd, + tags, tag, + exifData, gpsData, + tiffOffset = start + 6; + + // test for TIFF validity and endianness + if (file.getUint16(tiffOffset) == 0x4949) { + bigEnd = false; + } else if (file.getUint16(tiffOffset) == 0x4D4D) { + bigEnd = true; + } else { + if (debug) console.log("Not valid TIFF data! (no 0x4949 or 0x4D4D)"); + return false; + } + + if (file.getUint16(tiffOffset+2, !bigEnd) != 0x002A) { + if (debug) console.log("Not valid TIFF data! (no 0x002A)"); + return false; + } + + var firstIFDOffset = file.getUint32(tiffOffset+4, !bigEnd); + + if (firstIFDOffset < 0x00000008) { + if (debug) console.log("Not valid TIFF data! (First offset less than 8)", file.getUint32(tiffOffset+4, !bigEnd)); + return false; + } + + tags = readTags(file, tiffOffset, tiffOffset + firstIFDOffset, TiffTags, bigEnd); + + if (tags.ExifIFDPointer) { + exifData = readTags(file, tiffOffset, tiffOffset + tags.ExifIFDPointer, ExifTags, bigEnd); + for (tag in exifData) { + switch (tag) { + case "LightSource" : + case "Flash" : + case "MeteringMode" : + case "ExposureProgram" : + case "SensingMethod" : + case "SceneCaptureType" : + case "SceneType" : + case "CustomRendered" : + case "WhiteBalance" : + case "GainControl" : + case "Contrast" : + case "Saturation" : + case "Sharpness" : + case "SubjectDistanceRange" : + case "FileSource" : + exifData[tag] = StringValues[tag][exifData[tag]]; + break; + + case "ExifVersion" : + case "FlashpixVersion" : + exifData[tag] = String.fromCharCode(exifData[tag][0], exifData[tag][1], exifData[tag][2], exifData[tag][3]); + break; + + case "ComponentsConfiguration" : + exifData[tag] = + StringValues.Components[exifData[tag][0]] + + StringValues.Components[exifData[tag][1]] + + StringValues.Components[exifData[tag][2]] + + StringValues.Components[exifData[tag][3]]; + break; + } + tags[tag] = exifData[tag]; + } + } + + if (tags.GPSInfoIFDPointer) { + gpsData = readTags(file, tiffOffset, tiffOffset + tags.GPSInfoIFDPointer, GPSTags, bigEnd); + for (tag in gpsData) { + switch (tag) { + case "GPSVersionID" : + gpsData[tag] = gpsData[tag][0] + + "." + gpsData[tag][1] + + "." + gpsData[tag][2] + + "." + gpsData[tag][3]; + break; + } + tags[tag] = gpsData[tag]; + } + } + + // extract thumbnail + tags['thumbnail'] = readThumbnailImage(file, tiffOffset, firstIFDOffset, bigEnd); + + return tags; + } + + function findXMPinJPEG(file) { + + if (!('DOMParser' in self)) { + // console.warn('XML parsing not supported without DOMParser'); + return; + } + var dataView = new DataView(file); + + if (debug) console.log("Got file of length " + file.byteLength); + if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) { + if (debug) console.log("Not a valid JPEG"); + return false; // not a valid jpeg + } + + var offset = 2, + length = file.byteLength, + dom = new DOMParser(); + + while (offset < (length-4)) { + if (getStringFromDB(dataView, offset, 4) == "http") { + var startOffset = offset - 1; + var sectionLength = dataView.getUint16(offset - 2) - 1; + var xmpString = getStringFromDB(dataView, startOffset, sectionLength) + var xmpEndIndex = xmpString.indexOf('xmpmeta>') + 8; + xmpString = xmpString.substring( xmpString.indexOf( ' 0) { + json['@attributes'] = {}; + for (var j = 0; j < xml.attributes.length; j++) { + var attribute = xml.attributes.item(j); + json['@attributes'][attribute.nodeName] = attribute.nodeValue; + } + } + } else if (xml.nodeType == 3) { // text node + return xml.nodeValue; + } + + // deal with children + if (xml.hasChildNodes()) { + for(var i = 0; i < xml.childNodes.length; i++) { + var child = xml.childNodes.item(i); + var nodeName = child.nodeName; + if (json[nodeName] == null) { + json[nodeName] = xml2json(child); + } else { + if (json[nodeName].push == null) { + var old = json[nodeName]; + json[nodeName] = []; + json[nodeName].push(old); + } + json[nodeName].push(xml2json(child)); + } + } + } + + return json; + } + + + try { + var obj = {}; + if (xml.children.length > 0) { + for (var i = 0; i < xml.children.length; i++) { + var item = xml.children.item(i); + var attributes = item.attributes; + for(var idx in attributes) { + var itemAtt = attributes[idx]; + var dataKey = itemAtt.nodeName; + var dataValue = itemAtt.nodeValue; + + if(dataKey !== undefined) { + obj[dataKey] = dataValue; + } + } + var nodeName = item.nodeName; + + if (typeof (obj[nodeName]) == "undefined") { + obj[nodeName] = xml2json(item); + } else { + if (typeof (obj[nodeName].push) == "undefined") { + var old = obj[nodeName]; + + obj[nodeName] = []; + obj[nodeName].push(old); + } + obj[nodeName].push(xml2json(item)); + } + } + } else { + obj = xml.textContent; + } + return obj; + } catch (e) { + console.log(e.message); + } + } + + EXIF.getData = function(img, callback) { + if ((self.Image && img instanceof self.Image) + || (self.HTMLImageElement && img instanceof self.HTMLImageElement) + && !img.complete) + return false; + + if (!imageHasData(img)) { + getImageData(img, callback); + } else { + if (callback) { + callback.call(img); + } + } + return true; + } + + EXIF.getTag = function(img, tag) { + if (!imageHasData(img)) return; + return img.exifdata[tag]; + } + + EXIF.getIptcTag = function(img, tag) { + if (!imageHasData(img)) return; + return img.iptcdata[tag]; + } + + EXIF.getAllTags = function(img) { + if (!imageHasData(img)) return {}; + var a, + data = img.exifdata, + tags = {}; + for (a in data) { + if (data.hasOwnProperty(a)) { + tags[a] = data[a]; + } + } + return tags; + } + + EXIF.getAllIptcTags = function(img) { + if (!imageHasData(img)) return {}; + var a, + data = img.iptcdata, + tags = {}; + for (a in data) { + if (data.hasOwnProperty(a)) { + tags[a] = data[a]; + } + } + return tags; + } + + EXIF.pretty = function(img) { + if (!imageHasData(img)) return ""; + var a, + data = img.exifdata, + strPretty = ""; + for (a in data) { + if (data.hasOwnProperty(a)) { + if (typeof data[a] == "object") { + if (data[a] instanceof Number) { + strPretty += a + " : " + data[a] + " [" + data[a].numerator + "/" + data[a].denominator + "]\r\n"; + } else { + strPretty += a + " : [" + data[a].length + " values]\r\n"; + } + } else { + strPretty += a + " : " + data[a] + "\r\n"; + } + } + } + return strPretty; + } + + EXIF.readFromBinaryFile = function(file) { + return findEXIFinJPEG(file); + } + + if (typeof define === 'function' && define.amd) { + define('exif-js', [], function() { + return EXIF; + }); + } +}.call(this)); diff --git a/wire/modules/Inputfield/InputfieldImage/exif.min.js b/wire/modules/Inputfield/InputfieldImage/exif.min.js new file mode 100644 index 00000000..97b8a6ed --- /dev/null +++ b/wire/modules/Inputfield/InputfieldImage/exif.min.js @@ -0,0 +1 @@ +(function(){var q=false;var s=this;var f=function(A){if(A instanceof f){return A}if(!(this instanceof f)){return new f(A)}this.EXIFwrapped=A};if(typeof exports!=="undefined"){if(typeof module!=="undefined"&&module.exports){exports=module.exports=f}exports.EXIF=f}else{s.EXIF=f}var w=f.Tags={36864:"ExifVersion",40960:"FlashpixVersion",40961:"ColorSpace",40962:"PixelXDimension",40963:"PixelYDimension",37121:"ComponentsConfiguration",37122:"CompressedBitsPerPixel",37500:"MakerNote",37510:"UserComment",40964:"RelatedSoundFile",36867:"DateTimeOriginal",36868:"DateTimeDigitized",37520:"SubsecTime",37521:"SubsecTimeOriginal",37522:"SubsecTimeDigitized",33434:"ExposureTime",33437:"FNumber",34850:"ExposureProgram",34852:"SpectralSensitivity",34855:"ISOSpeedRatings",34856:"OECF",37377:"ShutterSpeedValue",37378:"ApertureValue",37379:"BrightnessValue",37380:"ExposureBias",37381:"MaxApertureValue",37382:"SubjectDistance",37383:"MeteringMode",37384:"LightSource",37385:"Flash",37396:"SubjectArea",37386:"FocalLength",41483:"FlashEnergy",41484:"SpatialFrequencyResponse",41486:"FocalPlaneXResolution",41487:"FocalPlaneYResolution",41488:"FocalPlaneResolutionUnit",41492:"SubjectLocation",41493:"ExposureIndex",41495:"SensingMethod",41728:"FileSource",41729:"SceneType",41730:"CFAPattern",41985:"CustomRendered",41986:"ExposureMode",41987:"WhiteBalance",41988:"DigitalZoomRation",41989:"FocalLengthIn35mmFilm",41990:"SceneCaptureType",41991:"GainControl",41992:"Contrast",41993:"Saturation",41994:"Sharpness",41995:"DeviceSettingDescription",41996:"SubjectDistanceRange",40965:"InteroperabilityIFDPointer",42016:"ImageUniqueID"};var z=f.TiffTags={256:"ImageWidth",257:"ImageHeight",34665:"ExifIFDPointer",34853:"GPSInfoIFDPointer",40965:"InteroperabilityIFDPointer",258:"BitsPerSample",259:"Compression",262:"PhotometricInterpretation",274:"Orientation",277:"SamplesPerPixel",284:"PlanarConfiguration",530:"YCbCrSubSampling",531:"YCbCrPositioning",282:"XResolution",283:"YResolution",296:"ResolutionUnit",273:"StripOffsets",278:"RowsPerStrip",279:"StripByteCounts",513:"JPEGInterchangeFormat",514:"JPEGInterchangeFormatLength",301:"TransferFunction",318:"WhitePoint",319:"PrimaryChromaticities",529:"YCbCrCoefficients",532:"ReferenceBlackWhite",306:"DateTime",270:"ImageDescription",271:"Make",272:"Model",305:"Software",315:"Artist",33432:"Copyright"};var p=f.GPSTags={0:"GPSVersionID",1:"GPSLatitudeRef",2:"GPSLatitude",3:"GPSLongitudeRef",4:"GPSLongitude",5:"GPSAltitudeRef",6:"GPSAltitude",7:"GPSTimeStamp",8:"GPSSatellites",9:"GPSStatus",10:"GPSMeasureMode",11:"GPSDOP",12:"GPSSpeedRef",13:"GPSSpeed",14:"GPSTrackRef",15:"GPSTrack",16:"GPSImgDirectionRef",17:"GPSImgDirection",18:"GPSMapDatum",19:"GPSDestLatitudeRef",20:"GPSDestLatitude",21:"GPSDestLongitudeRef",22:"GPSDestLongitude",23:"GPSDestBearingRef",24:"GPSDestBearing",25:"GPSDestDistanceRef",26:"GPSDestDistance",27:"GPSProcessingMethod",28:"GPSAreaInformation",29:"GPSDateStamp",30:"GPSDifferential"};var o=f.IFD1Tags={256:"ImageWidth",257:"ImageHeight",258:"BitsPerSample",259:"Compression",262:"PhotometricInterpretation",273:"StripOffsets",274:"Orientation",277:"SamplesPerPixel",278:"RowsPerStrip",279:"StripByteCounts",282:"XResolution",283:"YResolution",284:"PlanarConfiguration",296:"ResolutionUnit",513:"JpegIFOffset",514:"JpegIFByteCount",529:"YCbCrCoefficients",530:"YCbCrSubSampling",531:"YCbCrPositioning",532:"ReferenceBlackWhite"};var k=f.StringValues={ExposureProgram:{0:"Not defined",1:"Manual",2:"Normal program",3:"Aperture priority",4:"Shutter priority",5:"Creative program",6:"Action program",7:"Portrait mode",8:"Landscape mode"},MeteringMode:{0:"Unknown",1:"Average",2:"CenterWeightedAverage",3:"Spot",4:"MultiSpot",5:"Pattern",6:"Partial",255:"Other"},LightSource:{0:"Unknown",1:"Daylight",2:"Fluorescent",3:"Tungsten (incandescent light)",4:"Flash",9:"Fine weather",10:"Cloudy weather",11:"Shade",12:"Daylight fluorescent (D 5700 - 7100K)",13:"Day white fluorescent (N 4600 - 5400K)",14:"Cool white fluorescent (W 3900 - 4500K)",15:"White fluorescent (WW 3200 - 3700K)",17:"Standard light A",18:"Standard light B",19:"Standard light C",20:"D55",21:"D65",22:"D75",23:"D50",24:"ISO studio tungsten",255:"Other"},Flash:{0:"Flash did not fire",1:"Flash fired",5:"Strobe return light not detected",7:"Strobe return light detected",9:"Flash fired, compulsory flash mode",13:"Flash fired, compulsory flash mode, return light not detected",15:"Flash fired, compulsory flash mode, return light detected",16:"Flash did not fire, compulsory flash mode",24:"Flash did not fire, auto mode",25:"Flash fired, auto mode",29:"Flash fired, auto mode, return light not detected",31:"Flash fired, auto mode, return light detected",32:"No flash function",65:"Flash fired, red-eye reduction mode",69:"Flash fired, red-eye reduction mode, return light not detected",71:"Flash fired, red-eye reduction mode, return light detected",73:"Flash fired, compulsory flash mode, red-eye reduction mode",77:"Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected",79:"Flash fired, compulsory flash mode, red-eye reduction mode, return light detected",89:"Flash fired, auto mode, red-eye reduction mode",93:"Flash fired, auto mode, return light not detected, red-eye reduction mode",95:"Flash fired, auto mode, return light detected, red-eye reduction mode"},SensingMethod:{1:"Not defined",2:"One-chip color area sensor",3:"Two-chip color area sensor",4:"Three-chip color area sensor",5:"Color sequential area sensor",7:"Trilinear sensor",8:"Color sequential linear sensor"},SceneCaptureType:{0:"Standard",1:"Landscape",2:"Portrait",3:"Night scene"},SceneType:{1:"Directly photographed"},CustomRendered:{0:"Normal process",1:"Custom process"},WhiteBalance:{0:"Auto white balance",1:"Manual white balance"},GainControl:{0:"None",1:"Low gain up",2:"High gain up",3:"Low gain down",4:"High gain down"},Contrast:{0:"Normal",1:"Soft",2:"Hard"},Saturation:{0:"Normal",1:"Low saturation",2:"High saturation"},Sharpness:{0:"Normal",1:"Soft",2:"Hard"},SubjectDistanceRange:{0:"Unknown",1:"Macro",2:"Close view",3:"Distant view"},FileSource:{3:"DSC"},Components:{0:"",1:"Y",2:"Cb",3:"Cr",4:"R",5:"G",6:"B"}};function t(A,C,B){if(A.addEventListener){A.addEventListener(C,B,false)}else{if(A.attachEvent){A.attachEvent("on"+C,B)}}}function i(A){return !!(A.exifdata)}function e(D,G){G=G||D.match(/^data\:([^\;]+)\;base64,/mi)[1]||"";D=D.replace(/^data\:([^\;]+)\;base64,/gmi,"");var F=atob(D);var A=F.length;var C=new ArrayBuffer(A);var B=new Uint8Array(C);for(var E=0;E4?B:(J+8);K=[];for(D=0;D4?B:(J+8);return r(F,G,L-1);case 3:if(L==1){return F.getUint16(J+8,!H)}else{G=L>2?B:(J+8);K=[];for(D=0;DH.byteLength){return{}}}var A=u(H,G,G+B,o,D);if(A.Compression){switch(A.Compression){case 6:if(A.JpegIFOffset&&A.JpegIFByteCount){var F=G+A.JpegIFOffset;var C=A.JpegIFByteCount;A.blob=new Blob([new Uint8Array(H.buffer,F,C)],{type:"image/jpeg"})}break;case 1:console.log("Thumbnail image format is TIFF, which is not implemented.");break;default:console.log("Unknown thumbnail image format '%s'",A.Compression)}}else{if(A.PhotometricInterpretation==2){console.log("Thumbnail image format is RGB, which is not implemented.")}}return A}function r(B,D,C){var A="";for(n=D;n")+8;I=I.substring(I.indexOf("0){R["@attributes"]={};for(var O=0;O0){for(var F=0;F