    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}