まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

タグ:html5

バックエンドの技術を使わずにウェブのフロントエンドだけで、つまり HTML と JavaScript だけで QR コードリーダーを作ることに挑戦してみました:
smartphone_qr_code


まずは完成品で実際の挙動を確認ください。PC またはスマホで(できればスマホで)こちらの URL にアクセスしてください:
https://dotnsf.github.io/web_qrcode/

2021012501



↑Github Pages で公開しているページなので、フロントエンドだけで作られていることがわかると思います。クロスオリジンな AJAX なども使ってないので、興味ある方はソースコードも確認してください。

「ファイルを選択」ボタンをクリックします。スマホの場合はカメラが起動するので QR コードを撮影してください。また PC の場合は画像選択ボックスが表示されるので、QR コードが撮影された画像を選択してください。

すると画面下部に画像が表示され、同時に画面内の QR コードが(存在していれば)解析され、解析結果の文字列データが表示されます:
2021012502


フロントエンドだけでここまでできるので、後は例えばこの結果が URL 文字列になっていれば、そのアドレスのページを開くこともできますし、テキストデータだけを読み取った上でバックエンドサーバーにデータを送信する、といったサーバーとの連携処理も容易に可能です。


【ソースコードの解説】
ソースコードはこちらで MIT ライセンスで公開しています(GitHub のウェブホスティングである Github Pages を使ってそのままサービスも公開しています):
https://github.com/dotnsf/web_qrcode

特にサービスの肝である index.html ファイルはこちらから参照いただけます。実質的にこの1ファイルだけでカメラ起動から撮影、QR コード解析まで実現しています:
https://github.com/dotnsf/web_qrcode/blob/main/index.html


まずスマホでカメラを起動するボタンの部分は以下の HTML コードで実現しています(PC ブラウザでこのボタンをクリックすると、保存済みの画像ファイルを選択するダイアログが表示されます):
<input type="file" accept="image/*" capture="camera" name="file" id="file-image"/>

type="file" で accept="image/*" capture="camera" 属性のついた <input> タグをクリックすると、カメラ内蔵機種の場合はそのカメラが起動し、撮影した画像を選択したことになります。(PCのなど)カメラ内蔵機種でない場合は普通にファイル選択画面になりますが、accept 属性により画像ファイルしか選択できません(要するにここで QR コードを撮影するか、QR コードが撮影された画像を選択することを想定しています)。

また jQuery を使った JavaScript によって以下の処理が画面ロード直後に実行されています:
var canvas = null;
var ctx = null;
$(function(){
  canvas = document.getElementById( 'mycanvas' );
  ctx = canvas.getContext( '2d' );

  var file_image = document.getElementById( 'file-image' );
  file_image.addEventListener( 'change', selectReadFile, false );
});


HTML 内下部に設置された <canvas> と、そのコンテキストを取得します(この <canvas> に画像が描画されることになりますが、その処理は後述します)。また上述の <input> タグの値が変化した時(=カメラで撮影するか、画像ファイルが選択された時)に selectReadFile() 関数が実行されるよう定義されています:
<div>
  <canvas id="mycanvas"></canvas>
</div>

したがってカメラで撮影するか画像ファイルを選択すると、以下の JavaScript コードが実行されます:
function selectReadFile( e ){
  var file = e.target.files;
  var reader = new FileReader();
  reader.onload = function(){
    readDrawImg( reader, canvas, 0, 0 );
  }
  reader.readAsDataURL( file[0] );
}

HTML5 の Filesystem API を使って撮影された画像ファイルを読み込み、その内容を上述で取得した <canvas> に描画するための関数 readDrawImg() が実行されます。つまり撮影した画像が画面内にも表示されるようになります。

readDrawImg() 関数および、この中で実行される多くの関数の中身は以下のようになっています:
function readDrawImg( reader, canvas, x, y ){
  var img = readImg( reader );
  img.onload = function(){
    var w = img.width;
    var h = img.height;
    printWidthHeight( 'src-width-height', true, w, h );

    // resize
    var resize = resizeWidthHeight( 1024, w, h );
    printWidthHeight( 'dst-width-height', resize.flag, resize.w, resize.h );
    drawImgOnCav( canvas, img, x, y, resize.w, resize.h );
  }
}

function readImg( reader ){
  var result_dataURL = reader.result;
  var img = new Image();
  img.src = result_dataURL;

  return img;
}

function drawImgOnCav( canvas, img, x, y, w, h ){
  canvas.width = w;
  canvas.height = h;
  ctx.drawImage( img, x, y, w, h );

  checkQRCode();
}

function resizeWidthHeight( target_length_px, w0, h0 ){
  var length = Math.max( w0, h0 );
  if( length <= target_length_px ){
    return({
      flag: false,
      w: w0,
      h: h0
    });
  }

  var w1;
  var h1;
  if( w0 >= h0 ){
    w1 = target_length_px;
    h1 = h0 * target_length_px / w0;
  }else{
    w1 = w0 * target_length_px / h0;
    h1 = target_length_px;
  }

  return({
    flag: true,
    w: parseInt( w1 ),
    h: parseInt( h1 )
  });
}

function printWidthHeight( width_height_id, flag, w, h ){
  var wh = document.getElementById( width_height_id );
  wh.innerHTML = '幅: ' + w + ', 高さ: ' + h;
}

まず readImg() 関数の中で Filesystem API を用いて画像ファイルを読み込み、その結果を Image オブジェクトの src 属性に代入します(これで Image オブジェクトを drawImgOnCav() 関数で <canvas> に表示すると読み込んだ画像が表示されます)。またこの結果から画像の幅および高さを取得して、画面内に元画像のサイズとして表示されます。

drawImgOnCav() 関数の最後に checkQRCode() 関数が実行されています。画像の <canvas> への描画終了後にこの関数が実行され、画像に QR コードが写っていたらその内容を解析して、データを取り出します:
function checkQRCode(){
  var imageData = ctx.getImageData( 0, 0, canvas.width, canvas.height );
  var code = jsQR( imageData.data, canvas.width, canvas.height );
  if( code ){
    //console.log( code );
    alert( code.data );
  }else{
    alert( "No QR Code found." );
  }
}

具体的にはコンテキスト変数(ctx)から getImageData() 関数でイメージデータを取得し、その結果を jsQR を使って解析します。ややこしそうな QR コードの解析部分は実はライブラリによって一瞬で実現できてしまっています。jSQR バンザイ!

正しく QR コードが解析できた場合は解析結果の文字列(code.data)を alert() 関数で画面に表示し、解析できなかった場合は "No QR Code found." と表示される、という一連の処理が定義されています。


今回の処理では QR コードが正しく解析でした場合はそのまま表示しているだけですが、これが URL であればロケーションに指定してジャンプしてもよし、テキストなどのデータであればバックエンドの API にポストしてもよし、というわけで取り出したデータの使いみちは alert() の代わりに記述することで、自由に応用できると思います。


というわけで、Filesystem API と jsQR と <input> タグのカメラ拡張を使うことで、ウェブのフロントエンド技術だけで QR コードリーダーが実現できました。


ほぼ備忘録目的のブログエントリです。

HTML で <canvas> を使うと画面に単純な画像だけでなく、グラフィックコンテキストを使って自由度の高い図形や関数曲線を描くことができるようになります。個人的にもよく使っています。

特筆すべきはピクセル単位での画素情報を取得することもできる点です。ここでの画素情報とは RGB 値や輝度の値といった情報です。例えば <canvas id="mycanvas"></canvas> 内に描かれている内容のピクセル単位での画素情報を動的に取得するには以下のような JavaScript コードを実装すれば可能です:
  var canvas = document.getElementById( 'mycanvas' );
  if( !canvas || !canvas.getContext ){
    return false;
  }
  var ctx = canvas.getContext( '2d' );

  var imagedata = ctx.getImageData( 0, 0, ctx.canvas.width, ctx.canvas.height );
  for( var i = 0; i < imagedata.height; i ++ ){
    for( var j = 0; j < imagedata.width; j ++ ){
      var idx = ( j + i * imagedata.width ) * 4;

      var r = imagedata.data[idx];    //. R(0-255)
      var g = imagedata.data[idx+1];  //. G(0-255)
      var b = imagedata.data[idx+2];  //. B(0-255)
      var a = imagedata.data[idx+3];  //. 輝度(0-255)

        :
    }
  }

この中で画素情報を取得する際に使っているのが getImageData() 関数です。これは canvas 内の開始点座標と幅、高さを指定し、その指定範囲内の画素情報を配列で取り出すことができます。また上述例のように1つのピクセルの情報は配列内の4つの値(R, G, B, 輝度)として格納されており、1つずつ取り出して参照することも可能です。画像を独自に減色したりする際等に必要な情報を取り出すことができて、個人的にも便利に使っています。


このように便利な canvas と getImageData() 関数ですが、この getImageData() 実行時に以下のようなエラーが発生して、画素情報を取得できないケースがありました:
'The canvas has been tainted by cross-origin data.'

ん? "Cross-Origin" ?? どういうこと???


これが発生した時の <canvas> 内は、事前に以下のような処理を行って画像が表示されていました:
  var img = new Image();
  img.src = 'https://www.xxx.com/images/abc.png';
  img.addEventListener( 'load', function(){
    var canvas = document.getElementById( 'mycanvas' );
    if( !canvas || !canvas.getContext ){
      return false;
    }
    var ctx = canvas.getContext( '2d' );
    ctx.drawImage( img, 0, 0, canvas.width, canvas.height );
  }, false );

要するに <canvas> 内には https://www.xxx.com/images/abc.png という(架空の) URL で表現される画像ファイルが表示されている状態でした。単に画像を表示するだけなら <img src="https://www.xxx.com/images/abc.png"/> みたいに記述すればいいのですが、後で前述のような画素情報を取り出す処理を実行したかったので、<img> ではなく <canvas> を使って表示していたのでした、という事情がありました。が、このケースだと getImageData() 実行時に上述のようなクロスオリジン関連のエラーが発生して情報を取得することができなくなっていました。これはいったい・・?


エラーメッセージからなんとなく想像はできたのですが、この getImageData() 関数はクロスオリジンの画像が表示されている場合は CORS の制約がかかるようです。つまりこの例であれば www.xxx.com というサーバー上にある https://www.xxx.com/images/abc.png という画像を表示しています(上のコード例では drawImage() 関数によって実際に表示されます)。この drawImage() 関数はクロスオリジンに関係なく実行することができるので、(自サーバーではない)www.xxx.com というサーバー上にある画像に対して実行してもエラーが発生することなく実行され、画像は <canvas> 上に表示されます。

しかし getImageData() 関数についてはクロスオリジン制約があるようです。したがって自サーバー上ではない URL を指定して表示された画像に対して実行すると、今回のようなエラーが発生してしまう、という制約があるようでした。


このエラーを回避しようとすると、www.xxx.com 側で CORS の制約を緩和するような設定を行うか、それが難しい場合は目的の画像をクロスオリジンにならない形の URL で指定できるようにする必要があります(そのため静的な HTML や JavaScript だけだと実現が難しいです)。具体的にはサーバーサイドの処理で、例えば GET /getimage のような REST API を用意し、この API が目的の画像バイナリと Content-Type を返すようにする、といった対応が必要になります。


現にこういう方法で回避できちゃう CORS 制約は、処理が面倒くさくなるだけであまり意味ないと思っちゃうんだけどな。。


 

HTML5 Canvas を使って1年間かけて一周する「年間時計」を作ってみました:
https://dotnsf.github.io/yearclock/
2019112300


表示すると1分で一周する秒針と、1年で一周する年針(?)が表示されます。年針は針というよりはパイチャートの要領で面積で表示されます。ちなみに上図は 11 月後半に表示したものです。もうかなり進んでいますね。。

仕組みはそれほど複雑ではなく、HTML5 Canvas 内に円と弧を描画した上で秒針を setInterval() を使って1秒おきに描画しています。画像の透明度をうまく使って秒針の残像が残る形にしています。パックマン型に弧を描画する技術については先日公開したこちらのエントリを参照ください:
Canvas でパックマン型に塗りつぶした弧を描く


計算上では(うるう年でない年は)毎年 11月25日 の正午が一年間の 90% を経過する瞬間になります。今年も残すはあと1割!

HTML5 の Canvas を使うことで HTML の画面内にコンテキストを利用した図形を比較的自由に描くことができるようになります。この機能の1つである arc() 関数を使うと、下図のような「円の弧」を描画できます(塗りつぶすかどうかは選択できます):
2019112203


この弧を描く際のコードは以下のようなものです:
  :
<script>
  var canvas1 = document.getElementById( 'myCanvas1' );
  if( !canvas1 || !canvas1.getContext ){
    return false;
  }
  var ctx1 = canvas1.getContext( '2d' );

  //. 円の情報
  var r = 80;
  var x0 = 100;
  var y0 = 100;

  var deg = 190;
  ctx1.beginPath();
  ctx1.arc( x0, y0, r, -90 * Math.PI / 180, deg * Math.PI / 180, false );
  ctx1.fillStyle = "rgba( 255, 128, 128, 0.8 )";
  ctx1.fill();
</script>

<body>
<canvas id="myCanvas1"></canvas>
</body>
  :

で、このような図形を描くのが目的であればいいのですが、パックマンのような(円からピザの一部を切り取ったような)画像を描きたい場合はこれでは目的の画像とは異なります。なんとかしてパックマン型に塗りつぶした弧を描く方法はないでしょうか? というのが今回のテーマです。





2019112202


結論としては可能で、「arc() 関数の実行直後に円中心に向かって直線を引く」のがその答えになります:

  :
<script>
  var canvas1 = document.getElementById( 'myCanvas1' );
  if( !canvas1 || !canvas1.getContext ){
    return false;
  }
  var ctx1 = canvas1.getContext( '2d' );

  //. 円の情報
  var r = 80;
  var x0 = 100;
  var y0 = 100;

  var deg = 190;
  ctx1.beginPath();
  ctx1.arc( x0, y0, r, -90 * Math.PI / 180, deg * Math.PI / 180, false );
  ctx1.lineTo( x0, y0 );
  ctx1.fillStyle = "rgba( 255, 128, 128, 0.8 )";
  ctx1.fill();
</script>

<body>
<canvas id="myCanvas1"></canvas>
</body>
  :

これでパックマンが描画できました:
2019112200

 

Web アプリでドラッグ&ドロップ(以下 DnD と表記)すること自体はできるようになりました。HTML5 では DnD できる要素が多く定義されていたり、HTML5 以前にも mouseup イベントや mousedown イベントをハンドリングすることで独自に実装することは可能でした。 

ただモバイル Web で、つまりスマホのウェブブラウザから DnD を行うことはまだ困難が伴います。そもそもスマホの小さい画面で DnD を行うことが使いやすいかどうか、という根本的な問題もあると思っていて、それが理由かどうかは定かではありませんが、HTML5 の DnD API の多くはスマホブラウザからは使えないことが多いようです。

そういった事情を理解した上で、それでも現状でどこまでできるだろうか? という観点で実現方法を考えて実装し、公開してみました:
https://dotnsf.github.io/mobile_dnd_sample/


上記 URL にスマホのブラウザでアクセスすると以下のような画面になります。4角の枠内に5枚の付箋が貼られているイメージです:
2019111701


各付箋は指でドラッグして位置を変えることができるようにしています。下図はピンクの「ハロー」と書かれた付箋をドラッグして位置を変更した後の様子です:
2019111702


また各付箋はダブルタップすると編集モードになり、書かれた文字を編集することができるようになります。「ハロー」を編集して「ハロー!」にしてみました:
2019111703


編集モードで「OK」をタップすると編集後の文字列が反映されます:
2019111704


付箋を削除する場合は、一度ダブルタップして編集モードにして、「DELETE」をタップします:
2019111705


確認後に削除されて元の画面に戻ります。「abc」と書かれていた付箋が削除されました:
2019111706


新しい付箋を追加する場合は「NEW」をタップし、編集モードで内容を入力します:
2019111707


そして「OK」をタップすると新しい付箋が追加されます。この付箋も同様にドラッグして位置を変えたり、再度編集して内容を変更することができます:
2019111708



一連の機能紹介は以上です。一応スマホのウェブブラウザでもドラッグ&ドロップによる UI が実現できました。サンプルでは全て JavaScript を使ってフロントエンドだけで実現していますが、データベースとのバックエンド連携を加えることで永続化なども実現できると思っています。

詳しくは後述のソースコードを参照いただきたいのですが、今回のサンプルでは HTML5 Canvas を使って、Canvas 内に付箋に相当するオブジェクトを描画して実現しています。Canvas 内の Touch イベントを監視してドラッグを処理しています。また編集モード画面は Bootstrap のモーダルダイアログを使っています。


ソースコードはこちらです。実態は index.html ファイル1つで、全ての HTML と JavaScript がこの中に含まれています:
https://github.com/dotnsf/mobile_dnd_sample



このページのトップヘ