GAEで作るサムネイル画像作成サービス
Google App Engineでは画像変換サービスが提供されていて、画像サムネイルサービスをさくっと作れたりします。ということで作成手順をまとめてみました。
仕組み
サービスの仕組みは以下のとおりです。
画像のアップロード
- formのファイル選択を利用して、画像をサーバーにPOST。
- マルチパートフォームデータとして画像がサーバーに送られます。
- サーブレットでマルチパートフォームデータを解析し、画像データを取得。
- 解析には「commons-fileupload」を利用します。
- GAEの「ImageService」で画像データを処理し、サムネイルを作成します。
- 作成したサムネイルとオリジナルの画像をデータストアに保存。
- レスポンスとして、サムネイルの表示時に使うサムネイルIDを返却します。
画像は、他に適切な場所が見当たらなかったので、とりあえずデータストアに保存しています。
画像の参照
- サムネイル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>
ソースコード
一覧
- 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サーバーでは可能)のでご注意。