2015年10月21日
HTML5で画像の読み書きをしてみよう
HTML5で異彩なものとして、LocalStorageとFileAPIがあります。これまでCookieを利用し来訪情報などを記憶(容量は数KB程度)させていました。LocalStrorageはブラウザーが管理するKVS(KeyValueStore)で、高速アクセスができ、容量は5から10MBと十分な大きさを持っています。モダンブラウザーを用いる場合、LocalStoragを用いることで、データを活用した新しいコンテンツ表現が可能となります。
更にHTML5ではFileAPIによって、ローカルディスク上のファイル(LocalStorage以上のデータ量を取り扱うことができる)の読み書き、ファイルのドラッグ&ドロップなどが利用できるようになりました。
ここでは、FileAPIを利用して、画像ファイルのドラッグ&ドロップによる画像の読み込み、canvasへの展開、ヒストグラム(階調グラフ)の表示、JPEGファイルからExif(Exchangeable image file format)情報の抽出、緯度経度からGoogleMapによる位置表示、canvasから画像のローカルディスクへの書き出しを行います。
ローカルディスクへのファイル保存には、今回、FileSaver.jsを利用していますが、使い方が大変簡単です。
デモページを作りに当たり、下記のサイトなどの情報を集め、一つのページとしたものです。
参考サイト
FileAPI で画像を選択またはドラッグ&ドロップで画像を表示
【Canvas練習ノート】CanvasをPNG画像ダウンロード – toBlobとsaveAs
デモの流れ(処理はブラウザーのみで、サーバーに画像がアップされることはありません)
・ブラウザーに表示されているドロップエリアに画像ファイルをドロップします。
ファイルドロップ後、画像ファイル名(JPEGファイルでは、Exif情報も表示)、画像表示(640*480のエリアに入らない場合、縦横比はそのままで、640*480に入るサイズに変換)、RGBそれぞれのヒストグラムが表示されます。
・緯度経度(青色表示)が表示されている場合、青色部分をクリックすると、緯度経度が示すGoogleMap並びに住所が表示されます。
・ローカル保存ボタンを押すと、canvasから画像情報を読み、canvasサイズの画像がローカルディスクにダウンロードされます。元のJPEGファイルにあったExif情報は、canvasに反映されないため、ローカルディスクに保存されたJPEGファイルにはExif情報はありません。
・住所をクリックすると、表示されている住所がaddress.txtの中に書かれ、ローカルディスクにダウンロード保存されます。
動作確認
Chrome46、IE11
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>画像のドロップ、Canvas表示、階調グラフ表示、Exif情報取得、ローカル保存</title>
<style>
html, body {
font-size: 20px;
text-align: center;
}
div#drop-zone {
margin: 1rem auto;
width: 20rem;
height: 10rem;
border: 1px solid #333;
}
div#print_image {
margin: 1rem auto;
}
div#map_canvas {
margin: 1rem auto;
}
canvas {
border: 1px solid #333;
max-width: 100%;
height: auto;
}
</style>
<script type="text/javascript" src="piexif.js"></script>
<script type="text/javascript" src="FileSaver.js"></script>
<script type="text/javascript" src="canvas-toBlob.js"></script>
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"></script>
</head>
<body>
<div id="drop-zone">ここに画像をドロップ!</div>
<div id="print_img">
<p id="width-height">width: height: </p>
<p>Canvas(ratio:<span id="rate">100</span>%) <button onclick="saveAsFile()">ローカル保存</button></p>
<canvas id="canvas">Canvas対応のブラウザで開いて下さい。</canvas>
</div>
<div id="mapArea"><div id="map_canvas" style="width:480px; height:360px;"></div></div>
<script>
/*
Arranged by T. Osaka(2015/10/21)
FileAPI で画像を選択またはドラッグ&ドロップで画像を表示
http://cartman0.hatenablog.com/entry/2015/06/08/131855
hMatoba/piexifjs
https://github.com/hMatoba/piexifjs
eligrey/FileSaver.js
https://github.com/eligrey/FileSaver.js/
eligrey/canvas-toBlob.js
https://github.com/eligrey/canvas-toBlob.js/
【Canvas練習ノート】CanvasをPNG画像ダウンロード – toBlobとsaveAs
http://www.inazumatv.com/contents/archives/9655
*/
//canvas画像のローカル保存
function saveAsFile( e ) {
// e.preventDefault();
// e.stopPropagation();
var mes = document.getElementById("width-height").innerHTML;
mes = mes.substring(0, mes.indexOf(' '));
console.log("mes=" + mes);
canvas.toBlob(
function ( blob ) {
saveAs( blob, mes);
},
"image/jpeg"
);
}
//緯度経度からGoogleMapの表示
var latlng = new google.maps.LatLng(35.681382, 139.76608399999998);//東京駅
var geocoder = new google.maps.Geocoder();
var mapOptions = {
zoom: 15,
center: latlng,
mapTypeId: google.maps.MapTypeId.ROADMAP
}
var map = new google.maps.Map(document.getElementById('map_canvas'), mapOptions);
function codeLatLng(lat, lng) {
var latlng = new google.maps.LatLng(lat, lng);
geocoder.geocode({
'latLng': latlng
}, function(results, status) {
if (status == google.maps.GeocoderStatus.OK) {
map.setCenter(results[0].geometry.location);
/*
緯度・経度から住所を取得する
http://www.nanchatte.com/map/getAddressByLatLng.html
*/
var address = results[0].formatted_address.replace(/^日本, /, '');
print_Address(address);
console.log(address);
var marker = new google.maps.Marker({
map: map,
position: results[0].geometry.location
});
}
});
}
var print_img_id = 'print_img';
var print_DataURL_id = 'print_DataURL';
var canvas = document.getElementById('canvas');
if ( checkFileApi() && checkCanvas(canvas) ){
//ドラッグオンドロップ
var dropZone = document.getElementById('drop-zone');
dropZone.addEventListener('dragover', handleDragOver, false);
dropZone.addEventListener('drop', handleDragDropFile, false);
}
//canvas に対応しているか
function checkCanvas(canvas){
if (!canvas || !canvas.getContext){
return false;
}
return true;
}
// FileAPIに対応しているか
function checkFileApi() {
// Check for the various File API support.
if (window.File && window.FileReader && window.FileList && window.Blob) {
// Great success! All the File APIs are supported.
return true;
}
alert('The File APIs are not fully supported in this browser.');
return false;
}
//ファイルが選択されたら読み込む
function selectReadfile(e) {
var files = e.target.files;
var reader = new FileReader();
//dataURL形式でファイルを読み込む
reader.readAsDataURL(files[0]);
//ファイルの読込が終了した時の処理
reader.onload = function(){
readDrawImg(reader, canvas, 0, 0);
}
}
//JpegファイルからExif情報の取得
function printExif(dataURL) {
var Make, Model, DateTimeOriginal, GPSLatitudeRef, GPSLongitudeRef, GPSLatitude, GPSLongitude, GPSAltitude;
var originalImg = new Image();
originalImg.src = dataURL;
var exif = piexif.load(dataURL);
var ifds = ["0th", "Exif", "GPS", "Interop", "1st"];
var s = "";
for (var i=0; i<5; i++) {
var ifd = ifds[i];
var ifd_i = "";
for (var tag in exif[ifd]) {
var str;
if (exif[ifd][tag] instanceof Array) {
str = JSON.stringify(exif[ifd][tag]);
if (piexif.TAGS[ifd][tag]['name'] == 'GPSLatitude') {//[[35,1],[8,1],[2707,100]]
var v = exif[ifd][tag];
GPSLatitude = v[0][0] / v[0][1] + v[1][0] / v[1][1] / 60 + v[2][0] / v[2][1] / 3600;
}
if (piexif.TAGS[ifd][tag]['name'] == 'GPSLongitude') {//[[139,1],[36,1],[5962,100]]
var v = exif[ifd][tag];
GPSLongitude = v[0][0] / v[0][1] + v[1][0] / v[1][1] / 60 + v[2][0] / v[2][1] / 3600;
}
if (piexif.TAGS[ifd][tag]['name'] == 'GPSAltitude') {//[6985,839]
var v = exif[ifd][tag];
GPSAltitude = v[0] / v[1];
}
} else {
str = exif[ifd][tag];
if (piexif.TAGS[ifd][tag]['name'] == 'Make') Make = exif[ifd][tag];
if (piexif.TAGS[ifd][tag]['name'] == 'Model') Model = exif[ifd][tag];
if (piexif.TAGS[ifd][tag]['name'] == 'DateTimeOriginal') {//2013:11:08 11:33:01
DateTimeOriginal = exif[ifd][tag];
DateTimeOriginal = DateTimeOriginal.replace(":", "/");
DateTimeOriginal = DateTimeOriginal.replace(":", "/");
}
if (piexif.TAGS[ifd][tag]['name'] == 'GPSLatitudeRef') GPSLatitudeRef = exif[ifd][tag];
if (piexif.TAGS[ifd][tag]['name'] == 'GPSLongitudeRef') GPSLongitudeRef = exif[ifd][tag];
}
ifd_i += ("<tr><td class='te'>" + piexif.TAGS[ifd][tag]["name"] + "</td><td class='te'><div class='divtd'>" + str + "</div></td></tr>");
console.log(tag + " : " + piexif.TAGS[ifd][tag]['name'] + " "+ str);
}
s += ("<table class='t'><tr><th colspan='2' class='th'>" + ifd + "</th></tr>" + ifd_i + "</table>");
}
if(Make)console.log(Make);
if(Model)console.log(Model);
if(DateTimeOriginal)console.log(DateTimeOriginal);
if(GPSLatitudeRef)console.log(GPSLatitudeRef + GPSLatitude);
if(GPSLongitudeRef)console.log(GPSLongitudeRef + GPSLongitude);
if(GPSAltitude)console.log(GPSAltitude);
if(GPSLatitudeRef)print_Exif(GPSLatitude, GPSLongitude, GPSAltitude);
}
//ドラッグオンドロップ
function handleDragOver(e) {
e.stopPropagation();
e.preventDefault();
e.dataTransfer.dropEffect = 'copy'; // Explicitly show this is a copy.
}
function handleDragDropFile(e) {
e.stopPropagation();
e.preventDefault();
var files = e.dataTransfer.files; // FileList object.
console.log(files.length);
var file = files[0];
console.log("mimeType=" + file.type);
if(file.type.indexOf("image") != 0){
alert("画像ファイルをドロップしてください。");
return;
}
var reader = new FileReader();
//dataURL形式でファイルを読み込む
console.log(file.name + " " + file.size);
printMessage(file.name + " " + file.size);
//ファイルの読込が終了した時の処理
reader.onload = function(e){
var dataURL = e.target.result;
//console.log("target=" + dataURL);
if(file.type == 'image/jpeg')printExif(dataURL);
readDrawImg(reader, canvas, 0, 0);
};
reader.readAsDataURL(file);
}
function readDrawImg(reader, canvas, x, y){
var img = readImg(reader);
drawImgOnCav(canvas, img, x, y);
}
//ファイルの読込が終了した時の処理
function readImg(reader){
//ファイル読み取り後の処理
var result_dataURL = reader.result;
//console.log("result_dataURL:" + result_dataURL);
var img = new Image();
img.src = result_dataURL;
return img;
}
//キャンバスにImageを表示
function drawImgOnCav(canvas, img, x, y) {
img.onload = function(){
//640*480の矩形に入れ込む
var w = img.width / 640;
var h = img.height / 480;
if (w >= h && w > 1){
h = img.height / w;
w = 640;
document.getElementById("rate").innerHTML = Math.ceil(640/img.width*100);
}
else if (h > w && h > 1){
w = img.width / h;
h = 480;
document.getElementById("rate").innerHTML = 100;
document.getElementById("rate").innerHTML = Math.ceil(480/img.height*100);
}
else{
w = img.width;
h = img.height;
document.getElementById("rate").innerHTML = 100;
}
console.log("w:" + w + " h:" + h);
if(w==0 || h==0)return;
var ctx = canvas.getContext('2d');
var wrapper= document.getElementById("print_img");
canvas.width = w;//img.width;
canvas.height = h;//img.height;
ctx.drawImage(img, x, y, w, h);//img.width, img.height);
printWidthHeight( "width-height", img.width, img.height );
var ImageData = ctx.getImageData(0, 0, w, h);
bar_graph('canvas1', ImageData, 'red', 300, 80);
bar_graph('canvas2', ImageData, 'green', 300, 80);
bar_graph('canvas3', ImageData, 'blue', 300, 80);
}
}
//メッセージ表示
function printMessage( message ) {
document.getElementById("width-height").innerHTML = message;
}
//width, height表示
function printWidthHeight( width_height_id, width, height ) {
var w = width;
var h = height;
var mes = document.getElementById(width_height_id).innerHTML;
document.getElementById(width_height_id).innerHTML = mes + '<br/> width:' + w + ' height:' + h;
console.log('width:' + w + ' height:' + h);
}
//Exif表示
function print_Exif(lat, lon, h) {
var mes = document.getElementById("width-height").innerHTML;
document.getElementById("width-height").innerHTML = mes + '<br/> 緯度経度:<span style="cursor:pointer; color:blue;" onclick="codeLatLng('+lat + ', ' + lon +')">' +lat + ', ' + lon + '</span> 高度:' + h;
}
//address表示
function print_Address(address) {
var mes = document.getElementById("width-height").innerHTML;
document.getElementById("width-height").innerHTML = mes + '<br/><span id="address" onclick="saveAddress()" style="cursor:pointer; color:blue;">' + address + '</span>';
}
//address保存
function saveAddress() {
var addr = document.getElementById("address").innerHTML;
var fileName = "address.txt";
var blob = new Blob([addr], {type: "text/plain;charset=utf-8"});
saveAs(blob, fileName);
}
/*
Canvasで棒グラフ、折れ線グラフ、円グラフをつくる
http://cartman0.hatenablog.com/entry/2015/07/28/012339#sec-bar
*/
//階調グラフを描画
function bar_graph(id, ImageData, bar_color, bw, bh){
var canv = document.getElementById(id);
if (!canv) {
canv = document.createElement('canvas');
canv.id = id;
canv.setAttribute('width', bw);
canv.setAttribute('height', bh);
document.body.appendChild(canv);
}
var data = ImageData.data;
var w = ImageData.width;
var h = ImageData.height;
var stroke_opts = {
color: bar_color,
width: 1
};
var fill_opts = {
color: bar_color
};
var datas = new Array(256);
var p = 0;
if(bar_color == 'green') p = 1;
else if(bar_color == 'blue') p = 2;
for (var i = 0; i < 256; i++) datas[i]=0;//階調配列の初期化
for (var y = 0; y < h; y++) {//階調情報を求める
for (var x = 0; x < w; x++) {
var i = (x + y * w) * 4 + p;
datas[data[i]]++;
}
}
barGraph(canv, datas, stroke_opts, fill_opts);
function barGraph(canvas_obj, datas, stroke_opts, fill_opts){
var c = canvas_obj.getContext('2d');
// bar
var pos = 0;
var bar_width = canvas_obj.width / datas.length;
var mv = 0;
for (var i = 0; i < datas.length; i++){
if(mv < datas[i]) mv = datas[i];
}
for (var i = 0; i < datas.length; i++){
var yy = datas[i];
if(mv > canvas_obj.height) {
yy = yy / mv * canvas_obj.height;
yy = yy * 0.7;
}
var barPos = {
x: pos,
y: canvas_obj.height - yy,
w: bar_width
};
bar(c, datas[i], barPos, stroke_opts, fill_opts);
pos += bar_width;
}
function bar(context, data, barPos, stroke_opts, fill_opts) {
context.strokeStyle = stroke_opts.color;
context.lineWidth = stroke_opts.width;
context.strokeRect(barPos.x, barPos.y, barPos.w, data);
context.fillStyle = fill_opts.color;
context.fillRect(barPos.x, barPos.y, barPos.w, data);
}
}
}
</script>
</body>
</html>
記 大坂 哲司