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}