何度やっても同じ

ただの日記

ブログとか縦長のサイトで下にスクロールしまくってもサイドバーのコンテンツを追跡表示しつづけるあれを修正

position: fixed に設定すると、設定した瞬間のleft値で固定されてしまうので、横にスクロールした場合、追跡表示要素がx軸的にも同じ位置に表示されてしまう。じゃますぎ。

というわけで、

http://xfan.hateblo.jp/entry/2013/01/20/101604

var offsetLeft = self.offset().left;  // これと
$(window).scroll(function() {
  self.css($(this).scrollTop() > offsetTop ? {
    left : offsetLeft - $(this).scrollLeft(),  // これ
    // ...略
  } : initialCss);
});

などとコードを追加。元記事を修正しておいた。

GAEの開発環境という言葉がまぎらわしい

デプロイしたからといってそこが本番環境とは限らないのだよね。GAE上に開発用アプリケーションをつくることはよくあるわけで。

特にホスト名の管理が必要なアプリの場合。たとえば、あるURLを特定のホスト名でしか見れなくする、あるいはリダイレクトする、あるいはcanonical urlを設定したいときも。個人的にはよくある。

そんなときに、ローカルの開発環境ではこのホスト名、開発用アプリケーションにデプロイしたときはこのホスト名、本番環境ではこのホスト名、というのをヘタにコードにすると、

if (AppEngineUtil.isProduction() && env.isDevelopment()) {
  // ...
}

みたいな、どっちなんだよ!状態になったりして。

最近は↓のようなユーティリティをつくるようになりました。

public class HogeUtil {

    public static boolean isRunningOnServer() {
        return SystemProperty.environment.value() ==
            SystemProperty.Environment.Value.Production;
    }

    public static boolean isRunningOnLocalhost() {
        return SystemProperty.environment.value() ==
            SystemProperty.Environment.Value.Development;
    }
}

ブログとか縦長のサイトで下にスクロールしまくってもサイドバーのコンテンツを追跡表示しつづけるあれ

jQueryプラグインにした。あとでプロジェクトにまとめてgithubにUPしよう。

(function($) {
  $.fn.track = function(offset) {
    offset = offset || 0;
    this.each(function() {
      var self = $(this);
      var initialCss = {
        position: self.css("position"),
        top: self.css("top"),
        width: self.css("width")
      };
      var width = self.css("box-sizing") === "border-box" ? 
        self.outerWidth() : self.width();
      var offsetTop = self.offset().top - offset - parseInt(self.css("margin-top"));
      var offsetLeft = self.offset().left;
      $(window).scroll(function() {
        self.css($(this).scrollTop() > offsetTop ? {
          position: "fixed",
          top: offset,
          left: offsetLeft - $(this).scrollLeft(),
          width: width
        } : initialCss);
      });
    });
  };
})(jQuery);

特定の位置までスクロールされると該当要素のpositionをfixedに変更するのが基本なんだけど、fixedにしたときにtopプロパティがautoのままではまずいのと、widthがautoまたは%指定のように親要素に依存する設定ではまずいということで、その辺りをごにょっと。

そういえばImagesServiceで元画像がgifだと画像データがおかしくなる

ネガみたいな色になったり、PNGで出力しても透過背景がまっくろになったり。数年前にも同じ問題で悩んだ記憶はあるのだけど、どうやって解決したのか覚えていなくて再び悩んでしまいました。二度と悩まないようメモっておこうと思う。

解決方法は、この問題が起きるのは開発環境だけだから気にしない

ブロブストアに格納した画像データの扱い方

編集せずそのまま返したい場合

ふつうにBlobstoreからレスポンスに流し込めばおk。

BlobKey key = new BlobKey(asString("key"));
BlobstoreService blobstore = BlobstoreServiceFactory.getBlobstoreService();
blobstore.serve(key, response);

サムネイルを返したい場合

方法を3パターンに分けて紹介します。

ImagesService#getServingUrl をつかう

ImagesService#getServingUrlは、ブロブストアに格納した画像を取得する専用のURLを生成するメソッドです。つまり、毎回サムネイルが要求されるたびに実行するのではなく、画像がブロブストアにアップロードされたタイミングで一度だけURLの生成を行い、画像情報用のエンティティあたりに紐付けておくものです。

たとえばこんなモデルを作ってですね。。。

@Model(schemaVersion = 1)
public class Image implements Serializable {

    // keyプロパティやアクセサなどは省略

    private BlobKey blobKey;

    // ここにgetServingUrlで取得したURLを格納
    private String thumbnailUrl;
}

アップロード後の処理を行うコントローラでこんな感じでURLを生成しておきます。

// asBlobKeyの処理内容は察してね
BlobKey blobKey = asBlobKey("file");

String thumbnailUrl =
    ImagesServiceFactory.getImagesService().getServingUrl(
        ServingUrlOptions.Builder.withBlobKey(blobKey));

Image image = new Image();
image.setBlobKey(blobKey);
image.setThumbnailUrl(thumbnailUrl);
Datastore.put(image);

生成されるURLのメリットは高速であること、アプリケーションインスタンスが不要であることです。デメリットは、画像操作がリサイズとトリミングしかできないこと、publicなURLしか生成できないこと。とはいえサムネイル用途なら他の画像操作は通常不要ですし、URLは推測不可能な文字列なので、公開ページにリンクさえ張らなければ事実上のprivateなURLとしても扱えます。要するに、サムネイル用途ならgetServingUrlを使っておけば無難です。

getServingUrlにはオプションとして画像サイズやトリミングを行うかどうかの指定ができますが、これらの指定は一切しないことをオススメします。なぜかというと、サイズとトリミング指定は、基本となるURLにパラメータを付け加えることで実現できるからです。URLに固定でパラメータがついてると柔軟性がなくめんどくさいだけです。

URLにパラメータをつけるときのルールは以下の通り(Imagesサービスの概要ページからの抜粋)。URL末尾に =s32 みたいな文字列を付加するルールです。パラメータといってもクエリ文字列ではありません。

// Resize the image to 32 pixels (aspect-ratio preserved)
http://your_app_id.appspot.com/randomStringImageId=s32

// Crop the image to 32 pixels
http://your_app_id.appspot.com/randomStringImageId=s32-c

画像サイズの縦と横を別々に指定することはできません。縦横比率が維持されない使い方はできなくて(できる必要もないですが)、縦と横のうち長い方が指定サイズをオーバーしないように画像の縮小が行われます。

もうひとつささいなことですが個人的にはポイントだった特徴がありまして、getServingUrlのオプションで指定する画像サイズは、縮小されることがあっても拡大はされません。これはサムネイル生成の振る舞いとしては自然だと思うのですが、プログラムでブロブストアからImageオブジェクトを構築する手法(後述)ではこれを実現するのがなんだか面倒なのです。

画像が削除された場合などにURLを無効にしたい場合は、deleteServingUrlメソッドを使います。

ImagesServiceFactory.makeImageFromBlob をつかう

ブロブストアから画像を読み込んでImagesServiceで操作する手法です。以下のような感じ。binaryメソッドは勝手に作りました。処理内容は察してください。

Image image = ImagesServiceFactory.makeImageFromBlob(blobKey);
Transform resize = ImagesServiceFactory.makeResize(80, 80);
ImagesService service = ImagesServiceFactory.getImagesService();
image = service.applyTransform(resize, image, OutputEncoding.PNG);
byte[] data = image.getImageData();

return binary(data, "image/png");

この方法で困ってしまうのは、オリジナル画像が指定サイズよりも小さかった場合に、画像が拡大されてしまうことです。逆にそれを求めている場合や、その他(縮小とトリミング以外)の操作をしたい場合はgetServingUrlではなくこちらの方法を選択することになります。

ただ、また困ったことに、ブロブストアからmakeImageFromBlobで読み出したImageオブジェクトはいくつかのメソッドに対応しておらず、呼び出すとUnsupportedOperationExceptionを投げてきます。たとえば、上記の例で、画像の拡大はしたくないということで次のようなコードを書くと、Image#getWidthとImage#getHeightが使えないエラーが発生します。なんて理不尽なんでしょう。

Image image = ImagesServiceFactory.makeImageFromBlob(blobKey);
if (image.getHeight() > 80 || image.getWidth() > 80) {
    Transform resize = ImagesServiceFactory.makeResize(80, 80);
    // ...
}

これでは不便に感じることもありますよね。というわけで、最後のパターンです。

ブロブストアを自力で読み出してImageオブジェクトをつくる

サンプルソース書きます。単にブロブストアから読み出したデータでImageオブジェクトを作成しているだけなので、ふつうImagesServiceでできることは制限なく行えます。前記2つの方法でできないことをやりたい場合はこの方法を選択です。

BlobKey blobKey = new BlobKey(asString("id"));
BlobInfo info = new BlobInfoFactory().loadBlobInfo(blobKey);
int size = (int) info.getSize();
String contentType = info.getContentType();

BlobstoreService blobstore = BlobstoreServiceFactory.getBlobstoreService();
ByteArrayOutputStream out = new ByteArrayOutputStream();
int start = 0;

do {
    int end = Math.min(size, start + BlobstoreService.MAX_BLOB_FETCH_SIZE) - 1;
    byte[] buf = blobstore.fetchData(blobKey, start, end);
    out.write(buf);
    start = end + 1;
} while (start < size);

Image image = ImagesServiceFactory.makeImage(out.toByteArray());
if (image.getHeight() > 80 || image.getWidth() > 80) {
    Transform resize = ImagesServiceFactory.makeResize(80, 80);
    ImagesService service = ImagesServiceFactory.getImagesService();
    image = service.applyTransform(resize, image, OutputEncoding.PNG);
    contentType = "image/png";
}
byte[] data = image.getImageData();

return binary(data, contentType);

Silm3でリクエストパラメータをオブジェクトモデルに自動変換

employees[0].name
employees[0].address.zipCode
employees[0].address.line1
employees[1].name
employees[1].address.zipCode
employees[1].address.line1
...

たとえばこんなリクエストパラメータを受け取って、EmployeeとAddressオブジェクトのグラフを勝手に構築してほしい的な話です。実装してみた。

https://github.com/fxfan/Slim3.StructuredParamRequestHandler

プロパティの型が文字列しか対応してない点とか少々いいかげんですが。ぼちぼち手を加えていきます。

TwitterのOAuth認証プロセスがリダイレクトループしたとき

原因はいくつかパターンがあるようですが、サーバサイドがJavaの場合、誤ったURLリライティングによってリダイレクトループに陥ることがあります。というか、陥りました。

一見問題なさげな次のコードですが(slim3使ってます)。。。

StringBuffer callbackURL = request.getRequestURL();
int index = callbackURL.lastIndexOf("/");
callbackURL.replace(index, callbackURL.length(), "").append("/callback");

Twitter twitter = TwitterService.getTwitterInstance();
RequestToken requestToken = twitter.getOAuthRequestToken(callbackURL.toString());
sessionScope("requestToken", requestToken);

return redirect(requestToken.getAuthenticationURL());

slim3はこの後、redirectメソッドに渡したパスに対してresponse.encodeRedirectURL()を呼び出しておりまして、このコードだとrequestToken.getAuthenticationURL() の戻り値であるtwitter apiのURLをリライティングしてjsessionidをくっつけてしまう場合があるのです。次のような感じに。

http://api.twitter.com/oauth/authenticate;jsessionid=xxxxxx?oauth_token=xxxxxx

URLがこうなってしまうと、あとはtwitter内部の問題なので詳細は知りませんが、リダイレクトループに陥ってしまいます。

これを回避するには、こう。

StringBuffer callbackURL = request.getRequestURL();
int index = callbackURL.lastIndexOf("/");
callbackURL.replace(index, callbackURL.length(), "").append("/callback");

// こっちをURLリライティング
String rewritedURL = response.encodeRedirectURL(callbackURL.toString());

Twitter twitter = TwitterService.getTwitterInstance();
RequestToken requestToken = twitter.getOAuthRequestToken(rewritedURL);
sessionScope("requestToken", requestToken);

// 勝手にURLリライティングしないように自分でリダイレクトロジック書く
response.sendRedirect(requestToken.getAuthenticationURL());
return null;