    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 org.opengion.fukurou.system.OgRuntimeException ;         // 6.4.2.0 (2016/01/29)
    019import java.awt.color.ColorSpace;
    020import java.awt.color.ICC_ColorSpace;
    021import java.awt.color.ICC_Profile;
    022import java.awt.Color;                                                          // 6.0.2.3 (2014/10/10) mixImage(画像合成) 関係
    023import java.awt.Font;                                                           // 6.0.2.3 (2014/10/10) mixImage(画像合成) 関係
    024import java.awt.Graphics2D;                                                     // 6.0.2.3 (2014/10/10) mixImage(画像合成) 関係
    025import java.awt.FontMetrics;                                            // 6.0.2.3 (2014/10/10) mixImage(画像合成) 関係
    026import java.awt.image.BufferedImage;
    027import java.awt.image.ColorConvertOp;
    028import java.awt.Transparency;                                           // 7.0.1.1 (2018/10/22) 透過色処理 関係
    029import java.io.File;
    030import java.io.IOException;
    031import java.io.InputStream;
    032import java.io.ByteArrayOutputStream;
    033import java.util.Locale;
    034import java.util.Arrays;
    035import javax.media.jai.JAI;
    036
    037import javax.imageio.ImageIO;
    038import javax.imageio.IIOException;
    039
    040import com.sun.media.jai.codec.FileSeekableStream;
    041import com.sun.media.jai.util.SimpleCMYKColorSpace;
    042
    043import org.opengion.fukurou.system.Closer;                                                      // 6.4.2.0 (2016/01/29) package変更 fukurou.util → fukurou.system
    044import static org.opengion.fukurou.system.HybsConst.CR;                         // 6.1.0.0 (2014/12/26) refactoring
    045
    046/**
    047 * ImageUtil は、画像ファイル関連の処理を集めたユーティリティクラスです。
    048 *
    049 * ここでは、イメージファイルを BufferedImage にして取り扱います。
    050 * また、ImageResizer で処理していた static メソッドや、関連処理、
    051 * org.opengion.hayabusa.servlet.MakeImage の主要な処理もこちらに持ってきます。
    052 *
    053 * @version  6.0.2.3 (2014/10/10)
    054 * @author   Hiroki Nakamura
    055 * @since    JDK6.0,
    056 */
    057public final class ImageUtil {
    058
    059        private static final String ICC_PROFILE = "ISOcoated_v2_eci.icc";               // 5.5.3.4 (2012/06/19)
    060
    061        // 6.0.2.3 (2014/10/10) テキスト合成で指定できる設定値
    062        /** X軸に対して、テキストを画像の左寄せで表示します。 **/
    063        public static final int LEFT    = -1 ;
    064        /** X軸に対して、テキストを画像の中央揃えで表示します。 **/
    065        public static final int CENTER  = -2 ;
    066        /** X軸に対して、テキストを画像の右寄せで表示します。 **/
    067        public static final int RIGHT   = -3 ;
    068
    069        /** Y軸に対して、テキストを画像の上揃えで表示します。 **/
    070        public static final int TOP             = -4 ;
    071        /** Y軸に対して、テキストを画像の中央揃えで表示します。 **/
    072        public static final int MIDDLE  = -5 ;
    073        /** Y軸に対して、テキストを画像の下揃えで表示します。 **/
    074        public static final int BOTTOM  = -6 ;
    075
    076        /** 入力画像の形式 **/
    077        public static final String READER_SUFFIXES ;    // 5.6.5.3 (2013/06/28) 入力画像の形式 [bmp, gif, jpeg, jpg, png, wbmp]
    078        /** 出力画像の形式 **/
    079        public static final String WRITER_SUFFIXES ;    // 5.6.5.3 (2013/06/28) 出力画像の形式 [bmp, gif, jpeg, jpg, png, wbmp]
    080        // 5.6.5.3 (2013/06/28) 入力画像,出力画像の形式 を ImageIO から取り出します。
    081        static {
    082                final String[] rfn = ImageIO.getReaderFileSuffixes();
    083                Arrays.sort( rfn );
    084                READER_SUFFIXES = Arrays.toString( rfn );
    085
    086                final String[] wfn = ImageIO.getWriterFileSuffixes();
    087                Arrays.sort( wfn );
    088                WRITER_SUFFIXES = Arrays.toString( wfn );
    089        }
    090
    091        /**
    092         * すべてが staticメソッドなので、コンストラクタを呼び出さなくしておきます。
    093         *
    094         */
    095        private ImageUtil() {}
    096
    097        /**
    098         * 入力ファイル名を指定し、画像オブジェクトを作成します。
    099         *
    100         * @og.rev 5.4.3.5 (2012/01/17) CMYK対応
    101         * @og.rev 5.4.3.7 (2012/01/20) FAIでのファイル取得方法変更
    102         * @og.rev 5.4.3.8 (2012/01/24) エラーメッセージ追加
    103         * @og.rev 5.6.5.3 (2013/06/28) 入力画像の形式 を ImageIO から取り出します。
    104         * @og.rev 6.0.2.3 (2014/10/10) ImageResizer から、移植しました。
    105         *
    106         * @param fin 入力ファイル名
    107         * @return 読み込まれた画像オブジェクト(BufferedImage)
    108         */
    109        public static BufferedImage readFile( final String fin ) {
    110                // 5.6.5.3 (2013/06/28) 入力画像の形式 を ImageIO から取り出します。
    111                if( !ImageUtil.isReaderSuffix( fin ) ) {
    112                        final String errMsg = "入力ファイルは" + READER_SUFFIXES + "のいずれかの形式のみ指定可能です。"
    113                                                        + "File=[" + fin + "]";
    114                        throw new OgRuntimeException( errMsg );
    115                }
    116
    117                final File inFile = new File( fin );
    118                BufferedImage bi = null;
    119                try {
    120                        bi = ImageIO.read( inFile );
    121                }
    122                catch( final IIOException ex ) { // 5.4.3.5 (2012/01/17) 決めうち
    123                        // API的には、IllegalArgumentException と IOException しか記述されていない。
    124                        // 何もせずに、下の処理に任せます。
    125                        // 6.0.2.5 (2014/10/31) refactoring:Avoid empty catch blocks 警告対応
    126                        final String errMsg = "cmykToSRGB 処理が必要です。" + ex.getMessage();
    127                        System.err.println( errMsg );
    128                }
    129                catch( final IOException ex ) {
    130                        final String errMsg = "イメージファイルの読込に失敗しました。" + "File=[" + fin + "]";
    131                        throw new OgRuntimeException( errMsg,ex );
    132                }
    133
    134                // 6.0.0.1 (2014/04/25) IIOException の catch ブロックからの例外出力を外に出します。
    135                // bi == null は、結果のストリームを読み込みできないような場合、または、IO例外が発生した場合。
    136                if( bi == null ) {
    137                        FileSeekableStream fsstream = null;
    138                        try{
    139                                // 5.4.3.7 (2012/01/20) ファイルの開放がGC依存なので、streamで取得するように変更
    140                                // bi = cmykToSRGB(JAI.create("FileLoad",inFile.toString()).getAsBufferedImage(null,null));
    141                                fsstream = new FileSeekableStream(inFile.getAbsolutePath());
    142                                bi = cmykToSRGB(JAI.create("stream",fsstream).getAsBufferedImage(null,null));
    143                        }
    144                        catch( final IOException ex ){
    145                                final String errMsg = "イメージファイルの読込(JAI)に失敗しました。" + "File=[" + fin + "]";
    146                                throw new OgRuntimeException( errMsg,ex );
    147                        }
    148                        catch( final RuntimeException ex ) {            // 5.4.3.8 (2012/01/23) その他エラーの場合追加
    149                                final String errMsg = "イメージファイルの読込(JAI)に失敗しました。ファイルが壊れている可能性があります。" + "File=[" + fin + "]";
    150                                throw new OgRuntimeException( errMsg,ex );
    151                        }
    152                        finally{
    153                                Closer.ioClose(fsstream);
    154                        }
    155                }
    156
    157                return bi;
    158        }
    159
    160        /**
    161         * 画像オブジェクト と、出力ファイル名を指定し、ファイルに書き込みます。
    162         *
    163         * ImageIO に指定する formatName(ファイル形式)は、出力ファイル名の拡張子から取得します。
    164         * [bmp, gif, jpeg, jpg, png, wbmp] 位がサポートされています。
    165         *
    166         * @og.rev 6.0.2.3 (2014/10/10) 新規作成
    167         *
    168         * @param image 出力する画像オブジェクト(BufferedImage)
    169         * @param fout 出力ファイル名
    170         */
    171        public static void saveFile( final BufferedImage image , final String fout ) {
    172                final File outFile = new File( fout );
    173                try {
    174                        final String outSuffix = ImageUtil.getSuffix( fout );
    175                        ImageIO.write( image, outSuffix, outFile );
    176                }
    177                catch( final IOException ex ) {
    178                        final String errMsg = "イメージファイルの書き込みに失敗しました。" + "File=[" + fout + "]";
    179                        throw new OgRuntimeException( errMsg,ex );
    180                }
    181        }
    182
    183        /**
    184         * 入力ファイル名を指定し、画像ファイルの byte配列を作成します。
    185         *
    186         * ImageIO に指定する formatName(ファイル形式)は、出力ファイル名の拡張子から取得します。
    187         * [bmp, gif, jpeg, jpg, png, wbmp] 位がサポートされています。
    188         *
    189         * @og.rev 6.0.2.3 (2014/10/10) 新規作成
    190         *
    191         * @param fin 入力ファイル名
    192         * @return 読み込まれた画像ファイルの byte配列
    193         * @og.rtnNotNull
    194         */
    195        public static byte[] byteImage( final String fin ) {
    196                final ByteArrayOutputStream baOut = new ByteArrayOutputStream();
    197
    198                final BufferedImage img = ImageUtil.readFile( fin );
    199                try {
    200                        final String suffix = ImageUtil.getSuffix( fin );
    201                        ImageIO.write( img, suffix, baOut );
    202                }
    203                catch( final IOException ex ) {
    204                        final String errMsg = "イメージファイルの読み込みに失敗しました。" + "File=[" + fin + "]";
    205                        throw new OgRuntimeException( errMsg,ex );
    206                }
    207                finally {
    208                        Closer.ioClose( baOut );                // ByteArrayOutputStreamを閉じても、何の影響もありません。
    209                }
    210
    211                return baOut.toByteArray();
    212        }
    213
    214        /**
    215         * ファイル名から拡張子(小文字)を求めます。
    216         * 拡張子 が存在しない場合は、null を返します。
    217         *
    218         * @og.rev 5.6.5.3 (2013/06/28) private ⇒ public へ変更
    219         * @og.rev 6.0.2.3 (2014/10/10) ImageResizer から、移植しました。
    220         *
    221         * @param fileName ファイル名
    222         *
    223         * @return 拡張子(小文字)。なければ、null
    224         */
    225        public static String getSuffix( final String fileName ) {
    226                String suffix = null;
    227                if( fileName != null ) {
    228                        final int sufIdx = fileName.lastIndexOf( '.' );
    229                        if( sufIdx >= 0 ) {
    230                                suffix = fileName.substring( sufIdx + 1 ).toLowerCase( Locale.JAPAN );
    231                        }
    232                }
    233                return suffix;
    234        }
    235
    236        /**
    237         * ファイル名から入力画像になりうるかどうかを判定します。
    238         * コンストラクターの引数（入力画像）や、実際の処理の中（出力画像）で
    239         * 、変換対象となるかどうかをチェックしていますが、それを事前に確認できるようにします。
    240         *
    241         * @og.rev 5.6.5.3 (2013/06/28) 新規追加
    242         * @og.rev 5.6.6.1 (2013/07/12) getSuffix が null を返すケースへの対応
    243         * @og.rev 6.0.2.3 (2014/10/10) ImageResizer から、移植しました。
    244         *
    245         * @param fileName ファイル名
    246         *
    247         * @return 入力画像として使用できるかどうか。できる場合は、true
    248         */
    249        public static boolean isReaderSuffix( final String fileName ) {
    250                final String suffix = getSuffix( fileName );
    251
    252                return suffix != null && READER_SUFFIXES.indexOf( suffix ) >= 0 ;
    253        }
    254
    255        /**
    256         * ファイル名から出力画像になりうるかどうかを判定します。
    257         * コンストラクターの引数（入力画像）や、実際の処理の中（出力画像）で
    258         * 、変換対象となるかどうかをチェックしていますが、それを事前に確認できるようにします。
    259         *
    260         * @og.rev 5.6.5.3 (2013/06/28) 新規追加
    261         * @og.rev 5.6.6.1 (2013/07/12) getSuffix が null を返すケースへの対応
    262         * @og.rev 6.0.2.3 (2014/10/10) ImageResizer から、移植しました。
    263         *
    264         * @param fileName ファイル名
    265         *
    266         * @return 出力画像として使用できるかどうか。できる場合は、true
    267         */
    268        public static boolean isWriterSuffix( final String fileName ) {
    269                final String suffix = getSuffix( fileName );
    270
    271                return suffix != null && WRITER_SUFFIXES.indexOf( suffix ) >= 0 ;
    272        }
    273
    274        /**
    275         * 色変換を行います。
    276         * 変換元の色を、最初のビットから作ります。
    277         * なお、スキャンしながら、色変換が行われなかった場合は、逆からスキャンします。
    278         * つまり、背景色で輪郭抽出して、外周だけ透明にするという感じです。
    279         *
    280         * @og.rev 7.0.2.1 (2019/03/04) 元の色をイメージの端から自動取得(白決め打ちでない)属性追加
    281         *
    282         * @param img 変換対象のBufferedImage
    283         * @param tCol 変換後の色
    284         * @param mask 変換対象の色変動を抑えるためのﾏｽｸ(0x00f0f0f0など)
    285         */
    286        public static void changeColor( final BufferedImage img , final Color tCol , final int mask ) {
    287                final int wd = img.getWidth();
    288                final int ht = img.getHeight();
    289                final int fc = img.getRGB( 0,0 ) & mask;                // 変換元のRGB値は、一番端のﾋﾟｸｾﾙ
    290                final int tc = tCol.getRGB();                                   // 変換後のRGB値。例：new Color( 255,255,255,0 ) なら、透明
    291
    292                for( int y=0; y<ht; y++ ) {
    293                        boolean isRev = false;
    294                        for( int x=0; x<wd; x++ ) {
    295                                final int ic = img.getRGB( x,y ) & mask;
    296                                if( ic == fc ) {                                                        // 変換色チェック
    297                                        img.setRGB( x,y,tc );
    298                                }
    299                                else {
    300                                        isRev = true;                                                   // 反転処理を行う。
    301                                        break;                                                                  // 変換ができなかった。＝ 境界線
    302                                }
    303                        }
    304                        if( isRev ) {
    305                                for( int x=wd-1; x>=0; x-- ) {
    306                                        final int ic = img.getRGB( x,y ) & mask;
    307                                        if( ic == fc ) {                                                // 変換色チェック
    308                                                img.setRGB( x,y,tc );
    309                                        }
    310                                        else {
    311                                                break;                                                          // 変換ができなかった。＝ 境界線
    312                                        }
    313                                }
    314                        }
    315                }
    316        }
    317
    318        /**
    319         * 色変換を行います。
    320         * 例えば、背景色白を、透明に変換するなどです。
    321         *
    322         * ボーダー色は、背景色と異なる色の場合があるため、特別に用意しています。
    323         * ボーダーは、画像の周辺、3px を対象とします。
    324         *
    325         * @og.rev 6.0.2.3 (2014/10/10) 新規追加
    326         * @og.rev 7.0.1.0 (2018/10/15) 色変換に、元の色の変動を吸収するﾏｽｸ属性追加
    327         *
    328         * @param img 変換対象のBufferedImage
    329         * @param fCol 変換対象の色
    330         * @param tCol 変換後の色
    331         * @param mask 変換対象の色変動を抑えるためのﾏｽｸ(0x00f0f0f0など)
    332         */
    333        public static void changeColor( final BufferedImage img , final Color fCol , final Color tCol , final int mask ) {
    334                final int wd = img.getWidth();
    335                final int ht = img.getHeight();
    336                final int fc = fCol.getRGB() & mask;                    // 変換元のRGB値。
    337                final int tc = tCol.getRGB();                                   // 変換後のRGB値。例：new Color( 255,255,255,0 ) なら、透明
    338
    339                for( int y=0; y<ht; y++ ) {
    340                        for( int x=0; x<wd; x++ ) {
    341                                final int ic = img.getRGB( x,y ) & mask;
    342                                if( ic == fc ) {                                                                                                                        // 変換色チェック
    343                                        img.setRGB( x,y,tc );
    344                                }
    345                        }
    346                }
    347        }
    348
    349        /**
    350         * BufferedImageをISOCoatedのICCプロファイルで読み込み、RGBにした結果を返します。
    351         * (CMYKからRBGへの変換、ビット反転)
    352         * なお、ここでは、外部の ICC_PROFILE(ISOcoated_v2_eci.icc) を利用して、処理速度アップを図りますが、
    353         * 存在しない場合、標準の、com.sun.media.jai.util.SimpleCMYKColorSpace を利用しますので、エラーは出ません。
    354         * ただし、ものすごく遅いため、実用的ではありません。
    355         * ISOcoated_v2_eci.icc ファイルは、zip圧縮して、拡張子をjar に変更後、(ISOcoated_v2_eci.jar)
    356         * javaエクステンション((JAVA_HOME\)jre\lib\ext) にコピーするか、実行時に、CLASSPATHに設定します。
    357         *
    358         * @og.rev 5.4.3.5 (2012/01/17)
    359         * @og.rev 5.5.3.4 (2012/06/19) ICC_PROFILE の取得先を、ISOcoated_v2_eci.icc に変更
    360         * @og.rev 6.0.2.3 (2014/10/10) ImageResizer から、移植しました。(static にして)
    361         *
    362         * @param readImage BufferedImageオブジェクト
    363         *
    364         * @return 変換後のBufferedImage
    365         * @throws IOException 入出力エラーが発生したとき
    366         */
    367        public static BufferedImage cmykToSRGB( final BufferedImage readImage ) throws IOException {
    368                final ClassLoader loader = Thread.currentThread().getContextClassLoader();
    369                final InputStream icc_stream = loader.getResourceAsStream( ICC_PROFILE );
    370
    371                // 5.5.3.4 (2012/06/19) ICC_PROFILE が存在しない場合は、標準のSimpleCMYKColorSpace を使用。
    372                ColorSpace cmykCS = null;
    373                // 6.4.1.1 (2016/01/16) PMD refactoring. Avoid if (x != y) ..; else ..;
    374                if( icc_stream == null ) {
    375                        // 遅いので標準のスペースは使えない
    376                        final String errMsg = ICC_PROFILE + " が見つかりません。" + CR
    377                                                        + " CLASSPATHの設定されている場所に配備してください。"      +       CR
    378                                                        + " 標準のSimpleCMYKColorSpaceを使用しますのでエラーにはなりませんが、非常に遅いです。" ;
    379                        System.out.println( errMsg );
    380                        cmykCS = SimpleCMYKColorSpace.getInstance();
    381                }
    382                else {
    383                        final ICC_Profile prof = ICC_Profile.getInstance(icc_stream);   //変換プロファイル
    384                        cmykCS = new ICC_ColorSpace(prof);
    385                }
    386
    387                final BufferedImage rgbImage = new BufferedImage(readImage.getWidth(),readImage.getHeight(), BufferedImage.TYPE_INT_RGB);
    388                final ColorSpace rgbCS = rgbImage.getColorModel().getColorSpace();
    389                final ColorConvertOp cmykToRgb = new ColorConvertOp(cmykCS, rgbCS, null);
    390                cmykToRgb.filter(readImage, rgbImage);
    391
    392                final int width  = rgbImage.getWidth();
    393                final int height = rgbImage.getHeight();
    394                // 反転が必要
    395                for( int i=0;i<width;i++ ) {
    396                        for( int j=0;j<height;j++ ) {
    397                                int rgb = rgbImage.getRGB(i, j);
    398                                final int rr = (rgb & 0xff0000) >> 16;
    399                                final int gg = (rgb & 0x00ff00) >> 8;
    400                                final int bb =  rgb & 0x0000ff ;
    401                                rgb = (Math.abs(rr - 255) << 16) + (Math.abs(gg - 255) << 8) + (Math.abs(bb - 255));
    402                                rgbImage.setRGB(i, j, rgb);
    403                        }
    404                }
    405
    406                return rgbImage;
    407        }
    408
    409        /**
    410         * 画像イメージに、文字列を動的に合成作成して返します。
    411         *
    412         * 描画指定の位置(x,y)は、テキストの左下の位置を、画像イメージの、左上を起点(0,0)とした
    413         * 位置になります。
    414         * maxW , maxH を指定すると、テキストのフォントサイズをその範囲に収まるように自動調整します。
    415         *
    416         * @og.rev 6.0.2.3 (2014/10/10) 新規追加
    417         *
    418         * @param image 合成する元の画像オブジェクト
    419         * @param text  描画される文字列
    420         * @param xAxis テキストが描画される位置のx座標。または、{@link #LEFT LEFT},{@link #CENTER CENTER},{@link #RIGHT RIGHT} 指定で、自動計算する。
    421         * @param yAxis テキストが描画される位置のy座標。または、{@link #TOP TOP},{@link #MIDDLE MIDDLE},{@link #BOTTOM BOTTOM} 指定で、自動計算する。
    422         * @param maxW  テキストの最大幅(imageの幅と比較して小さい方の値。0以下の場合は、imageの幅)
    423         * @param maxH  テキストの最大高さ(imageの高さと比較して小さい方の値。0以下の場合は、imageの高さ)
    424         * @param font  描画されるテキストのフォント。null の場合は、初期値(Dialog.plain,12px)が使われる
    425         * @param color 描画されるテキストの色(Color)。null の場合は、Color.BLACK が使われる
    426         *
    427         * @return 合成された画像オブジェクト(BufferedImage)
    428         * @og.rtnNotNull
    429         * @see         #mixImage( BufferedImage, String, int, int, Font, Color )
    430         */
    431        public static BufferedImage mixImage( final BufferedImage image,
    432                                                                                        final String text, final int xAxis, final int yAxis, final int maxW, final int maxH,
    433                                                                                        final Font font, final Color color ) {
    434
    435                final int imgWidth  = image.getWidth();                                 // 画像の幅
    436                final int imgHeight = image.getHeight();                                        // 画像の高さ
    437
    438                final int maxWidth  = maxW <= 0 ? imgWidth  : Math.min( maxW,imgWidth );
    439                final int maxHeight = maxH <= 0 ? imgHeight : Math.min( maxH,imgHeight );
    440
    441                final Graphics2D gph = image.createGraphics();
    442                if( font != null ) { gph.setFont(  font  ); }           // new Font("Serif", Font.BOLD, 14)
    443
    444                float size = 5.0f;              // 小さすぎると見えないので、開始はこれくらいから行う。
    445                final float step = 0.5f;                // 刻み幅
    446                while( true ) {
    447                        final Font tmpFont = gph.getFont().deriveFont( size );
    448                        gph.setFont( tmpFont );
    449
    450                        final FontMetrics fm = gph.getFontMetrics();
    451                        final int txtWidth  = fm.stringWidth( text );
    452                        final int txtHeight = fm.getAscent();
    453
    454                        if( maxWidth < txtWidth || maxHeight < txtHeight ) {
    455                                size -= step;   // 一つ戻しておく。場合によっては、step分戻して、stepを小さくして続ける方法もある。
    456                                break;
    457                        }
    458                        size += step;
    459                }
    460                final Font newFont = gph.getFont().deriveFont( size );
    461
    462                return mixImage( image, text, xAxis, yAxis, newFont, color );
    463        }
    464
    465        /**
    466         * 画像イメージに、文字列を動的に合成作成して返します。
    467         *
    468         * 描画指定の位置(x,y)は、テキストの左下の位置を、画像イメージの、左上を起点(0,0)とした
    469         * 位置になります。
    470         *
    471         * @og.rev 6.0.2.3 (2014/10/10) org.opengion.hayabusa.servlet.MakeImage から、移植しました。
    472         *
    473         * @param image 合成する元の画像オブジェクト
    474         * @param text  描画される文字列
    475         * @param xAxis テキストが描画される位置のx座標。または、{@link #LEFT LEFT},{@link #CENTER CENTER},{@link #RIGHT RIGHT} 指定で、自動計算する。
    476         * @param yAxis テキストが描画される位置のy座標。または、{@link #TOP TOP},{@link #MIDDLE MIDDLE},{@link #BOTTOM BOTTOM} 指定で、自動計算する。
    477         * @param font  描画されるテキストのフォント。null の場合は、初期値(Dialog.plain,12px)が使われる
    478         * @param color 描画されるテキストの色(Color)。null の場合は、Color.BLACK が使われる
    479         *
    480         * @return 合成された画像オブジェクト(BufferedImage)
    481         * @og.rtnNotNull
    482         * @see         #mixImage( BufferedImage, String, int, int, int, int, Font, Color )
    483         */
    484        public static BufferedImage mixImage( final BufferedImage image,
    485                                                                                        final String text, final int xAxis, final int yAxis,
    486                                                                                        final Font font, final Color color ) {
    487
    488                final Graphics2D gph = image.createGraphics();
    489
    490        //      gph.setRenderingHint( java.awt.RenderingHints.KEY_TEXT_ANTIALIASING,java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON );
    491
    492                if( font  != null ) { gph.setFont(  font  ); }                  // new Font("Serif", Font.BOLD, 14)
    493                // 6.4.1.1 (2016/01/16) PMD refactoring. Avoid if (x != y) ..; else ..;
    494                if( color == null ) { gph.setColor( Color.BLACK ); }    // new Color(0,0,255) など
    495                else {                            gph.setColor( color ); }
    496
    497                // 実際の位置ではなく、X軸が、LEFT,CENTER,RIGHT 等の指定
    498                int x1 = xAxis ;
    499                if( x1 < 0 ) {
    500                        final int imgWidth = image.getWidth();                                  // 画像の幅
    501                        final FontMetrics fm = gph.getFontMetrics();
    502                        final int txtWidth = fm.stringWidth( text );                            // テキストの長さ
    503
    504                        switch( x1 ) {
    505                                case LEFT   : x1 = 0;                                                   // 左寄せなので、0
    506                                                                break;
    507                                case CENTER : x1 = imgWidth/2 - txtWidth/2;             // 画像の中心から、テキストの中心を引き算
    508                                                                break;
    509                                case RIGHT  : x1 = imgWidth - txtWidth;                 // 右寄せは、画像の右端からテキスト分を引き算
    510                                                                break;
    511                                default :
    512                                        final String errMsg = "X軸 で範囲外のデータが指定されました。" + "text=[" + text + "]"
    513                                                                                + " (x,y)=[" + xAxis + "," + yAxis + "]" ;
    514                                        throw new OgRuntimeException( errMsg );
    515                                //      break;          制御は移りません。
    516                        }
    517                }
    518
    519                // 実際の位置ではなく、Y軸が、TOP,MIDDLE,BOTTOM 等の指定
    520                final int Ydef = 2 ;    // 良く判らないが、位置合わせに必要。
    521                int y1 = yAxis ;
    522                if( y1 < 0 ) {
    523                        final int imgHeight = image.getHeight() -Ydef;                  // 画像の高さ
    524                        final FontMetrics fm = gph.getFontMetrics();
    525                        final int txtHeight = fm.getAscent() -Ydef;                             // テキストの幅(=Ascent)
    526
    527                        switch( y1 ) {
    528                                case TOP    : y1 = txtHeight;                                   // 上寄せは、テキストの幅分だけ下げる
    529                                                                break;
    530                                case MIDDLE : y1 = (imgHeight)/2 + (txtHeight)/2 ;      // 画像の中心から、テキストの中心分下げる(加算)
    531                                                                break;
    532                                case BOTTOM : y1 = imgHeight;                                   // 下寄せは、画像の高さ分-2
    533                                                                break;
    534                                default :
    535                                        final String errMsg = "Y軸 で範囲外のデータが指定されました。" + "text=[" + text + "]"
    536                                                                                + " (x,y)=[" + xAxis + "," + yAxis + "]" ;
    537                                        throw new OgRuntimeException( errMsg );
    538                                //      break;          制御は移りません。
    539                        }
    540                }
    541
    542                gph.drawString( text, x1, y1 );
    543                gph.dispose();          // グラフィックス・コンテキストを破棄
    544
    545                return image;
    546        }
    547
    548        /**
    549         * アプリケーションのサンプルです。
    550         *
    551         * 入力イメージファイルを読み取って、テキストを合成して、出力イメージファイル に書き込みます。
    552         * テキストの挿入位置を、X軸、Y軸で指定します。
    553         * X軸とY軸には、特別な記号があり、左寄せ、右寄せ等の指示が可能です。
    554         *
    555         * サンプルでは、new Font("Serif", Font.PLAIN, 14); と、new Color(0,0,255);（青色）を固定で渡しています。
    556         *
    557         * Usage: java org.opengion.fukurou.util.ImageUtil 入力ファイル 出力ファイル
    558         *                                                     -mix テキスト X軸 Y軸 [-fname=フォント名 -fstyle=スタイル -fsize=サイズ -color=カラー]
    559         *   X軸 指定(正の値は実際の位置)
    560         *    -1 ･･･ LEFT    左寄せ
    561         *    -2 ･･･ CENTER  中央揃え
    562         *    -3 ･･･ RIGHT   右寄せ
    563         *
    564         *   Y軸 指定(正の値は実際の位置)
    565         *    -4 ･･･ TOP     上揃え
    566         *    -5 ･･･ MIDDLE  中央揃え
    567         *    -6 ･･･ BOTTOM  下揃え
    568         *
    569         *   -fname=フォント名(初期値:Serif)
    570         *    Serif , SansSerif , Monospaced , Dialog , DialogInput
    571         *
    572         *   -fstyle=スタイル(初期値:0:PLAIN)を、数字で選びます。
    573         *    0:PLAIN ,  1:BOLD , 2:ITALIC
    574         *
    575         *   -fsize=サイズ(初期値:14)
    576         *    フォントサイズを整数で指定します。
    577         *
    578         *   -color=カラー
    579         *    色を表す文字列(BLUE,GREEN か、#808000 などの16bitRGB表記)
    580         *
    581         * Usage: java org.opengion.fukurou.util.ImageUtil 入力ファイル 出力ファイル
    582         *                                                     -trans [-color=カラー -alpha=透過率(0-100%)]
    583         *   -color=カラー(初期値:WHITE)
    584         *     透明色にする色を指定(BLUE,GREEN か、#808000 などの16bitRGB表記)
    585         *
    586         *   -alpha=透過率(0-100%)(初期値:0)
    587         *     透過率は、0:透明から100不透明まで指定します。
    588         *
    589         *   -mask=元の色にﾏｽｸを16進数24Bitで指定します(初期値:00f0f0f0)
    590         *
    591         *   -useBGColor 透明色にする色を元の一番端の色を使用する(初期値:false)
    592         *
    593         * @og.rev 6.4.5.1 (2016/04/28) mainメソッドの起動方法を変更します。
    594         * @og.rev 7.0.1.0 (2018/10/15) 色変換に、元の色の変動を吸収するﾏｽｸ属性追加
    595         * @og.rev 7.0.2.1 (2019/03/04) 元の色をイメージの端から自動取得(白決め打ちでない)属性追加
    596         *
    597         * @param  args  引数文字列配列 入力ファイル、出力ファイル、縦横最大サイズ
    598         */
    599        public static void main( final String[] args ) {
    600                if( args.length < 3 ) {
    601                        final String usage = "Usage: java org.opengion.fukurou.util.ImageUtil 入力ファイル 出力ファイル\n" +
    602                                                        "               -mix テキスト X軸 Y軸 [-fname=フォント名 -fstyle=スタイル -fsize=サイズ -color=カラー]\n" +
    603                                                        "\tX軸とY軸には、特別な記号があり、左寄せ、右寄せ等の指示が可能です。\n" +
    604                                                        "\t   X軸 指定(正の値は実際の位置)\n"       +
    605                                                        "\t    -1 ･･･ LEFT    左寄せ\n"                    +
    606                                                        "\t    -2 ･･･ CENTER  中央揃え\n"           +
    607                                                        "\t    -3 ･･･ RIGHT   右寄せ\n"                    +
    608                                                        "\t\n"                                                                  +
    609                                                        "\t   Y軸 指定(正の値は実際の位置)\n"       +
    610                                                        "\t    -4 ･･･ TOP     上揃え\n"                    +
    611                                                        "\t    -5 ･･･ MIDDLE  中央揃え\n"           +
    612                                                        "\t    -6 ･･･ BOTTOM  下揃え\n"                    +
    613                                                        "\t\n"                                                                  +
    614                                                        "\t   -fname=フォント名(初期値:Serif)\n"        +
    615                                                        "\t    Serif , SansSerif , Monospaced , Dialog , DialogInput\n" +
    616                                                        "\t\n"                                                                  +
    617                                                        "\t   -fstyle=スタイル(初期値:0:PLAIN)\n"      +
    618                                                        "\t    0:PLAIN ,  1:BOLD , 2:ITALIC\n"  +
    619                                                        "\t\n"                                                                  +
    620                                                        "\t   -fsize=サイズ(初期値:14)\n"             +
    621                                                        "\t    フォントサイズを整数で指定\n" +
    622                                                        "\t\n"                                                                  +
    623                                                        "\t   -color=カラー\n"                                     +
    624                                                        "\t    色を表す文字列(BLUE,GREEN か、#808000 などの16bitRGB表記)\n"   +
    625                                                        "\t\n"                                                                  +
    626                                                        "Usage: java org.opengion.fukurou.util.ImageUtil 入力ファイル 出力ファイル\n" +
    627                                                        "               -trans [-color=カラー -alpha=透過率(0-100%)]\n"               +
    628                                                        "\t   -color=カラー\n"                                     +
    629                                                        "\t    透明色にする色を指定(BLUE,GREEN か、#808000 などの16bitRGB表記)"  +
    630                                                        "\t   -alpha=透過率(0-100%)\n"                                                     +
    631                                                        "\t    透過率は、0:透明から100不透明まで指定します。\n" +
    632                                                        "\t   -mask=元の色にﾏｽｸを16進数24Bitで指定します(初期値:00f0f0f0)\n" +                  // 7.0.1.0 (2018/10/15) 色変換に、ﾏｽｸ属性追加
    633                                                        "\t   -useBGColor 透明色にする色を元の一番端の色を使用する(初期値:false)\n" ;  // 7.0.2.1 (2019/03/04) 透明色にする色を端から取得
    634                        System.out.println( usage );
    635                        return ;
    636                }
    637
    638                final String inImg  = args[0];
    639                final String outImg = args[1];
    640                final String imgType= args[2];
    641
    642//              final boolean isMix = imgType.equals( "-mix" );                 // 文字列合成
    643//              final boolean isTrn = imgType.equals( "-trans" );               // 透過色指定
    644
    645                final BufferedImage image = ImageUtil.readFile( inImg );
    646
    647                final boolean isMix = imgType.equals( "-mix" );                 // 文字列合成
    648                if( isMix ) {
    649                        final String text   = args[3];
    650                        final int x = Integer.parseInt( args[4] );
    651                        final int y = Integer.parseInt( args[5] );
    652
    653                        String  fname  = "Serif";
    654                        int             fstyle = Font.PLAIN;            // =0;
    655                        int             fsize  = 14;
    656                        Color   color  = Color.BLUE;
    657
    658                        for( int i=6; i<args.length; i++ ) {
    659                                if( args[i].startsWith( "-fname="       ) ) { fname             = args[i].substring( 7 ); }                                                                             // 7 = "-fname=".length()
    660                                if( args[i].startsWith( "-fstyle="      ) ) { fstyle    = Integer.parseInt(                      args[i].substring( 8 ) ); }            // 8 = "-fstyle=".length()
    661                                if( args[i].startsWith( "-fsize="       ) ) { fsize             = Integer.parseInt(                      args[i].substring( 7 ) ); }            // 7 = "-fsize=".length()
    662                                if( args[i].startsWith( "-color="       ) ) { color             = ColorMap.getColorInstance( args[i].substring( 7 ) ); }                // 7 = "-color=".length()
    663                        }
    664
    665                        // 6.9.8.0 (2018/05/28) FindBugs:条件は効果がない
    666//                      if( isMix ) {
    667                                final Font font = new Font( fname, fstyle, fsize );
    668                                ImageUtil.mixImage( image , text , x , y , font , color );
    669//                      }
    670                        ImageUtil.saveFile( image , outImg );
    671                }
    672
    673                final boolean isTrn = imgType.equals( "-trans" );       // 透過色指定
    674
    675                if( isTrn ) {
    676                        Color   fColor  = Color.WHITE;                  // 初期値は、白を透明に変換する。
    677                        int             alpha   = 0;
    678                        int             mask    = 0x00f0f0f0;                   // 7.0.1.0 (2018/10/15) 色変換時の誤差を吸収
    679                        boolean useBGcol= false;                                // 7.0.2.1 (2019/03/04)
    680//                      boolean debug   = false;
    681
    682                        for( int i=3; i<args.length; i++ ) {
    683                                if( args[i].startsWith( "-color="               ) ) { fColor    = ColorMap.getColorInstance(    args[i].substring( 7 ) ); }                     // 7 = "-color=".length()
    684                                if( args[i].startsWith( "-alpha="               ) ) { alpha             = 255/100 * Integer.parseInt(   args[i].substring( 7 ) ); }                     // 7 = "-alpha=".length()
    685                                if( args[i].startsWith( "-mask="                ) ) { mask              = Integer.parseInt(                             args[i].substring( 6 ) , 16 ); }        // 6 = "-mask=".length()
    686                                if( args[i].startsWith( "-useBGColor"   ) ) { useBGcol  = true; }                                                                                                                       // あればtrue 7.0.2.1 (2019/03/04)
    687                        }
    688
    689                        final Color tColor = new Color( fColor.getRed() , fColor.getGreen() , fColor.getBlue() , alpha );
    690
    691                        // 元のPNGが、完全な不透明だと、アルファ地設定が無視されるので、BufferedImage を作り直す必要がある。
    692                        final BufferedImage transImg ;
    693                        if( Transparency.OPAQUE == image.getTransparency() ) {          // 完全に不透明
    694                                final int   wd = image.getWidth();
    695                                final int   ht = image.getHeight();
    696                                final int[] px = image.getRGB( 0,0, wd, ht, null, 0, wd );
    697
    698                                transImg = new BufferedImage( wd,ht,BufferedImage.TYPE_INT_ARGB );      // 透明を持てる
    699                                transImg.setRGB( 0,0, wd, ht, px, 0 , wd );
    700                        }
    701                        else {
    702                                transImg = image;
    703                        }
    704
    705                        // 7.0.2.1 (2019/03/04) 元の色をイメージの端から自動取得(白決め打ちでない)属性追加
    706                        if( useBGcol ) {
    707                                System.out.println( inImg + " : 端色 → " + tColor + " 変換" );
    708                                ImageUtil.changeColor( transImg , tColor , mask );                                      // 7.0.2.1 (2019/03/04)
    709                        }
    710                        else {
    711                                System.out.println( inImg + " : " + fColor + " → " + tColor + " 変換" );
    712                                ImageUtil.changeColor( transImg , fColor , tColor , mask );                     // 7.0.1.0 (2018/10/15)
    713                        }
    714
    715                        ImageUtil.saveFile( transImg , outImg );
    716                }
    717        }
    718}