連続的にローカル写真を圧縮してアップロードする

概要

過去にもJavaScriptでローカルPC上の写真データをアップロードする方法について書いた記憶がありますが、今回の目標は、“連続的に”ということをクリアすることです。
複数のファイルを同時に選択し、CANVAS上に配置する時に縮小し、CANVAS上の画像データをサーバーにアップロードします。ファイル選択の一連の処理の自動化を試みます。

複数画像ファイルの選択

先ず、複数の画像ファイルを同時に取り込んでブラウザー上に表示する処理のテストプログラムです。とりあえず画像ファイルの読み込み評価を目的にしていますので、画像の縮尺比等は未考慮です。また、ファイルの読み込み結果(③)は、ファイル選択順(②)とは必ずしも一致しません。これは、ファイルサイズ等によって読込時間が異なり、読込完了順にブラウザーに表示する為です。今のところ必要性を感じていませんが、必要な場合は全ファイル読込後、順番にブラウザに表示するなどの対応が必要かもしれません。(→(A)複数画像ファイル取り込みプログラム

(1)操作概要
①テストプログラム初期画面(ページを開いた状態)
②“ファイル選択” ダイアログボックスで対象ファイル選択
③選択ファイルをブラウザー上に自動表示
(上段:ファイル名  , 下段:画像表示)

(2)テストプログラム
今回の目標は“連続的に”処理することです。1ファイルでは過去にも確認していると思います。ということで、行番63の<input>タグには複数選択できるように、“multiple”  を設定しています。
ファイル選択時、行番16~20の関数 fileReader() を呼び出します。行番17~19のループ処理内で一つづつ選択した File オブジェクトを関数 file_loader() に渡して、ファイル読込,ブラウザへの表示を行います。
最初は、行番17~19のループ内に関数 file_loader() の処理内容を組み込んでいたのですが、ファイル名・サイズ・種類等の関連情報取得とファイル読み込みが同期しないので、情報と画像が一致しない問題がありました。今回の様に分けることで改善されたことを確認できました。

<!doctype html>
<html lang="ja">

<head>
    <meta charset="utf-8">

    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>FILE SELECT</title>

    <style>
    </style>

    <script type="text/javascript">

    // 選択ファイル取得
    function fileReader(files) {
        for (var i = 0; i < files.length; i++){
           file_loader(files[i]);						// 各ファイル処理
        }
    }

    function file_loader(file){
	var file_src = file ;							// イメージ
	var file_nam = file.name ;						// ファイル名
        var file_typ = file.type ;						// ファイル種類
        var file_siz = file.size ;						// ファイルサイズ

        var reader = new FileReader();

     	if (file_typ.match('image.*') ) {
            reader.onload = function(event) {

		var fileLi = document.createElement("li");
		fileLi.innerHTML = file_nam ;

		var prpInf_object = document.getElementById("rlt_info") ;
		//parent_object.appendChild(fileLi);				// 後 追加
		prpInf_object.insertBefore(fileLi, prpInf_object.children[0]) ; // 前 追加

                var tagImg = document.createElement('img') ;
                tagImg.src = event.target.result ;
		tagImg.style.width="200px" ; tagImg.style.height="200px" ;

		var parent_object = document.getElementById("img_data") ;
    		//parent_object.appendChild(tagImg);				// 後 追加
		parent_object.insertBefore(tagImg, parent_object.children[0]) ;	// 前 追加
            }

	    reader.readAsDataURL(file_src);

        }else {
	    alert("対象外ファイルが選択されました。") ;
        }
    }

</script>

</head>

<body>


<input type="file" multiple onChange="fileReader(this.files)" accept="image/*">

<P>ここにファイル名が表示されます。</P>
<ul id="rlt_info"></ul>

<P>ここにファイル内容が表示されます。</P>
<div id="img_data"></div>

</body> 

</html>

 

画像ファイル  アップロード処理

次に選択した複数の画像ファイルをサーバーに連続アップロードする処理について検討しました。

(1)操作概要
①複数画像ファイル連続登録処理
下の図はファイル選択ページですが、ファイル選択すると選択画像をページ内に追加表示するのと同時にサーバーにアップロードします。上段の6つの写真は<img>要素ですが、下段の写真はCANVAS要素に描画した図になっており、選択した画像をCANVASに縮小し描画した後、描画内容をサーバーに連続的にアップロードします。


②複数登録画像ファイル結果表示(並び替え可)

①の登録処理と次の結果処理のページにはそれぞれボタンを配置し、相互にページを呼び出せる様にしています。今回の例では①も登録処理の結果は、次の図の中の写真のNo.18~22に登録されています。既に前の投稿で紹介している通り、各写真は並び替え出来る様になっています。また、ファイル全削除ボタンをつけて、登録したファイルを全て消去できる様にしています。選択画像のみ削除する様な機能もいずれ検討したいと思いますが、とりあえず今後の課題としておきます。
元写真サイズは、1~6(Mbyte/枚)程度で、データ登録時はサイズ的に1/100程度になっていますが、CANVASへの描画時等の負荷も大きいと思われます。最初処理が中断してしまうことがありましたので、遅延処理等を入れることで、とりあえず安定しました。処理条件が変わったら再調整は必要かもしれません。また、IE11は更にサイズの大きな画像では安定的に処理出来ませんでした。Google Chromeは画像ファイルを扱う上で安定していると感じています。

(2)テストプログラム
スクリプトを参考までに載せておきます。

①複数画像ファイル連続登録処理

<!doctype html>
<html lang="ja">

<head>
    <meta charset="utf-8">

    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>FILE SELECT</title>

    <style>
	canvas { float : left ; }
    </style>

    <script type="text/javascript">
    var page_cnt = 0 ; var pg_cn_all ;

    // 選択ファイル取得
    function fileReader(files) {
	page_cnt = 0 ; pg_cn_all = files.length ;
        for (var i = 0; files.length > i ; i++){
           file_loader(files[i]);							// 各ファイル処理
        }
    }

    // 選択ファイル描画・アップロード処理
    function file_loader(file){
	var file_src = file ;								// イメージ
	var file_nam = file.name ;							// ファイル名
        var file_typ = file.type ;							// ファイル種類
        var file_siz = file.size ;							// ファイルサイズ

	var cvs_l = document.getElementById("CVS_LG") ; var ctx_l = cvs_l.getContext("2d") ;
	var cvs_s = document.getElementById("CVS_SM") ; var ctx_s = cvs_s.getContext("2d") ;

        var reader = new FileReader();

     	if (file_typ.match('image.*') ) {
            reader.onload = function(event) {

		var g_img = new Image() ;
		g_img.onload = function() {
		    var result = save_canvas(file_nam);
		    var mk_result="【NG】  " ; if(result=="OK"){ mk_result="【OK】  " ; }

		    // 挿入写真サイズ取得
		    var img_o_w = g_img.width ; var img_o_h = g_img.height ;

		    var cvs_l_w = 250 ; var cvs_l_h = Math.floor( img_o_h * cvs_l_w / img_o_w ) ;
		    var cvs_s_w = 100 ; var cvs_s_h = Math.floor( img_o_h * cvs_s_w / img_o_w ) ;

		    cvs_l.width = cvs_l_w ; cvs_l.height = cvs_l_h ;
		    cvs_s.width = cvs_s_w ; cvs_s.height = cvs_s_h ;

		    ctx_l.drawImage( g_img , 0 , 0 , cvs_l_w , cvs_l_h ) ;
		    //ctx_l.strokeRect( 0 , 0 , cvs_l_w , cvs_l_h ) ;
		    ctx_s.drawImage( g_img , 0 , 0 , cvs_s_w , cvs_s_h ) ;
		    //ctx_s.strokeRect( 0 , 0 , cvs_s_w , cvs_s_h ) ;

		    // ファイル名追加
		    var fileLi = document.createElement("li");
		    fileLi.innerHTML = mk_result + file_nam ;

		    var prpInf_object = document.getElementById("rlt_info") ;
		    //parent_object.appendChild(fileLi);				// 後 追加
		    prpInf_object.insertBefore(fileLi, prpInf_object.children[0]) ; 	// 前 追加

		    // 画像ファイル追加
                    var tagImg = document.createElement('img') ;
                    tagImg = g_img ;
		    tagImg.style.width="200px" ; tagImg.style.height="200px" ;

		    var parent_object = document.getElementById("img_data") ;
    		    //parent_object.appendChild(tagImg);				// 後 追加
		    parent_object.insertBefore(tagImg, parent_object.children[0]) ;	// 前 追加

		    page_cnt = page_cnt + 1 ;
		    var div_page_no = document.getElementById("page_no") ;
		    div_page_no.innerHTML = String(page_cnt) + " ファイル完了(全" + String(pg_cn_all) + "ファイル)" ;

		}
		g_img.src = event.target.result ;
            }

	    reader.readAsDataURL(file_src);

        }else {
	    alert("対象外ファイルが選択されました。") ;
        }
    }

    function save_canvas(fl_nm){
	var pos_comma = fl_nm.lastIndexOf(".");
	var file_name = fl_nm.slice( 0 , pos_comma )

	    var imageType = "image/jpeg" ;
	    var tgt_cvs = ["CVS_LG", "CVS_SM"];
	    var fil_nam = [ file_name + "" , file_name + "_SM" ];
	    var dat_ary = ["",""] ;

	    // 大きい方のみ送付(下記にてループ数を1回にしている)
	    for ( var i = 0; 1 > i ; i++ ) {
 		var target_cvs = document.getElementById(tgt_cvs[i]) ;
		var base64 = target_cvs.toDataURL(imageType) ;
		var blob = Base64toBlob(base64) ;

		var formData = new FormData() ;
		formData.append( "hoge" , blob , fil_nam[i] + ".jpg" ) ;

		var rtn = "" ;
		var xmlhttp = createXMLHttpRequest() ;
		if( xmlhttp != null ){
		    xmlhttp.open( "POST" , "rcv_blob.php" , false );
		    //xmlhttp.setRequestHeader('Content-Type','multipart/form-data');
		    xmlhttp.send( formData ) ;

		    rtn = xmlhttp.responseText ;

		}else{
		    rtn = "ERROR (HTTP REQ)" ;
		}

		dat_ary[i] = fil_nam[i] + " : " + rtn ;
		
	    }
	    
	    return rtn ;
	    //alert("◆アップロード結果◆\n\n"+dat_ary[0]+"\n"+dat_ary[1]+"\n"+fl_nm);
    }

    function Base64toBlob(base64){
	
	    var tmp = base64.split(',') ;
	    var data = atob(tmp[1]) ;
	    var mime = tmp[0].split(':')[1].split(';')[0] ;
	    var buf = new Uint8Array(data.length);

	    for (var i=0 ; data.length > i ; i++ ){
		buf[i] = data.charCodeAt(i) ;
	    }

	    var blob = new Blob( [buf] , {type:mime} ) ;
	    return blob ;
    }

    function createXMLHttpRequest(){
	    if(window.XMLHttpRequest){return new XMLHttpRequest()}
	    if(window.ActiveXObject){
		try{return new ActiveXObject("Msxml2.XMLHTTP.6.0")}catch(e){}
		try{return new ActiveXObject("Msxml2.XMLHTTP.3.0")}catch(e){}
		try{return new ActiveXObject("Microsoft.XMLHTTP")}catch(e){}
            }
	    return false;
    }


</script>

</head>

<body>


<input type="file" multiple onChange="fileReader(this.files)" accept="image/*" style="margin:0 10px;">
<input type="button" value="構成編集ページに移動" onclick="location.href='./tmbl_musdrg_001'" style="margin:0 10px;">

<div id="page_no">―</div>

<P style = "clear:both;">ここにファイル名が表示されます。</P>
<ul id="rlt_info"></ul>

<P>ここにファイル内容が表示されます。</P>
<div id="img_data"></div>

<canvas id="CVS_LG"></canvas>
<canvas id="CVS_SM"></canvas>

</body> 

</html>


②複数登録画像ファイル結果表示(並び替え可)

<!doctype html>
<html lang="ja">

<head>
    <meta charset="utf-8">

    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>DRAG & DROP</title>

    <style>
	* { margin:0; }
	.draw_info th, .draw_info td { border:solid 1px #000000; text-align:center; vertical-align:middle; }
	.draw_info { border: solid 1px #000000; border-collapse:collapse; margin:5px 10px;}
	.div_dragbl { cursor:move; position:absolute;
		display: inline-block;
    		background-color: #fffacd;
    		background-position: center;
    		background-repeat: no-repeat;
    		margin: 5px;
    		width: 200px;
    		height: 200px;
    		border: 1px solid #000000;
    		background-size: cover;
	}
    </style>

    <script type="text/javascript">

	var mv_fg=""; 
	var mvs_x=0; var mvs_y=0;
	var tgt_id_no; var z_idx_ini=100;
	var X_STT_P=0; var Y_STT_P=100;
	var img_wdt=200; var img_hgt=150;
	var max_fnm=0; var row_num=0; var fct_num=0;
	var index_bef ; var index_cur ;
	var elm_order = [] ;
	var elm_chodr = [] ;

	// ◆初期設定◆
	function init_chk(){
	    get_window_info() ;
	    check_elements_order("0") ; 
	}

	// ◆マウスダウン◆
	window.onmousedown = function(e) {
	    mv_fg = "true" ;
	    tgt_id_no = event.target.id ;

	    event.preventDefault();  							// ◆重要◆ img要素移動時必須
	    event.stopPropagation();

	    if(tgt_id_no) {
		z_idx_ini = z_idx_ini + 1 ;
		document.getElementById(tgt_id_no).style.zIndex = z_idx_ini ;
		mvs_x = event.clientX - parseInt(document.getElementById(tgt_id_no).style.left.replace("px","")) ;
		mvs_y = event.clientY - parseInt(document.getElementById(tgt_id_no).style.top.replace("px","")) ;
	
		var cur_x = event.clientX - mvs_x ; var cur_y = event.clientY - mvs_y ;	// 対象要素基準座標(左上)
		index_bef = get_index_num_by_pos( cur_x , cur_y ) ;
	    }
	}

	// ◆マウスムーブ◆
	window.onmousemove = function(e) {
	    if(mv_fg == "true" && tgt_id_no) {
		var cur_x = event.clientX - mvs_x ; var cur_y = event.clientY - mvs_y ;	// 対象要素基準座標(左上)
		index_cur = get_index_num_by_pos( cur_x , cur_y ) ;

		document.getElementById(tgt_id_no).style.left = cur_x + "px" ;
		document.getElementById(tgt_id_no).style.top = cur_y + "px"  ;

		if(index_cur >= 0){ check_elements_order("1") ; }
	    }
	}

	// ◆マウスアップ◆
	window.onmouseup = function(e) {
	    if(mv_fg == "true" && tgt_id_no) {
	    	mv_fg = "false" ; tgt_id_no = "" ; 

	    	// 順番確定
	    	if(index_cur >= 0){
	    	    elm_order = [] ;
	    	    for(var i=0 ; elm_chodr.length > i ; i++) { elm_order[i] = elm_chodr[i] ; }
	    	}

	    	for(var i=0 ; elm_order.length > i ; i++) {
		    var cur_lin = Math.floor( i / max_fnm ) ;
		    var cur_row = i - cur_lin * max_fnm ;
		    document.getElementById(elm_order[i]).style.left = X_STT_P + cur_row * img_wdt + "px" ;
		    document.getElementById(elm_order[i]).style.top = Y_STT_P + cur_lin * img_hgt + "px" ;
	    	}
	    }
	}

	// ◆ウィンドウリサイズ◆
	window.onresize = function(e) { get_window_info() ; }


	// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■

	// 対象要素座標からインデックス番号を取得
	function get_index_num_by_pos(cur_x,cur_y){
	    var c_c_x = cur_x + img_wdt /2 ; var c_c_y = cur_y + img_hgt / 2 ;		// 対象要素中心位置
	    var tmp_x = c_c_x - X_STT_P ; var tmp_y = c_c_y - Y_STT_P ;			// 配置基準位置からの座標

	    var x_num=-1 ; var y_num=-1 ; var idx_num=-1;
	    if(tmp_x>0 && max_fnm*img_wdt>tmp_x && tmp_y>0 && (row_num+1)*img_hgt>tmp_y){
		x_num = Math.ceil(tmp_x/img_wdt) ; y_num = Math.ceil(tmp_y/img_hgt) ;
		idx_num = max_fnm * (y_num-1) + x_num -1 ;
	    }	//if(idx_num>fct_num){ idx_num = fct_num ; }   // (IDX番号制限時使用)

	    return idx_num ;								// 座標から計算したインデックス取得
	}


	// 要素配置初期化
	function get_window_info(){
	    var sW = window.innerWidth ; var sH = window.innerHeight ;			// ウィンドウサイズ
	    var elm_div = document.getElementById("draggable_div") ;			// 移動領域要素取得
	    var rect = elm_div.getBoundingClientRect() ;				// 移動領域取得
	    X_STT_P = Math.ceil(rect.left/10) + 20 ;					// 移動領域座標(X)
	    Y_STT_P = Math.ceil(rect.top/10) + 10 ;					// 移動領域座標(Y)

	    var drg_elm = document.getElementsByClassName("div_dragbl") ;		// 移動対象要素取得
	    var fct_gap = 40 ;								// Windowとのギャップ(両端合計)

	    fct_num = drg_elm.length ;							// 移動対象要素数取得
	    max_fnm = Math.floor((sW-fct_gap)/img_wdt) ;				// 1行最大表示要素数
	    row_num = Math.ceil(fct_num / max_fnm) ;					// 行数表示

	    for(var i=0 ; fct_num > i ; i++){
		var cur_lin = Math.floor( i / max_fnm ) ;
		var cur_row = i - cur_lin * max_fnm ;
		drg_elm[i].style.top = Y_STT_P + cur_lin * img_hgt + "px" ;
		drg_elm[i].style.left = X_STT_P + cur_row * img_wdt + "px" ;

		drg_elm[i].style.width = img_wdt + "px" ;
		drg_elm[i].style.height = img_hgt + "px" ;
		drg_elm[i].style.zIndex = z_idx_ini ;
	    }

	}

	// 要素配置確認
	function check_elements_order(cmd_no) {

	    if(cmd_no=="0"){
		// 初回呼び出し時
		var drg_elm = document.getElementsByClassName("div_dragbl") ;		// 移動対象要素取得
		elm_order = [] ;
		for(var i=0 ; drg_elm.length > i ; i++){
		    elm_order.push(drg_elm[i].id);
	        }
	    }else{
		// 要素移動途中(配列順序の更新) ※ index_bef , index_cur に基づき更新
		elm_chodr = [] ;
		for(var i=0 ; elm_order.length > i ; i++){
		    if(i==index_cur){ elm_chodr.push(tgt_id_no); }
		    if(tgt_id_no!=elm_order[i]){ elm_chodr.push(elm_order[i]); }
		}
		if(index_cur>fct_num-1){ elm_chodr.push(tgt_id_no); }
	    }
	}

	// サーバー側の画像ファイル全抹消
	function delete_all(){
		var xmlhttp=createXmlHttpRequest();
            	if(xmlhttp!=null){
        		xmlhttp.open("POST", "tmbl_musdrg_del.php", false);
			//xmlhttp.setRequestHeader('Content-Type','multipart/form-data');
        		xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
        		xmlhttp.send('data=RQ_DEL_FILE_ALL');

			var rtn=xmlhttp.responseText;
			alert("サーバー側画像ファイル抹消処理結果 : " + rtn ) ;

	    	}else{
			alert("ERROR!(HTTP通信API設定失敗)") ;
    	    	}

		location.reload() ;
	}


	// HTTP通信API設定
	function createXmlHttpRequest(){
    	    var xmlhttp=null;
    	    if(window.ActiveXObject){
        	try { xmlhttp=new ActiveXObject("Msxml2.XMLHTTP"); }
        	catch(e) { try { xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); }
            		   catch (e2){ }
                }
    	    }else if(window.XMLHttpRequest){
        	xmlhttp = new XMLHttpRequest();
    	    }
    	    return xmlhttp;
	}

    </script>
</head>

<body onLoad="init_chk()">

	<H2 style="margin:15px;">マウスドラッグで並び替えして下さい</H2>
	<input type="button" value="ページ再読込" onclick="location.reload()" style="margin:0 10px 0 25px ;">
	<input type="button" value="画像ファイル登録ページへ移動" onclick="location.href='./tmbl_musdrg_rg_001'" style="margin:0 10px;">
	<input type="button" value="画像ファイル全削除" onclick="delete_all()" style="margin:0 10px 0 25px ;">

	<BR>
	<div id="draggable_div" style="position:relative ; clear:both;">
	<?php
	    //$serch_file = glob("../image_sample/*.jpg") ;
	    $id_chked = 0 ;
	    $serch_file = glob("../image_upload/img_dtn/*.jpg") ;
	    for($ii=0; $ii<count($serch_file) ;$ii++){
		$id_no = substr("000000".strval($id_chked+$ii+1),-6) ;
		$tmp_fl_nm = basename($serch_file[$ii]);
		echo '<span class="div_dragbl" id="'.$id_no.'" title="'.$tmp_fl_nm.'" style="background-image: url(\''.$serch_file[$ii].'\')">'.$id_no.'</span>';
	    }

	    $id_chked = count($serch_file) ;
	    $serch_file = glob("../image_upload/img_new/*.jpg") ;
	    for($ii=0; $ii<count($serch_file) ;$ii++){
		$id_no = substr("000000".strval($id_chked+$ii+1),-6) ;
		$tmp_fl_nm = basename($serch_file[$ii]);

		$dat_inf = date("ymdHis")."_".substr("0000000000".strval(Math.floor(10000000*microtime())),-7) ;
		$tmp_fl_pt = '../image_upload/img_dtn/'.$dat_inf.'.jpg' ;

		rename( $serch_file[$ii] , $tmp_fl_pt ) ;

		echo '<span class="div_dragbl" id="'.$id_no.'" title="'.$dat_inf.'" style="background-image: url(\''.$tmp_fl_pt.'\')">'.$id_no.'</span>';
	    }

	?>
	</div>

</body> 

</html>

③ファイル登録処理

<?php

$result = move_uploaded_file($_FILES['hoge']['tmp_name'] , "../image_upload/img_new/".$_FILES['hoge']['name']  ) ;

if($result===true){
    echo "OK" ;
}else{
    echo "NG" ;
}
?>

④ファイル全削除処理

<?php

$data=htmlspecialchars($_POST['data']);
if($data != "RQ_DEL_FILE_ALL"){
	echo "ERR_CMD";
	exit();
}

$fil_cnt=0;
$serch_file = glob("../image_upload/img_dtn/*") ;
for($ii=0; $ii<count($serch_file) ;$ii++){ unlink($serch_file[$ii]); }

$serch_file = glob("../image_upload/img_new/*") ;
for($ii=0; $ii<count($serch_file) ;$ii++){ unlink($serch_file[$ii]); }

$fil_cnt=count(glob("../image_upload/img_dtn/*"))+count(glob("../image_upload/img_new/*"));

echo substr('000'.strval(intval($fil_cnt)),-3) ;
?>

 

まとめ

ブラウザーによる処理能力や挙動の違いを感じました。調整が及ばない場合は、ファイル容量の制限などの対応も必要かもしれません。もう少し注意しながら状況を確認してみようと思います。
また、画像サイズなども様々なので注意が必要です。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です