[Javascript] 画像アップロードに特化した便利なImageUploderライブラリ

2019年8月15日

Javascript テクノロジー プログラミング

画像アップロードの際に、画像の編集をしたいと思い、画像に特化したアップローダを作りました。 新規に構築するサービスで写真をアップロードするような機能を実装する場合、意外と面倒くさい手順が沢山存在します。 そうした手順をライブラリ化しておいたので、便利につかえるモジュールを作ってみました。 汎用性をもたせたつもりですが、機能要望がありましたら、コメントください。

機能一覧

初回版の機能は以下のとおりです。

1. 画像形式のアップロードに特化

拡張子は次の3つのみ、「jpg,png,gif」

2. EXIFライブラリを併用することで、javascriptでのexif情報の取得が可能

https://github.com/exif-js/exif-js

3. 複数画像のアップロードが可能

アップロード前に、画像を確認して、取り消しも可能。

4. スマホの場合に、写真撮影からそのままアップロードすることが可能。

撮影した写真をアップロード前に編集することも可能。 端末に写真を残さないセキュア対応も可能。

ソースコード

1つのディレクトリに以下の2つのソースコードと、「rotate.svg」「delete.svg」をいれて準備します。 ;$$fileupload = (function(){ // 起動scriptタグを選択 var __currentScriptTag = (function(){ var scripts = document.getElementsByTagName("script"); return __currentScriptTag = scripts[scripts.length-1].src; })(); // [共通関数] イベントセット var __event = function(target, mode, func){ if (target.addEventListener){target.addEventListener(mode, func, false)} else{target.attachEvent('on' + mode, function(){func.call(target , window.event)})} }; // [共通関数] URL情報分解 var __urlinfo = function(uri){ uri = (uri) ? uri : location.href; var data={}; var urls_hash = uri.split("#"); var urls_query = urls_hash[0].split("?"); var sp = urls_query[0].split("/"); var data = { uri : uri , url : sp.join("/") , dir : sp.slice(0 , sp.length-1).join("/") +"/" , file : sp.pop() , domain : sp[2] , protocol : sp[0].replace(":","") , hash : (urls_hash[1]) ? urls_hash[1] : "" , query : (urls_query[1])?(function(urls_query){ var data = {}; var sp = urls_query.split("#")[0].split("&"); for(var i=0;i<sp .length;i++){ var kv = sp[i].split("="); if(!kv[0]){continue} data[kv[0]]=kv[1]; } return data; })(urls_query[1]):[] }; return data; }; // [共通関数] DOMの上位検索 var __upperSelector = function(elm , selectors) { selectors = (typeof selectors === "object") ? selectors : [selectors]; if(!elm || !selectors){return;} var flg = null; for(var i=0; i<selectors.length; i++){ for (var cur=elm; cur; cur=cur.parentElement) { if (cur.matches(selectors[i])) { flg = true; break; } } if(flg){ break; } } return cur; } // [共通関数] ブラウザのFileAPIが利用できるかどうかチェックする var __checkFileAPI = function(){ // FileApi確認 if( window.File && window.FileReader && window.FileList && window.Blob) { return true; } else{ return false; } }; // [共通関数] JS読み込み時の実行タイミング処理(body読み込み後にJS実行する場合に使用) var __construct = function(){ switch(document.readyState){ case "complete" : new $$;break; case "interactive" : __event(window , "DOMContentLoaded" , function(){new $$});break; default : __event(window , "load" , function(){new $$});break; } }; // ---------- // インスタンスベースモジュール(初期設定処理) var $$ = function(options){ this.replaceOptions(options); this.options.cacheTime = (+new Date()); if(!this.options.currentPath && __currentScriptTag){ var pathinfo = __urlinfo(__currentScriptTag); this.options.currentPath = pathinfo.dir; } // set-css this.setCss(); this.setTypeFile(); // upload-button this.setButton(); }; $$.prototype.options = { cacheTime : null, currentPath : null, css_path : null, // 表示系cssの任意指定(デフォルトは起動スクリプトと同一階層) file_multi : true, // 複数ファイルアップロード対応 [ true : 複数 , false : 1つのみ] querys : {}, // input type="hidden"の任意値のセット(cgiに送信する際の各種データ) btn_selector : "#fileupload", // クリックするボタンのselectors(複数対応) view_bg_class : "fileUpload-image-bg", // 画像編集用モードのBG(base)要素のclass名 extensions : ["jpg","jpeg","png","gif","svg"], // 指定拡張子一覧 img_rotate_button : null, // 画像編集の回転機能アイコン(デフォルトは起動スクリプトと同一階層) img_delete_button : null, // 画像編集の削除機能アイコン(デフォルトは起動スクリプトと同一階層) file_select : function(e){console.log(e)} // submit直前の任意イベント処理 }; // [初期設定] インスタンス引数を基本設定(options)と入れ替える $$.prototype.replaceOptions = function(options){ for(var i in options){ this.options[i] = options[i]; } return this.options; }; // [初期設定] 基本CSSセット $$.prototype.setCss = function(){ var head = document.getElementsByTagName("head"); if(!head){return;} var css = document.createElement("link"); css.rel = "stylesheet"; css.href = (this.options.css_path !== null) ? this.options.css_path : this.options.currentPath + "images.css"; head[0].appendChild(css); }; $$.prototype.getBase = function(){ var lists = document.getElementsByClassName(this.options.view_bg_class); if(lists.length){ return lists[0]; } else{ return null; } }; // 処理用iframe内のform内のtype=fileを取得 $$.prototype.getForm_typeFile = function(){ return document.querySelector("input[name='fileupload_"+ this.options.cacheTime +"']"); }; // 編集画面の画像一覧リストの取得 $$.prototype.getEditImageLists = function(){ return document.querySelectorAll("."+this.options.view_bg_class+" ul li.pic"); }; $$.prototype.setTypeFile = function(){ var inp = document.createElement("input"); inp.type = "file"; inp.name = "fileupload_" + this.options.cacheTime; inp.multiple = (this.options.file_multi) ? "multiple" : ""; inp.style.setProperty("display","none",""); inp.accept = "image/gif,image/jpeg,image/png"; __event(inp , "change" , (function(e){ if(typeof this.options.file_select === "function" && __checkFileAPI()){ var input = e.currentTarget; this.viewImageEdit(input); } }).bind(this)); document.body.appendChild(inp); }; // [初期設定] データ送信ボタンのsubmit処理設定(複数対応) $$.prototype.setButton = function(){ var btns = document.querySelectorAll(this.options.btn_selector); for(var i=0; i<btns.length; i++){ __event(btns[i] , "click" , (function(e){this.clickFileButton(e)}).bind(this)); } }; // データ送信submitボタンクリック時の処理 $$.prototype.clickFileButton = function(e){ var typeFile = this.getForm_typeFile(); typeFile.click(); }; // postデータの拡張子確認 $$.prototype.checkExtension = function(filename){ }; // [画像編集] 送信前の画像編集操作処理 $$.prototype.viewImageEdit = function(targetInputForm){ this.viewBG(); this.viewImages(targetInputForm); }; // [画像編集] 画像回転処理 // [画像編集] 編集画面表示(複数画像対応) $$.prototype.viewImages = function(filesElement){ if(!filesElement){return;} var files = filesElement.files; if(!files || !files.length){return;} var bgs = document.getElementsByClassName(this.options.view_bg_class); if(!bgs || !bgs.length){return;} var bg = bgs[0]; var ul = document.createElement("ul"); bg.appendChild(ul); for(var i=0; i<files.length; i++){ var li = document.createElement("li"); li.className = "pic"; li.setAttribute("data-num" , i); ul.appendChild(li); var path = URL.createObjectURL(files[i]); var img = new Image(); // var img = document.createElement("img"); img.src = path; img.className = "picture"; img.setAttribute("data-num" , i); __event(img , "load" , (function(e){this.loadedImage(e)}).bind(this)); li.appendChild(img); var num = document.createElement("div"); num.className = "num"; li.appendChild(num); var control = document.createElement("div"); control.className = "control"; control.setAttribute("data-num" , i); li.appendChild(control); var rotateImage = new Image(); rotateImage.className = "rotate"; rotateImage.src = (this.options.img_rotate_button !== null) ? this.options.img_rotate_button : this.options.currentPath + "rotate.svg"; control.appendChild(rotateImage); __event(rotateImage , "click" , (function(e){this.clickRotateButton(e)}).bind(this)); var delImage = new Image(); delImage.className = "delete"; delImage.src = (this.options.img_delete_button !== null) ? this.options.img_delete_button : this.options.currentPath + "delete.svg"; control.appendChild(delImage); __event(delImage , "click" , (function(e){this.clickDeleteButton(e)}).bind(this)); } var li = document.createElement("li"); li.className = "submit"; ul.appendChild(li); var sendButton = document.createElement("button"); sendButton.innerHTML = "送信"; __event(sendButton , "click" , (function(e){this.clickSendButton(e)}).bind(this)); li.appendChild(sendButton); var cancelButton = document.createElement("button"); cancelButton.innerHTML = "キャンセル"; __event(cancelButton , "click" , (function(e){this.clickCancel(e)}).bind(this)); li.appendChild(cancelButton); }; // [画像編集] BG表示 $$.prototype.viewBG = function(){ var bg = document.createElement("div"); bg.className = this.options.view_bg_class; document.body.appendChild(bg); }; // [画像編集] rotateボタンを押した時の処理(左に90度回転) $$.prototype.clickRotateButton = function(e){ var target = e.currentTarget; // console.log(target.parentNode.getAttribute("data-num")); var num = target.parentNode.getAttribute("data-num"); if(num === null){return;} var targetImage = document.querySelector("."+this.options.view_bg_class+" ul li.pic[data-num='"+num+"'] img.picture"); if(!targetImage){return;} var rotateNum = targetImage.getAttribute("data-rotate"); rotateNum = (rotateNum) ? rotateNum : "0"; // 反時計回りに回転 switch(rotateNum){ case "0": rotateNum = 270; break; case "90": rotateNum = 0; break; case "180": rotateNum = 90; break; case "270": rotateNum = 180; break; } targetImage.setAttribute("data-rotate" , rotateNum); }; // $$.prototype.clickDeleteButton = function(e){ if(!confirm("アップロードリストから写真を破棄しますか?※直接撮影された写真は保存されません。")){return;} var target = e.currentTarget; // console.log(target.parentNode.getAttribute("data-num")); var num = target.parentNode.getAttribute("data-num"); if(num === null){return;} var targetListBase = document.querySelector("."+this.options.view_bg_class+" ul li.pic[data-num='"+num+"']"); if(!targetListBase){return;} targetListBase.parentNode.removeChild(targetListBase); // ラスト1つを削除した場合は、キャンセル扱い var lists = this.getEditImageLists(); if(!lists || !lists.length){ this.clickCancel(); } } // $$.prototype.clickCancel = function(){ var base = this.getBase(); if(base){ base.parentNode.removeChild(base); } var input = this.getForm_typeFile(); input.value = ""; }; // 画像を読み込んだ際のイベント処理 $$.prototype.loadedImage = function(e){ var img = e.currentTarget; var num = img.getAttribute("data-num"); if(typeof window.EXIF !== "undefined"){ var res = EXIF.getData(img , (function(img,e) { var exifData = EXIF.getAllTags(img); img.setAttribute("data-exif" , JSON.stringify(exifData)); }).bind(this , img)); } }; $$.prototype.clickSendButton = function(e){ var files = this.getForm_typeFile().files; var lists = this.getEditImageLists(); for(var i=0; i<lists.length; i++){ var num = lists[i].getAttribute("data-num"); // this.postFile(files[num]); this.postFiles_cache.push(files[num]); } if(this.postFiles_cache.length > 0){ this.postFile(lists[0]); } }; $$.prototype.postFiles_cache = []; $$.prototype.postFile = function(viewListElement){ if(!window.FormData){ console.log("データ送信機能がブラウザに対応していません。"); return; } if(!window.XMLHttpRequest){ console.log("AJAX機能がブラウザに対応していません。"); return; } // 全て送信完了したら編集画面を閉じる if(!this.postFiles_cache.length){ this.clickCancel(); return; } var fd = new FormData(); if(this.options.querys){ for(var i in this.options.querys){ fd.append(i , this.options.querys[i]); } } fd.append("imageFile" , this.postFiles_cache[0]); fd.append("info[name]" , this.postFiles_cache[0].name); fd.append("info[size]" , this.postFiles_cache[0].size); fd.append("info[type]" , this.postFiles_cache[0].type); fd.append("info[modi]" , this.postFiles_cache[0].lastModified); fd.append("info[date]" , this.postFiles_cache[0].lastModifiedDate); var img = viewListElement.querySelector(".picture"); var rotate = (img.getAttribute("data-rotate")) ? img.getAttribute("data-rotate") : ""; fd.append("info[rotate]" , rotate); var lists = this.getEditImageLists(); if(!lists.length){return;} var img = lists[0].querySelector("img"); var exifData = img.getAttribute("data-exif"); if(exifData){ fd.append("exif" , exifData); } var XHR = new XMLHttpRequest(); XHR.onreadystatechange = (function(XHR,e){ if (XHR.readyState==4 && XHR.status == 200){ console.log(XHR.responseText); if(this.postFiles_cache.length){ this.postFiles_cache.shift(); } var lists = this.getEditImageLists(); if(lists.length){ lists[0].parentNode.removeChild(lists[0]); } // 送信後の削除処理をした直後のエレメント一覧の取得 var lists = this.getEditImageLists(); if(lists.length){ setTimeout((function(lists,e){this.postFile(lists)}).bind(this,lists[0]) , 1000); } else{ this.clickCancel(); } } }).bind(this,XHR); XHR.open('POST', location.href); XHR.send(fd); }; return $$; })(); .fileUpload-image-bg{ position:fixed; display:block; top:0; left:0; background-color:rgba(0,0,0,0.5); width:100%; height:100%; z-index:1000; } .fileUpload-image-bg ul, .fileUpload-image-bg li{ list-style:none; padding:0; margin:0; border:0; width:100%; } .fileUpload-image-bg ul{ counter-reset:num; height:100%; overflow-Y:auto; padding:40px 0; } .fileUpload-image-bg ul li{ /* min-width:300px; max-width:640px; width:50%; */ width:300px; height:300px; text-align:center; position:relative; margin:20px auto; } .fileUpload-image-bg ul li.pic{ padding:4px; border:1px solid white; background-color:white; } .fileUpload-image-bg ul li.pic:before{ counter-increment: num; content: counter(num); position:absolute; top:0; left:0; display:inline-block; width:30px; height:30px; font-size:20px; color:white; text-shadow:2px 2px 4px black; z-index:100; } /* .fileUpload-image-bg li .num{ position:absolute; top:0; left:0; display:inline-block; width:30px; height:30px; font-size:20px; color:white; text-shadow:2px 2px 4px black; } */ .fileUpload-image-bg li .control{ position:absolute; top:calc(50% - 20px); width:100%; height:40px; /* text-align:center; */ /* display: -webkit-flex; display: flex; */ display:none; -webkit-justify-content: center; justify-content: center; -webkit-align-items: center; align-items: center; } .fileUpload-image-bg li:hover .control{ display: -webkit-flex; display: flex; } .fileUpload-image-bg li .control .rotate, .fileUpload-image-bg li .control .delete{ width:40px; height:40px; cursor:pointer; filter: drop-shadow(2px 2px 2px black); /* display:list-item; */ margin:0 20px; } .fileUpload-image-bg li .control .rotate:hover, .fileUpload-image-bg li .control .delete:hover{ opacity:0.5; } .fileUpload-image-bg img.picture{ width:100%; height:100%; display:block; margin:auto; vertical-align:middle; object-fit:contain; } .fileUpload-image-bg img.picture[data-rotate="90"]{ transform:rotate(90deg); } .fileUpload-image-bg img.picture[data-rotate="180"]{ transform:rotate(180deg); } .fileUpload-image-bg img.picture[data-rotate="270"]{ transform:rotate(270deg); } .fileUpload-image-bg li.submit button{ margin:10px 20px; }

実装方法

1. モジュールの読み込み

画像読み込みを行いたいHTMLファイルでモジュールの読み込みを行います。 <script src="lib/exif.js"></script> <script src="fileupload/images.js"></script>

2. 画像アップロードボタンを実装

アップロードボタンは、どんなエレメントでも構いません。 <button type="button" id="fileupload">画像アップロード</button>

3. 設定情報を付与してページ内で実行

<script> new $$fileupload({ form_action : "upload.php", querys : { "post1" : "data-1" }, file_select : function(e){ console.log(e); } }); </script>

【解説】

"querys"は、送信時に付与したい情報を追加することができます。 "file_select"は、アップロードを実行して完了後に発生するイベント用コールバック関数です。

受信用PHP

今回は、受信用のPHPコードはサンプルのみ載せておきます。 それぞれのサイトに合わせたコードで記述してお使いください。 <?php $data = array( "file" => $_FILES["imageFile"], "info" => $_POST["info"], "exif" => $exif ); print_r($data);

このブログを検索

ごあいさつ

このWebサイトは、独自思考で我が道を行くユゲタの少し尖った思考のTechブログです。 毎日興味がどんどん切り替わるので、テーマはマルチになっています。 もしかしたらアイデアに困っている人の助けになるかもしれません。