001/* 002 * Copyright (c) 2009 The openGion Project. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 013 * either express or implied. See the License for the specific language 014 * governing permissions and limitations under the License. 015 */ 016package org.opengion.fukurou.util; 017 018import java.awt.color.ColorSpace; 019import java.awt.color.ICC_ColorSpace; 020import java.awt.color.ICC_Profile; 021import java.awt.geom.AffineTransform; 022import java.awt.image.AffineTransformOp; 023import java.awt.image.BufferedImage; 024import java.awt.image.ColorConvertOp; 025import java.io.File; 026import java.io.IOException; 027import java.io.InputStream; 028import java.util.Locale; 029import java.util.Arrays; 030import javax.media.jai.JAI; 031 032import javax.imageio.ImageIO; 033import javax.imageio.IIOException; 034 035import com.sun.media.jai.codec.FileSeekableStream; 036import com.sun.media.jai.util.SimpleCMYKColorSpace; 037 038/** 039 * ImageResizer は、画像ファイルのリサイズを行うためのクラスです。 040 * ここでの使い方は、初期化時に、オリジナルの画像ファイルを指定し、 041 * 変換時に各縮小方法に対応したメソッドを呼び出し、画像を変換します。 042 * 変換方法としては、以下の3つがあります。 043 * ①最大サイズ(px)指定による変換 044 * 縦横の最大サイズ(px)を指定し、変換を行います。 045 * 横長の画像については、変換後の横幅=最大サイズとなり、縦幅については、横幅の 046 * 縮小率に従って決定されます。 047 * 逆に縦長の画像については、変換後の縦幅=最大サイズとなり、横幅については、縦幅の 048 * 縮小率に従って決定されます。 049 * ②縦横サイズ(px)指定による変換 050 * 縦横の変換後のサイズ(px)を個別に指定し、変換を行います。 051 * ③縮小率指定による変換 052 * "1"を元サイズとする縮小率を指定し、変換を行います。 053 * 縮小率は、縦横で同じ縮小率が適用されます。 054 * 入力フォーマットとしてはJPEG/PNG/GIFに、出力フォーマットとしてはJPEG/PNGに対応しています。 055 * 出力フォーマットについては、出力ファイル名の拡張子より自動的に決定されますが、一般的には 056 * サイズが小さくなるjpegファイルを推奨します。 057 * 入出力フォーマットについて、対応していないフォーマットが指定された場合は例外が発生します。 058 * また、縦横の出力サイズが入力サイズの縦横よりも両方大きい場合、変換は行われず、入力ファイルが 059 * そのままコピーされて出力されます。(拡大変換は行われません) 060 * 061 * @version 4.0 062 * @author Hiroki Nakamura 063 * @since JDK5.0, 064 */ 065public class ImageResizer { 066 private static final String CR = System.getProperty("line.separator"); // 5.5.3.4 (2012/06/19) 067 068 private static final String ICC_PROFILE = "ISOcoated_v2_eci.icc"; // 5.5.3.4 (2012/06/19) 069 070 private final BufferedImage inputImage; // 入力画像オブジェクト 071 072 private final int inSizeX; // 入力画像の横サイズ 073 private final int inSizeY; // 入力画像の縦サイズ 074 075 public static final String READER_SUFFIXES ; // 5.6.5.3 (2013/06/28) 入力画像の形式 [bmp, gif, jpeg, jpg, png, wbmp] 076 public static final String WRITER_SUFFIXES ; // 5.6.5.3 (2013/06/28) 出力画像の形式 [bmp, gif, jpeg, jpg, png, wbmp] 077 // 5.6.5.3 (2013/06/28) 入力画像,出力画像の形式 を ImageIO から取り出します。 078 static { 079 String[] rfn = ImageIO.getReaderFileSuffixes(); 080 Arrays.sort( rfn ); 081 READER_SUFFIXES = Arrays.toString( rfn ); 082 083 String[] wfn = ImageIO.getWriterFileSuffixes(); 084 Arrays.sort( wfn ); 085 WRITER_SUFFIXES = Arrays.toString( wfn ); 086 } 087 088 /** 089 * 入力ファイル名を指定し、画像縮小オブジェクトを初期化します。 090 * 091 * @og.rev 5.4.3.5 (2012/01/17) CMYK対応 092 * @og.rev 5.4.3.7 (2012/01/20) FAIでのファイル取得方法変更 093 * @og.rev 5.4.3.8 (2012/01/24) エラーメッセージ追加 094 * @og.rev 5.6.5.3 (2013/06/28) 入力画像の形式 を ImageIO から取り出します。 095 * 096 * @param in 入力ファイル名 097 */ 098 public ImageResizer( final String in ) { 099 BufferedImage bi = null; 100 // 5.6.5.3 (2013/06/28) 入力画像の形式 を ImageIO から取り出します。 101 if( !isReaderSuffix( in ) ) { 102 String errMsg = "入力ファイルは" + READER_SUFFIXES + "のいずれかの形式のみ指定可能です。" + "File=[" + in + "]"; 103 throw new RuntimeException( errMsg ); 104 } 105 106 File inFile = new File( in ); 107 try { 108 // inputImage = ImageIO.read( inFile ); 109 bi = ImageIO.read( inFile ); 110 } 111 catch ( IIOException ex ) { // 5.4.3.5 (2012/01/17) 決めうち 112 // API的には、IllegalArgumentException と IOException しか記述されていない。 113// FileSeekableStream fsstream = null; 114// try{ 115// // 5.4.3.7 (2012/01/20) ファイルの開放がGC依存なので、streamで取得するように変更 116// // bi = cmykToSRGB(JAI.create("FileLoad",inFile.toString()).getAsBufferedImage(null,null)); 117// fsstream = new FileSeekableStream(inFile.getAbsolutePath()); 118// bi = cmykToSRGB(JAI.create("stream",fsstream).getAsBufferedImage(null,null)); 119// } 120// catch( IOException ioe ){ 121// String errMsg = "イメージファイルの読込(JAI)に失敗しました。" + "File=[" + in + "]"; 122// throw new RuntimeException( errMsg,ioe ); 123// } 124// catch( Exception oe ){ // 5.4.3.8 (2012/01/23) その他エラーの場合追加 125// String errMsg = "イメージファイルの読込(JAI)に失敗しました。ファイルが壊れている可能性があります。" + "File=[" + in + "]"; 126// throw new RuntimeException( errMsg,oe ); 127// } 128// finally{ 129// Closer.ioClose(fsstream); 130// } 131 } 132 catch( IOException ex ) { 133 String errMsg = "イメージファイルの読込に失敗しました。" + "File=[" + in + "]"; 134 throw new RuntimeException( errMsg,ex ); 135 } 136 137 // 6.0.0.1 (2014/04/25) IIOException の catch ブロックからの例外出力を外に出します。 138 // bi == null は、結果のストリームを読み込みできないような場合、または、IO例外が発生した場合。 139 if( bi == null ) { 140 FileSeekableStream fsstream = null; 141 try{ 142 // 5.4.3.7 (2012/01/20) ファイルの開放がGC依存なので、streamで取得するように変更 143 // bi = cmykToSRGB(JAI.create("FileLoad",inFile.toString()).getAsBufferedImage(null,null)); 144 fsstream = new FileSeekableStream(inFile.getAbsolutePath()); 145 bi = cmykToSRGB(JAI.create("stream",fsstream).getAsBufferedImage(null,null)); 146 } 147 catch( IOException ioe ){ 148 String errMsg = "イメージファイルの読込(JAI)に失敗しました。" + "File=[" + in + "]"; 149 throw new RuntimeException( errMsg,ioe ); 150 } 151 catch( Exception oe ){ // 5.4.3.8 (2012/01/23) その他エラーの場合追加 152 String errMsg = "イメージファイルの読込(JAI)に失敗しました。ファイルが壊れている可能性があります。" + "File=[" + in + "]"; 153 throw new RuntimeException( errMsg,oe ); 154 } 155 finally{ 156 Closer.ioClose(fsstream); 157 } 158 } 159 160 inputImage = bi; 161 inSizeX = inputImage.getWidth(); 162 inSizeY = inputImage.getHeight(); 163 } 164 165 /** 166 * 縦横の最大サイズ(px)を指定し、変換を行います。 167 * 横長の画像については、変換後の横幅=最大サイズとなり、縦幅については、横幅の 168 * 縮小率に従って決定されます。 169 * 逆に縦長の画像については、変換後の縦幅=最大サイズとなり、横幅については、縦幅の 170 * 縮小率に従って決定されます。 171 * 172 * @param out 出力ファイル名 173 * @param maxSize 変換後の縦横の最大サイズ 174 */ 175 public void resizeByPixel( final String out, final int maxSize ) { 176 int sizeX = 0; 177 int sizeY = 0; 178 if( inSizeX > inSizeY ) { 179 sizeX = maxSize; 180 sizeY = inSizeY * maxSize / inSizeX; 181 } 182 else { 183 sizeX = inSizeX * maxSize / inSizeY; 184 sizeY = maxSize; 185 } 186 convert( inputImage, out, sizeX, sizeY ); 187 } 188 189 /** 190 * 縦横の変換後のサイズ(px)を個別に指定し、変換を行います。 191 * 192 * @param out 出力ファイル名 193 * @param sizeX 変換後の横サイズ(px) 194 * @param sizeY 変換後の縦サイズ(px) 195 */ 196 public void resizeByPixel( final String out, final int sizeX, final int sizeY ) { 197 convert( inputImage, out, sizeX, sizeY ); 198 } 199 200 /** 201 * "1"を元サイズとする縮小率を指定し、変換を行います。 202 * 縮小率は、縦横で同じ縮小率が適用されます。 203 * 204 * @param out 出力ファイル名 205 * @param ratio 縮小率 206 */ 207 public void resizeByRatio( final String out, final double ratio ) { 208 int sizeX = (int)( inSizeX * ratio ); 209 int sizeY = (int)( inSizeY * ratio ); 210 convert( inputImage, out, sizeX, sizeY ); 211 } 212 213 /** 214 * 画像の変換を行うための内部共通メソッドです。 215 * 216 * @og.rev 5.4.1.0 (2011/11/01) 画像によってgetTypeが0を返し、エラーになる不具合を修正 217 * @og.rev 5.6.5.3 (2013/06/28) 出力画像の形式 を ImageIO から取り出します。 218 * @og.rev 5.6.5.3 (2013/06/28) 5.6.6.1 (2013/07/12) getSuffix するタイミングを後ろにする。 219 * @og.rev 5.6.6.1 (2013/07/12) 拡張子の変更があるので、変換しない処理は、ない。 220 * 221 * @param inputImage 入力画像オブジェクト 222 * @param out 出力ファイル名 223 * @param sizeX 横サイズ(px) 224 * @param sizeY 縦サイズ(px) 225 */ 226 private void convert( final BufferedImage inputImage, final String out, final int sizeX, final int sizeY ) { 227 // 5.6.6.1 (2013/07/12) getSuffix するタイミングを後ろにする。 228 // 5.6.5.3 (2013/06/28) 出力画像の形式 を ImageIO から取り出します。 229 if( !isWriterSuffix( out ) ) { 230 String errMsg = "出力ファイルは" + WRITER_SUFFIXES + "のいずれかの形式のみ指定可能です。" + "File=[" + out + "]"; 231 throw new RuntimeException( errMsg ); 232 } 233 234 File outFile = new File( out ); 235 236 // 5.4.1.0 (2011/11/01) 画像によってgetTypeが0を返し、エラーになる不具合を修正 237 int type = inputImage.getType(); 238 BufferedImage resizeImage = null; 239 if( type == 0 ) { 240 resizeImage = new BufferedImage( sizeX, sizeY, BufferedImage.TYPE_4BYTE_ABGR_PRE ); 241 } 242 else { 243 resizeImage = new BufferedImage( sizeX, sizeY, inputImage.getType() ); 244 } 245 AffineTransformOp ato = null; 246 ato = new AffineTransformOp( 247 AffineTransform.getScaleInstance( 248 (double)sizeX/inSizeX, (double)sizeY/inSizeY ), null ); 249 ato.filter( inputImage, resizeImage ); 250 251 try { 252 // 5.6.6.1 (2013/07/12) getSuffix するタイミングを後ろにする。 253 String outSuffix = getSuffix( out ); 254 ImageIO.write( resizeImage, outSuffix, outFile ); 255 } 256 catch( IOException ex ) { 257 String errMsg = "イメージファイルの作成に失敗しました。" + "File=[" + out + "]"; 258 throw new RuntimeException( errMsg,ex ); 259 } 260 } 261 262 /** 263 * ファイル名から拡張子(小文字)を求めます。 264 * 拡張子 が存在しない場合は、null を返します。 265 * 266 * @og.rev 5.6.5.3 (2013/06/28) private ⇒ public へ変更 267 * 268 * @param fileName ファイル名 269 * 270 * @return 拡張子(小文字)。なければ、null 271 */ 272 public static String getSuffix( final String fileName ) { 273 String suffix = null; 274 if( fileName != null ) { 275 int sufIdx = fileName.lastIndexOf( '.' ); 276 if( sufIdx >= 0 ) { 277 suffix = fileName.substring( sufIdx + 1 ).toLowerCase( Locale.JAPAN ); 278 } 279 } 280 return suffix; 281 } 282 283 /** 284 * ファイル名から入力画像になりうるかどうかを判定します。 285 * コンストラクターの引数(入力画像)や、実際の処理の中(出力画像)で 286 * 、変換対象となるかどうかをチェックしていますが、それを事前に確認できるようにします。 287 * 288 * @og.rev 5.6.5.3 (2013/06/28) 新規追加 289 * @og.rev 5.6.6.1 (2013/07/12) getSuffix が null を返すケースへの対応 290 * 291 * @param fileName ファイル名 292 * 293 * @return 入力画像として使用できるかどうか。できる場合は、true 294 */ 295 public static final boolean isReaderSuffix( final String fileName ) { 296 String suffix = getSuffix( fileName ); 297 298 return suffix != null && READER_SUFFIXES.indexOf( suffix ) >= 0 ; 299 } 300 301 /** 302 * ファイル名から出力画像になりうるかどうかを判定します。 303 * コンストラクターの引数(入力画像)や、実際の処理の中(出力画像)で 304 * 、変換対象となるかどうかをチェックしていますが、それを事前に確認できるようにします。 305 * 306 * @og.rev 5.6.5.3 (2013/06/28) 新規追加 307 * @og.rev 5.6.6.1 (2013/07/12) getSuffix が null を返すケースへの対応 308 * 309 * @param fileName ファイル名 310 * 311 * @return 出力画像として使用できるかどうか。できる場合は、true 312 */ 313 public static final boolean isWriterSuffix( final String fileName ) { 314 String suffix = getSuffix( fileName ); 315 316 return suffix != null && WRITER_SUFFIXES.indexOf( suffix ) >= 0 ; 317 } 318 319 /** 320 * BufferedImageをISOCoatedのICCプロファイルで読み込み、RGBにした結果を返します。 321 * (CMYKからRBGへの変換、ビット反転) 322 * なお、ここでは、外部の ICC_PROFILE(ISOcoated_v2_eci.icc) を利用して、処理速度アップを図りますが、 323 * 存在しない場合、標準の、com.sun.media.jai.util.SimpleCMYKColorSpace を利用しますので、エラーは出ません。 324 * ただし、ものすごく遅いため、実用的ではありません。 325 * ISOcoated_v2_eci.icc ファイルは、zip圧縮して、拡張子をjar に変更後、(ISOcoated_v2_eci.jar) 326 * javaエクステンション((JAVA_HOME\)jre\lib\ext) にコピーするか、実行時に、CLASSPATHに設定します。 327 * 328 * @og.rev 5.4.3.5 (2012/01/17) 329 * @og.rev 5.5.3.4 (2012/06/19) ICC_PROFILE の取得先を、ISOcoated_v2_eci.icc に変更 330 * 331 * @param readImage BufferedImageオブジェクト 332 * 333 * @return 変換後のBufferedImage 334 * @throws IOException 入出力エラーが発生したとき 335 */ 336 public BufferedImage cmykToSRGB( final BufferedImage readImage ) throws IOException { 337 ClassLoader loader = Thread.currentThread().getContextClassLoader(); 338 InputStream icc_stream = loader.getResourceAsStream( ICC_PROFILE ); 339 340 // 5.5.3.4 (2012/06/19) ICC_PROFILE が存在しない場合は、標準のSimpleCMYKColorSpace を使用。 341 ColorSpace cmykCS = null; 342 if( icc_stream != null ) { 343 ICC_Profile prof = ICC_Profile.getInstance(icc_stream); //変換プロファイル 344 cmykCS = new ICC_ColorSpace(prof); 345 } 346 else { 347 // 遅いので標準のスペースは使えない 348 String errMsg = ICC_PROFILE + " が見つかりません。" + CR 349 + " CLASSPATHの設定されている場所に配備してください。" + CR 350 + " 標準のSimpleCMYKColorSpaceを使用しますのでエラーにはなりませんが、非常に遅いです。" ; 351 System.out.println( errMsg ); 352 cmykCS = SimpleCMYKColorSpace.getInstance(); 353 } 354 BufferedImage rgbImage = new BufferedImage(readImage.getWidth(), 355 readImage.getHeight(), BufferedImage.TYPE_INT_RGB); 356 ColorSpace rgbCS = rgbImage.getColorModel().getColorSpace(); 357 ColorConvertOp cmykToRgb = new ColorConvertOp(cmykCS, rgbCS, null); 358 cmykToRgb.filter(readImage, rgbImage); 359 360 int width = rgbImage.getWidth(); 361 int height = rgbImage.getHeight(); 362 // 反転が必要 363 for (int i=0;i<width;i++) { 364 for (int j=0;j<height;j++) { 365 int rgb = rgbImage.getRGB(i, j); 366 int rr = (rgb & 0xff0000) >> 16; 367 int gg = (rgb & 0x00ff00) >> 8; 368 int bb = (rgb & 0x0000ff); 369 rgb = (Math.abs(rr - 255) << 16) + (Math.abs(gg - 255) << 8) + (Math.abs(bb - 255)); 370 rgbImage.setRGB(i, j, rgb); 371 } 372 } 373 374 return rgbImage; 375 } 376 377 /** 378 * メイン処理です。 379 * Usage: java org.opengion.fukurou.util.ImageResizer [Input Filename] [OutputFilename] [MaxResize] 380 * 381 * @param args 引数文字列配列 入力ファイル、出力ファイル、縦横最大サイズ 382 */ 383 public static void main( final String[] args ) { 384 if( args.length < 3 ) { 385 LogWriter.log( "Usage: java org.opengion.fukurou.util.ImageResizer [Input Filename] [OutputFilename] [MaxResize]" ); 386 return ; 387 } 388 389 ImageResizer ir = new ImageResizer( args[0] ); 390 ir.resizeByPixel( args[1], Integer.parseInt( args[2] ) ); 391 } 392}