最終更新日:2020/01/16 原本2007/03/12

サーバサイドJavaにおける画像ファイルの読み書き

サーバサイドではじめるJavaグラフィック講座 3

今回はサーバサイドにおけるイメージの利用について考えてみます。基本であるイメージファイルの読み書きと、イメージを加工する手法について説明します。

はじめに

 前回までの説明で、Graphics/Graphics2Dによる図形描画の基本については一通り分かりました。今回はイメージの描画について説明を行います。ファイルからの読み込みや書き出し、イメージの描画、そしてイメージを加工するためのクリッピングなどについて説明を行いましょう。

過去の記事

対象読者

  • Javaの基本およびJavaによるWeb開発の基礎(JSP/サーブレット程度)をマスターしている人。
  • グラフィック関連のプログラミング経験があまりない人。
  • Javaのグラフィック処理を学び直したい人。

イメージファイル・アクセス

 イメージを利用する場合には、「イメージファイルからのイメージの読み込み」「イメージの加工」「イメージのファイルへの書き出し」といった操作を行うことになります。サーバサイドでは、場合によってファイルへの書き出しを行わず直接クライアントへ送信することもあるでしょうが、ここではファイル利用の基本ということで保存までをセットで考えることにしましょう。

 イメージファイルの読み書きは、以前はかなり大変でした。読み込みはそうでもありませんが、書き出しがかなり制約されていたのです。また、扱えるファイルのフォーマットも限定されていました。今現在、広く使われているJava2 5.0/6.0についてはこれらの点が改良されているので、かなり簡単にイメージファイルを利用することができます。

 では、これも簡単なサンプルを用意して説明することにしましょう。ファイル名を送信すると、そのファイルを読み込んで加工し、指定のファイル名で保存するサーブレットを作成します。これをJSPを通じてアクセスすることで、イメージの加工処理を行わせることにします。

 まずは、フロントエンドとなるJSPからです。ここでは「test.jsp」というファイル名で用意しておくことにします。

test.jsp
<%@ page language="java" contentType="text/html; charset=Shift_JIS"
    pageEncoding="Shift_JIS"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
    <head>
        <meta http-equiv="Content-Type" 
              content="text/html; charset=Shift_JIS">
        <title>Insert title here</title>
    </head>
    <body>
        ※編集するファイル名<br>
        <form method="post" action="./graphicsImg">
            <input type="text" name="path">
            <input type="submit">
        </form>
        <br><br>
        
        ※元のイメージ<br>
        <img src="./<%=request.getParameter("path") %>">
        <br><br>
        ※保存されたイメージ<br>
        <img src="./new_saved.png">
    </body>
</html>

 ここでは、フォームからファイル名を「./grahicsImg」に送信するようにしてあります。またその下に、送信したファイル名と、それを元に新たに保存されたイメージ(new_saved.png)をimgタグで表示するようにしてあります。元のイメージと保存されたイメージを並べて表示し、比較するようにしてみました。

サーブレットの作成

 続いて、サーブレットの作成です。ここでは「SampleGraphicsFromImageFile」というクラスとして用意し、これを「/graphicsImg」というURIでアクセスできるようにしておきます。

SampleGraphicsFromImageFile.java
package jp.tuyano.codezine;

import java.awt.*;
import java.awt.image.*;
import java.io.*;
import javax.imageio.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class SampleGraphicsFromImageFile extends HttpServlet
        implements javax.servlet.Servlet {
    private static final long serialVersionUID = 1L;

    public SampleGraphicsFromImageFile() {
        super();
    }

    protected void doGet(HttpServletRequest request,
            HttpServletResponse response)
            throws ServletException, IOException {
        
        this.doAction(request, response);
    }

    protected void doPost(HttpServletRequest request,
            HttpServletResponse response)
            throws ServletException, IOException {
        
        this.doAction(request, response);
    }

    public void doAction(HttpServletRequest request,
            HttpServletResponse response)
            throws ServletException, IOException {
        
        String fname = request.getParameter("path");
        String dir = getServletContext().getRealPath("/");
        BufferedImage im = null;
        
        // イメージの読み込み
        File f = new File(dir + fname);
        if (f.exists()) {
            try {
                im = ImageIO.read(f);
            } catch (IOException e) {
                System.out.println("can't read from file.");
                im = new BufferedImage(300,200,
                        BufferedImage.TYPE_INT_RGB); // 仮のイメージ
            }
        } else {
            im = new BufferedImage(300,200,
                    BufferedImage.TYPE_INT_RGB); // 仮のイメージ
        }
        
        // イメージ描画
        im = getModifiedImage(im);

        // イメージの書き出し
        FileOutputStream out = null;
        try {
            out = new FileOutputStream(dir + "new_saved.png");
            ImageIO.write(im,"png",out);
            out.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally{
            try {
                out.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
        
        // リダイレクト
        getServletContext().getRequestDispatcher("/test.jsp")
                .forward(request, response);
    }
    
    // 描画処理
    public BufferedImage getModifiedImage(BufferedImage im){
        Graphics2D g = im.createGraphics();
        g.setColor(Color.BLACK);
        g.setFont(new Font("Monospace",Font.BOLD,24));
        g.drawString("Modified Image.", 26, 26);
        g.drawString("Modified Image.", 23, 23);
        g.setColor(Color.RED);
        g.drawString("Modified Image.", 24, 24);
        g.dispose();
        return im;
    }
}
/WEB-INF/web.xmlへの追記
<servlet>
    <description>
    </description>
    <display-name>
    SampleGraphicsFromImageFile</display-name>
    <servlet-name>SampleGraphicsFromImageFile</servlet-name>
    <servlet-class>
    jp.tuyano.codezine.SampleGraphicsFromImageFile</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>SampleGraphicsFromImageFile</servlet-name>
    <url-pattern>/graphicsImg</url-pattern>
</servlet-mapping>

 JSPからファイル名を送信すると、そのファイル名のイメージを読み込み、テキストを追加して表示します。イメージファイルは適当なものを用意してください。

ファイルアクセスの手順

 サーブレットでは、doActionメソッドでファイルの読み込みと書き出しの処理を行っています。まず、ファイルの読み込みから見てみましょう。最初に、送信されたパラメータの値と、Webアプリケーションのパスから、読み込むファイルのパスを取得しておきます。

String fname = request.getParameter("path");
String dir = getServletContext().getRealPath("/");

File f = new File(dir + fname);

 ここでは、ServletContextgetRealPathを使ってディレクトリのパスを取得し、これを元にFileインスタンスを作成します。

if (f.exists()) {
    try {
        im = ImageIO.read(f);
    }

 まず、existsでファイルが存在しているかどうかを調べ、存在している場合にはImageIO.read読み込みを行います。これはさまざまな使い方ができますが、最も簡単なのはFileインスタンスを引数に渡して呼び出す方法でしょう。たったこれだけで、Fileインスタンスのファイルを読み込んでBufferedImageとして得ることができます。

 続いて、書き出し処理です。これは、ファイルのパスを指定してFileOutputStreamインスタンスを作成し、ImageIO.writeを呼び出します。

out = new FileOutputStream(dir + "new_saved.png");
ImageIO.write(im,"png",out);
out.flush();

 ImageIO.writeは、引数に書き出すBufferedImage、書き出すフォーマットを示すString、書き出し先となるOutputStreamをそれぞれ指定します。FileOutputStreamを指定すれば、そのままファイルとして保存することができるというわけです。

 このImageIOクラスが用意されて以後、イメージファイルのアクセスは劇的に簡単になりました。Fileを用意し、read/writeを呼び出すだけで済んでしまうのです。

イメージの描画

 続いて、読み込んだイメージを利用した描画について説明しましょう。イメージの描画は、Graphicsクラスに用意されているdrawImageメソッドで行うのが基本です。これは、単に位置を指定して描画を行わせる他、描画元と描画先の領域を指定し拡大縮小して描かせたりすることも可能です。

イメージの縮小描画
public BufferedImage getModifiedImage(BufferedImage im){
    int w1 = im.getWidth(),h1 = im.getHeight();
    int w2 = w1 / 2,h2 = h1 / 2;
    BufferedImage im2
       = new BufferedImage(w1,h1,BufferedImage.TYPE_INT_ARGB);
    Graphics2D g2 = im2.createGraphics();
    for(int i = 0;i < 2;i++)
        for(int j = 0;j < 2;j++){
            System.out.println(w2 * i + ":" + h2 * j);
            g2.drawImage(im,w2 * i,h2 * j,w2,h2,null);
        }
    g2.dispose();
    return im2;
}
イメージを縮小し、2×2に配置する。
イメージを縮小し、2×2に配置する。

 先の「SampleGraphicsFromImageFile」に用意しておいた描画処理のメソッドgetModifiedImageを修正して、イメージの描画を行わせてみました。元になるイメージを縦横2分の1に縮小し縦横2×2に並べて描画します。

 描画を行っているdrawImageは、引数の異なるいくつかのものが用意されています。ざっと整理してみましょう。

描画位置を指定する
drawImage(Image img, int x, int y, ImageObserver io);
描画する領域を指定する
drawImage(Image img, int x, int y, int width, int height, ImageObserver io);
描画元と描画先の領域をそれぞれ指定する
drawImage(Image img, int x1, int y2, int x2, int y2, 
                     int x3, int y3, int x4, int y4, ImageObserver io);

 いずれも、最初に描画するImageインスタンスを渡すという点は同じです。その後に続く一連のintパラメータによって描画の方法が変わります。

 1つ目は最も単純な方法で、描画する位置のみを指定し、そこにイメージを描画します。

 2つ目はイメージを拡大縮小して描画したい場合のやり方で、位置と大きさをそれぞれ指定して送信します。

 3つ目は、元のイメージの特定の領域を拡大縮小して描画したい場合の方法です。ここでは、描画元の領域の左上と右下の位置、そして描画先の領域の左上と右下の位置、計4つの位置を渡します。

 全ての方法の最後にあるImageObserverというのは、描画の状態を通達するインスタンスを指定します。これは、AWTなどのGUI利用プログラムでイメージを扱う際に、描画の状況を何らかの形でプログラムが把握する必要がある場合を考えて用意されたものと言えます。

 AWTやSwingでは、コンテナは基本的にImageObserverをimplementsしていますから、これは単純にthisを指定するだけで済みます。ですが、サーブレットにはImageObserverはimplementsされていませんから、自身を指定することはできません。

 ここでは、イメージ更新の通達は不要ということでnull指定してあります。もしイメージの状況を把握する必要がある場合には、サーブレット自身にImageObserverをimplementsして対応することができます。これには、まずサーブレットの定義を、次のように修正します。

public class SampleGraphicsFromImageFile extends HttpServlet
        implements Servlet, ImageObserver {

 ImageObserverは「java.awt」にありますので、あらかじめこれをimportしておきます。こうしてサーブレット自身にImageObserverをimplementsしたら、ImageObserverに必要となるメソッドを用意します。

public boolean imageUpdate(Image im, int info,
        int x, int y, int width, int height) {
    // ここに必要な処理を用意
    return true;
}

 これがイメージ更新時に呼び出されるメソッドです。returnされる値は、さらに更新情報が必要となるか否かを示すもので、必要であればtrueを、不要であればfalseを返します(falseを返した場合、以後、imageUpdateは呼ばれません)。ネットワーク経由でイメージを読み込むような場合には、イメージの更新状況をチェックする必要が生じるでしょう。こうした場合には、このimageUpdateで状況をチェックし、処理することが可能です。

 また、ここではdrawImageを使って縮小描画を行いましたが、前回説明した「アフィン変換」を利用することも可能です。drawImageも座標変換の影響を受けるので、これを利用して図形の縮小などを行えます。

座標変換による縮小
public BufferedImage getModifiedImage(BufferedImage im){
    int w1 = im.getWidth(),h1 = im.getHeight();
    BufferedImage im2
       = new BufferedImage(w1,h1,BufferedImage.TYPE_INT_ARGB);
    Graphics2D g2 = im2.createGraphics();
    
    for(int i = 0;i < 5;i++){
        g2.drawImage(im,0,0,null);
        g2.scale(0.9d, 0.9d);
    }
    g2.dispose();
    return im2;
}
座標変換を使い、イメージを連続縮小して描画する。
座標変換を使い、イメージを連続縮小して描画する。

 例えば、これは少しずつイメージを縮小描画する例です。このようにイメージも座標変換を使うことで、面白い効果を得ることができます。

クリッピング処理

 イメージの加工を行う場合、「イメージを切り抜く」という処理が必要となることも多々あります。単に原型のまま拡大縮小するなら簡単ですが、イメージの中から一部分だけを切り抜いて描画するようなことになると、「クリッピング」と呼ばれる処理を理解しておく必要があります。

 クリッピングとは、あらかじめ描画を行う領域を指定しておき、その領域内だけに描画を行う処理です。これは、Graphics2Dを利用することで比較的簡単に行うことができます。Graphics2Dではシェイプ(Shape)を利用して描画を行いますが、このシェイプを使ってクリッピングの領域を設定できる仕組みになっているのです。

 実際に、簡単なクリッピング処理の例を挙げておきましょう。

クリッピングした描画
// import java.awt.geom.*; を追加しておく

public BufferedImage getModifiedImage(BufferedImage im){
    int w1 = im.getWidth(),h1 = im.getHeight();
    BufferedImage im2
       = new BufferedImage(w1,h1,BufferedImage.TYPE_INT_ARGB);
    Graphics2D g2 = im2.createGraphics();
    g2.setColor(Color.PINK);
    g2.fillRect(0,0,w1,h1);
    
    Ellipse2D shape
       = new Ellipse2D.Double(w1 / 4,h1 / 4,w1 / 2,h1 / 2);
    g2.clip(shape);
    g2.drawImage(im, 0, 0, null);
    
    g2.dispose();
    return im2;
}
円形にクリッピング領域を設定しイメージを描画する。
円形にクリッピング領域を設定しイメージを描画する。

 これは、円形のクリッピングを設定してイメージを描画させるサンプルです。背景は淡いピンクで塗りつぶしてあります。イメージが切り抜かれて描画されているのが分かりますね。

 クリッピングの処理をしているのは、以下の部分です。

Ellipse2D shape
   = new Ellipse2D.Double(w1 / 4,h1 / 4,w1 / 2,h1 / 2);
g2.clip(shape);

 Ellipse2Dインスタンスを作成し、Graphics2Dclipでクリッピングに設定している、これだけです。これで、以後の描画はすべてクリッピング設定したシェイプの内部にのみ描かれるようになります。ここではdrawImageしていますが、基本的にはすべての描画処理はクリッピングの影響を受けます。

 clipは、連続して実行することでクリップする領域を合算できます。あるシェイプをclipした後に別のシェイプをclipすると、2つのシェイプの重なる領域がクリップされます。

 クリッピングを解除する場合は、setClipという現在のクリップ設定を変更するメソッドでnull設定します。

クリップの演算

 ただし、単純にシェイプをクリップするだけでは、描ける図形の形状も単純なものに限られてしまいます。繰り返しclipを使うことである程度の形状は作れますが、クリップの領域を追加したり削ったりをもっと柔軟に行いたい、という場合には別の方法をとった方が良いでしょう。

 「java.awt.geom」パッケージには、任意の形状を示すためのAreaというクラスが用意されています。これを利用することで、複数のシェイプを演算して領域を作成することができ、この機能を利用すれば、複雑な形でイメージを切り抜き描画できるようになります。簡単な例を挙げておきましょう。

Areaを使ったクリッピング処理
public BufferedImage getModifiedImage(BufferedImage im){
    int w1 = im.getWidth(),h1 = im.getHeight();
    BufferedImage im2
       = new BufferedImage(w1,h1,BufferedImage.TYPE_INT_ARGB);
    Graphics2D g2 = im2.createGraphics();
    g2.setColor(Color.PINK);
    g2.fillRect(0,0,w1,h1);
    
    Area area = new Area();
    Ellipse2D shape = new Ellipse2D.Double(im.getWidth() / 2 - 35,
            im.getHeight() / 2 - 35,70,70);
    area.add(new Area(shape));
    shape = new Ellipse2D.Double(im.getWidth() / 2 - 20,
            im.getHeight() / 2 - 20,40,40);
    area.subtract(new Area(shape));
    for(int i = 0;i < 16;i++){
        double x = (im.getWidth() / 2) +
                Math.sin(Math.PI / 8 * i) * 80;
        double y = (im.getHeight() / 2) +
                Math.cos(Math.PI / 8 * i) * 80;
        shape = new Ellipse2D.Double(x - 15,y - 15,30,30);
        area.add(new Area(shape));
    }
    g2.clip(area);
    g2.drawImage(im,0,0,null);
    
    g2.dispose();
    return im2;
}
Areaを使って複数の円を組み合わせたクリッピング領域を作成する。
Areaを使って複数の円を組み合わせたクリッピング領域を作成する。

 多数の円を組み合わせた形で図形をクリッピングして描画していますね。ここでは、Areaを複数作成してクリッピングの領域を作っています。Areaのインスタンス作成は、次のような形で行います。

new Area();
new Area(Shape shape);

 単純にnewするだけの他、Shapeインスタンスを引数にすることで、そのシェイプの形状のAreaを作成することができます。こうして作成したAreaは、Area同士で演算していくことが可能です。ここでは、以下の2通りの演算処理を行っています。

Areaに別のAreaの領域を加算する
[Area].add(Area area);
Areaから別のAreaの領域を減算する
[Area].subtract(Area area);

 加算すると、そのAreaの領域に引数で指定したAreaの領域を区分けた形状になります。また減算すると、そのAreaの領域から引数指定したAreaの領域を取り除いた形状となります。

 このように、Shapeを引数指定して作成した(形状情報を持っている)Areaを用意し、それらを演算することで複雑な形状を作成できます。また、ここではクリッピング領域にAreaを使いましたが、Areaはそのままfill/drawで描画に用いることもできます。複雑な図形を描くときには重宝するでしょう。

まとめ

 今回は、イメージファイルの読み書きから描画、クリッピング処理までイメージの利用全般について説明しました。単にイメージを読み込み、拡大縮小したり、切抜いて描画するだけであれば、これで十分できるようになるでしょう。

 ただし、イメージの処理には、こうした形状に関する処理だけでなく、イメージの状態をさまざまに変更する、いわゆる「フィルタ処理」と呼ばれるものもあります。本格的なイメージ処理にはこれらの知識は不可欠でしょう。

 ということで、次回は「イメージのフィルタ処理」を中心に説明を行う予定です。

修正履歴

  • 2008/01/15 09:37 サンプルコードSampleGraphicsFromImageFileにあった記述ミスを修正。