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