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