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