並び替えリストに画像を追加する

概  要

過去の投稿でjQuery UI “Sortable” を使った画像要素の並べ替えとCANVAS要素を使った画像の縮小・アップロード保存の検討を行いました。今回はこれら2つの内容にHTML要素の追加機能を加えることで、縮小アップロードした画像を並び替え対象に追加する検討を行ってみようと思います。
具体的には  jQuery UI で要素を並び替える②(→デモリンク)で作成した並び替え可能な写真リストにローカル画像のサイズを縮小し、アップロードする機能を追加したいと思います。

画像追加対応  サンプルプログラム

(1)プログラム概要
下図の通り、画像追加サンプルプログラムを作ってみました。
(→サンプルプログラムへのリンク
プログラム内容は下の図の通り、大きく上下2つのブロックに分かれています。上段では「ファイル選択」ボタンでファイル選択ダイアログを開き、ファイル選択しローカル画像を  CANVAS上に取り込みます。サンプル例では、県,写真タイトル,撮影者を入力後、「追加」ボタンをクリックすると画像と入力データをサーバー側と下段テーブルリストに同時に追加します。サーバーへのアップロードは、県,タイトル等の関連情報を同時にアップロードするので、少し変更していますが、基本的考え方は先回投稿と同じです。一方、テーブルリストへの追加は、JavaScriptプログラムから要素を追加するという私としては、初めての方法です。今回のブラウザーでの要素追加は意外と簡単で、プログラムからプログラムを追加しているみたいで面白いと感じました。
下段では新たに追加した画像も含めて、リストの並び替えが出来るようになっています。
一応、私の持っている格安SIMスマホでも動くことは確認できたのですが、画面にフィットしておらず、操作性の悪さは否めません。ただ、ファイル選択をクリックするとカメラを選択出来き、撮影した写真を直接CANVAS上に取り込むことが出来ました。

(2)登録ファイル構成
今回は画像と画像の関連情報を同時に登録しますので、サーバ側には画像ファイル(jpeg)と今回の例では県,写真タイトル,撮影者などの関連情報をテキストファイルに書き込んで保管しています。ページ読込を行った際にテキストファイルの情報と画像を取り込んでページを構成します。
下の図はサーバー側のファイル保管状態例ですが、3つの画像ファイルとこれら画像ファイルの関連情報を書き込んだテキストファイルを保管しています。テキストファイル内には、画像ファイル名に対する関連情報をカンマ(’ , ‘)で区切って書き込みます。

【サーバー側の画像ファイルとテキストファイル保管】
【テキストファイルの内容】
(3)プログラム解説
①  ファイル読込と初期画面表示(対象:Prg①)
ページ読み込み時に並び替え可能なテーブル部分を構成するHTMLを行番71~89のPHPプログラムで出力します。行番72ではテーブルリスト内に表示する画像高さを統一する為、設定しています。行番73でテキストファイルの関連情報を読み込んで、行番74で改行コードにより、1行単位で配列に分割格納します。行番76~88のループ内で各画像ファイルの関連情報に相当する配列データを1つづつ処理し、HTMLデータを出力します。行番77では1行分のデータを更にカンマ(’ , ‘)で分割し、ファイル名,県,写真タイトル,撮影者の順で配列に格納しています。行番78で画像ファイルのファイルパス、行番80で画像サイズを取得します。行番81では、元画像サイズと行番72で設定した画像高さより、縦横同じ縮尺になるように横方向の画像サイズを算出します。
行番83~87は取得した関連情報と画像表示サイズによりHTMLを直接出力する箇所になります。

②画像データアップロード(対象:Prg①,Prg②)
CANVAS上の画像データのアップロード保管については、先回の投稿で行っています。今回、少し異なるのは県,写真タイトル,撮影者などの関連情報を同時にアップロードしテキストファイルに保管することです。
ブラウザー側プログラムのPrg① 行番160でCANVASデータから変換したBlobをformdataに追加する時、同時に設定するファイル名に関連情報を含ませてしまうことにしました。行番145~149ではテキストフォームの中に入力されたデータをそれぞれ取得し配列変数に格納します。行番151では、配列変数の各データをカンマ(’ , ‘)区切で連結し、formDataにBlobデータ追加時のファイル名として、サーバーに送信します。
一方、サーバー側プログラムのPrg②では、行番3で関連情報を含むファイル名を取得し、行番5でテキストファイルにそのまま書き込みします。行番7でカンマ(’ , ‘)でデータを分割し、最初のデータをファイル名として、一時保管ファイルをコピー保管しています。行番11~15では保管処理の結果を返します。

③テーブルリストへの要素追加(対象:Prg①)
画像データアップロード時にブラウザー上のテーブルリストに内容を反映させる処理です。②で画像データアップロード後、ページ全体をリロードし、改めて全て再出力する方法も考えましたが、DOMという仕組を使って、JavaScriptでノード(要素、属性、テキスト)を追加・削除・置換したり出来る様なので試してみました。尚、既に何度か使っているID値からノードを首取得する ‘getElementById’ もDOMの仕組の一つです。
テーブルリストへの要素追加は、Blobをサーバーに送信後に呼び出す関数(行番170)で実行します。この関数の中身は行番229~260に記載しています。行番230~236では受け取った関連情報を変数に格納し、行番238~241では受け取ったCANVASサイズから、縦横同じ縮尺比率となる様に表示サイズを算出します。
関連情報、表示サイズ等から、行番243~246ではテーブル内の<TD>要素のHTMLを作成します。
行番255で<TR>要素を生成し、行番256でその中に作成した<TD>要素を追加します。行番258で並び替え対象範囲であるID属性が “sortable”である<tbody >要素を取得し、行番259で<tbody >要素の中に<TD>要素を含む<TR>要素ごと追加しています。
参考までに行番135~139では、テーブルリスト内の<TR>要素数を取得し、登録する最大要素数を制限しています。

④プログラムコード
下記がプログラムコードです。

【Prg①:ブラウザー側ページ構成プログラム】

<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>要素追加テスト</title>

  <style>
    body {line-height: 100%; }
    p { line-height : 0 px ; margin : 3px ; }
    table { border : solid 1px #000000 ; border-collapse: collapse; }
    th,td { border : solid 1px #000000 ; height : 250px ; text-align:center; }
    th { background-color: #99cc00 ; height : 25px ;}

    .ui-state-highlight { background-color: #ffff00; height:250px; }
    .dsp_no{ width:45px; }
    .prfctr{ width:75px; }
    .itm_nm{ width:430px; }
  </style>


  <script type="text/javascript" src="./js/jquery-3.3.1.min.js"></script>
  <script type="text/javascript" src="./js/jquery-ui-1.12.1/jquery-ui.min.js"></script>
  <script type="text/javascript" src="./js/jquery.ui.touch-punch.min.js"></script>
  <script>
    $( function() {
      	$( "#sortable" ).sortable({
        	placeholder: 'ui-state-highlight'
      	});

      	$( "#sortable" ).disableSelection();

      	$('#sortable').on('sortstop', function (e, ui) {
    		// ソートが完了したら実行される。
    		var rows = $('#sortable .dsp_no');
    		for (var i = 0, rowTotal = rows.length; rowTotal > i ; i += 1) {
        		$($('.dsp_no')[i]).text(i + 1);
    		}
      	});
    } );
  </script>
</head>


<body>

<h2>◆画像を追加します。◆</h2>

<TABLE style="border-style:none;"><TR>
<TD style="height:10px;border-style:none;"><input type="file" id="file"></TD>
<TD style="height:10px;border-style:none;width:3px;"></TD>
<TD style="height:10px;border-style:none;"><input type="text" id="add_prefec" style="width:65px; text-align:center;"></TD>
<TD style="height:10px;border-style:none;"><input type="text" id="add_ttlnam" style="width:200px; text-align:center;"></TD>
<TD style="height:10px;border-style:none;"><input type="text" id="add_psninf" style="width:200px; text-align:center;"></TD>
<TD style="height:10px;border-style:none;width:3px;"></TD>
<TD style="height:10px;border-style:none;"><input type="button" id="save_canvas" value="追加" onclick="save_canvas();"></TD>

<TD style="height:10px;border-style:none;width:3px;"></TD>
<!--<TD style="height:10px;border-style:none;"><input type="button" id="add_tr_element" value="要素追加テスト" onclick="add_tr_element();"></TD>-->
</TR></TABLE>
<div><canvas id="cvs01"></canvas></div>

<h2>◆自由に順番を入れ替えて下さい。◆</h2>
<table>
    <thead>
        <tr><th>番号</th><th>県</th><th>イメージ</th></tr>
    </thead>

    <tbody id="sortable">

	<?php
	    $img_h = 220 ;
	    $dat_tmp = file_get_contents("./add_pict/img_rcd.txt");
	    $dat_ary = explode("\n",$dat_tmp);
	    $cnt = count($dat_ary)-1;
	    for( $i=0;$i<$cnt;$i++ ) {
		$dsp_ary = explode(",",$dat_ary[$i]) ;
		$img_pth = "./add_pict/".$dsp_ary[0] ;

		$img_inf_ary = getimagesize( $img_pth );
		$img_w = floor($img_inf_ary[0] * $img_h / $img_inf_ary[1]) ;
		
		echo "<tr>" ;
		echo "<td class=\"dsp_no\">".strval($i+1)."</td><td class=\"prfctr\">".$dsp_ary[1]."</td>" ;
		echo "<td class=\"itm_nm\"><img src=".$img_pth." alt=".$dsp_ary[2]." width=\"".strval($img_w)."\" height=\"".strval($img_h)."\" border=\"0\">";
		echo "<BR><P>".$dsp_ary[2]." &copy ".$dsp_ary[3]."</P></td>" ;
		echo "</tr>" ;
	    }
	?>

    </tbody>
</table>

<p>写真引用元 : <a href="https://search.find47.jp" target="_blank">FIND/47</a></p>
<p>写真はすべて<a href="https://creativecommons.org/licenses/by/4.0/" target="_blank">クリエイティブ・コモンズ・ライセンス(表示4.0 国際)</a>のもと<BR>掲載を許諾されています。</p>


  <script>
	document.getElementById("file").addEventListener("change", function (e) {
	    var file = e.target.files; 
            var reader = new FileReader(); 

            //ファイルが複数読み込まれた際に、1つめを選択 
            reader.readAsDataURL(file[0]); 

            //ファイルが読み込めたら 
            reader.onload = function () { 
                var src = reader.result; 
                drawCanvas(src); 
            }; 
        }, false); 


     	function drawCanvas(source) {
	    var image = new Image(); 
            image.src = source; 

            image.onload = function () {
		var img_h = image.height ;
		var img_w = image.width ;

		var cvs_01 = document.getElementById('cvs01'); 
		if (cvs_01.getContext) { 
			var ctx_01 = cvs_01.getContext('2d'); 
		    	var cmp_h_01 = 400 ;
		    	var cmp_w_01 = Math.floor(img_w * cmp_h_01 / img_h) ;
                    	cvs_01.height = cmp_h_01;
		    	cvs_01.width = cmp_w_01;
                    	ctx_01.drawImage(image, 0 , 0 , cmp_w_01 , cmp_h_01 );
            	}
	    }
    	}

	function save_canvas(){
		var tr_cnt = document.getElementsByTagName('tr').length ;
		if(tr_cnt-2 >= 20){
			alert ("最大登録数超過! 処理を中断します。(既登録数:"+String(tr_cnt-2)+")");
			exit ;
		}

		var imageType = "image/jpeg" ;
		var tgt_cvs = "cvs01";
		var fil_nam = create_file_name() ;

		var add_inf = [] ;
		add_inf[0] = fil_nam + ".jpg" ;
		add_inf[1] = document.getElementById("add_prefec").value ;
		add_inf[2] = document.getElementById("add_ttlnam").value ;
		add_inf[3] = document.getElementById("add_psninf").value ;

		var snd_inf = add_inf[0] + "," + add_inf[1] + "," + add_inf[2] + "," + add_inf[3] ;

 		var target_cvs = document.getElementById(tgt_cvs) ;
		var cvs_h = target_cvs.height ;
		var cvs_w = target_cvs.width ;
		var base64 = target_cvs.toDataURL(imageType) ;
		var blob = Base64toBlob(base64) ;

		var formData = new FormData() ;
		formData.append( "hoge" , blob , snd_inf ) ;

		var rtn = "" ;
		var xmlhttp = createXMLHttpRequest() ;
		if( xmlhttp != null ){
		    xmlhttp.open( "POST" , "rcv_blob_add_pict.php" , false );
		    xmlhttp.send( formData ) ;

		    rtn = xmlhttp.responseText ;

		    var rtn_add = add_tr_element(add_inf[0],add_inf[1],add_inf[2],add_inf[3],cvs_w,cvs_h,tr_cnt) ;

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

		alert("◆アップロード結果◆\n\n" + rtn );

	}

	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;
	}

	//ファイル名を生成する
	function create_file_name(){
		var today=new Date() ;
		var dt_inf = [] ;
		dt_inf[0] = String(today.getFullYear());
		dt_inf[1] = "00"+String(today.getMonth()+1);
		dt_inf[2] = "00"+String(today.getDate());
		dt_inf[3] = "00"+String(today.getHours());
		dt_inf[4] = "00"+String(today.getMinutes());
		dt_inf[5] = "00"+String(today.getSeconds());
		dt_inf[6] = "000000000000" + String(Math.floor(100000000*Math.random())) ;
		dt_inf[6] = dt_inf[6].substr(-10,10) ;

		var dt_rtn = "" ;
		for ( var i=0 ; 6 > i ; i++ ){
			if( i != 6 ) { dt_inf[i] = dt_inf[i].substr(-2,2) ; }
			dt_rtn = dt_rtn + dt_inf[i] ;
		}

		dt_rtn = dt_rtn + "_" + dt_inf[6] ;

		return dt_rtn ;
	}

	function add_tr_element(par01,par02,par03,par04,cvs_w,cvs_h,tr_cnt){
		var tr_num = tr_cnt - 1 ;

		var dsp_num = String(tr_num) ;
		var dsp_prf = par02;		//'北海道' ;
		var dsp_ttl = par03;		//'静寂に包まれる支笏湖' ;
		var dsp_psn = par04;		//'Yo_Takehana' ;
		var dsp_fil = par01;		//'190120232612_0061096602.jpg' ;

		var dsp_h = 220 ;
		var dsp_w = Math.floor(cvs_w * dsp_h / cvs_h) ;
		var str_h = String(dsp_h) ;
		var str_w = String(dsp_w) ;

		var ins_html = '<td class="dsp_no">'+dsp_num+'</td>';
		ins_html = ins_html + '<td class="prfctr">'+dsp_prf+'</td>' ;
		ins_html = ins_html + '<td class="itm_nm"><img src="./add_pict/'+dsp_fil+'" alt='+dsp_ttl+' width="'+str_w+'" height="'+str_h+'" border="0">' ;
		ins_html = ins_html + '<BR><P>'+dsp_ttl+' &copy '+dsp_psn+'</P></td>' ;


		//echo "<td class=\"dsp_no\">".strval($i+1)."</td><td class=\"prfctr\">".$dsp_ary[1]."</td>" ;
		//echo "<td class=\"itm_nm\"><img src=".$img_pth." alt=".$dsp_ary[2]." width=\"".strval($img_w)."\" height=\"".strval($img_h)."\" border=\"0\">";
		//echo "<BR><P>".$dsp_ary[2]."&copy ".$dsp_ary[3]."</P></td>" ;



		var tr_element = document.createElement("tr");
		tr_element.innerHTML = ins_html ;

		var parent_object = document.getElementById("sortable");
    		parent_object.appendChild(tr_element);
	}

  </script>

</body>
</html>

【Prg②:サーバー側ファイル保管プログラム】

<?php

$tmp_str = $_FILES['hoge']['name'];

file_put_contents("./add_pict/img_rcd.txt", $tmp_str."\n" , FILE_APPEND | LOCK_EX);

$tmp_ary = explode(',', $tmp_str);

$result = move_uploaded_file($_FILES['hoge']['tmp_name'] , "./add_pict/".$tmp_ary[0] ) ;

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

 

まとめ

一応、イメージしていた様な感じにはなりました。スマホでの表示や操作など、いろいろ問題もありますが・・・。
次回は並び替えたリストの順番を更新させて、次に開いた時や他の人が開いた時に、並び替えた順番が反映される様に検討してみたいと思います。(現状はブラウザ上で並び替えた画像イメージはリセットされ、登録した順番に表示されます。)

 

コメントを残す

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