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}