無料で使えるシステムトレードフレームワーク「Jiji」 をリリースしました!

・OANDA Trade APIを利用した、オープンソースのシステムトレードフレームワークです。
・自分だけの取引アルゴリズムで、誰でも、いますぐ、かんたんに、自動取引を開始できます。

GAEで作るサムネイル画像作成サービス

Google App Engineでは画像変換サービスが提供されていて、画像サムネイルサービスをさくっと作れたりします。ということで作成手順をまとめてみました。

仕組み

サービスの仕組みは以下のとおりです。

画像のアップロード
  1. formのファイル選択を利用して、画像をサーバーにPOST。
    • マルチパートフォームデータとして画像がサーバーに送られます。
  2. サーブレットでマルチパートフォームデータを解析し、画像データを取得。
  3. GAEの「ImageService」で画像データを処理し、サムネイルを作成します。
  4. 作成したサムネイルとオリジナルの画像をデータストアに保存。
  5. レスポンスとして、サムネイルの表示時に使うサムネイルIDを返却します。

画像は、他に適切な場所が見当たらなかったので、とりあえずデータストアに保存しています。

画像の参照
  1. サムネイルidとサイズを指定してGETリクエストを送ると、画像が返されます。
/thumbnail?id=<サムネイルID>&size=<サイズ。small or large。省略するとオリジナル画像になります>

動作サンプル

動作サンプルはこちらから
画像をアップロードするとサムネイルIDが返されるので、それを指定して以下のURLにアクセスするとサムネイルを参照できます。上から、小、大、オリジナルです。

http://20100322.latest.unageanu-test.appspot.com/thumbnail?id=<サムネイルID>&size=small
http://20100322.latest.unageanu-test.appspot.com/thumbnail?id=<サムネイルID>&size=large
http://20100322.latest.unageanu-test.appspot.com/thumbnail?id=<サムネイルID>

処理可能な画像の形式

アップロード可能な画像は、ImageServiceで処理可能な以下の5つになります。

サムネイル画像の形式は「png」で固定です。

ソースコード

一覧
thumbnail.html
画像を選択するformを持つHTMLファイルです。
ThumbnailServlet
POSTでサムネイルの作成、GETでサムネイルの返却を行うサーブレットです。フォームデータの解析やレスポンスの作成などHTTP依存の処理を行い、サムネイルの作成はThumbnailServiceに委譲します。
ThumbnailService
サムネイルの作成とデータストアへの保存を担うサービスです。
ThumbnailServiceImpl
ThumbnailServiceの実装。
Thumbnail
データストアに保存するサムネイルのデータモデル。
thumbnail.html

画像を選択するformを用意します。「enctype="multipart/form-data"」とする点のみ注意。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
  </head>
  <body>
    <form action="/thumbnail"  method="POST" enctype="multipart/form-data">
      <input type="file"  name="imgfile" />
      <input type="submit" />
    </form>
  </body>
</html>
ThumbnailServlet

doPOSTでフォームデータを解析してサムネイルを作成、doGETでサムネイルを取り出して返します。

  • フォームデータの解析は「commons-fileupload」付属の「ServletFileUpload」を使って行います。
  • アップロード可能な画像の最大サイズはとりあえず500KBに制限。ちなみにImageServiceでは1MBまで処理可能。
package test.thumbnail;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.fileupload.FileItemIterator;
import org.apache.commons.fileupload.FileItemStream;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

import test.thumbnail.ThumbnailService.Size;

/**
 * サムネイルサーブレット。
 */
public class ThumbnailServlet extends HttpServlet {
    
    /** SID */
    private static final long serialVersionUID = -2099637840301775480L;
    
    /**ロガー*/
    private static final Logger logger;
    static {
        logger = Logger.getLogger(
                ThumbnailServlet.class.getName());
        logger.setLevel(Level.ALL);
    }
    
    private static final ThumbnailService thumbnailService
        = new ThumbnailServiceImpl();
    
    //  画像データを受け取ってサムネイルを作成する。
    public void doPost(HttpServletRequest req, HttpServletResponse resp) 
    throws ServletException, IOException {
        
        ServletFileUpload upload = new ServletFileUpload();
        upload.setSizeMax(500000); // アップロード可能な画像の最大サイズを500KBに制限
        
        try {
            FileItemIterator iterator = upload.getItemIterator(req);
            while (iterator.hasNext()) {
                try {
                    FileItemStream item = iterator.next();
                    if (item.isFormField()) continue; // フォームデータであればスルー。
                    
                    // 画像データを読み込んでサムネイルを作成する。
                    long id = thumbnailService.create( read( item ) );
                    
                    // サムネイルのIDをレスポンスとして返す。
                    write( String.valueOf(id).getBytes("UTF-8"), "text/html", resp ); 
                    logger.log( Level.INFO, "create icon. id=" + String.valueOf(id) );
                    return;
                } catch ( FileUploadException e ) {
                    logger.log( Level.SEVERE, "put failed", e );
                }
            }
        } catch ( FileUploadException e ) {
            logger.log( Level.SEVERE, "put failed", e );
            throw new IOException(e);
        }
    }
    
    //  画像を返す。
    public void doGet(HttpServletRequest req, HttpServletResponse resp) 
    throws ServletException, IOException {
        
        // パラメータ解決
        Long id = null;
        Size size = Size.ORIGINAL;
        try {
            id = Long.valueOf( req.getParameter( "id" ) );
        } catch ( NumberFormatException e ) {
            resp.setStatus(404);
            return;
        }
        try {
            String str = req.getParameter( "size" );
            if (str != null) size = Size.valueOf( str.toUpperCase() );
        } catch (IllegalArgumentException e) {}
        
        // サムネイルの取り出しと出力
        Thumbnail thumbnail = thumbnailService.get(id);
        switch( size ) {
            case ORIGINAL: 
                write( thumbnail.getOriginal().getBytes(), resolveContentType(thumbnail.getType()), resp ); 
                break;
            case SMALL: 
                write( thumbnail.getSmall().getBytes(), "image/png", resp ); 
                break;
            case LARGE: 
                write( thumbnail.getLarge().getBytes(), "image/png", resp ); 
                break;
        }
    }
    /**
     * コンテントタイプを解決する。
     * @param type 画像種別
     * @return コンテントタイプ
     */
    String resolveContentType( String type ) {
        if ( "ICO".equals(type) ) {
            return "image/vnd.microsoft.icon";
        } else {
            return "image/" + type.toLowerCase();
        }
    }
    
    /**
     * データをレスポンスに出力する。
     * @param bytes データ
     * @param contentType コンテントタイプ
     * @param resp {@link HttpServletResponse}
     * @throws IOException 入出力例外
     */
    void write( byte[] bytes, String contentType, HttpServletResponse resp ) 
    throws IOException {
        resp.setContentType(contentType); 
        ServletOutputStream out = null;
        try {
            out = resp.getOutputStream();
            out.write(bytes, 0, bytes.length);
        } finally {
            if ( out != null ) try {
                out.close();
            } catch ( IOException e ) {
                logger.log( Level.SEVERE, "failed to close stream.", e );
            }
        }
    }

    /**
     * FileItemStreamからバイトデータを読み込む
     * @param item FileItemStream
     * @return バイトデータ
     * @throws IOException 入出力例外
     */
    byte[] read(FileItemStream item) throws IOException {
        InputStream in = null; 
        try {
            in = item.openStream();
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            int l = 0;
            byte[] buffer = new byte[1024];
            while ((l = in.read(buffer, 0, buffer.length)) != -1) {
                out.write(buffer, 0, l);
            }
            return out.toByteArray();
        } finally {
            if ( in != null ) try {
                in.close();
            } catch ( IOException e ) {
                logger.log( Level.SEVERE, "failed to close stream.", e );
            }
        }
    }
}
ThumbnailService

サムネイル作成サービスのインターフェイス

package test.thumbnail;

import java.io.IOException;

/**
 * サムネイル生成サービス
 */
public interface ThumbnailService {
    
    /**
     * サムネイルを作成する。
     * @param image 画像データ
     * @return 作成したサムネイルのID
     * @throws IOException
     */
    long create(byte[] image);

    /**
     * サムネイルを取得する。
     * @param id 画像id
     * @return サムネイル
     * @throws IOException
     */
    Thumbnail get(long id);
    
    /**画像サイズ*/
    enum Size {
        /**オリジナル*/
        ORIGINAL(-1,-1),
        /**大きい*/
        LARGE(400,400),
        /**小さい*/
        SMALL(150,150);
        
        /***/
        public final int width;
        /**高さ*/
        public final int height;
        
        private Size( int width,  int height ) {
            this.width = width;
            this.height = height;
        }
    }
}
ThumbnailServiceImpl

ThumbnailServiceの実装です。

  • ImageServiceを利用して大、小のサムネイルを生成してデータストアに保存します。
  • サムネイルは「縦横の比率を保ったまま短辺にあわせてリサイズ後、中心部をトリミング」して生成。画像がサムネイルサイズより小さい場合は拡大します。
  • ImageServiceでは変換後の画像をjpegまたはpngで出力できます。以下のコードでは特に明示していないのでデフォルトのpngになります。
package test.thumbnail;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManager;
import javax.jdo.PersistenceManagerFactory;

import com.google.appengine.api.datastore.Blob;
import com.google.appengine.api.images.Image;
import com.google.appengine.api.images.ImagesService;
import com.google.appengine.api.images.ImagesServiceFactory;
import com.google.appengine.api.images.Transform;

/**
 * {@link ThumbnailService}の実装。
 */
public class ThumbnailServiceImpl 
implements ThumbnailService {
    
    @Override
    public long create(byte[] imageData) {
        
        // ImageServiceを利用してサムネイルを作成。
        ImagesService imagesService = ImagesServiceFactory.getImagesService();
        Image image = ImagesServiceFactory.makeImage(imageData);
        Blob original = new Blob(image.getImageData());
        Blob small = createThumbnail( image.getImageData(), Size.SMALL, imagesService );
        Blob large = createThumbnail( image.getImageData(), Size.LARGE, imagesService );
        
        // サムネイルをデータストアに保存
        PersistenceManager pm = null;
        try {
            pm = createPersistenceManager();
            Thumbnail thumbnail = new Thumbnail(
                    original, small, large, image.getFormat().name(), new Date()); 
            pm.makePersistent(thumbnail);
            return thumbnail.getId(); // IDを返却
        } finally {
            if ( pm != null ) pm.close();
        }
    }
    
    @Override
    public Thumbnail get(long id) {
        PersistenceManager pm = null;
        try {
            pm = createPersistenceManager();
            return pm.getObjectById(Thumbnail.class, id);
        } finally {
            if ( pm != null ) pm.close();
        }
    }
    
    /**
     * サムネイル画像を生成する。生成規則は以下のとおり。
     * <ol>
     *   <li>縦横の比率を保ったまま、短辺にあわせてリサイズ</li>
     *   <li>中心部をトリミング</li>
     * </ol>
     * @param image 元画像
     * @param size サムネイルサイズ
     * @param imagesService {@link ImagesService}
     * @return 生成した画像データ(PNG形式)
     */
    private Blob createThumbnail( byte[] imageData, Size size, ImagesService imagesService ) {
        // 縦横の比率を保ったまま短辺にあわせてリサイズ後、中心部をトリミング
        Image image = ImagesServiceFactory.makeImage(imageData);
        List<Transform> ts = new ArrayList<Transform>();
        float wr = ((float) size.width ) / image.getWidth();
        float hr = ((float) size.height ) / image.getHeight();
        if ( wr > hr  ) {
            float nh =  image.getHeight()*wr;
            float r =  (1.0F - ((float) size.height ) / nh ) /2;
            ts.add(ImagesServiceFactory.makeResize( size.width, (int) Math.floor( nh ) ));
            ts.add(ImagesServiceFactory.makeCrop( 0, r, 1.0, 1.0-r ));
        } else {
            float nw =  image.getWidth()*hr;
            float r =  (1.0F - ((float) size.width ) / nw ) /2;
            ts.add(ImagesServiceFactory.makeResize( (int) Math.floor( nw ), size.height ));
            ts.add(ImagesServiceFactory.makeCrop( r, 0, 1.0-r, 1.0 ));
        }
        Transform t = ImagesServiceFactory.makeCompositeTransform(ts);
        Image thumbnail = imagesService.applyTransform(t, image);
        return new Blob(thumbnail.getImageData());
    }

    /**
     * {@link PersistenceManager}を取得する。
     * @return {@link PersistenceManager}
     */
    private static PersistenceManager createPersistenceManager() {
        synchronized( ThumbnailServiceImpl.class ) {
            if (pmfInstance == null) pmfInstance = JDOHelper.getPersistenceManagerFactory("transactions-optional");
            return pmfInstance.getPersistenceManager();
        }
    }
    /**
     * {@link PersistenceManagerFactory}
     */
    private static PersistenceManagerFactory pmfInstance = null;
}
Thumbnail

最後は、データストアに格納するモデルクラスです。画像データはBlob型のフィールドにしています。

package test.thumbnail;

import java.util.Arrays;

import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.IdentityType;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;

/**
 * サムネイル
 */
@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Thumbnail {

    /**
     * ID
     */
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Long id;

    /**
     * 作成日時
     */
    @Persistent
    private java.util.Date createDate;

    /**
     * オリジナル画像
     */
    @Persistent
    private com.google.appengine.api.datastore.Blob original;

    /**
     * サムネイル(小)
     */
    @Persistent
    private com.google.appengine.api.datastore.Blob small;

    /**
     * サムネイル(大)
     */
    @Persistent
    private com.google.appengine.api.datastore.Blob large;

    /**
     * 画像種別
     */
    @Persistent
    private String type = "";

    /**
     * コンストラクタ
     */
    public Thumbnail (){}

    /**
     * コンストラクタ
     *
     * @param original
     *        オリジナル画像
     * @param small
     *        サムネイル(小)
     * @param large
     *        サムネイル(大)
     * @param type
     *        画像種別
     * @param createDate
     *        作成日時
     */
    public Thumbnail (
        com.google.appengine.api.datastore.Blob original,
        com.google.appengine.api.datastore.Blob small,
        com.google.appengine.api.datastore.Blob large,
        String type,
        java.util.Date createDate ) {
        this.original = original;
        this.small = small;
        this.large = large;
        this.type = type;
        this.createDate = createDate;
    }
    /**
     * コンストラクタ
     *
     * @param id
     *        ID
     * @param original
     *        オリジナル画像
     * @param small
     *        サムネイル(小)
     * @param large
     *        サムネイル(大)
     * @param type
     *        画像種別
     * @param createDate
     *        作成日時
     */
    public Thumbnail (
        Long id,
        com.google.appengine.api.datastore.Blob original,
        com.google.appengine.api.datastore.Blob small,
        com.google.appengine.api.datastore.Blob large,
        String type,
        java.util.Date createDate ) {
        this.id = id;
        this.original = original;
        this.small = small;
        this.large = large;
        this.type = type;
        this.createDate = createDate;
    }

    /**
     * IDを取得する。
     * @return ID
     */
    public Long getId () {
        return id;
    }
    /**
     * IDを設定する。
     * @param id ID
     */
    public void setId ( Long id ) {
        this.id = id;
    }
    /**
     * 作成日時を取得する。
     * @return 作成日時
     */
    public java.util.Date getCreateDate () {
        return createDate;
    }
    /**
     * 作成日時を設定する。
     * @param createDate 作成日時
     */
    public void setCreateDate ( java.util.Date createDate ) {
        this.createDate = createDate;
    }
    /**
     * オリジナル画像を取得する。
     * @return オリジナル画像
     */
    public com.google.appengine.api.datastore.Blob getOriginal () {
        return original;
    }
    /**
     * オリジナル画像を設定する。
     * @param original オリジナル画像
     */
    public void setOriginal ( com.google.appengine.api.datastore.Blob original ) {
        this.original = original;
    }
    /**
     * サムネイル(小)を取得する。
     * @return サムネイル(小)
     */
    public com.google.appengine.api.datastore.Blob getSmall () {
        return small;
    }
    /**
     * サムネイル(小)を設定する。
     * @param small サムネイル(小)
     */
    public void setSmall ( com.google.appengine.api.datastore.Blob small ) {
        this.small = small;
    }
    /**
     * サムネイル(大)を取得する。
     * @return サムネイル(大)
     */
    public com.google.appengine.api.datastore.Blob getLarge () {
        return large;
    }
    /**
     * サムネイル(大)を設定する。
     * @param large サムネイル(大)
     */
    public void setLarge ( com.google.appengine.api.datastore.Blob large ) {
        this.large = large;
    }
    /**
     * 画像種別を取得する。
     * @return 画像種別
     */
    public String getType () {
        return type;
    }
    /**
     * 画像種別を設定する。
     * @param type 画像種別
     */
    public void setType ( String type ) {
        this.type = type;
    }
    public boolean equals ( Object obj ) {
        if ( obj == null ) { return false; }
        if ( obj instanceof Thumbnail ) {
            Object[] that = getValues((Thumbnail) obj);
            return Arrays.deepEquals(that, getValues(this)) ;
        }
        return false;
    }
    public int hashCode () {
        return Arrays.deepHashCode(getValues(this)) ;
    }
    private static Object[] getValues( Thumbnail v) {
        return new Object[] {
            v.id,
            v.createDate,
            v.original,
            v.small,
            v.large,
            v.type
        };
    }
}

補足

  • ローカルサーバーとGAEサーバーとで出力される画像がだいぶ違います・・・。ローカルだと縮小時のギザギザが気になったのですが、デプロイしてみたらそうでもない。
  • あと、ローカルではtiffは処理できない(GAEサーバーでは可能)のでご注意。