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.model;
017
018import java.io.File;                                                                    // 6.2.0.0 (2015/02/27)
019import java.io.IOException;
020import java.io.OutputStream;
021// import java.io.FileOutputStream;                                             // 8.5.4.2 (2024/01/12)
022import java.io.BufferedOutputStream;
023import java.util.Locale;
024import java.util.Map;                                                                   // 6.0.2.3 (2014/10/10) 画像関連
025import java.util.HashMap;                                                               // 6.0.2.3 (2014/10/10) 画像関連
026import java.util.List;                                                                  // 8.0.1.0 (2021/10/29)
027import java.util.function.BiConsumer;                                   // 8.1.0.1 (2022/01/07)
028import java.nio.file.Files;                                                             // 8.5.4.2 (2024/01/12) PMD 7.0.0 AvoidFileStream 対応
029
030import org.apache.poi.util.Units;                                               // 7.2.9.0 (2020/10/12)
031import org.apache.poi.common.usermodel.HyperlinkType;   // 6.5.0.0 (2016/09/30) poi-3.15
032import org.apache.poi.ss.util.WorkbookUtil;
033import org.apache.poi.ss.usermodel.Workbook;
034import org.apache.poi.ss.usermodel.Sheet;
035import org.apache.poi.ss.usermodel.Row;
036import org.apache.poi.ss.usermodel.Cell;
037import org.apache.poi.ss.usermodel.CellType;                    // 6.5.0.0 (2016/09/30) poi-3.15
038import org.apache.poi.ss.usermodel.CellStyle;
039import org.apache.poi.ss.usermodel.VerticalAlignment;   // 6.5.0.0 (2016/09/30) poi-3.15
040import org.apache.poi.ss.usermodel.BorderStyle;                 // 6.5.0.0 (2016/09/30) poi-3.15
041import org.apache.poi.ss.usermodel.Font;
042import org.apache.poi.ss.usermodel.IndexedColors;
043import org.apache.poi.ss.usermodel.RichTextString;
044import org.apache.poi.ss.usermodel.Hyperlink;
045import org.apache.poi.ss.usermodel.CreationHelper;
046import org.apache.poi.ss.usermodel.Drawing;                             // 6.0.2.3 (2014/10/10) 画像関連
047import org.apache.poi.ss.usermodel.Shape;                               // 8.0.3.1 (2021/12/28) 画像関連
048import org.apache.poi.ss.usermodel.ClientAnchor;                // 6.0.2.3 (2014/10/10) 画像関連
049import org.apache.poi.ss.usermodel.Picture;                             // 6.0.2.3 (2014/10/10) 画像関連
050
051import org.apache.poi.hssf.usermodel.HSSFWorkbook;              // .xls
052
053// import org.apache.poi.POIXMLDocumentPart;                    // 6.2.4.2 (2015/05/29) テキスト変換処理  8.1.2.3 (2022/05/20) 復活
054import org.apache.poi.ooxml.POIXMLDocumentPart;                 // 7.0.0.0 (2018/10/01) poi-ooxml-3.17.jar → poi-ooxml-4.0.0.jar  8.1.2.3 (2022/05/20) 復活
055
056import org.apache.poi.xssf.usermodel.XSSFDrawing;               // 6.2.4.2 (2015/05/29) テキスト変換処理  8.1.2.3 (2022/05/20) 復活
057import org.apache.poi.xssf.usermodel.XSSFShape;                 // 6.2.4.2 (2015/05/29) テキスト変換処理
058import org.apache.poi.xssf.usermodel.XSSFSimpleShape;   // 6.2.4.2 (2015/05/29) テキスト変換処理
059import org.apache.poi.xssf.usermodel.XSSFShapeGroup;    // 8.0.3.1 (2021/12/28)
060import org.apache.poi.xssf.usermodel.XSSFTextParagraph; // 6.2.4.2 (2015/05/29) テキスト変換処理  8.1.2.3 (2022/05/20) 復活
061import org.apache.poi.xssf.usermodel.XSSFTextRun;               // 6.2.4.2 (2015/05/29) テキスト変換処理  8.1.2.3 (2022/05/20) 復活
062import org.apache.poi.xssf.usermodel.XSSFAnchor;                // .xslx 8.1.2.3 (2022/05/20)
063import org.apache.poi.xssf.usermodel.XSSFClientAnchor;  // 8.5.0.0 (2023/04/21) XSSFAnchor オブジェクトの NullPointerException 対策
064import org.apache.poi.xssf.streaming.SXSSFWorkbook;             // .xlsx 6.3.7.0 (2015/09/04) 制限あり 高速、低メモリ消費
065
066import org.opengion.fukurou.system.OgRuntimeException ; // 6.4.2.0 (2016/01/29)
067// import org.opengion.fukurou.system.Closer;                   // 8.5.4.2 (2024/01/12) PMD 7.0.0 CloseResource 対応
068import org.opengion.fukurou.util.ImageUtil;                             // 6.0.2.3 (2014/10/10) 画像関連
069
070import static org.opengion.fukurou.system.HybsConst.CR;                         // 6.1.0.0 (2014/12/26) refactoring
071import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;      // 6.1.0.0 (2014/12/26) refactoring
072
073/**
074 * POI による、EXCELバイナリファイルに対する、データモデルクラスです。
075 *
076 * 共通的な EXCEL処理 を集約しています。
077 * staticメソッドによる簡易的なアクセスの他に、順次処理も可能なように
078 * 現在アクセス中の、Workbook、Sheet、Row、Cell オブジェクトを内部で管理しています。
079 *
080 * 入力形式は、openXML形式にも対応しています。
081 * ファイルの内容に応じて、.xlsと.xlsxのどちらで読み取るかは、内部的に
082 * 自動判定されます。
083 *
084 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
085 * @og.rev 8.1.2.3 (2022/05/20) テキスト変換処理 復活
086 * @og.group その他
087 *
088 * @version  6.0
089 * @author   Kazuhiko Hasegawa
090 * @since    JDK7.0,
091 */
092// 8.5.5.1 (2024/02/29) spotbugs CT_CONSTRUCTOR_THROW(コンストラクタで、Excweptionを出さない) class を final にすれば、警告は消える。
093// public class ExcelModel {
094public final class ExcelModel {
095        /** このプログラムのVERSION文字列を設定します。 {@value} */
096        private static final String VERSION = "8.5.4.2 (2024/01/12)" ;
097
098        private static final String DEF_SHEET_NAME = "Sheet" ;
099
100        // 6.0.2.3 (2014/10/10) ImageUtil の Suffix と、Workbook.PICTURE_TYPE_*** の関連付けをしておきます。
101        // Suffix 候補は、[bmp, gif, jpeg, jpg, png, wbmp] だが、対応する PICTURE_TYPE は一致しない。
102        /** staticイニシャライザ後、読み取り専用にするので、ConcurrentHashMap を使用しません。 */
103        private static final Map<String,Integer> PICTURE_TYPE ;
104        static {
105                PICTURE_TYPE = new HashMap<>() ;
106                PICTURE_TYPE.put( "png"  , Integer.valueOf( Workbook.PICTURE_TYPE_PNG   ) );
107                PICTURE_TYPE.put( "jpeg" , Integer.valueOf( Workbook.PICTURE_TYPE_JPEG  ) );
108                PICTURE_TYPE.put( "jpg"  , Integer.valueOf( Workbook.PICTURE_TYPE_JPEG  ) );
109        }
110
111        /** エラー発生時のキーとなる、EXCELファイル名 */
112        private final String inFilename ;
113        /** 6.1.0.0 (2014/12/26) オープンしたファイル形式を記憶(ピリオドを含む) */
114        private final String sufix              ;
115
116        /** 現在処理中の Workbook */
117        private final Workbook  wkbook  ;
118        /** 現在処理中の Sheet */
119        private Sheet                   sheet   ;
120        /** 現在処理中の Row */
121        private Row                             rowObj  ;
122
123        /** 雛形シートのインデックス */
124        private int refSheetIdx = -1;
125
126        /** poi.xssf対応 */
127        private final CreationHelper createHelper       ;
128
129        /** 共通のセルスタイル */
130        private CellStyle style                 ;
131        /** Hyperlink用のセルスタイル(青文字+下線) */
132        private CellStyle hLinkStyle    ;
133
134        /** 標準セル幅の5倍を最大幅とする。 */
135        private int maxColCount                 = 5 ;
136        /** データ行の開始位置。未設定時は、-1 */
137        private int dataStartRow                = -1;
138        /** カラム幅の自動調整を行うかどうか(true:行う/false:行わない) */
139        private boolean isAutoCellSize  ;
140
141        /** Sheet一覧を先頭Sheetに作成する場合のSheet名 */
142        private String addTitleSheet    ;
143
144        /** 6.5.0.0 (2016/09/30) セルの計算式の再計算をさせるシート名の配列。 */
145        private String[] recalcSheetNames       ;
146
147        /**
148         * EXCELファイルのWookbookのデータ処理モデルを作成します。
149         *
150         * ここでは、既存のファイルを読み込んで、データ処理モデルを作成しますので、
151         * ファイルがオープンできなければエラーになります。
152         *
153         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
154         * @og.rev 6.2.0.0 (2015/02/27) ファイル引数を、String → File に変更
155         *
156         * @param   file  EXCELファイル
157         * @see         #ExcelModel( File , boolean )
158         */
159        public ExcelModel( final File file ) {
160                this( file,true );
161        }
162
163        /**
164         * EXCELファイルのWookbookのデータ処理モデルを作成します。
165         *
166         * isOpen条件によって、ファイルオープン(true)か、新規作成(false)が分かれます。
167         * ファイルオープンの場合は、EXCELの読み込み以外に、追記するとか、雛形参照する
168         * 場合にも、使用します。
169         * ファイルオープンの場合は、当然、ファイルがオープンできなければエラーになります。
170         *
171         * isOpen=新規作成(false) の場合は、ファイル名の拡張子で、XSSFWorkbook か HSSFWorkbook を
172         * 判定します。.xlsx の場合⇒XSSFWorkbook オブジェクトを使用します。
173         *
174         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
175         * @og.rev 6.0.2.3 (2014/10/10) POIUtil#createWorkbook( String ) を使用するように変更
176         * @og.rev 6.1.0.0 (2014/12/26) 入力ファイルの拡張子判定の対応
177         * @og.rev 6.2.0.0 (2015/02/27) ファイル引数を、String → File に変更
178         * @og.rev 6.2.2.0 (2015/03/27) マクロ付Excel(.xlsm)対応
179         * @og.rev 6.3.7.0 (2015/09/04),5.9.0.0 (2015/09/04) 標準を、SXSSFWorkbook に切り替えてみる。
180         *
181         * @param   file   EXCELファイル
182         * @param   isOpen true:ファイルオープン/false:新規作成
183         * @see         #ExcelModel( File )
184         */
185        public ExcelModel( final File file , final boolean isOpen ) {
186                inFilename      = file.getName();
187
188                final int idx = inFilename.lastIndexOf( '.' );  // 拡張子の位置
189                if( idx >= 0 ) {
190                        sufix = inFilename.substring( idx ).toLowerCase( Locale.JAPAN );                // ピリオドを含む
191                }
192                else {
193                        final String errMsg = "ファイルの拡張子が見当たりません。(.xls か .xlsx/.xlsm を指定下さい)" + CR
194                                                        + " filename=[" + file + "]"  + CR ;
195                        throw new IllegalArgumentException( errMsg );
196                }
197
198                if( isOpen ) {
199                        wkbook = POIUtil.createWorkbook( file );
200                }
201                else {
202                        // 新規の場合、ファイル名に.xlsxで終了した場合⇒.xlsx形式ファイル作成、その他⇒.xls形式ファイル作成
203                        if( ".xlsx".equals( sufix ) || ".xlsm".equals( sufix ) ) {              // 6.2.2.0 (2015/03/27)
204                                // 6.3.7.0 (2015/09/04),5.9.0.0 (2015/09/04) 標準を、SXSSFWorkbook に切り替えてみる。
205        //                      wkbook = new XSSFWorkbook();
206                                wkbook = new SXSSFWorkbook();   // 機能制限有:シートや行の削除や、AutoCellSize の指定ができないなど。
207                        }
208                        else if( ".xls".equals( sufix ) ) {
209                                wkbook = new HSSFWorkbook();
210                        }
211                        else {
212                                final String errMsg = "ファイルの拡張子が不正です。(.xls か .xlsx/.xlsm のみ可能)" + CR
213                                                                + " filename=[" + file + "]"  + CR ;
214                                throw new IllegalArgumentException( errMsg );
215                        }
216                }
217
218                createHelper = wkbook.getCreationHelper();              // poi.xssf対応
219        }
220
221        /**
222         * 内部 Workbook に、フォント名、フォントサイズを設定します。
223         * fontName(フォント名)は、"MS Pゴシック" など名称になります。
224         * fontPoint は、フォントの大きさを指定します。
225         * 内部的には、setFontHeightInPoints(short)メソッドで設定します。
226         *
227         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
228         *
229         * @param       fontName        フォント名 ("MS Pゴシック" など。nullの場合セットしません)
230         * @param       fontPoint       フォントの大きさ (0やマイナスの場合はセットしません)
231         */
232        public void setFont( final String fontName , final short fontPoint ) {
233        //      System.out.println( "FontName=" + fontName + " , Point=" + fontPoint );
234
235                if( style == null ) { style = wkbook.createCellStyle(); }
236
237                final Font font = wkbook.createFont();
238        //      final Font font = wkbook.getFontAt( style.getFontIndex() );                             // A,B などのヘッダーもフォントが
239                if( fontName != null ) {
240                        font.setFontName( fontName );   // "MS Pゴシック" など
241                }
242                if( fontPoint > 0 ) {
243                        font.setFontHeightInPoints( fontPoint );
244                }
245
246                style.setFont( font );
247        }
248
249        /**
250         * データ設定する セルに、罫線を追加します。
251         *
252         * ここで設定するのは、罫線の種類と、罫線の色ですが、内部的に固定にしています。
253         *   Border=CellStyle.BORDER_THIN
254         *   BorderColor=IndexedColors.BLACK
255         *
256         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
257         * @og.rev 6.5.0.0 (2016/09/30) poi-3.15 対応(Cell.CELL_TYPE_XXXX → CellType.XXXX)
258         */
259        public void setCellStyle() {
260                if( style == null ) { style = wkbook.createCellStyle(); }
261
262        //      style.setBorderBottom(  CellStyle.BORDER_THIN );        // 6.5.0.0 (2016/09/30) poi-3.12
263        //      style.setBorderLeft(    CellStyle.BORDER_THIN );        // 6.5.0.0 (2016/09/30) poi-3.12
264        //      style.setBorderRight(   CellStyle.BORDER_THIN );        // 6.5.0.0 (2016/09/30) poi-3.12
265        //      style.setBorderTop(             CellStyle.BORDER_THIN );        // 6.5.0.0 (2016/09/30) poi-3.12
266
267                style.setBorderBottom(  BorderStyle.THIN );                     // 6.4.6.0 (2016/05/27) poi-3.15
268                style.setBorderLeft(    BorderStyle.THIN );                     // 6.5.0.0 (2016/09/30) poi-3.15
269                style.setBorderRight(   BorderStyle.THIN );                     // 6.5.0.0 (2016/09/30) poi-3.15
270                style.setBorderTop(             BorderStyle.THIN );                     // 6.5.0.0 (2016/09/30) poi-3.15
271
272                style.setBottomBorderColor(     IndexedColors.BLACK.getIndex() );
273                style.setLeftBorderColor(       IndexedColors.BLACK.getIndex() );
274                style.setRightBorderColor(      IndexedColors.BLACK.getIndex() );
275                style.setTopBorderColor(        IndexedColors.BLACK.getIndex() );
276
277        //      style.setVerticalAlignment( CellStyle.VERTICAL_TOP );   // isAutoCellSize=true 文字は上寄せする。        // 6.5.0.0 (2016/09/30) poi-3.12
278                style.setVerticalAlignment( VerticalAlignment.TOP  );   // isAutoCellSize=true 文字は上寄せする。        // 6.5.0.0 (2016/09/30) poi-3.15
279        //      style.setWrapText( true );                                                              // isAutoCellSize=true 折り返して表示する。
280        }
281
282        /**
283         * 全てのSheetに対して、autoSizeColumn設定を行うかどうか指定します(初期値:false)。
284         *
285         * autoSize設定で、カラム幅が大きすぎる場合、現状では、
286         * 初期カラム幅の5倍を限度にしています。
287         *
288         * なお、autoSizeColumn設定は負荷の大きな処理なので、saveFile(String)の
289         * 中で実行されます。(セーブしなければ実行されません。)
290         * よって、指定は、いつ行っても構いません。
291         *
292         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
293         *
294         * @param flag autoSizeColumn設定を行うかどうか [true:自動カラム幅設定を行う/false:行わない]
295         * @see #useAutoCellSize( boolean,int )
296         */
297        public void useAutoCellSize( final boolean flag ) {
298                isAutoCellSize = flag;
299        }
300
301        /**
302         * 全てのSheetに対して、autoSizeColumn設定を行うかどうか指定します(初期値:false)。
303         *
304         * autoSize設定で、カラム幅が大きすぎる場合、現状では、
305         * 初期カラム幅のcount倍を限度に設定します。
306         * ただし、count がマイナスの場合は、無制限になります。
307         *
308         * なお、autoSizeColumn設定は負荷の大きな処理なので、saveFile(String)の
309         * 中で実行されます。(セーブしなければ実行されません。)
310         * よって、指定は、いつ行っても構いません。
311         *
312         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
313         *
314         * @param flag autoSizeColumn設定を行うかどうか [true:自動カラム幅設定を行う/false:行わない]
315         * @param count 最大幅を標準セル幅の何倍にするかを指定。マイナスの場合は、無制限
316         * @see #useAutoCellSize( boolean )
317         */
318        public void useAutoCellSize( final boolean flag, final int count ) {
319                isAutoCellSize = flag;
320                maxColCount    = count ;
321        }
322
323        /**
324         * EXCELで、出力処理の最後にセルの計算式の再計算をさせるシート名の配列を指定します。
325         *
326         * null の場合は、再計算しません。
327         * なお、再計算は、saveFile(String)の中で実行されます。(セーブしなければ実行されません。)
328         *
329         * @og.rev 6.5.0.0 (2016/09/30) セルの計算式の再計算をさせる recalcSheetNames 属性の追加。
330         *
331         * @param  sheets 対象シート名の配列
332         */
333        public void setRecalcSheetName( final String[] sheets ){
334                recalcSheetNames = sheets;
335        }
336
337        /**
338         * データ行の書き込み開始位置の行番号を設定します。
339         *
340         * これは、autoSize設定で、自動調整するカラムを、ヘッダーではなく、
341         * データ部で計算する場合に使用します。
342         *
343         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
344         *
345         * @param st  データ行の開始位置。未設定時は、-1
346         * @see #useAutoCellSize( boolean )
347         */
348        public void setDataStartRow( final int st ) {
349                dataStartRow = st;
350        }
351
352        /**
353         * Sheet一覧を先頭Sheetに作成する場合のSheet名を指定します。
354         *
355         * これは、Workbook に含まれる Sheet 一覧を作成する場合に、利用可能です。
356         *
357         * この処理は、#saveFile( File ) 処理時に、実行されます。
358         *
359         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
360         *
361         * @param shtName  Sheet一覧のSheet名
362         * @see #makeAddTitleSheet()
363         */
364        public void setAddTitleSheet( final String shtName ) {
365                addTitleSheet = shtName;
366        }
367
368        /**
369         * 内部 Workbookの Sheet数を返します。
370         *
371         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
372         *
373         * @return      シート数
374         */
375        public int getNumberOfSheets() {
376                return wkbook.getNumberOfSheets();
377        }
378
379        /**
380         * 内部 Workbookより、雛形Sheetをセットします。
381         *
382         * これは、雛形シートを使用する場合に、使います。このメソッドが呼ばれると、
383         * 雛形シートを使用すると判定されます。
384         * 雛形シート名が、内部 Workbook に存在しない場合は、エラーになります。
385         * ただし、null をセットした場合は、最初のシートを雛形シートとして使用すると
386         * 判定します。
387         *
388         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
389         *
390         * @param       refSheetName    参照シート名(nullの場合、参照シート使用する場合は、先頭のシート)
391         */
392        public void setRefSheetName( final String refSheetName ) {
393                // 参照シート名の指定がない場合は、最初のシート
394                refSheetIdx = refSheetName == null ? 0 : wkbook.getSheetIndex( refSheetName );
395
396                if( refSheetIdx < 0 ) {         // 参照シート名が存在しなかった。
397                        final String errMsg = "指定の参照シート名は存在しませんでした。" + CR
398                                                        + " inFilename=[" + inFilename + "] , refSheetName=[" + refSheetName + "]"  + CR ;
399                        throw new IllegalArgumentException( errMsg );
400                }
401        }
402
403        /**
404         * 内部 Workbookより、新しいSheetを作ります。
405         *
406         * 先に雛形シートを指定している場合は、その雛形シートから作成します。
407         * 指定していない場合は、新しいシートを作成します。
408         * 雛形シートを参照する場合は、雛形シートそのものを返します。
409         * また、雛形シートの枚数を超える場合は、前の雛形シートをコピーします。
410         * 雛形シートが存在しない場合は、新しいシートを作成します。
411         *
412         * シート名は、重複チェックを行い、同じ名前のシートの場合は、(1),(2)が付けられます。
413         * shtName が null の場合は、"Sheet" が割り振られます。
414         *
415         * この処理を行うと、内部の Sheet にも、ここで作成された Sheet が設定されます。
416         *
417         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
418         * @og.rev 6.2.2.3 (2015/04/10) 雛形シートにそのままデータを書き込んでいく。
419         * @og.rev 6.5.0.0 (2016/09/30) 雛形シート名をそのまま使用する場合は、isOverwrite に、true を指定します。
420         *
421         * @param       shtName シート名 (重複する場合は、(2)、(3)のような文字列を追加、nullの場合は、"Sheet")
422         * @param       isOverwrite     雛形シート名をそのまま使用する場合は、true を指定します。
423         */
424        public void createSheet( final String shtName , final boolean isOverwrite ) {
425                // 参照シートを使う場合(整合性の問題で、両方ともチェックしておきます)
426
427                // 6.2.2.3 (2015/04/10) 雛形シートにそのままデータを書き込んでいく。
428                final int shtNo ;
429                if( refSheetIdx < 0 ) {                                                                 // 雛形シートを使用しない。
430                        sheet = wkbook.createSheet();
431                        shtNo = wkbook.getNumberOfSheets() - 1;
432                }
433                else if( refSheetIdx >= wkbook.getNumberOfSheets() ) {  // シート数が雛形より超えている。
434                        sheet = wkbook.cloneSheet( refSheetIdx-1 );                     // 最後の雛形シートをコピーします。
435                        shtNo = wkbook.getNumberOfSheets() - 1;
436                        refSheetIdx++ ;
437                }
438                else {
439                        sheet = wkbook.getSheetAt( refSheetIdx );                       // 雛形シートをそのまま使用
440                        shtNo = refSheetIdx;
441                        refSheetIdx++ ;
442                }
443
444                // 6.5.0.0 (2016/09/30) 雛形シート名をそのまま使用する場合。
445                if( !isOverwrite ) {
446                        setSheetName( shtNo , shtName );
447                }
448        }
449
450        /**
451         * 内部 Workbook の指定のシート番号の Sheet の名前を設定します。
452         *
453         * 指定のシート名が、既存のシートになければ、そのまま設定します。
454         * すでに、同じ名前のシートが存在する場合は、そのシート名の後に
455         * (1)、(2)、(3)のような文字列を追加します。
456         * shtName が null の場合は、"Sheet" が割り振られます。
457         *
458         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
459         * @og.rev 6.2.5.1 (2015/06/12) シート名重複が自分自身の場合は、(1)等の追加は行わない。
460         *
461         * @param       shtNo           シート番号
462         * @param       shtName シート名 (重複する場合は、(1)、(2)のような文字列を追加、nullの場合は、"Sheet")
463         */
464        public void setSheetName( final int shtNo, final String shtName ) {
465                String tempName = shtName == null ? DEF_SHEET_NAME : WorkbookUtil.createSafeSheetName( shtName ) ;
466                int cnt = 1;
467
468                // 6.2.5.1 (2015/06/12) シート名重複が自分自身の場合は、(1)等の追加は行わない。
469                // ※ EXCELのシート名は、大文字、小文字だけでなく、全角半角の区別もしない。
470                final String nowName = wkbook.getSheetName( shtNo );
471                if( tempName != null && !tempName.equals( nowName ) ) {                 // 全く同一の場合は、何もしない。
472                        if( shtNo == wkbook.getSheetIndex( tempName ) ) {                       // シート名判定が、自身の場合
473                                wkbook.setSheetName( shtNo,tempName );
474                        }
475                        else {
476                                while( wkbook.getSheetIndex( tempName ) >= 0 ) {                // シート名が存在している場合
477                                        tempName = WorkbookUtil.createSafeSheetName( shtName + "(" + cnt + ")" );
478                                        if( tempName.length() >= 31 ) {                                         // 重複時の追加文字分を減らす。
479                                                tempName = tempName.substring( 0,26 ) + "(" + cnt + ")" ;       // cnt3桁まで可能
480                                        }
481                                        cnt++;
482                                }
483                                wkbook.setSheetName( shtNo,tempName );
484                        }
485                }
486        }
487
488        /**
489         * 内部 Workbook の 指定のSheet番号のシート名前を返します。
490         *
491         * シートが存在しない場合は、null を返します。
492         *
493         * この処理を行うと、内部の Sheet にも、ここで見つけた Sheet が設定されます。
494         *
495         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
496         *
497         * @param        shtNo          シート番号
498         *
499         * @return      shtName シート名
500         */
501        public String getSheetName( final int shtNo ) {
502                final int shLen = wkbook.getNumberOfSheets();
503
504                String shtName = null;
505                if( shtNo < shLen ) {
506                        sheet = wkbook.getSheetAt( shtNo );             // 現在の sheet に設定する。
507                        shtName = sheet.getSheetName();
508                }
509
510                return shtName ;
511        }
512
513        /**
514         * 内部 Workbook の 指定のSheet名のシート番号を返します。
515         *
516         * シートが存在しない場合は、-1 を返します。
517         * この処理を行うと、内部の Sheet にも、ここで見つけた Sheet が設定されます。
518         * シートが存在しない場合、内部の Sheet オブジェクトも null がセットされますのでご注意ください。
519         *
520         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
521         *
522         * @param        shtName                シート名
523         *
524         * @return      シート番号(名前のシートがなければ、-1)
525         */
526        public int getSheetNo( final String shtName ) {
527                sheet = wkbook.getSheet( shtName );                                     // シート名がマッチしなければ、null
528
529                return wkbook.getSheetIndex( shtName ) ;                        // シート名がマッチしなければ、-1
530        }
531
532        /**
533         * Excelの指定Sheetオブジェクトを削除します。
534         *
535         * 削除するシートは、シート番号でFrom-To形式で指定します。
536         * Fromも Toも、削除するシート番号を含みます。
537         * 例えば、0,3 と指定すると、0,1,2,3 の 4シート分を削除します。
538         *
539         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
540         *
541         * @param        fromNo         削除する開始シート番号(含む)
542         * @param        toNo           削除する終了シート番号(含む)
543         */
544        public void removeSheet( final int fromNo,final int toNo ) {
545                for( int shtNo=toNo; shtNo>=fromNo; shtNo-- ) {                 // 逆順に処理します。
546                        wkbook.removeSheetAt( shtNo );
547                }
548        }
549
550        /**
551         * 内部 Workbookの 現在Sheet の最初の行番号を返します。
552         *
553         * 行は、0 から始まります。
554         * この処理は、内部Sheetが作成されているか、null でない場合のみ実行できます。
555         *
556         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
557         *
558         * @return      最初の行番号
559         */
560        public int getFirstRowNum() {
561                return sheet.getFirstRowNum();
562        }
563
564        /**
565         * 内部 Workbookの 現在Sheet の最後の行番号を返します。
566         *
567         * 最終行は、含みます。よって、行数は、getLastRowNum()+1になります。
568         * この処理は、内部Sheetが作成されているか、null でない場合のみ実行できます。
569         *
570         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
571         *
572         * @return      最後の行番号
573         */
574        public int getLastRowNum() {
575                return sheet.getLastRowNum();
576        }
577
578        /**
579         * Excelの指定行のRowオブジェクトを作成します。
580         *
581         * 指定行の Row オブジェクトが存在しない場合は、新規作成します。
582         * この処理を実行すると、指定行の Rowオブジェクトが内部 Row に設定されます。
583         * この処理は、内部Sheetが作成されているか、null でない場合のみ実行できます。
584         *
585         * この処理を行うと、内部の Rowオブジェクトが設定されます。
586         *
587         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
588         *
589         * @param        rowNo          行の番号
590         */
591        public void createRow( final int rowNo ) {
592                rowObj = sheet.getRow( rowNo );
593                if( rowObj == null ) { rowObj = sheet.createRow( rowNo ); }
594        }
595
596        /**
597         * Excelの指定行以降の余計なRowオブジェクトを削除します。
598         *
599         * 指定行の Row オブジェクトから、getLastRowNum() までの行を、削除します。
600         * この処理は、内部Sheetが作成されているか、null でない場合のみ実行できます。
601         *
602         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
603         *
604         * @param        startRowNum            指定以降の余計な行を削除
605         */
606        public void removeRow( final int startRowNum ) {
607                final int stR = startRowNum;
608                final int edR = sheet.getLastRowNum();
609
610                for( int rowNo=edR; rowNo>=stR && rowNo>=0; rowNo-- ) {                 // 逆順に処理します。
611                        final Row rowObj = sheet.getRow( rowNo );
612                        if( rowObj != null ) { sheet.removeRow( rowObj ); }
613                }
614        }
615
616        /**
617         * Excelの処理中のRowオブジェクトの指定カラム以降の余計なCellオブジェクトを削除します。
618         *
619         * 指定行の Row オブジェクトから、getLastCellNum() までのカラムを、削除します。
620         * この処理は、内部Rowが作成されているか、null でない場合のみ実行できます。
621         *
622         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
623         *
624         * @param        startCellNum           指定以降の余計なカラムを削除
625         */
626        public void removeCell( final int startCellNum ) {
627                final int stC = startCellNum;
628                final int edC = rowObj.getLastCellNum();
629
630                for( int colNo=edC; colNo>=stC; colNo-- ) {                     // 逆順に処理します。
631                        final Cell colObj = rowObj.getCell( colNo );
632                        if( colObj != null ) { rowObj.removeCell( colObj ); }
633                }
634        }
635
636        /**
637         * row にあるセルのオブジェクト値を設定します。
638         *
639         * 行が存在しない場合、行を追加します。
640         * この処理を行うと、内部の Rowオブジェクトがなければ新規作成されます。
641         *
642         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
643         *
644         * @param   vals  新しい配列値。
645         * @param   rowNo   値が変更される行(無視されます)
646         */
647        public void setValues( final String[] vals,final int rowNo ) {
648                if( rowObj == null ) { createRow( rowNo ); }
649
650                if( vals != null ) {
651                        for( int colNo=0; colNo<vals.length; colNo++ ) {
652                                setCellValue( vals[colNo],colNo );
653                        }
654                }
655        }
656
657        /**
658         * row にあるセルのオブジェクト値を設定します。
659         *
660         * 行が存在しない場合、行を追加します。
661         * 引数に、カラムがNUMBER型かどうかを指定することが出来ます。
662         * この処理を行うと、内部の Rowオブジェクトがなければ新規作成されます。
663         *
664         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
665         *
666         * @param   vals  新しい配列値。
667         * @param   rowNo   値が変更される行(無視されます)
668         * @param       isNums  セルが、NUMBER型の場合は、true/それ以外は、false
669         */
670        public void setValues( final String[] vals,final int rowNo,final boolean[] isNums ) {
671                if( rowObj == null ) { createRow( rowNo ); }
672
673                if( vals != null ) {
674                        for( int colNo=0; colNo<vals.length; colNo++ ) {
675                                setCellValue( vals[colNo],colNo,isNums[colNo] );
676                        }
677                }
678        }
679
680        /**
681         * Excelの指定セルにデータを設定します。
682         *
683         * ここで設定する行は、現在の内部 Row です。
684         * Row を切り替えたい場合は、#createRow( int ) を呼び出してください。
685         * このメソッドでは、データを文字列型として設定します。
686         * この処理は、内部Rowが作成されているか、null でない場合のみ実行できます。
687         *
688         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
689         *
690         * @param       dataVal    String文字列
691         * @param       colNo           セルの番号(0,1,2・・・・)
692         * @see         #setCellValue( String,int,boolean )
693         */
694        public void setCellValue( final String dataVal , final int colNo ) {
695                setCellValue( dataVal,colNo,false );
696        }
697
698        /**
699         * Excelの指定セルにデータを設定します。
700         *
701         * ここで設定する行は、現在の内部 Row です。
702         * Row を切り替えたい場合は、#createRow( int ) を呼び出してください。
703         * このメソッドでは、引数のデータ型をNUMBER型の場合は、doubleに変換して、
704         * それ以外は文字列としてとして設定します。
705         * この処理は、内部Rowが作成されているか、null でない場合のみ実行できます。
706         *
707         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
708         *
709         * @param       dataVal         String文字列
710         * @param       colNo           セルの番号(0,1,2・・・・)
711         * @param       isNumber        セルが、NUMBER型の場合は、true/それ以外は、false
712         * @see         #createRow( int )
713         * @see         #setCellValue( String,int )
714         */
715        public void setCellValue( final String dataVal , final int colNo , final boolean isNumber ) {
716                Cell colObj = rowObj.getCell( colNo );
717                if( colObj == null ) { colObj = rowObj.createCell( colNo ); }
718
719                if( style != null ) { colObj.setCellStyle(style); }
720
721                // CELL_TYPE_NUMERIC 以外は、String扱いします。
722                if( isNumber ) {
723                        final Double dbl = parseDouble( dataVal );
724                        if( dbl != null ) {
725                                // 2.0.0 (2024/01/12) PMD 7.0.0 UnnecessaryBoxing
726//                              colObj.setCellValue( dbl.doubleValue() );
727                                colObj.setCellValue( dbl );
728                                return ;                // Double 変換できた場合は、即抜けます。
729                        }
730                }
731
732                final RichTextString richText = createHelper.createRichTextString( dataVal );
733                colObj.setCellValue( richText );
734        }
735
736        /**
737         * Excelの指定セルにHyperlinkを設定します。
738         *
739         * ここで設定する行は、現在の内部 Row です。
740         * Row を切り替えたい場合は、#createRow( int ) を呼び出してください。
741         * このメソッドで設定するHyperlinkは、Sheetに対する LINK_DOCUMENT です。
742         * 先に、セルに対する値をセットしておいてください。
743         * Hyperlinkは、文字に対して、下線 と 青字 のスタイル設定を行います。
744         *
745         * Link文字列(シート名) が、null や ゼロ文字列の場合は、処理を行いません。
746         *
747         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
748         * @og.rev 6.5.0.0 (2016/09/30) poi-3.15 対応(Hyperlink.LINK_XXXX → HyperlinkType.XXXX)
749         *
750         * @param       linkVal         Link文字列(シート名)
751         * @param       colNo           セルの番号(0,1,2・・・・)
752         * @see         #setCellValue( String,int )
753         */
754        public void setCellLink( final String linkVal , final int colNo ) {
755                if( linkVal == null || linkVal.isEmpty() ) { return; }
756
757                Cell colObj = rowObj.getCell( colNo );
758                if( colObj == null ) { colObj = rowObj.createCell( colNo ); }
759
760                if( hLinkStyle == null ) {
761                        hLinkStyle = wkbook.createCellStyle();
762                        if( style != null ) { hLinkStyle.cloneStyleFrom(style); }
763
764                        final Font font = wkbook.createFont();
765                        font.setColor( IndexedColors.BLUE.getIndex() );         // リンクは青文字
766                        font.setUnderline( Font.U_SINGLE );                                     // 下線付
767
768                        hLinkStyle.setFont( font );
769                }
770                colObj.setCellStyle(hLinkStyle);
771
772        //      final Hyperlink hLink = createHelper.createHyperlink( Hyperlink.LINK_DOCUMENT );                // 6.5.0.0 (2016/09/30) poi-3.12
773                final Hyperlink hLink = createHelper.createHyperlink( HyperlinkType.DOCUMENT );                 // 6.5.0.0 (2016/09/30) poi-3.15
774                hLink.setAddress( "'" + linkVal + "'!A1" );
775                colObj.setHyperlink( hLink );
776        }
777
778        /**
779         * 現在のRow にあるセルの属性値を配列で返します。
780         *
781         * Rowオブジェクトが存在しない場合は、長さ0の配列を返します。
782         * また、Rowオブジェクトの中の セルオブジェクトが存在しない場合は、
783         * null がセットされます。
784         *
785         * この処理は、内部Sheetが作成されているか、null でない場合のみ実行できます。
786         * この処理を実行すると、指定行の Rowオブジェクトが内部 Row に設定されます。
787         *
788         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
789         * @og.rev 6.3.9.0 (2015/11/06) ExcelModel#getValues(int) では、nullは返さない。
790         * @og.rev 6.3.9.1 (2015/11/27) メソッドの出口は、最後の1か所にすべきです(PMD)。
791         *
792         * @param       rowNo           行の番号
793         * @return      指定されたセルの属性値。Rowがnullの場合は、長さ0の配列を返します。
794         * @og.rtnNotNull
795         */
796        public String[] getValues( final int rowNo ) {
797                rowObj = sheet.getRow( rowNo );
798
799                final int len = rowObj == null ? 0 : rowObj.getLastCellNum();           // 含まないので、length と同じ意味になる。
800                final String[] vals = new String[len];                          // 6.3.9.1 (2015/11/27) メソッドの出口
801
802                for( int colNo=0; colNo<len; colNo++ ) {
803                        final Cell colObj = rowObj.getCell( colNo );
804                        vals[colNo] = POIUtil.getValue( colObj );
805                }
806
807                return vals ;
808        }
809
810        /**
811         * 現在のrow にあるセルの属性値を返します。
812         *
813         * セルオブジェクトが存在しない場合は、null を返します。
814         *
815         * この処理は、内部Sheetが作成されているか、null でない場合のみ実行できます。
816         * この処理を実行すると、指定行の Rowオブジェクトが内部 Row に設定されます。
817         *
818         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
819         * @og.rev 6.3.9.1 (2015/11/27) メソッドの出口は、最後の1か所にすべきです(PMD)。
820         *
821         * @param   rowNo     値が参照される行
822         * @param   colNo     値が参照される列
823         *
824         * @return  指定されたセルの値 T
825         */
826        public String getValue( final int rowNo, final int colNo ) {
827                rowObj = sheet.getRow( rowNo );
828
829                return rowObj == null ? null : POIUtil.getValue( rowObj.getCell( colNo ) );
830        }
831
832        /**
833         * 指定のシートの行・列の箇所に、イメージファイルを挿入します。
834         *
835         * ここでは、セル範囲ではなく、指定の行列の箇所に、アンカーを設定して、画像ファイルを
836         * 挿入します。一応、リサイズして、元の大きさ近くに戻しますが、縦横比が変わってしまいます。
837         * 正確に挿入する場合は、セル範囲の指定と、マージンを指定しなければなりませんが、
838         * 微調整が必要です。
839         *
840         * この処理で使用される Sheetオブジェクトは一時的に作成されます。(キャッシュされません)
841         * 一連処理のどのタイミングで実行しても、内部の状態には影響はありません。
842         *
843         * @og.rev 6.0.2.3 (2014/10/10) 新規作成
844         *
845         * @param   imgFile   挿入するイメージファイル名
846         * @param   shtNo     シート番号
847         * @param   rowNo     挿入する行
848         * @param   colNo     挿入する列
849         */
850        public void addImageFile( final String imgFile, final int shtNo, final int rowNo, final int colNo ) {
851                addImageFile( imgFile,shtNo,rowNo,colNo,rowNo,colNo,0,0,0,0 );
852        }
853
854        /**
855         * 指定のシートの行・列の箇所に、イメージファイルを挿入します。
856         *
857         * ここでは、セル範囲ではなく、指定の行列の箇所に、アンカーを設定して、画像ファイルを
858         * 挿入します。一応、リサイズして、元の大きさ近くに戻しますが、縦横比が変わってしまいます。
859         * 正確に挿入する場合は、セル範囲の指定と、マージンを指定しなければなりませんが、
860         * 微調整が必要です。
861         *
862         * この処理で使用される Sheetオブジェクトは一時的に作成されます。(キャッシュされません)
863         * 一連処理のどのタイミングで実行しても、内部の状態には影響はありません。
864         *
865         * @og.rev 6.0.2.3 (2014/10/10) 新規作成
866         * @og.rev 6.4.6.0 (2016/05/27) poi-3.15 準備
867         * @og.rev 6.8.2.4 (2017/11/20) poi-3.17 で、警告: [rawtypes] raw型が見つかりました対応
868         * @og.rev 7.2.9.0 (2020/10/12) ClientAnchorのオフセット指定は、Units.EMU_PER_PIXEL が単位
869         *
870         * @param   imgFile   挿入するイメージファイル名
871         * @param   shtNo     シート番号
872         * @param   row1      挿入する行(開始)
873         * @param   col1      挿入する列(開始)
874         * @param   row2      挿入する行(終了-含まず)
875         * @param   col2      挿入する列(終了-含まず)
876         * @param   dx1       開始セルのX軸座標のオフセット(ピクセル)
877         * @param   dy1       開始セルのY軸座標のオフセット(ピクセル)
878         * @param   dx2       終了セルのX軸座標のオフセット(ピクセル)
879         * @param   dy2       終了セルのY軸座標のオフセット(ピクセル)
880         */
881        public void addImageFile( final String imgFile , final int shtNo ,
882                                                                final int row1 , final int col1 , final int row2 , final int col2 ,
883                                                                final int dx1  , final int dy1  , final int dx2  , final int dy2   ) {
884                final String suffix   = ImageUtil.getSuffix( imgFile );
885                final Integer picType = PICTURE_TYPE.get( suffix );
886
887                // 実験した結果、bmp,gif,tif については、PICTURE_TYPE_PNG で、挿入できた。
888                // 6.4.1.1 (2016/01/16) PMD refactoring. Avoid if (x != y) ..; else ..;
889                // 2.0.0 (2024/01/12) PMD 7.0.0 UnnecessaryBoxing
890//              final int pictureType = picType == null ? Workbook.PICTURE_TYPE_PNG : picType.intValue() ;
891                final int pictureType = picType == null ? Workbook.PICTURE_TYPE_PNG : picType ;
892
893                final byte[] imgs = ImageUtil.byteImage( imgFile );
894
895                final int pictureIdx = wkbook.addPicture( imgs, pictureType );
896
897                final Sheet sheet = wkbook.getSheetAt( shtNo );
898                // 6.8.2.4 (2017/11/20) poi-3.17 で、警告: [rawtypes] raw型が見つかりました対応
899                final Drawing<?> patriarch = sheet.createDrawingPatriarch();            // 昔は一度しか実行できなかったようです。
900        //      final Drawing patriarch = sheet.createDrawingPatriarch();                       // 昔は一度しか実行できなかったようです。
901
902//              final ClientAnchor anchor = patriarch.createAnchor( dx1,dy1,dx2,dy2,col1,row1,col2,row2 );
903                final int px = Units.EMU_PER_PIXEL;                                                                     // 7.2.9.0 (2020/10/12)
904                final ClientAnchor anchor = patriarch.createAnchor( px*dx1,px*dy1,px*dx2,px*dy2,col1,row1,col2,row2 );
905
906                // ClientAnchor anchor = createHelper.createClientAnchor();     でも作成可能。
907
908                // MOVE_AND_RESIZE, MOVE_DONT_RESIZE, DONT_MOVE_AND_RESIZE から、決め打ち。
909        //      anchor.setAnchorType( ClientAnchor.MOVE_DONT_RESIZE );                                  // 6.4.6.0 (2016/05/27) poi-3.12
910                anchor.setAnchorType( ClientAnchor.AnchorType.MOVE_DONT_RESIZE );               // 6.4.6.0 (2016/05/27) poi-3.15
911
912                final Picture pic = patriarch.createPicture( anchor, pictureIdx );
913                // セルの範囲指定がゼロの場合、画像サイズもゼロになる為、リサイズしておく。
914                if( row1 == row2 || col1 == col2 ) { pic.resize(); }    // resize すると、anchor のマージンが無視されるようです。
915        }
916
917        /**
918         * 内部 Workbook オブジェクトをファイルに書き出します。
919         *
920         * Excelの形式は、ここで指定する出力ファイルの拡張子ではなく、コンストラクタで
921         * 指定したファイルの拡張子で決まります。
922         * 異なる形式の拡張子を持つファイルを指定した場合、強制的に、オープンした
923         * Workbook の形式の拡張子を追加します。
924         *
925         * 拡張子は、Excel 2007以降の形式(.xlsx)か、Excel 2003以前の形式(.xls) が指定できます。
926         * 拡張子が未設定の場合は、オープンした Workbook の形式に合わせた拡張子を付与します。
927         *
928         * isAutoCellSize=true の場合は、ここで全Sheetに対してCell幅の自動調整が行われます。
929         *
930         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
931         * @og.rev 6.1.0.0 (2014/12/26) 入力ファイルの拡張子判定の対応
932         * @og.rev 6.2.0.0 (2015/02/27) ファイル引数を、String → File に変更
933         * @og.rev 6.5.0.0 (2016/09/30) セルの計算式の再計算をさせる recalcSheetNames 属性の追加。
934         * @og.rev 8.5.4.2 (2024/01/12) PMD 7.0.0 CloseResource 対応
935         *
936         * @param       file    セーブするファイル
937         */
938        public void saveFile( final File file ) {
939                final File saveFile ;
940                String fname = file.getName();
941                if( fname.toLowerCase(Locale.JAPAN).endsWith( sufix ) ) {
942                        saveFile = file;
943                }
944                else {
945                        final int idx = fname.lastIndexOf( '.' );
946                        if( idx >= 0 ) { fname = fname.substring( 0,idx ); }
947                        saveFile = new File( file.getParent() , fname + sufix );
948                }
949
950                if( isAutoCellSize ) { POIUtil.autoCellSize( wkbook, maxColCount, dataStartRow ); }
951
952                // 6.5.0.0 (2016/09/30) セルの計算式の再計算をさせる recalcSheetNames 属性の追加。
953                if( recalcSheetNames != null && recalcSheetNames.length > 0 ) {
954                        for( final String shtName : recalcSheetNames ) {
955                                final Sheet sht = wkbook.getSheet( shtName );                   // シート名がマッチしなければ、null
956                                if( sht != null ) { sht.setForceFormulaRecalculation(true); }
957                        }
958                }
959
960                // こちらの都合で、TitleSheet は、autoCellSize ではなく、Sheet#autoSizeColumn(int) を使用して、自動計算させる。
961                if( addTitleSheet != null ) { makeAddTitleSheet(); }
962
963                // 8.5.4.2 (2024/01/12) PMD 7.0.0 CloseResource 対応
964//              OutputStream fileOut = null ;
965//              try {
966//                      fileOut = new BufferedOutputStream( new FileOutputStream( saveFile ) );         // 6.1.0.0 (2014/12/26)
967                // 8.5.4.2 (2024/01/12) PMD 7.0.0 AvoidFileStream 対応
968//              try ( OutputStream fileOut = new BufferedOutputStream( new FileOutputStream( saveFile ) ) ) {   // 6.1.0.0 (2014/12/26)
969                try ( OutputStream fileOut = new BufferedOutputStream( Files.newOutputStream( saveFile.toPath() ) ) ) { // 6.1.0.0 (2014/12/26)
970                        wkbook.write( fileOut );
971                        wkbook.close();
972                }
973                catch( final IOException ex ) {
974                        final String errMsg = "ファイルへ書込み中にエラーが発生しました。" + CR
975                                                        + "  File=" + saveFile + CR
976                                                        + ex.getMessage() ;
977                        throw new OgRuntimeException( errMsg,ex );
978                }
979//              finally {
980//                      Closer.ioClose( fileOut );
981//              }
982        }
983
984        /**
985         * 内部 Workbook オブジェクトのSheet一覧のSheetを、先頭に追加します。
986         *
987         * これは、Workbook に含まれる Sheet 一覧を作成する場合に、利用可能です。
988         *
989         * この処理は、内部のWorkbook、Sheetオブジェクトに依存して実行されます。
990         * また、単独ではなく、#saveFile( File ) 実行時に、addTitleSheet が
991         * 設定されている場合のみ、実行されます。
992         *
993         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
994         *
995         * @see         #saveFile( File )
996         * @see         #setAddTitleSheet( String )
997         */
998        private void makeAddTitleSheet() {
999                sheet = wkbook.createSheet();
1000                final String shtNm = sheet.getSheetName();                              // Sheet名の取得
1001                wkbook.setSheetOrder( shtNm,0 );                                        // そのSheetを先頭に移動
1002                setSheetName( 0,addTitleSheet );                                        // そのSheet名を変更 → これが、TitleSheet
1003
1004                int rowNo = 0;
1005                createRow( rowNo++ );                                                           // 先頭行(インスタンス共通のRowオブジェクト)作成
1006                setCellValue( "No"       , 0 );
1007                setCellValue( "Sheet", 1 );
1008
1009                final int shCnt = wkbook.getNumberOfSheets();
1010                for( int shtNo=1; shtNo<shCnt; shtNo++,rowNo++ ) {
1011                        final String nm = wkbook.getSheetName( shtNo );
1012
1013                        createRow( rowNo );                                                                     // 行の追加作成
1014                        setCellValue( String.valueOf( rowNo ),0,true );         // 行番号として、数字型で登録
1015                        setCellValue( nm , 1 );                                                         // シートの値を書き込む
1016                        setCellLink(  nm , 1 );                                                         // シートへのリンクを作成する。
1017                }
1018
1019                sheet.autoSizeColumn( 0 );
1020                sheet.autoSizeColumn( 1 );
1021        }
1022
1023//      /**
1024//       * 指定の Workbook の全Sheetを対象に、実際の有効行と有効カラムを取得します。
1025//       *
1026//       * ※ 現在、唯一LibreOfficeでのみ、xslx 変換できますが、有効行とカラムが
1027//       *    シュリンクされず、無駄な行とカラムが存在します。
1028//       *    これは、xsl で出力されたファイルから有効な値を取得して、xslxに適用させるための
1029//       *    機能で、本来きちんとした有効範囲の xslx が生成されれば、不要な処理です。
1030//       *
1031//       * 配列は、[0]=行の最大値(Sheet#getLastRowNum())と、[1]は有効行の中の列の
1032//       * 最大値(Row#getLastCellNum())を、シートごとにListに追加していきます。
1033//       *
1034//       * @og.rev 8.0.1.0 (2021/10/29) 全Sheetを対象に、実際の有効行と有効カラムを取得
1035//       * @og.rev 8.0.3.0 (2021/12/17) 処理が中途半端だったので、廃止します。
1036//       *
1037//       * @return      シートごとの有効行の配列リスト
1038//       * @see         #activeWorkbook( List )
1039//       */
1040//      public List<int[]> getLastRowCellNum() {
1041//              return POIUtil.getLastRowCellNum( wkbook );
1042//      }
1043
1044        /**
1045         * Workbook の全Sheetを対象に、空行を取り除き、全体をシュリンクします。
1046         *
1047         * この処理は、#saveFile( File ) の直前に行うのがよいでしょう。
1048         *
1049         * ここでは、Row を逆順にスキャンし、Cellが 存在しない間は、行を削除します。
1050         * 途中の空行の削除ではなく、最終行からの連続した空行の削除です。
1051         *
1052         * isCellDel=true を指定すると、Cellの末尾削除を行います。
1053         * 有効行の最後のCellから空セルを削除していきます。
1054         * 表形式などの場合は、Cellのあるなしで、レイアウトが崩れる場合がありますので
1055         * 処理が不要な場合は、isCellDel=false を指定してください。
1056         *
1057         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
1058         *
1059         * @param       isCellDel       Cellの末尾削除を行うかどうか(true:行う/false:行わない)
1060         */
1061        public void activeWorkbook( final boolean isCellDel ) {
1062                POIUtil.activeWorkbook( wkbook, isCellDel );
1063        }
1064
1065        /**
1066         * 指定の Workbook の全Sheetを対象に、実際の有効行と有効カラムを元に全体をシュリンクします。
1067         *
1068         * ※ 現在、唯一LibreOfficeでのみ、xslx 変換できますが、有効行とカラムが
1069         *    シュリンクされず、無駄な行とカラムが存在します。
1070         *    これは、xsl で出力されたファイルから有効な値を取得して、xslxに適用させるための
1071         *    機能で、本来きちんとした有効範囲の xslx が生成されれば、不要な処理です。
1072         *
1073         * 引数のListオブジェクトに従って、無条件に処理を行います。
1074         *
1075         * @og.rev 8.0.1.0 (2021/10/29) 全Sheetを対象に、実際の有効行と有効カラムを取得
1076         * @og.rev 8.0.3.0 (2021/12/17) シート毎の行数Listに変更。
1077         *
1078//       * @param       rcList          シートごとの有効行の配列リスト
1079         * @param       rowCntList              シートごとの有効行の配列リスト
1080//       * @see         #getLastRowCellNum()
1081         * @see         #activeWorkbook( boolean )
1082         */
1083//      public void activeWorkbook( final List<int[]> rcList ) {
1084        public void activeWorkbook( final List<Integer> rowCntList ) {
1085                POIUtil.activeWorkbook( wkbook, rowCntList );
1086        }
1087
1088        /**
1089         * Workbook の全Sheetを対象に、テキスト変換処理を行います(XSLX限定)。
1090         *
1091         * この処理は、#saveFile( File ) の直前に行うのがよいでしょう。
1092         * #activeWorkbook( boolean ) との順番は構いません。
1093         *
1094         * ・シート名の一覧をピックアップします。
1095         * ・セル値を、セル単位にピックアップします。
1096         * ・オブジェクト文字列を、改行単位にピックアップし、結果を合成します。
1097         *
1098         * ここでは、内部的に、TextConverterインターフェースを作成して処理します。
1099         *
1100         * @og.rev 6.2.4.2 (2015/05/29) テキスト変換処理
1101         * @og.rev 6.3.1.0 (2015/06/28) TextConverterに、引数(cmnt)を追加
1102         * @og.rev 6.3.9.0 (2015/11/06) Java 8 ラムダ式に変更
1103         *
1104         * @param       convMap 変換対象を管理するMapオブジェクト
1105         * @see         #textConverter( TextConverter )
1106         */
1107        public void textConverter( final Map<String,String> convMap ) {
1108                textConverter(
1109                        ( val,cmnt ) -> convMap.get( val )
1110                );
1111
1112        //      textConverter(
1113        //              new TextConverter<String,String>() {
1114        //                      /**
1115        //                       * 入力文字列を、変換します。
1116        //                       *
1117        //                       * @param       val  入力文字列
1118        //                       * @param       cmnt コメント
1119        //                       * @return      変換文字列(変換されない場合は、null)
1120        //                       */
1121        //                      @Override
1122        //                      public String change( final String val , final String cmnt ) {
1123        //                              return convMap.get( val );
1124        //                      }
1125        //              }
1126        //      );
1127        }
1128
1129        /**
1130         * Workbook の全Sheetを対象に、テキスト変換処理を行います(XSLX限定)。
1131         *
1132         * この処理は、#saveFile( File ) の直前に行うのがよいでしょう。
1133         * #activeWorkbook( boolean ) との順番は構いません。
1134         *
1135         * ・シート名の一覧をピックアップします。
1136         * ・セル値を、セル単位内の改行単位にピックアップし、結果を合成ます。
1137         * ・オブジェクト文字列を、改行単位にピックアップし、結果を合成します。
1138         *
1139         * ここでは、シート名、セルテキスト、SimpleShapeオブジェクトのテキストを
1140         * input に、TextConverterインターフェース の change メソッドを呼び出します。
1141         * 戻り値が、null でないなら、元のデータと置き換えます。
1142         * 戻り値が、null の場合は、そのまま読み飛ばします。(なにもしません)
1143         * EXCELへの書き戻しが発生しますので、万一、ファイル破損で、開けなくなる場合を
1144         * 想定して、バックアップファイルは、各自で準備してください。
1145         *
1146         * @og.rev 6.2.4.2 (2015/05/29) テキスト変換処理
1147         * @og.rev 6.2.5.0 (2015/06/05) xsl形式のオブジェクト取得…はできなかった。
1148         * @og.rev 6.3.1.0 (2015/06/28) TextConverterに、引数(cmnt)を追加
1149         * @og.rev 6.3.9.0 (2015/11/06) セルに値をセットするときに、セルタイプを考慮する。
1150         * @og.rev 8.0.3.1 (2021/12/28) テキスト取得処理にgetShapeTextを使用する。
1151         * @og.rev 8.1.2.3 (2022/05/20) オブジェクト文字列の変換で、drawing の null 対応
1152         * @og.rev 8.4.0.0 (2023/01/30) オブジェクト文字列の変換ミス対応とメソッド化
1153         * @og.rev 8.5.4.0 (2023/12/01) コメント(cmnt)のシート名をオリジナルシート名に取り出す
1154         *
1155         * @param       conv    TextConverterインターフェース
1156         * @see         #textConverter( Map )
1157         */
1158//      @SuppressWarnings(value={"deprecation"})        // poi-3.15
1159        public void textConverter( final TextConverter<String,String> conv ) {
1160        //      if( ".xlsx".equals( sufix ) || ".xlsm".equals( sufix ) ) {
1161                        final int shCnt = wkbook.getNumberOfSheets();
1162                        for( int shtNo=0; shtNo<shCnt; shtNo++ ) {
1163                                final Sheet sht = wkbook.getSheetAt( shtNo );
1164                                final String shtNmOrg = sht.getSheetName();
1165                                // シート名の変換
1166        //                      final String shtNm = conv.change( sht.getSheetName() , "Sheet" + shtNo + ":" );
1167//                              final String shtNm = conv.change( sht.getSheetName() , "Sheet" + shtNo + ":Name" );     // 8.5.0.0 (2023/04/21)
1168                                final String shtNm = conv.change( shtNmOrg , "Sheet" + shtNo + ":Name" );                       // 8.5.4.0 (2023/12/01) Modify
1169                                if( shtNm != null ) {
1170                                        setSheetName( shtNo,shtNm );                    // 同一シート対策済みのメソッドを呼び出す。
1171                                }
1172
1173                                // セル値の変換
1174                                final int stR = Math.max( sht.getFirstRowNum(),0 );             // stR が、マイナスのケースがある。
1175                                final int edR = sht.getLastRowNum();
1176
1177                                for( int rowNo=stR; rowNo<=edR; rowNo++ ) {
1178                                        final Row rowObj = sht.getRow( rowNo );
1179                                        if( rowObj != null ) {
1180                                                final int stC = Math.max( rowObj.getFirstCellNum(),0 );         // stC が、マイナスのケースがある。
1181                                                final int edC = rowObj.getLastCellNum();
1182                                                for( int colNo=stC; colNo<=edC; colNo++ ) {
1183                                                        final Cell colObj = rowObj.getCell( colNo );
1184//                                                      if( colObj != null && colObj.getCellType() != Cell.CELL_TYPE_BLANK ) {          // 6.5.0.0 (2016/09/30) poi-3.12
1185//                                                      if( colObj != null && colObj.getCellTypeEnum() != CellType.BLANK ) {            // 6.5.0.0 (2016/09/30) poi-3.15
1186                                                        if( colObj != null && colObj.getCellType() != CellType.BLANK ) {                        // 8.0.0.0 (2021/07/31) poi-4.1.2.jar → poi-5.0.0.jar
1187//                                                              final String cmnt= "Sheet" + shtNo + ":" + POIUtil.getCelKigo( rowNo,colNo );
1188                                                                final String cmnt= shtNmOrg + ":" + POIUtil.getCelKigo( rowNo,colNo );  // 8.5.4.0 (2023/12/01) Modify
1189                                                                final String val = crConv( conv, POIUtil.getValue( colObj ),cmnt );             // 改行対応
1190                                                                if( val != null ) {
1191                                                                        POIUtil.setValue( colObj,val );         // 6.3.9.0 (2015/11/06)
1192                                                //                      colObj.setCellValue( val );
1193
1194                                                                }
1195                                                        }
1196                                                }
1197                                        }
1198                                }
1199
1200                                // 8.0.3.1 (2021/12/28) オブジェクト文字列の変換
1201                                final Drawing<?> drawing = sht.getDrawingPatriarch();
1202                                // 8.1.2.3 (2022/05/20) オブジェクト文字列の変換で、drawing の null 対応
1203                                if( drawing instanceof XSSFDrawing ) {
1204        //                      if( drawing != null ) {
1205                                        for (final XSSFShape shape : ((XSSFDrawing)drawing).getShapes() ) {
1206                                                // 8.4.0.0 (2023/01/30) オブジェクト文字列の変換ミス対応とメソッド化
1207                                                shapeConvert( shtNo,shape,conv );
1208
1209                                //              final String shpNm = shape.getShapeName();
1210                                //              final String cmnt = "Sheet" + shtNo + ":" + shpNm ;
1211                                //              conv.change( getShapeText( (XSSFShape)shape,null ),cmnt );
1212                                        }
1213                                }
1214                                // 8.1.2.3 (2022/05/20) オブジェクト文字列の変換で、drawing の null 対応
1215                                else {
1216                                        // 8.1.2.3 (2022/05/20) オブジェクト文字列の変換 復活
1217                                        if( sht instanceof POIXMLDocumentPart ) {
1218                                                for( final POIXMLDocumentPart pxdp : ((POIXMLDocumentPart)sht).getRelations() ) {
1219                                                        if( pxdp instanceof XSSFDrawing ) {
1220                                                                for( final XSSFShape shape : ((XSSFDrawing)pxdp).getShapes() ) {
1221                                                                        // 8.4.0.0 (2023/01/30) オブジェクト文字列の変換ミス対応とメソッド化
1222                                                                        shapeConvert( shtNo,shape,conv );
1223
1224                                        //                              // 8.0.3.1 (2021/12/28) テキスト取得処理にgetShapeTextを使用する。
1225                                        //                              final String shpNm = shape.getShapeName();
1226                                        //                              final String cmnt = "Sheet" + shtNo + ":" + shpNm ;
1227                                        //                              conv.change( getShapeText( shape ),cmnt );
1228
1229                                                                        // 8.1.2.3 (2022/05/20) オブジェクト文字列の変換 復活
1230                                                                        // 8.4.0.0 (2023/01/30) shapeConvert のメソッド化
1231                                                        //              final XSSFAnchor anc = shape.getAnchor();
1232                                                        //              final String ancSt = "XY(" + anc.getDx1() + "-" + anc.getDy1() + ")" ;
1233                                                        //              int cnt = 0;
1234                                                        //              if( shape instanceof XSSFSimpleShape ) {
1235                                                        //                      for( final XSSFTextParagraph para : ((XSSFSimpleShape)shape).getTextParagraphs() ) {
1236                                                        //                              for( final XSSFTextRun text : para.getTextRuns() ) {
1237                                                        //                                      final String cmnt= "Sheet" + shtNo + ":" + ancSt + ":(" + cnt++ + ")" ;
1238                                                        //                                      final String val = crConv( conv,text.getText() , cmnt );
1239                                                        //                                      if( val != null ) {
1240                                                        //                                              text.setText( val );
1241                                                        //                                      }
1242                                                        //                              }
1243                                                        //                      }
1244                                                        //              }
1245                                                                }
1246                                                        }
1247                                                }
1248                                        }
1249                                }
1250
1251                        // 6.2.5.0 (2015/06/05) xsl形式のオブジェクト取得…はできなかった。
1252                        //      else if( sht instanceof HSSFSheet ) {
1253                        //              HSSFPatriarch patri = ((HSSFSheet)sht).getDrawingPatriarch();
1254                        //              for( final HSSFShape shape : patri.getChildren() ) {
1255                        //                      if( shape instanceof HSSFTextbox ) {
1256                        //                              HSSFRichTextString rts = ((HSSFSimpleShape)shape).getString();
1257                        //                              if( rts != null ) {
1258                        //                                      final String val = crConv( conv,rts.getString() );
1259                        //                                      if( val != null ) {
1260                        //                                              HSSFRichTextString rts2 = new HSSFRichTextString( val );
1261                        //                                              ((HSSFSimpleShape)shape).setString( rts2 );
1262                        //                                      }
1263                        //                              }
1264                        //                      }
1265                        //              }
1266                        //      }
1267                        }
1268        //      }
1269        }
1270
1271        /**
1272         * XSSFShape を引数に、XSSFSimpleShape の場合に、変換処理を行います。
1273         *
1274         * @og.rev 8.4.0.0 (2023/01/30) オブジェクト文字列の変換ミス対応とメソッド化
1275         * @og.rev 8.5.0.0 (2023/04/21) XSSFAnchor オブジェクトの NullPointerException 対策
1276         *
1277         * @param       shtNo   シート番号
1278         * @param       shape   XSSFShapeインターフェース
1279         * @param       conv    TextConverterインターフェース
1280         * @see         #textConverter( Map )
1281         */
1282        private void shapeConvert( final int shtNo , final XSSFShape shape , final TextConverter<String,String> conv ) {
1283                final XSSFAnchor anc = shape.getAnchor();
1284
1285                // 8.5.0.0 (2023/04/21) XSSFAnchor オブジェクトの NullPointerException 対策
1286                final String ancSt ;
1287                if( anc instanceof XSSFClientAnchor ) {
1288                        final XSSFClientAnchor anc2 = (XSSFClientAnchor)anc;
1289        //              if( ClientAnchor.AnchorType.DONT_MOVE_AND_RESIZE.equals(anc2.getAnchorType()) ) {
1290        //                      ancSt = "XY1(" + anc2.getPosition()  + ")" ;
1291
1292                                final String kigo = POIUtil.getCelKigo( anc2.getRow1(),anc2.getCol1() )
1293                                                        + "-" + POIUtil.getCelKigo( anc2.getRow2(),anc2.getCol2() ) ;
1294                                ancSt = "(" + kigo  + ")" ;
1295        //              }
1296        //              else {
1297        //                      ancSt = "XY2(" + anc2.getDx1() + "-" + anc2.getDy1() + ")" ;
1298        //              }
1299                }
1300                else {
1301                        ancSt = "(" + anc.getDx1() + "-" + anc.getDy1() + ")" ;
1302                }
1303
1304//              final String ancSt = "XY(" + anc.getDx1() + "-" + anc.getDy1() + ")" ;
1305        //      int cnt = 0;
1306                if( shape instanceof XSSFSimpleShape ) {
1307                        for( final XSSFTextParagraph para : ((XSSFSimpleShape)shape).getTextParagraphs() ) {
1308                                for( final XSSFTextRun text : para.getTextRuns() ) {
1309        //                              final String cmnt= "Sheet" + shtNo + ":" + ancSt + ":(" + cnt++ + ")" ;
1310                                        final String cmnt= "Sheet" + shtNo + ":Shape " + ancSt ;
1311                                        final String val = crConv( conv,text.getText() , cmnt );
1312                                        if( val != null ) {
1313                                                text.setText( val );
1314                                        }
1315                                }
1316                        }
1317                }
1318        }
1319
1320        /**
1321         * Workbook の全SheetのShapeを対象に、テキストをスキャンします(XSLX限定)。
1322         *
1323         * 引数のBiConsumerは、ラムダ式として適用できます。
1324         * シート毎のShapeから、#getShapeText(XSSFShape,BiConsumer) を呼び出して、
1325         * テキストが存在した場合に、その時のXSSFSimpleShapeとテキストを引数のラムダ式に渡します。
1326         *
1327         * @og.rev 8.1.0.1 (2022/01/07) テキストベースのリンク作成
1328         *
1329         * @param       bicon   BiConsumer関数型インターフェース
1330         */
1331        public void xssfShapeScan( final BiConsumer<XSSFSimpleShape,String> bicon ) {
1332                final int shCnt = wkbook.getNumberOfSheets();
1333                for( int shtNo=0; shtNo<shCnt; shtNo++ ) {
1334                        final Sheet sht = wkbook.getSheetAt( shtNo );
1335
1336                        // 8.0.3.1 (2021/12/28) オブジェクト文字列の変換
1337                        final Drawing<?> drawing = sht.getDrawingPatriarch();
1338                        for (final Shape shape : drawing) {
1339                                if( shape instanceof XSSFShape ) {
1340                                        getShapeText( (XSSFShape)shape, bicon );
1341                                }
1342                        }
1343                }
1344        }
1345
1346        /**
1347         * 現在のシートを選択済み(true)か、非選択済み(false)に設定します。
1348         *
1349         * 通常は、シートは、先頭シート以外は、非選択状態になっています。
1350         * シートを選択済みにすることで、印刷範囲を指定する事ができます。
1351         *
1352         * @og.rev 6.3.9.0 (2015/11/06) 新規追加
1353         *
1354         * @param       isSelect        true:シート選択/false:非選択
1355         */
1356        public void sheetSelected( final boolean isSelect ) {
1357                sheet.setSelected( isSelect );
1358        }
1359
1360        /**
1361         * Workbook の雛形シートのTextConverter した、新しいSheetを作成します。
1362         *
1363         * 正確には、
1364         *   1.雛形シートを、コピーして、新しいSheet(shtName)を、作成します。
1365         *   2.雛形シートが指定されていない場合は、一番最後のシートをコピーします。
1366         *   3.そのシートに対して、TextConverter を行い、文字列変換します。
1367         *
1368         * @og.rev 6.3.9.0 (2015/11/06) 新規追加
1369         * @og.rev 6.5.0.0 (2016/09/30) poi-3.15 対応(Cell.CELL_TYPE_XXXX → CellType.XXXX)
1370         * @og.rev 8.0.3.1 (2021/12/28) テキスト取得処理にgetShapeTextを使用する。
1371         *
1372         * @param       conv    TextConverterインターフェース
1373         * @param       shtName         シート名
1374         * @see         #textConverter( Map )
1375         */
1376//      @SuppressWarnings(value={"deprecation"})        // poi-3.15
1377        public void sheetCopy( final TextConverter<String,String> conv , final String shtName ) {
1378                int shtNo = wkbook.getNumberOfSheets() - 1;
1379                if( refSheetIdx >= 0 && refSheetIdx < shtNo ) {         // 雛形シートをコピーする。
1380                        sheet = wkbook.cloneSheet( refSheetIdx );
1381                }
1382                else {
1383                        sheet = wkbook.cloneSheet( shtNo );                             // 最後のシートをコピーします。
1384                }
1385                shtNo++ ;                                                                                       // シート番号を増やしておく。
1386
1387                // シート名の変換
1388                setSheetName( shtNo,shtName );                                          // 同一シート対策済みのメソッドを呼び出す。
1389
1390                // セル値の変換
1391                final int stR = Math.max( sheet.getFirstRowNum(),0 );           // stR が、マイナスのケースがある。
1392                final int edR = sheet.getLastRowNum();
1393
1394                for( int rowNo=stR; rowNo<=edR; rowNo++ ) {
1395                        final Row rowObj = sheet.getRow( rowNo );
1396                        if( rowObj != null ) {
1397                                final int stC = Math.max( rowObj.getFirstCellNum(),0 );         // stC が、マイナスのケースがある。
1398                                final int edC = rowObj.getLastCellNum();
1399                                for( int colNo=stC; colNo<=edC; colNo++ ) {
1400                                        final Cell colObj = rowObj.getCell( colNo );
1401//                                      if( colObj != null && colObj.getCellType() != Cell.CELL_TYPE_BLANK ) {          // 6.5.0.0 (2016/09/30) poi-3.12
1402//                                      if( colObj != null && colObj.getCellTypeEnum() != CellType.BLANK ) {            // 6.5.0.0 (2016/09/30) poi-3.15
1403                                        if( colObj != null && colObj.getCellType() != CellType.BLANK ) {                        // 8.0.0.0 (2021/07/31) poi-4.1.2.jar → poi-5.0.0.jar
1404                                                final String cmnt= "Sheet" + shtNo + ":" + POIUtil.getCelKigo( rowNo,colNo );   //
1405        //                                      final String val = crConv( conv, POIUtil.getValue( colObj ),null );             // 改行対応
1406                                                final String val = crConv( conv, POIUtil.getValue( colObj ),cmnt );             // 8.5.0.0 (2023/04/21)
1407                                                if( val != null ) {
1408                                                        POIUtil.setValue( colObj,val );
1409                                //                      colObj.setCellValue( val );
1410                                                }
1411                                        }
1412                                }
1413                        }
1414                }
1415
1416                // 8.0.3.1 (2021/12/28) オブジェクト文字列の変換
1417                final Drawing<?> drawing = sheet.getDrawingPatriarch();
1418                for (final Shape shape : drawing) {
1419                        if( shape instanceof XSSFShape ) {
1420                                shapeConvert( shtNo,(XSSFShape)shape,conv );    // 8.5.0.0 (2023/04/21) shapeConvert のメソッド化
1421
1422        //                      final String shpNm = shape.getShapeName();
1423        //                      final String cmnt = "Sheet" + shtNo + ":" + shpNm ;
1424        //                      conv.change( getShapeText( (XSSFShape)shape,null ),cmnt );
1425                        }
1426                }
1427
1428        //      // オブジェクト文字列の変換
1429        //      if( sheet instanceof POIXMLDocumentPart ) {
1430        //              for( final POIXMLDocumentPart pxdp : ((POIXMLDocumentPart)sheet).getRelations() ) {
1431        //                      if( pxdp instanceof XSSFDrawing ) {
1432        //                              for( final XSSFShape shape : ((XSSFDrawing)pxdp).getShapes() ) {
1433        //                                      // 8.0.3.1 (2021/12/28) テキスト取得処理にgetShapeTextを使用する。
1434        //                                      conv.change( getShapeText( shape ),null );
1435
1436                                //              final org.apache.poi.xssf.usermodel.XSSFAnchor anc = shape.getAnchor();
1437                                //              if( shape instanceof XSSFSimpleShape ) {
1438                                //                      for( final XSSFTextParagraph para : ((XSSFSimpleShape)shape).getTextParagraphs() ) {
1439                                //                              for( final XSSFTextRun text : para.getTextRuns() ) {
1440                                //                                      final String val = crConv( conv,text.getText() , null );
1441                                //                                      if( val != null ) {
1442                                //                                              text.setText( val );
1443                                //                                      }
1444                                //                              }
1445                                //                      }
1446                                //              }
1447        //                              }
1448        //                      }
1449        //              }
1450        //      }
1451        }
1452
1453        /**
1454         * XSSFShapeから、テキスト文字列を取得します(XSLX限定)。
1455         *
1456         * XSSFSimpleShapeの場合は、そのまま#getText()を実行します。
1457         * XSSFShapeGroupの場合は、XSSFSimpleShapeに順次分解して文字列を連結していきます。
1458         * 途中に存在する改行コードは削除しておきます。
1459         *
1460         * @og.rev 8.0.3.1 (2021/12/28) テキスト取得処理にgetShapeTextを使用する。
1461         * @og.rev 8.1.0.1 (2022/01/07) BiConsumerの引数付きメソッドに修正
1462         *
1463         * @param       shape   XSSFShapeオブジェクト
1464         * @param       bicon   BiConsumer関数オブジェクト
1465         * @return      シェープから取得した文字列
1466         */
1467//      private String getShapeText( final XSSFShape shape ) {
1468        private String getShapeText( final XSSFShape shape, final BiConsumer<XSSFSimpleShape,String> bicon ) {
1469                // 8.5.5.1 (2024/02/29) PMD 7.0.0 OnlyOneReturn メソッドには終了ポイントが 1 つだけ必要
1470                String rtn = "";
1471
1472                if( shape instanceof XSSFSimpleShape ) {
1473                        final String txt = ((XSSFSimpleShape)shape).getText().replace("\n","");
1474                        if( bicon != null && !txt.isEmpty() ) {
1475                                bicon.accept( (XSSFSimpleShape)shape,txt );
1476                        }
1477//                      return txt;
1478                        rtn = txt;
1479                }
1480                else if( shape instanceof XSSFShapeGroup ) {
1481        //              final StringBuilder buf = new StringBuilder();
1482                        for( final XSSFShape shape2 : (XSSFShapeGroup)shape ) {
1483                                final String txt = getShapeText( shape2,bicon );
1484                                if( !txt.isEmpty() ) {          // 見つかった時点で終了
1485//                                      return txt;
1486                                        rtn = txt;
1487                                        break;
1488                                }
1489        //                      buf.append( getShapeText( shape2 ) );
1490                        }
1491        //              return buf.toString();
1492                }
1493//              return "";
1494                return rtn;
1495        }
1496
1497        /**
1498         * Workbook の全Sheetを対象に、テキスト変換処理を行います(XSLX限定)。
1499         *
1500         * この処理は、#saveFile( File ) の直前に行うのがよいでしょう。
1501         * #activeWorkbook( boolean ) との順番は構いません。
1502         *
1503         * ・シート名の一覧をピックアップします。
1504         * ・セル値を、セル単位内の改行単位にピックアップし、結果を合成ます。
1505         * ・オブジェクト文字列を、改行単位にピックアップし、結果を合成します。
1506         *
1507         * ここでは、シート名、セルテキスト、SimpleShapeオブジェクトのテキストを
1508         * input に、TextConverterインターフェース の change メソッドを呼び出します。
1509         * 戻り値が、null でないなら、元のデータと置き換えます。
1510         * 戻り値が、null の場合は、そのまま読み飛ばします。(なにもしません)
1511         * EXCELへの書き戻しが発生しますので、万一、ファイル破損で、開けなくなる場合を
1512         * 想定して、バックアップファイルは、各自で準備してください。
1513         *
1514         * @og.rev 6.2.4.2 (2015/05/29) テキスト変換処理
1515         * @og.rev 6.3.1.0 (2015/06/28) TextConverterに、引数(cmnt)を追加
1516         *
1517         * @param       conv    TextConverterインターフェース
1518         * @param       val             改行処理を行う元の値
1519         * @param       cmnt    コメント
1520         * @return      改行処理の結果の値(対象が無ければ、null)
1521         * @see         #textConverter( Map )
1522         */
1523        private String crConv( final TextConverter<String,String> conv , final String val , final String cmnt ) {
1524                String rtn = null;
1525                if( val != null ) {
1526                        if( val.contains( "\n" ) ) {                                            // 改行がある場合(EXCEL のセル内改行コードは、LF(0A)=\n のみ。
1527                                final String[] val2 = val.split( "\\n" );               // 改行で分割する。
1528                                boolean flag = false;
1529                                for( int i=0; i<val2.length; i++ ) {
1530                                        final String val3 = conv.change( val2[i],cmnt );        // 6.3.1.0 (2015/06/28)
1531                                        if( val3 != null ) { val2[i] = val3; flag = true; }
1532                                }
1533                                if( flag ) {
1534                                        final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
1535                                        buf.append( val2[0] );
1536                                        for( int i=1; i<val2.length; i++ ) {
1537                                                buf.append( '\n' ).append( val2[i] );           // LF(\n)で、セパレートしているので、LF のみ追加する。
1538                                        }
1539                                        rtn = buf.toString();
1540                                }
1541                        }
1542                        else {                                                                                          // 改行がない場合
1543                                rtn = conv.change( val,cmnt );                                  // 6.3.1.0 (2015/06/28)
1544                        }
1545                }
1546                return rtn;
1547        }
1548
1549        /**
1550         * シート一覧を、内部の Workbook から取得します。
1551         *
1552         * 取得元が、Workbook なので、xls , xlsx どちらの形式でも取り出せます。
1553         *
1554         * EXCEL上のシート名を、配列で返します。
1555         *
1556         * @og.rev 6.2.6.0 (2015/06/19) 新規作成
1557         *
1558         * @return      シート名の配列
1559         * @see         POIUtil#getSheetNames( Workbook )
1560         */
1561        public String[] getSheetNames() {
1562                return POIUtil.getSheetNames( wkbook );
1563        }
1564
1565        /**
1566         * 名前定義一覧を内部の Workbook から取得します。
1567         *
1568         * EXCEL上に定義された名前を、配列で返します。
1569         * ここでは、名前とFormulaをタブで連結した文字列を配列で返します。
1570         * Name オブジェクトを削除すると、EXCELが開かなくなったりするので、
1571         * 取りあえず一覧を作成して、手動で削除してください。
1572         * なお、名前定義には、非表示というのがありますので、ご注意ください。
1573         *
1574         * @og.rev 6.2.6.0 (2015/06/19) 新規作成
1575         *
1576         * @return      名前定義(名前+TAB+Formula)の配列
1577         * @see         POIUtil#getNames( Workbook )
1578         * @og.rtnNotNull
1579         */
1580        public String[] getNames() {
1581                return POIUtil.getNames( wkbook );
1582        }
1583
1584        /**
1585         * 書式のスタイル一覧を内部の Workbook から取得します。
1586         *
1587         * EXCEL上に定義された書式のスタイルを、配列で返します。
1588         * 書式のスタイルの名称は、CellStyle にメソッドが定義されていません。
1589         * 実クラスである HSSFCellStyle にキャストして使用する
1590         * 必要があります。(XSSFCellStyle にも名称を取得するメソッドがありません。)
1591         *
1592         * ※ EXCEL2010 ホームタブ→セルのスタイル は、一つづつしか削除できません。
1593         *    マクロは、開発タブ→Visual Basic で、挿入→標準モジュール を開き
1594         *    テキストを張り付けてください。
1595         *    実行は、開発タブ→マクロ で、マクロ名を選択して、実行します。
1596         *    最後は、削除してください。
1597         *
1598         * @og.rev 6.2.6.0 (2015/06/19) 新規作成
1599         *
1600         * @return      書式のスタイル一覧
1601         * @see         POIUtil#getStyleNames( Workbook )
1602         * @og.rtnNotNull
1603         */
1604        public String[] getStyleNames() {
1605                return POIUtil.getStyleNames( wkbook );
1606        }
1607
1608        /**
1609         * 文字列を Double オブジェクトに変換します。
1610         *
1611         * これは、引数の カンマ(,) を削除した文字列から、Double オブジェクトを生成します。
1612         * 処理中に、文字列が解析可能な double を含まない場合(NumberFormatException)
1613         * また、引数が、null,ゼロ文字列,'_', エラー の時には、null を返します。
1614         *
1615         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
1616         * @og.rev 6.3.9.0 (2015/11/06) もう少し判りやすくする。(処理速度は落ちてます。)
1617         *
1618         * @param       value   Doubleに変換する元の文字列
1619         *
1620         * @return      変換後のDoubleオブジェクト(エラー発生時や変換不可の場合は、null)
1621         */
1622        private Double parseDouble( final String value ) {
1623                Double rtn = null ;
1624
1625                try {
1626//                      if( value == null || value.isEmpty() || value.equals( "_" ) ) {
1627                        if( value == null || value.isEmpty() || "_".equals( value ) ) { // 8.5.4.2 (2024/01/12) PMD 7.0.0 LiteralsFirstInComparisons
1628                                rtn = null;
1629                        }
1630                        else if( value.indexOf( ',' ) < 0 ) {
1631                                rtn = Double.valueOf( value );          // 6.0.2.4 (2014/10/17) メソッドが非効率だった。
1632                        }
1633                        else {
1634                                // 6.3.9.0 (2015/11/06) もう少し判りやすくする。(処理速度は落ちてます。)
1635                                rtn = Double.valueOf( value.replaceAll( ",","" ) );
1636                        }
1637                }
1638                catch( final NumberFormatException ex ) {               // 文字列が解析可能な数値を含まない場合
1639                        final String errMsg = "Double変換できませんでした。" + CR
1640                                                                + ex.getMessage() + CR
1641                                                                + "  value=" + value;
1642                        System.err.println( errMsg );
1643                        rtn = null;
1644                }
1645
1646                return rtn ;
1647        }
1648
1649        /**
1650         * アプリケーションのサンプルです。
1651         *
1652         * Usage: java org.opengion.fukurou.model.ExcelModel 入力ファイル名 [出力ファイル名] ・・・
1653         *  通常は標準出力に行単位に、セルをタブ区切り出力します。
1654         *  出力ファイル名 を指定すると、EXCEL ファイルとしてセーブし直します。
1655         *  その場合は、以下のパラメータも使用できます。
1656         *   -CS      CellStyleを 設定します。
1657         *   -AS      useAutoCellSizeを 設定します。
1658         *   -FN=***  FontNameを 設定します。
1659         *   -FP=**   FontPointを 設定します。
1660         *   -IMG     画像ファイルを挿入します。(-IMG 画像ファイル名 シート番号 行 列)をスペース区切りで続けます。
1661         *
1662         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
1663         *
1664         * @param       args    コマンド引数配列
1665         */
1666        public static void main( final String[] args ) {
1667                if( args.length == 0 ) {
1668                        final String usage = "Usage: java org.opengion.fukurou.model.ExcelModel 入力ファイル名 [出力ファイル名] ・・・\n" +
1669                                                "\t-CS      CellStyleを 設定します。        \n" +
1670                                                "\t-TC      TextConverterを実行します。     \n" +
1671                                                "\t-AS      useAutoCellSizeを 設定します。  \n" +
1672                                                "\t-FN=***  FontNameを 設定します。         \n" +
1673                                                "\t-FP=**   FontPointを 設定します。        \n" +
1674                                                "\t-IMG     画像ファイルを挿入します。          \n" +
1675                                                "\t    (-IMG ファイル名 シート番号 行 列)          \n" ;
1676                        System.err.println( usage );
1677                        return ;
1678                }
1679
1680                final ExcelModel excel = new ExcelModel( new File( args[0] ) , true );
1681
1682                excel.activeWorkbook( true );                   // 余計な行を削除します。
1683
1684                if( args.length > 1 ) {
1685                        final File outFile = new File( args[1] );                       // 6.2.0.0 (2015/02/27)
1686                        boolean isCS = false;
1687                        boolean isAS = false;
1688                        boolean isTC = false;                           // 6.2.4.2 (2015/05/29) テキスト変換処理
1689                        String  fn   = null;
1690                        short   fp   = -1;
1691
1692                        // 8.5.4.2 (2024/01/12) PMD 7.0.0 AvoidReassigningLoopVariables
1693//                      for( int i=2; i<args.length; i++ ) {
1694//                              final String prm = args[i];
1695//
1696//                              if( "-CS".equalsIgnoreCase( prm ) ) { isCS = true; }    // 6.4.1.1 (2016/01/16) PMD refactoring. Position literals first in String comparisons for EqualsIgnoreCase.
1697//                              if( "-AS".equalsIgnoreCase( prm ) ) { isAS = true; }    // 6.4.1.1 (2016/01/16) PMD refactoring. Position literals first in String comparisons for EqualsIgnoreCase.
1698//                              if( "-TC".equalsIgnoreCase( prm ) ) { isTC = true; }    // 6.4.1.1 (2016/01/16) PMD refactoring. Position literals first in String comparisons for EqualsIgnoreCase.
1699//                              if( prm.startsWith( "-FN" ) ) { fn   = prm.substring( 3 ); }
1700//                              if( prm.startsWith( "-FP" ) ) { fp   = Short.parseShort( prm.substring( 3 ) ); }
1701//                              if( "-IMG".equalsIgnoreCase( prm ) ) {                                  // 6.4.1.1 (2016/01/16) PMD refactoring. Position literals first in String comparisons for EqualsIgnoreCase.
1702//                                      final String img = args[++i];
1703//                                      final int  shtNo = Integer.parseInt( args[++i] );
1704//                                      final int  rowNo = Integer.parseInt( args[++i] );
1705//                                      final int  colNo = Integer.parseInt( args[++i] );
1706//
1707//                                      excel.addImageFile( img,shtNo,rowNo,colNo );
1708//                              }
1709//                      }
1710                        final int len = args.length;
1711                        int idx = 2;
1712                        while( idx < len ) {
1713                                final String prm = args[idx];
1714
1715                                if( "-CS".equalsIgnoreCase( prm ) ) { isCS = true; }    // 6.4.1.1 (2016/01/16) PMD refactoring. Position literals first in String comparisons for EqualsIgnoreCase.
1716                                if( "-AS".equalsIgnoreCase( prm ) ) { isAS = true; }    // 6.4.1.1 (2016/01/16) PMD refactoring. Position literals first in String comparisons for EqualsIgnoreCase.
1717                                if( "-TC".equalsIgnoreCase( prm ) ) { isTC = true; }    // 6.4.1.1 (2016/01/16) PMD refactoring. Position literals first in String comparisons for EqualsIgnoreCase.
1718                                if( prm.startsWith( "-FN" ) ) { fn   = prm.substring( 3 ); }
1719                                if( prm.startsWith( "-FP" ) ) { fp   = Short.parseShort( prm.substring( 3 ) ); }
1720                                if( "-IMG".equalsIgnoreCase( prm ) ) {                                  // 6.4.1.1 (2016/01/16) PMD refactoring. Position literals first in String comparisons for EqualsIgnoreCase.
1721                                        if( idx + 4 < len ) {
1722                                                final String img = args[++idx];
1723                                                final int  shtNo = Integer.parseInt( args[++idx] );
1724                                                final int  rowNo = Integer.parseInt( args[++idx] );
1725                                                final int  colNo = Integer.parseInt( args[++idx] );
1726
1727                                                excel.addImageFile( img,shtNo,rowNo,colNo );
1728                                        }
1729                                        else {
1730                                                System.out.println( "-IMG ファイル名 シート番号 行 列 の形式ではありません。" );
1731                                        }
1732                                }
1733                                idx++ ;
1734                        }
1735
1736                        if( isCS ) { excel.setCellStyle(); }
1737                        excel.useAutoCellSize( isAS );
1738                        excel.setFont( fn,fp );
1739
1740                        // 6.2.4.2 (2015/05/29) テキスト変換処理
1741                        if( isTC ) {
1742                                // 6.3.9.0 (2015/11/06) Java 8 ラムダ式に変更
1743                                // 処理が複数行に別れるのは判りにくいので良くない。
1744                                excel.textConverter(
1745                                        ( val,cmnt ) -> {
1746                                                System.out.println( val );                      // すべてのテキストを読み取る。
1747                                                return null;                                            // 変換せず。
1748                                        }
1749                                );
1750                        }
1751
1752                        excel.saveFile( outFile );
1753                }
1754                else {
1755                        final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
1756
1757                        final int shLen = excel.getNumberOfSheets();
1758                        for( int shtNo=0; shtNo<shLen; shtNo++ ) {
1759                                final String shtName = excel.getSheetName( shtNo );
1760
1761                                final int stRow = excel.getFirstRowNum();
1762                                final int edRow = excel.getLastRowNum();
1763                                for( int rowNo=stRow; rowNo<=edRow; rowNo++ ) {
1764                                        buf.setLength(0);               // Clearの事
1765                                        buf.append( shtName ).append( '\t' ).append( rowNo );
1766                                        final String[] vals = excel.getValues( rowNo );
1767                                        if( vals != null ) {
1768                                                // 8.5.4.2 (2024/01/12) PMD 7.0.0 ForLoopCanBeForeach
1769//                                              for( int colNo=0; colNo<vals.length; colNo++ ) {
1770//                                                      final String val = vals[colNo] == null ? "" : vals[colNo];
1771                                                for( final String val : vals ) {
1772                                                        buf.append( '\t' ).append( val == null ? "" : val );
1773                                                }
1774                                        }
1775                                        System.out.println( buf );
1776                                }
1777                                System.out.println();
1778                        }
1779                }
1780        }
1781}