FILE APIとCANVASを使ってローカル画像を縮小する

 

概要

過去2回の投稿でjQuery UI “Sortable” を使って、幾つかのHTML要素の並べ替えを行いました。大きな画像ファイルでも読み込んで並び替えすることは出来ましたが、読み込み時に少し時間が掛かっていました。また、PHPを使って並び替え要素HTMLを自動生成しましたが、元となる画像ファイル等は予めサーバー側に保管したものでした。
そこで、今回はローカル画像ファイルのサイズを縮小し、サーバーに転送する処理について検討してみたいと思います。

ローカル画像ファイルの縮小

パソコンに保管されているローカル画像ファイルをHTML5 CANVAS 上に取り込む際に画像サイズを縮小します。
下の図は元々大きな画像を縦方向画素数が、400,250,100 になる様に縦横とも同比率で縮小しCANVAS上に表示したものです。
デモプログラムでは、ブラウザー画面の一番上にある「ファイルを選択」ボタンをクリックすると、ファイル選択ダイアログが表示されますので、画像ファイル選択後ダイアログを閉じると、対象ファイルが縮小表示されます。(→デモプログラムへリンク

プログラムについて概要をメモしておきます。行番10で「ファイルを選択」ボタンを形成します。行番11~14はファイルを読み出した結果を出力する領域です。
行番17は、ファイル読み出し(行番10)を監視し、変化があれば関数(行番17~29)を実行します。行番18では選択したファイルリストを配列として取得します。行番19では実際に確定したファイルを読み込む為のFileReaderオブジェクトを宣言し、行番22で配列の最初のファイル(index=0)を読み込みます。
行番25~28は読込完了時の処理で、行番26で読込結果を取得し、行番27で描画を実行する別関数(drawCanvas)を呼び出します。
描画を行う別の関数 “drawCanvas”(行番32~74)では取得した画像情報を3つのサイズに縮小し、それぞれCANVAS上に描画しています。CANVASへの描画については過去の記事にも書いているので省略します。
<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>file_reader</title>
</head>

<body>
    <div><input type="file" id="file"></div>
    <div id="txt00"></div>
    <div id="txt01"></div><div><canvas id="cvs01"></canvas></div>
    <div id="txt02"></div><div><canvas id="cvs02"></canvas></div>
    <div id="txt03"></div><div><canvas id="cvs03"></canvas></div>

    <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 ;
		document.getElementById('txt00').innerHTML = "(元画像サイズ:W"+String(img_w)+" * H"+String(img_h)+")" ; 

		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 );
			document.getElementById('txt01').innerHTML = "(W"+String(cmp_w_01)+" * H"+String(cmp_h_01)+")" ; 
            	}

		var cvs_02 = document.getElementById('cvs02'); 
		if (cvs_02.getContext) { 
			var ctx_02 = cvs_02.getContext('2d'); 
		    	var cmp_h_02 = 250 ;
		    	var cmp_w_02 = Math.floor(img_w * cmp_h_02 / img_h) ;
                    	cvs_02.height = cmp_h_02;
		    	cvs_02.width = cmp_w_02;
                    	ctx_02.drawImage(image, 0 , 0 , cmp_w_02 , cmp_h_02 );
			document.getElementById('txt02').innerHTML = "(W"+String(cmp_w_02)+" * H"+String(cmp_h_02)+")" ; 
            	}

		var cvs_03 = document.getElementById('cvs03'); 
		if (cvs_02.getContext) { 
			var ctx_03 = cvs_03.getContext('2d'); 
		    	var cmp_h_03 = 100 ;
		    	var cmp_w_03 = Math.floor(img_w * cmp_h_03 / img_h) ;
                    	cvs_03.height = cmp_h_03;
		    	cvs_03.width = cmp_w_03;
                    	ctx_03.drawImage(image, 0 , 0 , cmp_w_03 , cmp_h_03 );
			document.getElementById('txt03').innerHTML = "(W"+String(cmp_w_03)+" * H"+String(cmp_h_03)+")" ; 
            	}
	    }
    	} 
     </script> 

</body> 

</html>

 

CANVAS上の画像データをアップロード

次にCANVAS上に取り込んだ画像データをサーバーにアップロードする方法を検討しました。(→デモプログラムへリンク
デモプログラムでは、ブラウザー画面の一番上にある「ファイルを選択」ボタンから、画像データをCANVAS上に取り込んだ後、「保存」ボタンをクリックすると大・中・小の各画像データをサーバー側にアップロード保管します。
下の図はアップロード結果をメッセージボックスに表示している状態です。大・中・小のアップロード画像ファイルの名前はそれぞれ固定し、「保存」ボタン下のリンクから別ウィンドウを開きアップロード画像を確認出来る様にしています。(→デモプログラムへリンク

アップロードした画像は下記の通り縮小出来ています。

分類 ファイル名 画素数 サイズ 比率
元画像 2816X2112 1.25MB 26.60
fil01.jpg 533X400 47KB 1 (基準)
 fil02.jpg 333X250 22KB 0.46
 fil03.jpg 133X100 6KB 0.13

 

【プログラム概要】
CANVAS上にローカル画像を取り込む処理は基本的に同じです。行番11はサーバーにデータをアップロード保管するトリガーとなる「保存」ボタン、行番13~15はアップロードファイルへのリンクを形成します。行番83~138はアップロード保管のJavaScriptプログラムです。行番89~111では、3回ループを回し、3つのキャンバスについて、画像データをそれぞれアップロードします。
アップロード手順は下記表の通りです。アップロードされたデータはサーバー側のPHPプログラムで処理し保存します。

(1)ブラウザー側プログラム(HTML+JavaScript)
◆CANVASデータアップロード手順◆

行番 アップロード処理手順 備考
91 対象CANVASのデータURIを取得
92 データURIに含まれるBASE64変換されたデータをBlobデータに変換 関数呼出
(115~128)
94~95 FormDataにBlobデータを追加
97~107  FormDataをサーバー送信

◆ブラウザー側プログラム◆

<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>file_reader</title>
</head>

<body>
    <div><input type="file" id="file"></div>
    <div><input type="button" id="save_canvas" value="保存" onclick="save_canvas();"></div>

    <a href="./fil01.jpg" target="_blank">① 画像ファイル(大)</a>
    <a href="./fil02.jpg" target="_blank">② 画像ファイル(中)</a>
    <a href="./fil03.jpg" target="_blank">③ 画像ファイル(小)</a>

    <div id="txt00"></div>
    <div id="txt01"></div><div><canvas id="cvs01"></canvas></div>
    <div id="txt02"></div><div><canvas id="cvs02"></canvas></div>
    <div id="txt03"></div><div><canvas id="cvs03"></canvas></div>

    <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 ;
		document.getElementById('txt00').innerHTML = "(元画像サイズ:W"+String(img_w)+" * H"+String(img_h)+")" ; 

		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 );
			document.getElementById('txt01').innerHTML = "① 画像ファイル(大)(W"+String(cmp_w_01)+" * H"+String(cmp_h_01)+")" ; 
            	}

		var cvs_02 = document.getElementById('cvs02'); 
		if (cvs_02.getContext) { 
			var ctx_02 = cvs_02.getContext('2d'); 
		    	var cmp_h_02 = 250 ;
		    	var cmp_w_02 = Math.floor(img_w * cmp_h_02 / img_h) ;
                    	cvs_02.height = cmp_h_02;
		    	cvs_02.width = cmp_w_02;
                    	ctx_02.drawImage(image, 0 , 0 , cmp_w_02 , cmp_h_02 );
			document.getElementById('txt02').innerHTML = "② 画像ファイル(中)(W"+String(cmp_w_02)+" * H"+String(cmp_h_02)+")" ; 
            	}

		var cvs_03 = document.getElementById('cvs03'); 
		if (cvs_02.getContext) { 
			var ctx_03 = cvs_03.getContext('2d'); 
		    	var cmp_h_03 = 100 ;
		    	var cmp_w_03 = Math.floor(img_w * cmp_h_03 / img_h) ;
                    	cvs_03.height = cmp_h_03;
		    	cvs_03.width = cmp_w_03;
                    	ctx_03.drawImage(image, 0 , 0 , cmp_w_03 , cmp_h_03 );
			document.getElementById('txt03').innerHTML = "③ 画像ファイル(小)(W"+String(cmp_w_03)+" * H"+String(cmp_h_03)+")" ; 
            	}
	
	    }
    	}

	function save_canvas(){
	    var imageType = "image/jpeg" ;
	    var tgt_cvs = ["cvs01", "cvs02", "cvs03"];
	    var fil_nam = ["fil01", "fil02", "fil03"];
	    var dat_ary = ["","",""] ;

	    for (  var i = 0; 3 > 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.send( formData ) ;

		    rtn = xmlhttp.responseText ;

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

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

	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> 

</body> 

</html>

(2)サーバー側プログラム(PHP)

<?php

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

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

アップロード関連メモ

CANVASデータアップロードに関連して確認した使い方、用語等を記録しておきます。

(1)ブラウザー側プログラム関連(HTML+JavaScript)

1 canvas.toDataURL([type], [0.0~1.0] )
対象CANVASのデータURIを返す。
第1引数:“image/png” (省略時含む),  “image/jpeg”
第2引数:品質レベル(0.0~1.0(省略時含む))
データURI
外部データを直接WEBページに埋め込む手法。例えば〈img〉タグ src 属性に画像ファイルパス等ではなく、下記の通りデータURIを直接指定可能とのことです。
<img src=”data:image/jpeg;base64,/CLrc/ (略)CH5Bu/“>
※上記srcに設定している下線部が、データURI
Mime Type:image/jpegエンコード:base64,BASE64変換データ
3 BASE64
データを英/数/記号( a~z , A~Z , 0~9 , + , / )の64文字とデータ長を揃える( = )で表す。変換手順は次の通りです。①対象データをバイナリ変換 ②6ビット分割  ※余る場合は”0”を追加し6ビットにする ③変換ルールに基づき文字変換
4 Brob  :  JavaScript等からバイナリデータを格納し、扱うためのデータ型。CANVASデータを直接Brobデータ型に変換する為に  canvas.toBrob() メソッドもある様ですが、ブラウザサポート状況等から、BASE64変換後Brob変換しています。
5 window.atob(str)  :  BASE64形式でエンコードされたデータをでコードします。
6 str.charCodeAt(index)  :  与えられたインデックスに位置する文字のUTF‐16コード(0~65535)の整数を返します。
7 Uint8Array
バイナリへのアクセスを符号なし整数で行いたい時に使用する型付き配列状オブジェクト。
8 BLOB (Binary Large Object)
大きな画像,音声等を扱う時に用いることが出来るデータ型の一つ。バイナリデータをJavaScriptで扱えるようにする。
9 FormData
Formの内容を管理する機能を持つインタフェース。XMLHttpRequest.send() メソッドを用いて簡単に送信可能なデータセット構築手段を提供。下記の様にBlobデータ,ファイル名をペアにしてデータ追加することが可能。
var formData = new FormData();
formData.append(‘hoge’ , blob (or File), ‘test.jpg’)

 

(2)サーバー側プログラム関連(PHP)

1 move_uploaded_file( $_FILES[‘hoge’][‘tmp_name’]  ,  “./”.$_FILES[‘hoge’][‘name’]  )
アップロードされた一時ファイルを移動します。
$_FILES[‘項目名’][‘tmp_name’] : アップロードされた一時ファイルのファイルパスです。
※一時ファイルは一定時間経過すると自動削除
”./”.$_FILES[‘hoge’][‘name’]  :  任意。ここではルートパス(“./”)にfomDataに設定したファイル名で移動します。
※(1)-9で、項目名‘hoge’,ファイル名‘test.jpg’を設定
$_FILES 
PHP定義済変数。HTTP POST でアップロードされたファイルの下記の値を取得するアップロード変数。
$_FILES[‘hoge’][‘name’]:ファイル名
$_FILES[‘hoge’][‘type’]:ファイルのMIMEタイプ(※1)
$_FILES[‘hoge’][‘tmp_name’]:一時保存ファイル名
$_FILES[‘hoge’][‘error’]:アップロード時のエラーコード
$_FILES[‘hoge’][‘size’]:ファイルサイズ(バイト単位)
※1:値の信頼性は低いとのこと。

 

まとめ

アップロード関連メモで確認したことを自分なりに整理してみて、少しですが理解が深まりました。半年くらい前にBlobを使って分割アップロード(chunked upload)の記事を見た時はさっぱり判りませんでしたが、またトライしてみます。
次回はアップロードしたファイルをソート(並び替え)リストに追加する処理を検討してみようと考えています。(あくまで予定です。)

コメントを残す

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