鈴木うどんの横須賀おもしろ生活

撮った写真や思ったことや技術ネタなど。出来るだけ大きなディスプレイで見ると良いと思う。ここでの発言は個人の見解であり、所属する組織の公式見解ではありません。

ブログの写真を良い感じの大きさで表示できるようにした;目先の問題にとらわれると禄でも無いという話

伊達や酔狂のレベルではあるけれど、写真ブログを気取っているからには、ブログエントリの画像は可能な限り良い感じに表示させたいと思っている。良い感じというのは鑑賞するのに適切なサイズで表示させたいということだ。一般的に用いられているディスプレイデバイスは解像度もサイズも鑑賞に不足しているので、結局はなるべく大きなサイズで表示したいということになる。本来であれば300dpiでA3サイズ程度に表示できれば満足であるけれども、27インチのWQHD解像度のディスプレイで目一杯の大きさで表示させてみると、それなりに迫力があって、まぁこれでも良いかという感じである。はてな界隈で有名な平民日記などは、以前から写真が大きくて表示さるようになっていて良いなあと思っていた。

さて、闇雲に画像を大きく表示しようとしても、デバイスによっては表示領域以上に大きく表示されてしまうという問題に直面する。例えば、幅1600pxの画像を掲載しようとしても、ほとんどの場合で問題となってしまうだろう。デジタルスチルカメラで撮影した写真のアスペクト比は4:3か3:2がほとんどなので、その高さは1000px強となる。現在、一般的と思われるフルHD解像度のディスプレイは1080ラインであるから、一見問題ないようであるが、残念ながら一般的なWebブラウザは上下に操作用のUIを配置しているので、(ワイド画面はただでさえ横に表示領域が長いにもかかわらず!)縦の表示領域が狭くなり画像がはみ出してしまうだろう。だからと言って、1024px幅程度のフルHD解像度に配慮したサイズに一律に調整してしまうと、WQHD解像度のような高解像度ディスプレイで閲覧した場合での迫力がいささか不足してしまう。

これまで、この問題に対してCSSの@media記述を用いて表示領域のサイズごとにimg要素に対してmax-widthプロパティを指定することで対処していた。max-widthプロパティはその名の通り、要素の幅の最大値を設定するプロパティだ。最近のブラウザは要素の高さを司るheightプロパティにautoの値を設定しておけば、ブラウザが幅に合わせて自動的にアスペクト比を保ったまま画像の拡大縮小の処理を行う。その結果、多様な解像度のディスプレイでもほぼ同じように画面いっぱいに画像を表示することができるようになった。このアプローチは環境によって表示サイズを調整するのに容易な選択であった。

こうして環境に対する柔軟性を持たせていたが、実は横構図の写真の場合についてしか対処しておらず、縦構図の写真では横構図のそれと比べて異様に大きく表示されてしまう場合がある。なぜならば、 このアプローチは幅の観点からしかケアしてないからだ。実例を挙げるとすると、上記同様に1600px幅の写真の場合では、縦構図の場合では横幅は1000px強の画像となる。現在では幾分か控えめな表示領域であるXGAの場合であっても、幅1000px強の画像は、幅の観点からだけ見れば十分画面に収まるサイズとなってしまう。しかしながら、実際は縦の表示領域が768pxしか無いので、高さが1600pxにも及ぶ画像は大幅にはみ出して表示されてしまう。

この問題へのCSSだけを用いた対処は、現状では幅を指定して自動的に高さを調整する機能はあれど逆は無いという仕様の制約から非常に困難であると考える。このような仕様はWebページの特性、つまり基本的なWebページのモデルの考え方に由来していると思われる。Webページは太古から存在する形態である巻物状(volumen)のモデルをベースとしており、横書き文化圏においては高さの制約にまつわる考え方が全く存在してない。一方で、現在一般的な書籍は冊子本状(codex)は、幅だけでなく高さの制約が存在している。現在、このようなWebページの冊子本庄形態の表示を実現する取り組みとして、Web上の有志によってJavaScriptを用いて、プレゼンテーション用のスライドをブラウザ上で実現す活動などがいくつか観測されている。しかしながら、Webの標準化団体であるW3C上でそのような機能を実現する標準化活動のワーキンググループは存在していないように見える。

このように、CSSだけで適切なサイズで画像を表示することは困難であると判明したので、JavaScriptを用いて対症療法的に縦写真を横写真の大きさに可能な限り近づけるコードを書くことにした。通常のブログサービスの場合CSSが編集可能な程度のカスタマイズの幅しか無いが、はてなBlogでは自由に記述したJavaScriptを追加することができるので便利である。というわけで、いくらか複雑なコードとなってしまったが、表示領域の横幅を取得し、アスペクト比を保ちながら高さが取得した値になるよう縮小処理するコードを書いてページのフッタに追加した。しかしながら、結果は無残なものであり、画像は画面からはみ出てしまう場合もあれば、タブレットなどの縦長の表示領域を持つデバイスでは必要以上に縮小されてしまったりと、元と比較しても体験の質はほとんど変わらないばかりかむしろ劣化してしまった。

なぜこのようなことになってしまったのであろうか?それは本来は可能な限り写真を画面いっぱいに表示させたいという課題に対して取り組んでいたにも関わらず、縦構図と横構図の写真の表示サイズがバラバラになるという事象に対して課題意識を持たずに対処していたことが原因であろう。所謂「本質を見失っていた」という訳である。目先に発生した問題と思われる事象をひっくり返して逆にするだけでは往々にして課題解決とはならないものであるという当たり前のことを忘れていたのであった。

しかしながら、実はこの手の現象はしばしば発生しているのかもしれない気がしている。例えばアジャイル開発上でのIssueの登録は、ほとんどの場合でリジェクトされることは無いが、そのIssueに対して、いつの間にか誰かが誤った対症療法を施してしまって酷いことになってしまうという現象は想像するのに容易い。

恐らくは、事実そのものを何も考えずにIssueに登録してしまうとひどいことになってしまうのだろう。「◯◯できるようにする」というところまで噛み砕いてみることによって、そこではじめて議論が成立するようになる。今回の場合で考えると、「縦構図と横構図の写真でサイズがバラバラになってしまう」というのは正しく事象を示しているから誰も批判出来ないが、この記述は大きな危険性を孕んでいる。このIssueをひっくり返して逆にすると「縦構図と横構図の写真でサイズを統一して表示するようにする」となってしまって、本来の目的と乖離するものとなってしまうからだ。適切なIssueに噛み砕くとすると「写真を表示領域からはみ出させずになるべく大きく表示するようにする」といった程度のものだろう。今回の場合では1人だけでコードを書いていので、誤りに気づいてすぐに方針変更できたが、ある程度の人数のチームでやっていると、誰かが過ちに気づいても軌道修正には時間がかかりそうだ。

というわけで、はじめの方針に立ち返り、幅と高さの双方を考慮して画面いっぱいに画像を収めるコードを追加した。すると、元よりかはいくらかシンプルなコードとなった。

今回の一件を通し、目先の問題だけにとらわれていると本当に禄でもないということを痛感した。しかしながら、いかなる状況においても俯瞰して物事を洞察し思考を進めることが可能であるとは限らない。そのため、「仕組み」で解決するべきであることは明らかであるが、この実現はなかなか難しい。

!function imageSizeOptimizer($){
    var TARGET_IMAGES_SELECTER_STRING = "div.entry-content p img";
    var ENTRY_INNER_SELECTER_STRING = "div.entry-inner";
    var $targetImages;
    var $entryInner;
    var $window = $(window);

    var getActualDimension = function(image) {
        var run, mem, w, h, key = "actual";
        // for Firefox, Safari, Google Chrome
        if ("naturalWidth" in image) {
            return {width: image.naturalWidth, height: image.naturalHeight};
        }
        if ("src" in image) { // HTMLImageElement
            if (image[key] && image[key].src === image.src){
                return  image[key];
            }
            if (document.uniqueID) { // for IE
                w = $(image).css("width");
                h = $(image).css("height");
            } else { // for Opera and Other
                // keep current style
                mem = {w: image.width, h: image.height}; 
                // remove attributes in the case that img-element has set width and height (for webkit browsers)
                $(this).removeAttr("width").removeAttr("height").css({width:"",  height:""});    
                w = image.width;
                h = image.height;
                image.width  = mem.w; // restore
                image.height = mem.h;
            }
            return image[key] = {width: w, height: h, src: image.src}; // bond
        }
        // HTMLCanvasElement
        return {width: image.width, height: image.height};
    }
    var imageSizeOptimize = function(targets){
        var maxWidth = $window.width();
        var maxHeight = Math.min($window.width(), $window.height());
        var innerWidth = $entryInner.width();
        Array.prototype.forEach.call(targets, function(imageElm){
            var tempImage = new Image();
            // process after complete to load image
            tempImage.onload = function(){
                var actualDimension = getActualDimension(tempImage);
                var ratio = actualDimension.width / actualDimension.height;
                var height = Math.min(maxHeight, actualDimension.height);
                var width = height * ratio;
                if(width > maxWidth){
                    width = maxWidth;
                    height = width / ratio;
                }
                if(width > innerWidth){
                    $(imageElm).css("position", "relative")
                        .css("left", (innerWidth - width)/2);
                } else {
                    $(imageElm).css("position", "static")
                }
                imageElm.width = width;
                imageElm.height = height;
            };
            tempImage.src = imageElm.src;
        });
    }
    $(document).ready(function(){
        $targetImages = $(TARGET_IMAGES_SELECTER_STRING);
        $entryInner = $(ENTRY_INNER_SELECTER_STRING);
        imageSizeOptimize($targetImages);
        /* in the case of window resized */
        var timer = false;
        $window.resize(function() {
            if (timer !== false) {
                clearTimeout(timer);
            }
            timer = setTimeout(function() {
                imageSizeOptimize($targetImages);
            }, 200);
        });
    });
}(jQuery);

Reference

最新のコードは以下から入手可能である。

hatenablog-unofficial-modules/image_size_optimize.js at master · udonchan/hatenablog-unofficial-modules

HTML5を用いたブラウザ上でのプレゼンテーションツールの有名なものに以下のBunkrが存在している。

Bunkr is a collaborative online presentation tool

img要素のオリジナルの画像のサイズを取得するコードのクロスブラウザ対応は以下のサイトを参照した。

JavaScript で画像本来のサイズ(幅, 高さ)を取得する | dogmap.jp