    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 * ②縦横サイズ(px)指定による変換
    049 *   縦横の変換後のサイズ(px)を個別に指定し、変換を行います。
    050 * ③縮小率指定による変換
    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}