/** *@file upload.js *@version 20250730 sn *@type JS * * Komponente zum Hochladen einer oder mehrerer Dateien mit Fortschrittsanzeige. Setzt die Server-seitige Verwendung eines speziellen PHP-Skipts voraus. */ /** * @class KUpload * @param {object} opts * @constructor */ function KUpload(opts) { // TODO (Maik) added opts to set image shrink options for DenkmalGIS-MV // TODO (Maik) add resource here and below also the s-function: this.res = { ShrinkProgressState: {de:"{n} / {num} Dateien optimiert", en:"{n} / {num} files optimized"} }; this.Lang = "de"; this.DefaultLang = "en"; this.opts=opts; this.fieldName="file"; this.running=false; this.uploadedTotalBytes=0; this.uploadFiles; this.countProgress=null; this.win=null; // define in WinOpen() as table this.winModal=null; //define this.inpUpload=null; // HTMLElement::input@type=file var d=document.createElement("div");with(d.style){position="absolute";width="0";height="0";overflow="hidden";};document.body.appendChild(d); this.inpUpload=document.createElement("input");this.inpUpload.type="file";d.appendChild(this.inpUpload); var m=this; m.inpUpload.onchange=function(){if(!m.running) m.UploadPrepare(m.inpUpload.files);}; } KUpload.prototype.s=function(id){ var r=this.res[id]; if(r && Object.keys(r).length) return r[this.Lang] || r[this.DefaultLang] || r[Object.keys(r)[0]]; else return "["+id+"]"; }; /** * @fn UploadShow * @memberof KUpload * @param {string} url * @param {boolean} multiple * @param {function} completedFunc * @param {function} checkFunc * @param {function} beforeFunc * @param {function} afterFunc * Public method. * Open browsers file dialog. */ KUpload.prototype.UploadShow=function(url,multiple,completedFunc,checkFunc,beforeFunc,afterFunc){ if(this.running) return; this.uploadCompleteFunc=completedFunc; this.checkFunc=checkFunc; this.beforeFunc=beforeFunc; this.afterFunc=afterFunc; this.uploadUrl=url; this.inpUpload.multiple=multiple?true:false; this.inpUpload.click(); }; /** * @fn Upload * @memberof KUpload * @param {string} url * @param {FileList} files * @param {function} completedFunc * @param {function} checkFunc * @param {function} beforeFunc * @param {function} afterFunc * Public method. * Begin file upload. */ KUpload.prototype.Upload=function(url,files,completedFunc,checkFunc,beforeFunc,afterFunc){ if(this.running) return; this.uploadUrl=url; this.uploadCompleteFunc=completedFunc; this.checkFunc=checkFunc; this.beforeFunc=beforeFunc; this.afterFunc=afterFunc; this.UploadPrepare(files); }; /** * @fn UploadPrepare * @memberof KUpload * @param {FileList} files * @call this.UploadInit() * @callby this.Upload(), Event::onchange of HTMLElement::input * Private method. */ KUpload.prototype.UploadPrepare=function(files){ var m=this,a=-1,upFiles=[]; var doFunc=function(){ a++; if(a==files.length){ if(upFiles.length) m.UploadInit(upFiles); }else{ if(files[a].size){ if(m.checkFunc){ m.checkFunc(files[a],function(file,formData){ if(file) upFiles.push({file:file,formData:formData?formData:{}}); doFunc(); }); }else{ upFiles.push({file:files[a],formData:{}}); doFunc(); } }else doFunc(); } }; doFunc(); }; /** * @fn UploadInit * @memberof KUpload * @param upFiles * @call this.UploadStart() * @referredby this.UploadPrepare() * Private method. */ KUpload.prototype.UploadInit=function(upFiles){ this.uploadFiles=upFiles; this.uploadedTotalBytes=0; this.uploadTotalBytes=0; for(var a=0;a{ let uploadFile = this.uploadFiles[index]; console.log(`called uploadShrink ${uploadFile.file.name}`); this.UploadShrink(uploadFile).then(shrinkedFile => { // ShrinkedFile is null if shrink option is not set or file is not an image or the image isn't too big: if(shrinkedFile){ // Set shrinked file and adjust total size: this.uploadTotalBytes+=shrinkedFile.size - uploadFile.file.size; uploadFile.file=shrinkedFile; console.log(`replaced upload file by shrinked file ${shrinkedFile.name} with size ${this.uploadFiles[index].file.size}`,uploadFile); } }).catch(err => { console.error(`Error shrinking file ${uploadFile.file.name}: ${err}`); }).finally(() => { index++; if(this.opts.shrink){ this.shrinkProgressState.innerHTML=this.s("ShrinkProgressState").replace(/{n}/,index).replace(/{num}/,this.uploadFiles.length); this.shrinkProgress.value=index; } // Shrink next file and if all are shrinkted then start upload: if (index < this.uploadFiles.length) uploadShrink(index); else { if(this.opts.shrink) { t.deleteRow(-1); t.deleteRow(-1); } // TODO (Maik) ab hier der alte Code: tr=t.insertRow(0);td=tr.insertCell(0); this.sizeProgress=document.createElement("progress");td.appendChild(this.sizeProgress);this.sizeProgress.style.width=(KUpload.WindowWidth()/2)+"px"; tr=t.insertRow(1);this.sizeProgressState=tr.insertCell(0);this.sizeProgressState.style.textAlign="center"; tr=t.insertRow(0);if(this.uploadFiles.length===1) tr.style.display="none";td=tr.insertCell(0); this.countProgress=document.createElement("progress");td.appendChild(this.countProgress);this.countProgress.style.width=(KUpload.WindowWidth()/2)+"px"; this.countProgress.max=this.uploadTotalBytes; tr=t.insertRow(1);if(this.uploadFiles.length===1) tr.style.display="none";this.countProgressState=tr.insertCell(0);this.countProgressState.style.textAlign="center"; this.WinResize(); this.UploadIDs=[]; //this.running=true; this.UploadFile(0); } }); }; uploadShrink(0); }; /** * @fn UploadShrink * @memberof KUpload * Private method. */ KUpload.prototype.UploadShrink = async function(uploadFile) { if(!this.opts.shrink) return null; var file = uploadFile.file; if (file.size <= this.opts.maxImageSize) { console.log(`file ${file.name} not shrinked, because ${file.size} < maxImageSize ${this.opts.maxImageSize}`); return null; } if (file.type.startsWith("image/jpeg")) return this.ShrinkJPEG(uploadFile); else if (file.type.startsWith("image/png")) return this.ShrinkPNG(file); else if (file.type.includes("gif")) return this.ShrinkPNG(file,true); // true = convert too big GIF to PING, because canvas.toBlob(resolve, "image/gif")) cannot produce a GIF file format! else { console.log(`file ${file.name} not shrinked, because not supported for file type ${file.type}`); return null; } }; /** * @fn UploadReadExif * @memberof KUpload * Private method. */ KUpload.prototype.UploadReadExif = function(uploadFile) { return new Promise((resolve, reject) => { if(typeof Exif==="undefined") { console.error("KIG exif.js is missing - cannot read EXIF information!"); reject(); } else Exif.read(uploadFile.file,function(exif){ if(!exif) reject(); else { uploadFile.dateTimeOriginal = exif["EXIF"][36867]; // 36867 = DateTimeOriginal console.log('DateTimeOriginal:', uploadFile.dateTimeOriginal); //uploadFile.orientation = exif["IFD0"][274]; // 274 = Orientation //console.log('Orientation:', uploadFile.orientation); resolve(); } }); }); }; /** * @fn ShrinkJPEG * @memberof KUpload * Private method. */ KUpload.prototype.ShrinkJPEG = async function(uploadFile) { var file = uploadFile.file; console.log(`ShrinkJPEG processes file ${file.name}`); // Read EXIF-Infos of original JPEG, because in shriked JPEG it's lost: await this.UploadReadExif(uploadFile); // Read the JPEG: const imageBitmap = await createImageBitmap(file); let canvas = document.createElement("canvas"); let ctx = canvas.getContext("2d"); let width = imageBitmap.width; let height = imageBitmap.height; let quality = 0.85; // 0.92; // Calculate estimated dimensions for maxImageSize: const estimatedBytesPerPixel = 0.12; // konservative Schätzung const scale = Math.sqrt(this.opts.maxImageSize / estimatedBytesPerPixel / (width * height)); console.log(`estimated JPEG image scale: ${scale}`); if (scale < 1.0) { width = Math.floor(width * scale); height = Math.floor(height * scale); console.log(`estimated JPEG image dimensions: ${width} x ${height}`); } canvas.width = width; canvas.height = height; ctx.drawImage(imageBitmap, 0, 0, width, height); // draws in right orientation by using the EXIF flag! let blob = await new Promise(resolve => canvas.toBlob(resolve, "image/jpeg", quality)); console.log(`estimated resize of file ${file.name} from ${file.size} to ${blob.size} with quality ${quality} and resolution ${width} x ${height}`); if (blob.size > file.size) { console.log(`file ${file.name} not shrinked, because resized file is bigger than original`); return null; // if original file size is below (e.g. has lower quality) then keep it! } // Reduce resolution: while (blob.size > this.opts.maxImageSize && (width > this.opts.minImagePixel || height > this.opts.minImagePixel)) { width = Math.round(width * 0.9); height = Math.round(height * 0.9); canvas.width = width; canvas.height = height; ctx.drawImage(imageBitmap, 0, 0, width, height); blob = await new Promise(resolve => canvas.toBlob(resolve, "image/jpeg", quality)); console.log(`resized dimensions of file ${file.name} from ${file.size} to ${blob.size} with quality ${quality} and resolution ${width} x ${height}`); } // Reduce qualtiy: while (blob.size > this.opts.maxImageSize && quality > 0.70) { quality -= 0.05; blob = await new Promise(resolve => canvas.toBlob(resolve, "image/jpeg", quality)); console.log(`reduced quality of file ${file.name} from ${file.size} to ${blob.size} with quality ${quality} and resolution ${width} x ${height}`); } return new File([blob], file.name, { type: "image/jpeg" }); }; KUpload.prototype.ShrinkPNG = async function(file, isGIF) { const imageBitmap = await createImageBitmap(file); let width = imageBitmap.width; let height = imageBitmap.height; const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); let blob; // Calculate estimated dimensions for maxImageSize: const estimatedBytesPerPixel = 0.8; // konservative Schätzung const scale = Math.sqrt(this.opts.maxImageSize / estimatedBytesPerPixel / (width * height)); console.log(`estimated PING image scale: ${scale}`); if (scale < 1.0) { width = Math.floor(width * scale); height = Math.floor(height * scale); console.log(`estimated PING image dimensions: ${width} x ${height}`); } // Reduce dimensions: do { canvas.width = width; canvas.height = height; ctx.clearRect(0, 0, width, height); ctx.drawImage(imageBitmap, 0, 0, width, height); blob = await new Promise(resolve => canvas.toBlob(resolve, "image/png")); console.log(`resized dimensions of file ${file.name} to ${width} x ${height} and reduced size from ${file.size} to ${blob.size}`); // Schrittweise Verkleinerung (10 %), falls zu groß if (blob.size > this.opts.maxImageSize) { width = Math.max(this.opts.minImagePixel, Math.round(width * 0.9)); height = Math.max(this.opts.minImagePixel, Math.round(height * 0.9)); } } while (blob.size > this.opts.maxImageSize && width > this.opts.minImagePixel && height > this.opts.minImagePixel); return new File([blob], isGIF? file.name.replace(".gif",".png"):file.name, {type: "image/png"}); }; /** * @fn UploadFile * @memberof KUpload * @param index * Private method. */ KUpload.prototype.UploadFile=function(index){ var m=this; m.countProgressState.innerHTML=(index+1)+" / "+m.uploadFiles.length; m.sizeProgress.value=0; m.sizeProgress.max=m.uploadFiles[index].file.size; m.sizeProgressState.innerHTML="0 %"; if(m.beforeFunc) m.beforeFunc(m.uploadFiles[index].file,m.uploadFiles[index].formData); var r=new XMLHttpRequest(); r.open("POST",this.uploadUrl,true); r.onload=function(){ if(r.readyState===4){ if(r.status===200){ if(r.responseText.substr(0,3)=="ID="){ var newId=parseInt(r.responseText.substr(3)); if(newId){ if(m.afterFunc) m.afterFunc(m.uploadFiles[index].file,m.uploadFiles[index].formData,newId); m.UploadIDs.push(newId); if(index==m.uploadFiles.length-1){ m.UploadComplete(); }else{ m.uploadedTotalBytes+=m.uploadFiles[index].file.size; m.UploadFile(index+1); } }else{ alert("Could not parse ID from '"+r.responseText.substr(3)+"'"); m.UploadComplete(); } }else{ alert(r.responseText.replace(/ERR=/g,"")); m.UploadComplete(); } }else{ console.error(r.statusText); m.UploadComplete(); } } }; r.onerror=function(e){ alert("an error occurred while uploading the file"); m.UploadComplete(); }; r.onabort=function(e){ alert("file upload canceled"); m.UploadComplete(); }; r.upload.addEventListener("progress",function(e){ if(e.lengthComputable){ m.sizeProgress.value=e.loaded; m.sizeProgressState.innerHTML=Math.round((100/e.total)*e.loaded)+" %"; } m.countProgress.value=m.uploadedTotalBytes+e.loaded; }); var fd=new FormData(); var ffd=m.uploadFiles[index].formData; if(typeof ffd==="object") for(var key in ffd) fd.append(key,typeof ffd[key]==='string' || ffd[key] instanceof String || ffd[key] instanceof Blob?ffd[key]:JSON.stringify(ffd[key])); // TODO (Maik) da die Shrink-Funktion die Metadaten entfernt, hier das Aufnahmedatum und die Orientierung senden: if(this.opts.shrink){ if(m.uploadFiles[index].dateTimeOriginal) fd.append("DateTimeOriginal",m.uploadFiles[index].dateTimeOriginal); if(m.uploadFiles[index].orientation) fd.append("Orientation",m.uploadFiles[index].orientation); } fd.append(m.fieldName,m.uploadFiles[index].file); r.send(fd); }; /** * @fn UploadComplete * @memberof KUpload * Private method. */ KUpload.prototype.UploadComplete=function(){ if(this.running){ this.running=false; this.WinClose(); this.uploadCompleteFunc(this.UploadIDs); } }; /** * @fn WinOpen * @memberof * @param o * @param closable * @returns {DOMnode::div | DOMnode::iframe} * Private method. * defines an append to DOM * this.winModal as HTMLtable * this.win as HTMLiframe or HTMLdiv */ KUpload.prototype.WinOpen=function(o,closable){ this.winModal=document.createElement("table");document.body.appendChild(this.winModal); var st=this.winModal.style;st.position="absolute";st.left="0";st.top="0";st.width="100%";st.height="100%";st.borderSpacing="0";st.borderCollapse="collapse";st.backgroundColor="rgba(0,0,0,0.5)";st.zIndex=9998;st.cursor="not-allowed"; var m=this; this.winModal.ondblclick=function(){m.WinClose()}; var tr=this.winModal.insertRow(0); var td=tr.insertCell(0); td.innerHTML=" "; if(typeof(o)=="undefined" || o=="div") this.win=document.createElement("div"); else if(o=="iframe"){ this.win=document.createElement("iframe");this.win.frameBorder=1;this.win.scrolling="no"; } st=this.win.style;st.position="absolute";st.zIndex=9999;st.backgroundColor="#F4F5F6";st.border="black solid 1px"; document.body.appendChild(this.win); this.WinResize(); return this.win; }; /** * @fn WinResize * @memberof KUpload * @param w * @param h * Private method. */ KUpload.prototype.WinResize=function(w,h){ if(w>0 && h>0){this.win.style.width=w+"px";this.win.style.height=h+"px";} this.win.style.left=(KUpload.WindowWidth()-this.win.offsetWidth)/2+"px"; this.win.style.top=(KUpload.WindowHeight()-this.win.offsetHeight)/2+"px"; }; /** * @fn WinClose * @memberof KUpload * Private method. */ KUpload.prototype.WinClose=function(){ if(this.win){document.body.removeChild(this.win);this.win=null;document.body.removeChild(this.winModal);} }; /** * @fn WindowWidth * @returns * Private method. */ KUpload.WindowWidth=function(){ return window.innerWidth && document.documentElement.clientWidth? Math.min(window.innerWidth, document.documentElement.clientWidth) : window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; }; /** * @fn WindowHeight * @returns * Private method. */ KUpload.WindowHeight=function(){ return window.innerHeight && document.documentElement.clientHeight? Math.min(window.innerHeight, document.documentElement.clientHeight) : window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight; };