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}