「im4java」を使って Java から ImageMagick を呼び出して画像変換や画像情報取得をする

前回紹介した、コマンドラインから画像の変換処理などができる ImageMagick。

今回はこの ImageMagick を Java プログラムから呼び出す時に使える、im4java というライブラリを紹介する。

im4java は、ImageMagick のコマンドファイル (convert.exeidentify.exe など) を呼び出すためのラッパーである。つまり im4java 単体では動作せず、利用する環境には ImageMagick をインストールしておく必要がある

im4java のダウンロードと環境構築

im4java は以下からダウンロードできる。最新版は Ver.1.4.0 で、ImageMagick 7系にも対応しているが、Convert コマンドに関しては magick.exe だけでなく convert.exe もインストールしておかないと恐らく動かない。

ダウンロードしたアーカイブを解凍すると、im4java-1.4.0.jar というファイルが入っているので、これを Java プロジェクトのビルドパスに通す。他に依存するライブラリがなく、JAR ファイル1つで完結するのはありがたい。

im4java の使い方

im4java は ImageMagick のコマンドライン・オプションがほとんどそのままの形で記述できるので、変換内容に関しては記述しやすいと思う。が、入力ファイルや出力ファイルの指定方法が少々分かりづらかったので、色々と調べながら組み上げた。

以下に ConvertIdentify のコマンドを im4java から呼び出すサンプルを用意した。それぞれのコマンドで、ファイルのフルパスを文字列で指定する方法と、BufferedImage オブジェクトとして画像を受け渡しする方法を作成した。

/**
 * 元の画像と、変換した画像の出力先をフルパスで指定することで、
 * 元の画像を読み取り、指定のパスに変換後のファイルを生成する。
 * 
 * @throws IOException
 * @throws InterruptedException
 * @throws IM4JavaException
 */
public void convertFromFile() throws IOException, InterruptedException, IM4JavaException {
  // 元画像 : フルパスが分かれば良いので、必ずしも File オブジェクトで用意していなくても良い
  File inputImgFile = new File("C:/Test/Test.jpg");
  
  // ImageMagick 引数指定
  IMOperation op = new IMOperation();
  
  // Input となる画像の指定
  //   元画像のフルパスを与える
  op.addImage(inputImgFile.getAbsolutePath());
  
  // リサイズするサイズを指定
  //   第1引数が幅、第2引数が高さ
  //   op.resize(800, null); とすると、幅 800px にし、高さは元画像の縦横比から自動算出させる
  //   高さをベースにリサイズしたい場合は op.resize(null, 800); のように指定する
  op.resize(800, null);
  
  // 画質の指定
  op.quality(80.0);
  
  // 不要な画像情報は削除する
  op.strip();
  
  // 出力先の指定
  //   ファイルの出力先をフルパスで指定する
  //   変換後の形式を変更する場合はファイルの拡張子で指定すれば良いみたい
  op.addImage("C:/Test/Converted.png");
  
  // コマンドライン引数を確認する
  System.out.println(op.getCmdArgs());
  
  // ImageMagick の Convert コマンドを実行するオブジェクトを生成する
  //   V7 系の場合はデフォルトだと Magick コマンドしかないので、
  //   インストール時に Convert コマンドもインストールするようにしておかないと動かないかも?(未検証)
  ConvertCmd convert = new ConvertCmd();
  
  // ImageMagick のパスを指定する
  convert.setSearchPath("C:/Program Files/ImageMagick-7.0.5-Q16");
  // 以下でも同義
  //   ProcessStarter.setGlobalSearchPath("C:/Program Files/ImageMagick-7.0.5-Q16");
  
  // Convert コマンドを実行する
  //   第1引数はコマンドライン引数を詰めたオブジェクト
  //   変換した画像は「出力先の指定」で指定したパスに吐かれている
  convert.run(op);
}
/**
 * 元の画像を BufferedImage オブジェクトで用意し、
 * 変換後の画像を BufferedImage オブジェクトとして受け取る。
 * 
 * @return BufferedImage オブジェクト
 * @throws IOException
 * @throws InterruptedException
 * @throws IM4JavaException
 */
public BufferedImage convertFromBufferedImage() throws IOException, InterruptedException, IM4JavaException {
  // 元画像 : BufferedImage で用意する
  BufferedImage inputImg = ImageIO.read(new File("C:/Test/Test.jpg"));
  
  // ImageMagick 引数指定
  IMOperation op = new IMOperation();
  
  // Input となる画像の指定
  //   元画像の BufferedImage は ConvertCmd#run() で第2引数に指定するので
  //   ココはプレースホルダとして引数を与えないでおく
  op.addImage();
  
  // リサイズするサイズを指定
  op.resize(800, null);
  // 画質の指定
  op.quality(80.0);
  // 不要な画像情報は削除する
  op.strip();
  
  // 出力ファイルの形式設定
  //   元画像と同じ形式に変換するのであれば "-" で良い
  //   変換後の形式を変更する場合は "png:-" のように指定する
  op.addImage("-");
  
  // コマンドライン引数を確認する
  System.out.println(op.getCmdArgs());
  
  // ImageMagick の Convert コマンドを実行するオブジェクトを生成する
  ConvertCmd convert = new ConvertCmd();
  // ImageMagick のパスを指定する
  convert.setSearchPath("C:/Program Files/ImageMagick-7.0.5-Q16");
  
  // 結果のストリームを BufferedImage に変換して返してくれるオブジェクトを用意する
  Stream2BufferedImage s2b = new Stream2BufferedImage();
  
  // コマンドの出力先を標準出力から先程生成したオブジェクトに変更する
  //   これで Stream2BufferedImage オブジェクトが変換後の画像を受け取れるようになる
  convert.setOutputConsumer(s2b);
  
  // Convert コマンドを実行する
  //   第2引数に元画像の BufferedImage を指定する
  //   変換後の画像は先程生成した Stream2BufferedImage オブジェクトが受け取る
  convert.run(op, inputImg);
  
  // 変換後の画像の BufferedImage を取得する
  BufferedImage outputImg = s2b.getImage();
  
  // 変換後の画像の情報を表示するとか~
  System.out.println("width  : " + outputImg.getWidth());
  System.out.println("height : " + outputImg.getHeight());
  
  // BufferedImage を直接ファイルとして出力するとか~
  ImageIO.write(outputImg, "JPG", new File("C:/Test/Converted_1.jpg"));
  
  // バイト配列で受け取ったあとファイルに出力するとか~
  ByteArrayOutputStream bos = new ByteArrayOutputStream();
  ImageIO.write(outputImg, "JPG", bos);
  // バイト配列として受け取る
  byte[] bytes = bos.toByteArray();
  // バイト配列からファイルに出力する (ファイルが存在していた場合は上書き)
  Files.write(new File("C:/Test/Converted_2.jpg").toPath(), bytes, StandardOpenOption.CREATE_NEW, StandardOpenOption.TRUNCATE_EXISTING);
  
  return outputImg;
  
  // BufferedImage に関して
  //   http://www.ne.jp/asahi/hishidama/home/tech/java/image.html#h2_write
  // Files に関して
  //   http://www.ne.jp/asahi/hishidama/home/tech/java/files.html
  //   http://waman.hatenablog.com/entry/20120515/1337044411
}
/**
 * 対象の画像のフルパスを指定することで画像情報を取得する
 * 
 * @return 画像情報 : "画像形式 (スペース) 幅 (スペース) 高さ (スペース)" の形式で返す
 *         アニメーション Gif の場合はこの情報がコマの数だけ連結して返される
 */
public String identifyFromFile() {
  // 対象の画像のフルパス
  final String _inputImgPath = "C:/Test/Test.jpg";
  
  // ImageMagick 引数指定
  IMOperation op = new IMOperation();
  
  // Input となる画像を指定する
  op.addImage(_inputImgPath);
  
  // 取得する画像情報の出力フォーマットを指定する
  //   format については以下を参照
  //   https://www.imagemagick.org/script/escape.php
  //   %m : 画像形式を調べて取得する (拡張子偽装されている場合は判別した正しい画像形式が分かる)
  //   %e : 拡張子を単純に返すのみ (拡張子偽装してあっても偽装された拡張子が返る)
  //   %w : 画像の幅 (px)
  //   %h : 画像の高さ (px)
  //   アニメーション Gif を読み込んだ場合はコマ数の分だけ結果が連結して返されるので
  //   結果が繋がってしまわないように末尾にスペースを入れておくと良いかも
  op.format("%m %w %h ");
  
  // ImageMagick の Identify コマンドを実行するオブジェクトを生成する
  IdentifyCmd identify = new IdentifyCmd();
  // ImageMagick のパスを指定する
  identify.setSearchPath("C:/Program Files/ImageMagick-7.0.5-Q16");
  
  // 標準出力の結果を ArrayList<String> で受け取るためのオブジェクトを用意する
  ArrayListOutputConsumer output = new ArrayListOutputConsumer();
  
  // コマンドの出力先を標準出力から先程生成したオブジェクトに変更する
  //   これで Identify コマンドの結果が ArrayListOutputConsumer に格納される
  identify.setOutputConsumer(output);
  
  // Identify コマンドを実行する
  try {
    identify.run(op);
  }
  catch(IOException | InterruptedException | IM4JavaException e) {
    // 画像ではないファイルを取り込んだりすると例外が発生する
    e.printStackTrace();
    return null;
  }
  
  // 結果の文字列を受け取る
  //   ArrayListOutputConsumer#getOutput() で List を受け取る
  //   標準出力に出力される1行ごとに分かれて格納されている
  List<String> lines = output.getOutput();
  
  // 結果文字列を整形する
  //   IMOperation#format() で指定した形だと結果は1行しかないので lines の要素数は 1 のはず
  //   そこで空文字で結合することで1つの String にしてしまう
  // Apache Commons Lang の StringUtils で結合する場合
  final String _result = StringUtils.join(lines.toArray());
  // Java8 以降は String クラスで同様のことができる
  final String _resultForJava8 = String.join("", lines);
  
  return _result;
}
/**
 * 対象の画像を BufferedImage オブジェクトで用意し、画像情報を取得する
 * 
 * @return 画像情報 : "画像形式 (スペース) 幅 (スペース) 高さ (スペース)" の形式で返す
 *         アニメーション Gif の場合はこの情報がコマの数だけ連結して返される
 * @throws IOException
 */
public String identifyFromBufferedImage() throws IOException {
  // 対象の画像を BufferedImage オブジェクトで用意する
  BufferedImage inputImg = ImageIO.read(new File("C:/Test/Test.jpg"));
  
  // ImageMagick 引数指定
  IMOperation op = new IMOperation();
  
  // Input となる画像の指定
  //   対象の画像の BufferedImage は Identify#run() で第2引数に指定するので
  //   ココはプレースホルダとして引数を与えないでおく
  op.addImage();
  
  // 取得する画像情報の出力フォーマットを指定する
  op.format("%m %w %h ");
  
  // ImageMagick の Identify コマンドを実行するオブジェクトを生成する
  IdentifyCmd identify = new IdentifyCmd();
  // ImageMagick のパスを指定する
  identify.setSearchPath("C:/Program Files/ImageMagick-7.0.5-Q16");
  
  // 標準出力の結果を ArrayList<String> で受け取るためのオブジェクトを用意する
  ArrayListOutputConsumer output = new ArrayListOutputConsumer();
  // コマンドの出力先を標準出力から先程生成したオブジェクトに変更する
  identify.setOutputConsumer(output);
  
  // Identify コマンドを実行する
  //   第2引数に対象の画像の BufferedImage を指定する
  try {
    identify.run(op, inputImg);
  }
  catch(IOException | InterruptedException | IM4JavaException e) {
    // 画像ではないファイルを取り込んだりすると例外が発生する
    e.printStackTrace();
    return null;
  }
  
  // 結果の文字列を受け取る
  List<String> lines = output.getOutput();
  
  // 結果文字列を整形する (Java8 以降でのみ可能)
  final String _result = String.join("", lines);
  
  return _result;
}

ほとんどコード中にコメントで記述したが、注意したいところを以下にまとめておく。

// ImageMagick のパスを指定する方法は2通り

// コマンドクラスに直接指定する方法
ConvertCmd convert = new ConvertCmd();
convert.setSearchPath("C:/Program Files/ImageMagick-7.0.5-Q16");

// 事前に im4java が全体で使えるように指定する方法
ProcessStarter.setGlobalSearchPath("C:/Program Files/ImageMagick-7.0.5-Q16");

どちらで書いても特に変わらない。

ImageMagick とこの im4java ライブラリを使えば、ウェブアプリからアップロードされた画像をサーバサイドで変換し、圧縮版をレスポンスで返す、といったことが可能になる。

im4java の動きがよく分からない場合は、公式の API Docs と、StackOverflow での既出の質問が参考になる。