package org.maachang.util.image;

import java.awt.Graphics;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.image.AreaAveragingScaleFilter;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.FilteredImageSource;
import java.awt.image.IndexColorModel;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashSet;

import javax.media.jai.JAI;
import javax.media.jai.LookupTableJAI;
import javax.media.jai.ParameterBlockJAI;
import javax.media.jai.PlanarImage;
import javax.media.jai.RenderedOp;
import javax.media.jai.operator.ColorQuantizerDescriptor;
import javax.media.jai.operator.EncodeDescriptor;

import com.sun.media.jai.codec.ImageCodec;
import com.sun.media.jai.codec.JPEGEncodeParam;
import com.sun.media.jai.codec.MemoryCacheSeekableStream;
import com.sun.media.jai.codec.PNGEncodeParam;
import com.sun.media.jai.codec.SeekableStream;

/**
 * BaseImageConvertor.
 * <BR><BR>
 * JAI(Java-Advanced-Imaging)変換コアプログラム.
 *
 * @version 2007/11/29
 * @author  masahito suzuki
 * @since   MaachangBase 1.05
 */
public class BaseImageConvertor {
    /**
     * 最大減色カラー長.
     */
    public static final int MAX_COLOR = 255 ;
    
    /**
     * ルックアップオペレーション.
     */
    private static final String LOOKUP = "Lookup" ;
    
    /**
     * 誤差拡散ディザ名.
     */
    private static final String ERROR_DIFFUSION = "ErrorDiffusion" ;
    
    /**
     * 誤差拡散パラメータ.
     */
    private static final String DIFFUSION_PARAM = "colorMap" ;
    
    /**
     * 変換フォーマットタイプ : JPEG.
     */
    private static final String[] FORMAT_JPEG = { "jpeg","jpg" } ;
    
    /**
     * 変換フォーマットタイプ : PNG.
     */
    private static final String FORMAT_PNG = "png" ;
    
    /**
     * 変換フォーマットタイプ : GIF.
     */
    private static final String FORMAT_GIF = "gif" ;
    
    
    
    /**
     * 最小レート.
     */
    private static final double MIN_RATE = 0.0f ;
    
    /**
     * 最大レート.
     */
    private static final double MAX_RATE = 1.0f ;
    
    
    /** コンストラクタ. */
    private BaseImageConvertor(){}
    
    
    
    
    /**
     * 画像情報を読み込む.
     * <BR><BR>
     * InputStreamから、画像情報を読み込みます.
     * <BR>
     * @param stream 読み込み対象のInputStreamを設定します.
     * @return BufferedImage 対象の画像情報が返されます.
     * @exception IOException IO例外.
     */
    public static final BufferedImage readImage( InputStream stream )
        throws IOException {
        SeekableStream stm = null ;
        RenderedOp op = null ;
        BufferedImage ret = null ;
        try{
            stm = BaseImageConvertor.getSeekableStream( stream ) ;
            op = JAI.create( "stream",stm ) ;
            stm.close() ;
            stm = null ;
            ret = BaseImageConvertor.convertJaiToImage( op ) ;
            op = null ;
        }catch( IOException io ){
            throw io ;
        }catch( Exception e ){
            throw new IOException( e.getMessage() ) ;
        }finally{
            if( stm != null ){
                try{
                    stm.close() ;
                }catch( Exception e ){
                }
            }
            stm = null ;
            op = null ;
        }
        return ret ;
    }
    
    /**
     * 対象ファイルフォーマットを取得.
     * <BR><BR>
     * 対象のファイルフォーマットを取得します.
     * <BR>
     * @param stream 読み込み対象のInputStreamを設定します.
     * @return String フォーマット情報が返されます.<BR>
     *                 情報が不正な場合[null]が返されます.
     */
    public static final String getImageFormat( InputStream stream ) {
        String[] names = null ;
        SeekableStream stm = null ;
        String ret = null ;
        try{
            stm = BaseImageConvertor.getSeekableStream( stream ) ;
            names = ImageCodec.getDecoderNames( stm ) ;
            stm.close() ;
            stm = null ;
            if( names == null || names.length <= 0 ){
                ret = null ;
            }
            else{
                ret = names[ 0 ] ;
            }
        }catch( Exception e ){
            ret = null ;
        }finally{
            if( stm != null ){
                try{
                    stm.close() ;
                }catch( Exception ee ){
                }
            }
            stm = null ;
            names = null ;
        }
        
        return ret ;
    }
    
    /**
     * 画像情報を書き込む.
     * <BR><BR>
     * 画像情報を書き込みます.
     * <BR>
     * @param out 出力先のストリームを設定します.
     * @param image 書き込み対象の画像情報を設定します.
     * @param format 書き込みフォーマットを設定します.<BR>
     *               書き込みフォーマットは[jpeg,png,gif]です.
     * @param rate 出力レートを設定します.<BR>
     *             品質が一番高いのは[1.0f]です.<BR>
     *             また、この設定はJPEG以外は意味を持ちません.
     * @exception IOException IO例外.
     */
    public static final void writeImage( OutputStream out,BufferedImage image,String format,double rate )
        throws IOException {
        
        if( out == null || image == null || format == null || format.length() <= 0 ){
            throw new IOException( "指定引数は不正です" ) ;
        }
        
        // フォーマット名を整頓.
        format = format.trim().toLowerCase() ;
        
        // イメージ情報を整頓.
        image = BaseImageConvertor.convertTrimImage( image ) ;
        
        // 変換フォーマット情報がJPEGの場合.
        if(
            FORMAT_JPEG[ 0 ].equals( format ) == true ||
            FORMAT_JPEG[ 1 ].equals( format ) == true
        )
        {
            rate = ( rate <= MIN_RATE ) ? MIN_RATE :
                ( ( rate >= MAX_RATE ) ? MAX_RATE : rate ) ;
            BaseImageConvertor.writeJPEG( out,image,rate ) ;
        }
        // 変換フォーマット情報がPNGの場合.
        else if( FORMAT_PNG.equals( format ) == true ){
            BaseImageConvertor.writePNG( out,image ) ;
        }
        // 変換フォーマット情報がGIFの場合.
        else if( FORMAT_GIF.equals( format ) == true ){
            BaseImageConvertor.writeGIF( out,image ) ;
        }
        else{
            throw new IOException(
                "指定フォーマット(" + format +
                ")は対象外です"
            ) ;
        }
        
    }
    
    /**
     * 拡大縮小処理.
     * <BR><BR>
     * 比率を設定して、拡大/縮小処理を行います.
     * <BR>
     * @param image 変換元の画像情報を設定します.
     * @param x 変換X条件を設定します.
     * @param y 変換Y条件を設定します.
     * @return BufferedImage 変換されたイメージ情報が返されます.
     */
    public static final BufferedImage convertImage( BufferedImage image,int x,int y ) {
        Image im = null ;
        Graphics gp = null ;
        BufferedImage ret = null ;
        
        if( image == null || x <= 0 || y <= 0 ){
            return null ;
        }
        
        im = Toolkit.getDefaultToolkit().createImage(
            new FilteredImageSource(
                image.getSource(),
                new AreaAveragingScaleFilter( x,y )
            )
        ) ;
        
        ret = new BufferedImage( x,y,BufferedImage.TYPE_3BYTE_BGR ) ;
        gp = ret.getGraphics() ;
        gp.drawImage( im,0,0,null ) ;
        gp.dispose() ;
        
        im = null ;
        gp = null ;
        
        return ret ;
    }
    
    /**
     * 減色処理.
     * <BR><BR>
     * 減色処理を行います.
     * <BR>
     * @param image 変換元の画像情報を設定します.
     * @param color 減色カラー値を設定します.
     * @return BufferedImage 減色された画像情報が返されます.
     * @exception IOException IO例外.
     */
    
    public static final BufferedImage decColorImage( BufferedImage image,int color )
        throws IOException {
        int ucol ;
        IndexColorModel im = null ;
        BufferedImage ret = null ;
        
        if( image.getColorModel() instanceof IndexColorModel ){
            
            im = ( IndexColorModel )image.getColorModel() ;
            ucol = BaseImageConvertor.getUseColor( im ) ;
            
            if( im.getMapSize() < 32 ){
                
                if( ucol < color ){
                    color = ucol ;
                }
                
                ret = BaseImageConvertor.convertIndex(
                    BaseImageConvertor.convertRGB( image ),
                    color
                ) ;
                
            }
            else if( ucol > color ){
                
                ret = BaseImageConvertor.convertIndex(
                    BaseImageConvertor.convertRGB( image ),
                    color
                ) ;
                
            }
            else{
                ret = image ;
            }
            
        }
        else{
            
            ret = BaseImageConvertor.convertTrimImage( image ) ;
            
            if( ret.getColorModel().getPixelSize() >= 24 ){
                ret = BaseImageConvertor.convertIndex( ret,color ) ;
            }
            
        }
        
        return ret ;
        
    }
    
    /**
     * JPEGで書き込む.
     */
    private static final void writeJPEG( OutputStream out,BufferedImage image,double rate )
        throws IOException {
        JPEGEncodeParam jp = null ;
        PlanarImage pi = null ;
        image = BaseImageConvertor.convertRGB( image ) ;
        jp = new JPEGEncodeParam();
        jp.setQuality( ( float )rate );
        pi = PlanarImage.wrapRenderedImage( image );
        JAI.create( "encode",pi,out,FORMAT_JPEG[ 0 ],jp ) ;
        out.flush() ;
        
    }
    
    /**
     * PNGで書き込む.
     */
    private static final void writePNG( OutputStream out,BufferedImage image )
        throws IOException {
        int i,j ;
        int len ;
        
        byte[] bin = null ;
        int[] plt = null ;
        ColorModel cm = null ;
        IndexColorModel idx = null ;
        ByteArrayOutputStream bo = null ;
        PNGEncodeParam.Palette pngPal = null ;
        PNGEncodeParam.RGB pngRGB = null ;
        
        try{
            
            if( ( cm = image.getColorModel() ) instanceof IndexColorModel ){
                
                idx = ( IndexColorModel )cm ;
                
                plt = new int[ idx.getMapSize() * 3 ] ;
                len = plt.length ;
                
                for( i = 0,j = 0 ; i < len ; i += 3,j ++ ){
                    plt[ i ] = idx.getRed( j ) ;
                    plt[ i+1 ] = idx.getGreen( j ) ;
                    plt[ i+2 ] = idx.getBlue( j ) ;
                }
                
                idx = null ;
                
                pngPal = ( PNGEncodeParam.Palette )PNGEncodeParam.getDefaultEncodeParam( image ) ;
                pngPal.setPalette( plt ) ;
                plt = null ;
                
                bo = new ByteArrayOutputStream() ;
                EncodeDescriptor.create( image,bo,FORMAT_PNG,pngPal,null ) ;
                
                pngPal = null ;
                
            }
            else{
                
                pngRGB = ( PNGEncodeParam.RGB )PNGEncodeParam.getDefaultEncodeParam( image ) ;
                bo = new ByteArrayOutputStream() ;
                EncodeDescriptor.create( image,bo,FORMAT_PNG,pngRGB,null ) ;
                
                pngRGB = null ;
                
            }
            
            bin = bo.toByteArray() ;
            bo.close() ;
            bo = null ;
            
            out.write( bin ) ;
            out.flush() ;
            bin = null ;
            
        }catch( IOException io ){
            throw io ;
        }finally{
            
            if( bo != null ){
                try{
                    bo.close() ;
                }catch( Exception e ){
                }
            }
            
            bin = null ;
            plt = null ;
            cm = null ;
            idx = null ;
            bo = null ;
            pngPal = null ;
            pngRGB = null ;
            
        }
        
    }
    
    /**
     * GIFで書き込む.
     */
    private static final void writeGIF( OutputStream out,BufferedImage image )
        throws IOException {
        GifEncoder ge = null ;
        
        try{
            ge = new GifEncoder( image ) ;
            ge.write( out ) ;
            out.flush() ;
        }catch( IOException io ){
            throw io ;
        }catch( Exception e ){
            throw new IOException( e.getMessage() ) ;
        }finally{
            ge = null ;
        }
    }
    
    
    
    /**
     * インデックスカラーに変換.
     */
    private static final BufferedImage convertIndex( BufferedImage image,int color ) {
        ColorModel cm = null ;
        LookupTableJAI lt = null ;
        ParameterBlockJAI pb = null ;
        PlanarImage pi = null ;
        
        cm = BaseImageConvertor.getDecPalette( image,color ) ;
        lt = BaseImageConvertor.getLookupTable( cm ) ;
        
        pb = new ParameterBlockJAI( ERROR_DIFFUSION ) ;
        pb.addSource( image ) ;
        pb.setParameter( DIFFUSION_PARAM,lt ) ;
        
        pi = JAI.create( ERROR_DIFFUSION,pb,null ) ;
        
        return pi.getAsBufferedImage( pi.getBounds(),cm ) ;
    }
    
    /**
     * 減色パレットを取得.
     */
    private static ColorModel getDecPalette( BufferedImage image,int color ) {
        int i ;
        int len ;
        int max ;
        
        byte[] r = null ;
        byte[] g = null ;
        byte[] b = null ;
        byte[][] bin = null ;
        ParameterBlockJAI pj = null ;
        PlanarImage pi = null ;
        LookupTableJAI lt = null ;
        
        pj = new ParameterBlockJAI( "ColorQuantizer" ) ;
        pj.addSource( image ) ;
        pj.setParameter( "quantizationAlgorithm",ColorQuantizerDescriptor.NEUQUANT ) ;
        pj.setParameter( "maxColorNum",color ) ;
        
        pi = JAI.create( "ColorQuantizer",pj,null ) ;
        lt = ( LookupTableJAI )pi.getProperty( "LUT" ) ;
        
        bin = lt.getByteData() ;
        
        len = lt.getNumEntries() ;
        max = BaseImageConvertor.getColor( len ) ;
        
        r = new byte[ max ] ;
        g= new byte[ max ] ;
        b = new byte[ max ] ;
        
        for( i = 0 ; i < len ; i ++ ){
            r[ i ] = bin[ 0 ][ i ] ;
            g[ i ] = bin[ 1 ][ i ] ;
            b[ i ] = bin[ 2 ][ i ] ;
        }
        
        return new IndexColorModel(
            BaseImageConvertor.getBits( max ),
            max,
            r,g,b
        ) ;
    }
    
    /**
     * RGBカラー変換.
     */
    private static final BufferedImage convertRGB( BufferedImage image ) {
        int col ;
        
        byte[][] dat = null ;
        LookupTableJAI lt = null ;
        IndexColorModel cm = null ;
        BufferedImage ret = null ;
        
        if( image.getColorModel() instanceof IndexColorModel ){
            
            cm = ( IndexColorModel )image.getColorModel() ;
            col = cm.getMapSize() ;
            
            dat = new byte[ 3 ][ col ] ;
            cm.getReds( dat[ 0 ] ) ;
            cm.getGreens( dat[ 1 ] ) ;
            cm.getBlues( dat[ 2 ] ) ;
            
            lt = new LookupTableJAI( dat );
            ret = JAI.create( LOOKUP,image,lt).getAsBufferedImage() ;
            
        }
        else{
            ret = image ;
        }
        
        return ret ;
    }
    
    /**
     * 対象イメージを整頓.
     */
    private static final BufferedImage convertTrimImage( BufferedImage image ) {
        Graphics gp = null ;
        BufferedImage ret = null ;
        
        if( image.getColorModel().getPixelSize() >= 32 ){
            
            ret = new BufferedImage(
                image.getWidth(),
                image.getHeight(),
                BufferedImage.TYPE_3BYTE_BGR
            ) ;
            
            gp = ret.createGraphics() ;
            gp.drawImage( image,0,0,null ) ;
            gp.dispose() ;
            
        }
        else{
            ret = image ;
        }
        
        return ret ;
    }
    
    /**
     * 実際に利用されているカラーを取得.
     */
    private static final int getUseColor( IndexColorModel im ) {
        int i ;
        int sz ;
        int col ;
        
        HashSet<Integer> hs = null ;
        
        sz = im.getMapSize() ;
        hs = new HashSet<Integer>( sz ) ;
        for( i = 0 ; i < sz ; i ++ ){
            col = ( int )(
                ( ( im.getRed( i ) << 16 ) & 0x00ff0000 ) |
                ( ( im.getGreen( i ) << 8 ) & 0x0000ff00 ) |
                ( im.getBlue( i ) & 0x000000ff )
            ) ;
            hs.add( new Integer( col ) ) ;
        }
        
        return hs.size() ;
    }
    
    /**
     * カラーモデルに対するルックアップテーブルを取得.
     */
    private static final LookupTableJAI getLookupTable( ColorModel model ) {
        int i ;
        int color ;
        
        byte[][] dat = null ;
        
        color = ( ( IndexColorModel )model ).getMapSize() ;
        dat = new byte[ 3 ][ color ] ;
        for( i = 0 ; i < color ; i ++ ){
            dat[ 0 ][ i ] = ( byte )model.getRed( i ) ;
            dat[ 1 ][ i ] = ( byte )model.getGreen( i ) ;
            dat[ 2 ][ i ] = ( byte )model.getBlue( i ) ;
        }
        
        return new LookupTableJAI( dat ) ;
        
    }
    
    /**
     * カラー値を取得.
     */
    private static int getColor( int col ) {
        if( col > 128 ){
            return 255 ;
        }else if( col > 64){
            return 128 ;
        }else if( col > 32 ){
            return 64 ;
        }else if( col > 16 ){
            return 32 ;
        }else if( col > 8 ){
            return 16 ;
        }else if( col > 4){
            return 8 ;
        }else if( col > 2){
            return 4 ;
        }else if( col == 2 ){
            return 2 ;
        }else{
            return 1 ;
        }
    }
    
    /**
     * カラー値に対する有効ビット値を取得.
     */
    private static int getBits( int col ) {
        if( col > 128 ){
            return 8 ;
        }else if( col > 64){
            return 7 ;
        }else if( col > 32 ){
            return 6 ;
        }else if( col > 16 ){
            return 5 ;
        }else if( col > 8 ){
            return 4 ;
        }else if( col > 4){
            return 3 ;
        }else if( col > 2){
            return 2 ;
        }else{
            return 1 ;
        }
    }
    
    /**
     * JAIイメージ情報(RenderedOp)をBufferedImageに変換.
     */
    private static final BufferedImage convertJaiToImage( RenderedOp in ) {
        if( in != null ){
            return in.getAsBufferedImage() ;
        }
        
        return null ;
    }
    
    /**
     * InputStreamからSeekableStreamに変換.
     */
    private static final SeekableStream getSeekableStream( InputStream stream )
        throws Exception {
        SeekableStream ret = null ;
        
        if( stream instanceof SeekableStream ){
            ret = ( SeekableStream )stream ;
        }
        else{
            ret = new MemoryCacheSeekableStream( stream ) ;
        }
        
        return ret ;
    }

}

