riina-k.net

音楽・小説・プログラミングを手がけるリイナの拠点。

画像ロード時のエラー制御で試行錯誤

開発中のCDデータベースを試しにこのサーバにアップロードしたところ、
開発環境であるローカルホストでは発生しなかった「画像へのリクエストで500エラー」という状況が発生。

実はCDのジャケット画像をサーバ内部の見えないところでキャッシュしておいて、パラメータで出力する画像を選択、キャッシュが存在すればそのまま出力、存在しなければ404を返すというPHPを組んでいた。

404エラーと500エラーの双方に対応するため、まずAjaxで500エラーを捕捉するよう、次のように書いてみた。

PHP

まずはAjaxでジャケット画像の縦横サイズを取得できるよう、独自ヘッダでサイズを出力。

header( 'Content-type: ' . $this->mime );
header( 'Content-Length: ' . $this->bytes );
// 独自ヘッダ
header( 'X-Width: ' . $this->width );
header( 'X-Height: ' . $this->height );

HTML

ジャケット画像が正常に読み込まれるまではローディング中を示す例のくるくる回るやつを表示しておいて、alt属性に読み込みたいURLを書いておく。
※ {{ …… }} はSmartyのタグ、$pkgはパッケージ情報のインスタンス、$imgは画像情報のインスタンス。

<tr id="{{$pkg->htmlid|escape}}">
	<td class="jacket">
		<img src="material/loading.gif" width="16" height="16" alt="{{$img->pathWeb|escape}}" title="{{$title_artist}}" />
	</td>
</tr>

javascript

コールバック関数内で、altに記述されたURLを読み込み、ステータスが200になったらsrcにジャケット画像のURLを設定。

function loadJacketImage(jqImage, count)
{
	$.ajax(
		{
			jq: jqImage,	// 対象となるimgタグのjQueryオブジェクト
			count: count,	// 再試行回数
			url: jqImage.attr("alt"),
			success: function(data, status, xhr){
				var width = xhr.getResponseHeader("X-Width");
				var height = xhr.getResponseHeader("X-Height");
				this.jq.attr("src", this.url).attr("alt", "").attr("width", width).attr("height", height);
			},
			error: function(xhr, status, error){
				if ( xhr.status == 404 ) {
					this.jq.replaceWith("no image");
				} else {
					if ( this.count > 0 ) {
						// 再試行
						loadJacketImage(this.jq, this.count-1);
					} else {
						this.jq.replaceWith("error");
					}
				}
			}
		}
	);
}

$(function(){
	$(".jacket img").each(function(){
		loadJacketImage( $(this), 3 );
	});
});

しかしこの方法には大きな罠があり、「Ajaxで取得できた画像のURLをsrcに設定する」タイミングでもう一度リクエストが飛んでしまい、そのときに500エラーとなってしまうという状況が時々発生してしまう。
キャッシュしてくれないんかい……

そこでイベントハンドラを利用した以下のような方法を試してみた。

HTML

<tr id="{{$pkg->htmlid|escape}}">
	<td class="jacket">
		<img src="{{$img->pathWeb|escape}}" width="{{$img->width|escape}}" height="{{$img->height|escape}}" alt="{{$title_artist}}" />
	</td>
</tr>

javascript

img要素のイベントハンドラで、エラー時に単純にリロードさせるためにsrcを上書き。

$(function(){
	$(".jacket img").bind("error", function(e){
		// リロード
		this.src = this.src;
	});
});

今度はイベントハンドラで404と500の区別が付かない。
そもそもPHPのほうで404が返ってくるようなURLは最初から書かないように変更し、500エラーが返ってきてもリロードが働いて正常に表示されるようになった。
ただ、PHPが何らかの影響でエラーを吐き続けた場合、リロードが無限ループしてしまうので、Ajaxの時と同様に再試行回数を設定できるようにしたい。

あとやっぱり例のくるくる欲しいよね。
ってことでいじくり回して、最終的にこんな感じ。

HTML

ジャケット画像が存在しない場合はあらかじめ「no image」を表示、存在する場合は例のくるくるに対してalt指定でジャケット画像のURL、横幅、高さをカンマ区切りで設定。

<tr id="{{$pkg->htmlid|escape}}">
	<td class="jacket">{{strip}}
		{{if $img->exists }}
			<img src="material/loading.gif" width="16" height="16" alt="{{$img->pathWeb|escape}},{{$img->width|escape}},{{$img->height|escape}}" title="{{$title_artist}}" />
		{{else}}
			no image
		{{/if}}
	{{/strip}}</td>
</tr>

javascript

成功時に例のくるくるをジャケット画像で置き換え、エラー時には指定回数だけリロードを試み、一定回数(このサンプルの場合は3回)を超えると例のくるくるを「error」という文字列で置き換え。
コールバック関数内で置換対象のくるくると試行回数を見失ってしまうと意味が無いので、jQueryの内部APIで要素に関連付けておく。コールバック関数に引数を渡す方法がめっちゃめんどくさいの

$(function(){
	$(".jacket img").each(function(){
		var tmp = this.alt.split(",");
		var jqImg = $("<img />");
		
		// エラー時のイベントハンドラ
		jqImg.bind("error", function(e){
			// 再試行回数だけリロード
			var tryCount = jQuery.data( this, "tryCount" );
			if ( --tryCount > 0 ) {
				jQuery.data( this, "tryCount", tryCount );
				this.src = this.src;
			} else {
				// 試行回数切れ
				var id = jQuery.data( this, "target" );
				$("#" + id + " img").replaceWith("error");
			}
		});
		// ロード時のイベントハンドラ
		jqImg.bind("load", function(e){
			var id = jQuery.data( this, "target" );
			$("#" + id + " img").replaceWith( $(this) );
			// 後始末
			jQuery.removeData( this, "tryCount" );
			jQuery.removeData( this, "target" );
		});
		
		jQuery.data( jqImg.get(0), "tryCount", 3 );								// 再試行回数を設定
		jQuery.data( jqImg.get(0), "target", $(this).parents("tr").get(0).id );	// 画像置換対象のID
		jqImg.attr("src", tmp[0]).attr("width", tmp[1]).attr("height", tmp[2]).attr("alt", $(this).attr("title"));
	});
});

ここまでやってようやく気が済んだのでした。
そもそもPHPが500を返すほどサーバに負荷をかけてる事実をなんとかするべきじゃ?

個人的に「これは組込み関数として必要だろ」っていうjavascript関数

// オブジェクトの結合
function merge()
{
	var result = {};
	for( var i=0; i<arguments.length; i++ ){
		var arg = arguments[i];
		for ( var itm in arg ) {
			if ( arg.hasOwnProperty(itm) ) {
				result[itm] = arg[itm];
			}
		}
	}
	return result;
}

// 配列 keys に存在する要素をキーとする値だけをオブジェクト obj から取り出す
function slice(obj, keys)
{
	var result = {};
	for ( var idx in keys ) {
		key = keys[idx];
		result[key] = ( obj.hasOwnProperty(key) ? obj[key] : null );
	}
	return result;
}

// PHPの array_walk() のクローン
function walk(obj, callback, param)
{
	for ( var idx in obj ) {
		var item = obj[idx];
		if ( typeof item != 'undefined' && typeof item != 'function' ) {
			callback(item, idx, param);
		}
	}
}

この3つ。
merge関数は「objectをマージする – javascript:humming bird」を参考に。
slice関数については関数名が適切じゃないかもしれない。
(本来 slice っていうと、配列のi番目からj番目までを切り出すっていう意味っぽいから)

この merge, slice, walk を組み合わせると、こんなことができる。(一応前提としてjQueryを使用、ほかのライブラリでも同様の記述はできるはず)

$(function(){
	// 元のデータを保持するオブジェクトを生成
	var obj = {
		id: 123,
		title: 'rk.tv',
		link: [
			'http://riina-k.net/',
			'http://riina-k.tv'
		]
	};
	// そこにプロパティを追加
	obj = merge( obj, { author: 'Riina K.', param: 'foooo' } );

	// さらに必要なプロパティのみを切り出す
	var target = slice( obj, ['title', 'author'] );

	function printHTML(item, key, jq)
	{
		jq.append( '<dt>' + key + '</dt>' + '<dd>' + item + '</dd>' );
	}

	// 切り出したプロパティそれぞれにコールバック関数を適用し、dlタグを出力
	$('body').append('<dl id="output"/>');
	walk( target, printHTML, $('dl#output') );
});

実行結果

<dl id="output"><dt>title</dt><dd>rk.tv</dd><dt>author</dt><dd>Riina K.</dd></dl>

オブジェクトやコールバック関数を多用するjavascriptだからこそ、こんな関数たちが居てくれると便利。

サークル立ち上げ記念参拝

本日2012年10月27日をもって、同人音楽サークル「rk.tv : あーけーどてぃーゔぃー」を正式に立ち上げました。
いままで正式じゃなかったんかい

それを記念して、秩父札所二十三番「音楽寺」に参拝してきました。
音楽寺境内

秩父市街地の西側の山全体が「秩父ミューズパーク」という広大な公園になっていて、音楽寺はその北端付近にある。

大きな地図で見る

その寺名にあやかって、音楽を志す人たちが訪れることも少なくないようで、境内の掲示板には名刺やイベントのフライヤーが。

私も真似してみた。
リイナ、音楽寺に見参

明治17年に起きた「秩父事件」のときに鳴らされた鐘も撞いて完璧。

「あの花」巡礼のついでに立ち寄ってサークル立ち上げを思い付いたなんて言ってませんよ

全PC・スマフォに名前をつけてみた

私の持っているすべてのPCとスマートフォンに、スター・ウォーズになぞらえて名前をつけてみた。

PC

いずれも帝国軍のスター・デストロイヤー。

  • メイン機(Windows XP) → 「Executer
    エグゼキューター、ダース・ヴェイダーの旗艦。旧三部作すべてに登場する。
  • メイン機(Windows 7) → 「Lusankya
    ルサンキア、エグゼキューターの姉妹艦。バクタ大戦のさなかで新共和国軍に拿捕され、ユージャン・ヴォング戦争でその真価を発揮する。
  • VAIO(Windows 7) → 「Iron Fist
    アイアン・フィスト、グランドモフ・ズンジの旗艦。
  • MacMini(Mac OSX 10.4) → 「Avenger
    アヴェンジャー、エグゼキューターと共に活躍。

携帯端末

こちらは各勢力の小型船や戦闘機。

  • HTC EVO 3D ISW12HT(Android 4.0) → 「YT-1300 Millennium Falcon
    ミレニアム・ファルコン、言わずと知れたハン・ソロ船長の愛機。
  • Motorola Photon ISW11M(Android 2.3) → 「Incom T-65 X-wing starfighter
    Xウイング、こちらも言わずと知れた反乱同盟軍の主力戦闘機。
  • Apple iPhone 4(iOS 5.0) → 「Ro’ik chuun m’arh Ksstarr
    クスター、ユージャン・ヴォング戦争で新共和国軍に拿捕されたユージャン・ヴォングの巡航艦。またの名をトリックスター。
  • Apple iPod touch 4(iOS 5.0) → 「Firespray-31 Slave I
    スレーヴ・ワン、賞金稼ぎボバ・フェットの愛機。

さらに、携帯端末の中で唯一回線契約をしているEVO3DからのテザリングSSIDは「Hydian Way」ハイディアン・ウェイ、銀河の開拓を推し進めたハイパースペース・レーン。(スター・ウォーズ・ファンでもある高瀬一矢氏作曲のI’veの楽曲名と同じ)

よく家の無線LANが落ちるのでテザリングでPCを使うことも少なくないのだけど、ミレニアム・ファルコンから飛ばす電波をアイアン・フィストで拾うという、実際のスター・ウォーズではあり得ない光景も。(ミレニアム・ファルコンのハン・ソロとアイアン・フィストのグランドモフ・ズンジは宿敵)

それにしても日本語のスター・ウォーズ関連サイトは情報量が少ないねー、英語版Wookieepediaとか凄いのに。

HTC EVO 3D ISW12HT を Android 4.0 にアップデートしてみた

私がメインの携帯として使っているauのEVO 3Dが、Android 4.0にアップデート可能になったという公式発表があった。
auスマートフォン「HTC EVO 3D ISW12HT」の「OSアップデート」についてのお知らせ

さっそくアップデートを試してみた。
念のためデータをHTC Syncでバックアップ。
アドレス帳のバックアップ先がOutlook Express限定ってのはどうなのよ。

とまぁ、作業なんぞをしてる間にアップデート完了。
UIがいろいろ変更されてる。

Android 2.3.4 のときのUI
EVO 3D (Android 2.3.4)
Andid 4.0.3 のUI
EVO 3D (Android 4.0.3)


モノ子ッッ可愛いよおかわいいいよォモノ子ォォアアァァアアァッ!!
ロック画面のみだったアプリケーションショートカット欄がホーム画面に追加され、ここにフォルダを配置することで非常に使いやすくなる。
デフォルトのフォントが変更になっているようで、半角文字に若干の違和感。
噂にもなってる通りバッテリー消費が早くなった……
あと気になるのが、Google IME で入力する際に妙にもたつくこと。

まぁAndroid高速化とバッテリー消費軽減について研究しますかー。

テーマ「FLAT」のHTML5化

導入しているWordPressテーマ「FLAT」をHTML5に対応させてみる。

……しかし、WordPress本体が直接HTMLを吐き出している箇所が多くあるため、完全なHTML5化は本体を改造しない限り不可能。
そんなわけで、「div id=”header”」を「header」に書き換えるなど、基本的なタグの置き換えに留まる。

と、ここで問題が。
このテーマは常に右下に「ページ最上部に戻るためのボタン」が表示されているが、
リンク先が「#header」だったため、headerタグに置き換えてしまうとリンクが機能しなくなる。
そこで、無条件でスクロールするようjavascriptを書き換えてみた。

scroll.js

	jQuery('a[href*=#header],a[href*=#respond]').click(function() {
		if (location.pathname.replace(/^\//,'') == this.pathname.replace(/^\//,'') && location.hostname == this.hostname) {
			var $target = jQuery(this.hash);
			$target = $target.length && $target || jQuery('[name=' + this.hash.slice(1) +']');
			if ($target.length) {
				var targetOffset = $target.offset().top;
				jQuery('html,body').animate({ scrollTop: targetOffset }, 1200, 'quart');
				return false;
			}
		}
	});

これを以下のように書き換えて、リンク先が「#header」の場合は無条件でページの先頭までスクロールするように。

	jQuery('a[href*=#header]').click(function() {
		if (location.pathname.replace(/^\//,'') == this.pathname.replace(/^\//,'') && location.hostname == this.hostname) {
			jQuery('html,body').animate({ scrollTop: 0 }, 1200, 'quart');
			return false;
		}
	});

	jQuery('a[href*=#respond]').click(function() {
		if (location.pathname.replace(/^\//,'') == this.pathname.replace(/^\//,'') && location.hostname == this.hostname) {
			var $target = jQuery(this.hash);
			$target = $target.length && $target || jQuery('[name=' + this.hash.slice(1) +']');
			if ($target.length) {
				var targetOffset = $target.offset().top;
				jQuery('html,body').animate({ scrollTop: targetOffset }, 1200, 'quart');
				return false;
			}
		}
	});

return top