/*
 * Copyright (c) 2009 The openGion Project.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package org.opengion.fukurou.model;

import java.io.File;									// 6.2.0.0 (2015/02/27)
import java.io.IOException;
import java.io.OutputStream;
import java.io.FileOutputStream;
import java.io.BufferedOutputStream;
import java.util.Locale;
import java.util.Map;									// 6.0.2.3 (2014/10/10) 画像関連
import java.util.HashMap;								// 6.0.2.3 (2014/10/10) 画像関連
import java.util.List;									// 8.0.1.0 (2021/10/29)
import java.util.function.BiConsumer;					// 8.1.0.1 (2022/01/07)

import org.apache.poi.util.Units;						// 7.2.9.0 (2020/10/12)

import org.apache.poi.common.usermodel.HyperlinkType;	// 6.5.0.0 (2016/09/30) poi-3.15
import org.apache.poi.ss.util.WorkbookUtil;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;			// 6.5.0.0 (2016/09/30) poi-3.15
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.VerticalAlignment;	// 6.5.0.0 (2016/09/30) poi-3.15
import org.apache.poi.ss.usermodel.BorderStyle;			// 6.5.0.0 (2016/09/30) poi-3.15
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.RichTextString;
import org.apache.poi.ss.usermodel.Hyperlink;
import org.apache.poi.ss.usermodel.CreationHelper;
import org.apache.poi.ss.usermodel.Drawing;				// 6.0.2.3 (2014/10/10) 画像関連
import org.apache.poi.ss.usermodel.Shape;				// 8.0.3.1 (2021/12/28) 画像関連
import org.apache.poi.ss.usermodel.ClientAnchor;		// 6.0.2.3 (2014/10/10) 画像関連
import org.apache.poi.ss.usermodel.Picture;				// 6.0.2.3 (2014/10/10) 画像関連

import org.apache.poi.hssf.usermodel.HSSFWorkbook;		// .xls

// import org.apache.poi.POIXMLDocumentPart;				// 6.2.4.2 (2015/05/29) ﾃｷｽﾄ変換処理  8.1.2.3 (2022/05/20) 復活
import 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) 復活

import org.apache.poi.xssf.usermodel.XSSFDrawing;		// 6.2.4.2 (2015/05/29) ﾃｷｽﾄ変換処理  8.1.2.3 (2022/05/20) 復活
import org.apache.poi.xssf.usermodel.XSSFShape;			// 6.2.4.2 (2015/05/29) ﾃｷｽﾄ変換処理
import org.apache.poi.xssf.usermodel.XSSFSimpleShape;	// 6.2.4.2 (2015/05/29) ﾃｷｽﾄ変換処理
import org.apache.poi.xssf.usermodel.XSSFShapeGroup;	// 8.0.3.1 (2021/12/28)
import org.apache.poi.xssf.usermodel.XSSFTextParagraph;	// 6.2.4.2 (2015/05/29) ﾃｷｽﾄ変換処理  8.1.2.3 (2022/05/20) 復活
import org.apache.poi.xssf.usermodel.XSSFTextRun;		// 6.2.4.2 (2015/05/29) ﾃｷｽﾄ変換処理  8.1.2.3 (2022/05/20) 復活
import org.apache.poi.xssf.usermodel.XSSFAnchor;		// .xslx 8.1.2.3 (2022/05/20)
import org.apache.poi.xssf.usermodel.XSSFClientAnchor;	// 8.5.0.0 (2023/04/21) XSSFAnchor ｵﾌﾞｼﾞｪｸﾄの NullPointerException 対策
import org.apache.poi.xssf.streaming.SXSSFWorkbook;		// .xlsx 6.3.7.0 (2015/09/04) 制限あり　高速、低ﾒﾓﾘ消費

import org.opengion.fukurou.system.OgRuntimeException ;	// 6.4.2.0 (2016/01/29)
import org.opengion.fukurou.system.Closer;
import org.opengion.fukurou.util.ImageUtil;				// 6.0.2.3 (2014/10/10) 画像関連

import static org.opengion.fukurou.system.HybsConst.CR;				// 6.1.0.0 (2014/12/26) refactoring
import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;	// 6.1.0.0 (2014/12/26) refactoring

/**
 * POI による、EXCELﾊﾞｲﾅﾘﾌｧｲﾙに対する、ﾃﾞｰﾀﾓﾃﾞﾙｸﾗｽです。
 *
 * 共通的な EXCEL処理 を集約しています。
 * staticﾒｿｯﾄﾞによる簡易的なｱｸｾｽの他に、順次処理も可能なように
 * 現在ｱｸｾｽ中の、Workbook、Sheet、Row、Cell ｵﾌﾞｼﾞｪｸﾄを内部で管理しています。
 *
 * 入力形式は、openXML形式にも対応しています。
 * ﾌｧｲﾙの内容に応じて、.xlsと.xlsxのどちらで読み取るかは、内部的に
 * 自動判定されます。
 *
 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
 * @og.rev 8.1.2.3 (2022/05/20) ﾃｷｽﾄ変換処理 復活
 * @og.group その他
 *
 * @version  6.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK7.0,
 */
public class ExcelModel {
	/** このﾌﾟﾛｸﾞﾗﾑのVERSION文字列を設定します。	{@value} */
	private static final String VERSION = "8.5.0.0 (2023/04/21)" ;

	private static final String DEF_SHEET_NAME = "Sheet" ;

	// 6.0.2.3 (2014/10/10) ImageUtil の Suffix と、Workbook.PICTURE_TYPE_*** の関連付けをしておきます。
	// Suffix 候補は、[bmp, gif, jpeg, jpg, png, wbmp] だが、対応する PICTURE_TYPE は一致しない。
	/** staticｲﾆｼｬﾗｲｻﾞ後、読み取り専用にするので、ConcurrentHashMap を使用しません。 */
	private static final Map<String,Integer> PICTURE_TYPE ;
	static {
		PICTURE_TYPE = new HashMap<>() ;
		PICTURE_TYPE.put( "png"  , Integer.valueOf( Workbook.PICTURE_TYPE_PNG   ) );
		PICTURE_TYPE.put( "jpeg" , Integer.valueOf( Workbook.PICTURE_TYPE_JPEG  ) );
		PICTURE_TYPE.put( "jpg"  , Integer.valueOf( Workbook.PICTURE_TYPE_JPEG  ) );
	}

	private final String inFilename ;		// ｴﾗｰ発生時のｷｰとなる、EXCELﾌｧｲﾙ名
	private final String sufix		;		// 6.1.0.0 (2014/12/26) ｵｰﾌﾟﾝしたﾌｧｲﾙ形式を記憶(ﾋﾟﾘｵﾄﾞを含む)

	private final Workbook	wkbook	;		// 現在処理中の Workbook
	private Sheet			sheet	;		// 現在処理中の Sheet
	private Row				rowObj	;		// 現在処理中の Row

	private int refSheetIdx	= -1;			// 雛形ｼｰﾄのｲﾝﾃﾞｯｸｽ

	private final CreationHelper createHelper	;	// poi.xssf対応

	private CellStyle style			;		// 共通のｾﾙｽﾀｲﾙ
	private CellStyle hLinkStyle	;		// Hyperlink用のｾﾙｽﾀｲﾙ(青文字＋下線)

	private int maxColCount			= 5 ;	// 標準ｾﾙ幅の５倍を最大幅とする。
	private int dataStartRow		= -1;	// ﾃﾞｰﾀ行の開始位置。未設定時は、-1
	private boolean isAutoCellSize	;		// ｶﾗﾑ幅の自動調整を行うかどうか(true:行う/false:行わない)

	private String addTitleSheet	;		// Sheet一覧を先頭Sheetに作成する場合のSheet名

	private String[] recalcSheetNames	;	// 6.5.0.0 (2016/09/30) ｾﾙの計算式の再計算をさせるｼｰﾄ名の配列。

	/**
	 * EXCELﾌｧｲﾙのWookbookのﾃﾞｰﾀ処理ﾓﾃﾞﾙを作成します。
	 *
	 * ここでは、既存のﾌｧｲﾙを読み込んで、ﾃﾞｰﾀ処理ﾓﾃﾞﾙを作成しますので、
	 * ﾌｧｲﾙがｵｰﾌﾟﾝできなければｴﾗｰになります。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 * @og.rev 6.2.0.0 (2015/02/27) ﾌｧｲﾙ引数を、String → File に変更
	 *
	 * @param   file  EXCELﾌｧｲﾙ
	 * @see		#ExcelModel( File , boolean )
	 */
	public ExcelModel( final File file ) {
		this( file,true );
	}

	/**
	 * EXCELﾌｧｲﾙのWookbookのﾃﾞｰﾀ処理ﾓﾃﾞﾙを作成します。
	 *
	 * isOpen条件によって、ﾌｧｲﾙｵｰﾌﾟﾝ(true)か、新規作成(false)が分かれます。
	 * ﾌｧｲﾙｵｰﾌﾟﾝの場合は、EXCELの読み込み以外に、追記するとか、雛形参照する
	 * 場合にも、使用します。
	 * ﾌｧｲﾙｵｰﾌﾟﾝの場合は、当然、ﾌｧｲﾙがｵｰﾌﾟﾝできなければｴﾗｰになります。
	 *
	 * isOpen=新規作成(false) の場合は、ﾌｧｲﾙ名の拡張子で、XSSFWorkbook か HSSFWorkbook を
	 * 判定します。.xlsx の場合⇒XSSFWorkbook ｵﾌﾞｼﾞｪｸﾄを使用します。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 * @og.rev 6.0.2.3 (2014/10/10) POIUtil#createWorkbook( String ) を使用するように変更
	 * @og.rev 6.1.0.0 (2014/12/26) 入力ﾌｧｲﾙの拡張子判定の対応
	 * @og.rev 6.2.0.0 (2015/02/27) ﾌｧｲﾙ引数を、String → File に変更
	 * @og.rev 6.2.2.0 (2015/03/27) ﾏｸﾛ付Excel(.xlsm)対応
	 * @og.rev 6.3.7.0 (2015/09/04),5.9.0.0 (2015/09/04) 標準を、SXSSFWorkbook に切り替えてみる。
	 *
	 * @param   file   EXCELﾌｧｲﾙ
	 * @param   isOpen true:ﾌｧｲﾙｵｰﾌﾟﾝ/false:新規作成
	 * @see		#ExcelModel( File )
	 */
	public ExcelModel( final File file , final boolean isOpen ) {
		inFilename	= file.getName();

		final int idx = inFilename.lastIndexOf( '.' );	// 拡張子の位置
		if( idx >= 0 ) {
			sufix = inFilename.substring( idx ).toLowerCase( Locale.JAPAN );		// ﾋﾟﾘｵﾄﾞを含む
		}
		else {
			final String errMsg = "ﾌｧｲﾙの拡張子が見当たりません。(.xls か .xlsx/.xlsm を指定下さい)" + CR
							+ " filename=[" + file + "]"  + CR ;
			throw new IllegalArgumentException( errMsg );
		}

		if( isOpen ) {
			wkbook = POIUtil.createWorkbook( file );
		}
		else {
			// 新規の場合、ﾌｧｲﾙ名に.xlsxで終了した場合⇒.xlsx形式ﾌｧｲﾙ作成、その他⇒.xls形式ﾌｧｲﾙ作成
			if( ".xlsx".equals( sufix ) || ".xlsm".equals( sufix ) ) {		// 6.2.2.0 (2015/03/27)
				// 6.3.7.0 (2015/09/04),5.9.0.0 (2015/09/04) 標準を、SXSSFWorkbook に切り替えてみる。
	//			wkbook = new XSSFWorkbook();
				wkbook = new SXSSFWorkbook();	// 機能制限有：ｼｰﾄや行の削除や、AutoCellSize の指定ができないなど。
			}
			else if( ".xls".equals( sufix ) ) {
				wkbook = new HSSFWorkbook();
			}
			else {
				final String errMsg = "ﾌｧｲﾙの拡張子が不正です。(.xls か .xlsx/.xlsm のみ可能)" + CR
								+ " filename=[" + file + "]"  + CR ;
				throw new IllegalArgumentException( errMsg );
			}
		}

		createHelper = wkbook.getCreationHelper();		// poi.xssf対応
	}

	/**
	 * 内部 Workbook に、ﾌｫﾝﾄ名、ﾌｫﾝﾄｻｲｽﾞを設定します。
	 * fontName(ﾌｫﾝﾄ名)は、"ＭＳ Ｐｺﾞｼｯｸ" など名称になります。
	 * fontPoint は、ﾌｫﾝﾄの大きさを指定します。
	 * 内部的には、setFontHeightInPoints(short)ﾒｿｯﾄﾞで設定します。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 *
	 * @param	fontName	ﾌｫﾝﾄ名 ("ＭＳ Ｐｺﾞｼｯｸ" など。nullの場合ｾｯﾄしません)
	 * @param	fontPoint	ﾌｫﾝﾄの大きさ (0やﾏｲﾅｽの場合はｾｯﾄしません)
	 */
	public void setFont( final String fontName , final short fontPoint ) {
	//	System.out.println( "FontName=" + fontName + " , Point=" + fontPoint );

		if( style == null ) { style = wkbook.createCellStyle(); }

		final Font font = wkbook.createFont();
	//	final Font font = wkbook.getFontAt( style.getFontIndex() );				// A,B などのﾍｯﾀﾞｰもﾌｫﾝﾄが
		if( fontName != null ) {
			font.setFontName( fontName );	// "ＭＳ Ｐｺﾞｼｯｸ" など
		}
		if( fontPoint > 0 ) {
			font.setFontHeightInPoints( fontPoint );
		}

		style.setFont( font );
	}

	/**
	 * ﾃﾞｰﾀ設定する ｾﾙに、罫線を追加します。
	 *
	 * ここで設定するのは、罫線の種類と、罫線の色ですが、内部的に固定にしています。
	 *   Border=CellStyle.BORDER_THIN
	 *   BorderColor=IndexedColors.BLACK
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 * @og.rev 6.5.0.0 (2016/09/30) poi-3.15 対応(Cell.CELL_TYPE_XXXX → CellType.XXXX)
	 */
	public void setCellStyle() {
		if( style == null ) { style = wkbook.createCellStyle(); }

	//	style.setBorderBottom(	CellStyle.BORDER_THIN );	// 6.5.0.0 (2016/09/30) poi-3.12
	//	style.setBorderLeft(	CellStyle.BORDER_THIN );	// 6.5.0.0 (2016/09/30) poi-3.12
	//	style.setBorderRight(	CellStyle.BORDER_THIN );	// 6.5.0.0 (2016/09/30) poi-3.12
	//	style.setBorderTop(		CellStyle.BORDER_THIN );	// 6.5.0.0 (2016/09/30) poi-3.12

		style.setBorderBottom(	BorderStyle.THIN );			// 6.4.6.0 (2016/05/27) poi-3.15
		style.setBorderLeft(	BorderStyle.THIN );			// 6.5.0.0 (2016/09/30) poi-3.15
		style.setBorderRight(	BorderStyle.THIN );			// 6.5.0.0 (2016/09/30) poi-3.15
		style.setBorderTop(		BorderStyle.THIN );			// 6.5.0.0 (2016/09/30) poi-3.15

		style.setBottomBorderColor(	IndexedColors.BLACK.getIndex() );
		style.setLeftBorderColor(	IndexedColors.BLACK.getIndex() );
		style.setRightBorderColor(	IndexedColors.BLACK.getIndex() );
		style.setTopBorderColor(	IndexedColors.BLACK.getIndex() );

	//	style.setVerticalAlignment( CellStyle.VERTICAL_TOP );	// isAutoCellSize=true 文字は上寄せする。	// 6.5.0.0 (2016/09/30) poi-3.12
		style.setVerticalAlignment( VerticalAlignment.TOP  );	// isAutoCellSize=true 文字は上寄せする。	// 6.5.0.0 (2016/09/30) poi-3.15
	//	style.setWrapText( true );								// isAutoCellSize=true 折り返して表示する。
	}

	/**
	 * 全てのSheetに対して、autoSizeColumn設定を行うかどうか指定します(初期値:false)。
	 *
	 * autoSize設定で、ｶﾗﾑ幅が大きすぎる場合、現状では、
	 * 初期ｶﾗﾑ幅の５倍を限度にしています。
	 *
	 * なお、autoSizeColumn設定は負荷の大きな処理なので、saveFile(String)の
	 * 中で実行されます。(ｾｰﾌﾞしなければ実行されません。)
	 * よって、指定は、いつ行っても構いません。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 *
	 * @param flag autoSizeColumn設定を行うかどうか [true:自動ｶﾗﾑ幅設定を行う/false:行わない]
	 * @see	#useAutoCellSize( boolean,int )
	 */
	public void useAutoCellSize( final boolean flag ) {
		isAutoCellSize = flag;
	}

	/**
	 * 全てのSheetに対して、autoSizeColumn設定を行うかどうか指定します(初期値:false)。
	 *
	 * autoSize設定で、ｶﾗﾑ幅が大きすぎる場合、現状では、
	 * 初期ｶﾗﾑ幅のcount倍を限度に設定します。
	 * ただし、count がﾏｲﾅｽの場合は、無制限になります。
	 *
	 * なお、autoSizeColumn設定は負荷の大きな処理なので、saveFile(String)の
	 * 中で実行されます。(ｾｰﾌﾞしなければ実行されません。)
	 * よって、指定は、いつ行っても構いません。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 *
	 * @param flag autoSizeColumn設定を行うかどうか [true:自動ｶﾗﾑ幅設定を行う/false:行わない]
	 * @param count 最大幅を標準ｾﾙ幅の何倍にするかを指定。ﾏｲﾅｽの場合は、無制限
	 * @see	#useAutoCellSize( boolean )
	 */
	public void useAutoCellSize( final boolean flag, final int count ) {
		isAutoCellSize = flag;
		maxColCount    = count ;
	}

	/**
	 * EXCELで、出力処理の最後にｾﾙの計算式の再計算をさせるｼｰﾄ名の配列を指定します。
	 *
	 * null の場合は、再計算しません。
	 * なお、再計算は、saveFile(String)の中で実行されます。(ｾｰﾌﾞしなければ実行されません。)
	 *
	 * @og.rev 6.5.0.0 (2016/09/30) ｾﾙの計算式の再計算をさせる recalcSheetNames 属性の追加。
	 *
	 * @param  sheets 対象ｼｰﾄ名の配列
	 */
	public void setRecalcSheetName( final String[] sheets ){
		recalcSheetNames = sheets;
	}

	/**
	 * ﾃﾞｰﾀ行の書き込み開始位置の行番号を設定します。
	 *
	 * これは、autoSize設定で、自動調整するｶﾗﾑを、ﾍｯﾀﾞｰではなく、
	 * ﾃﾞｰﾀ部で計算する場合に使用します。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 *
	 * @param st  ﾃﾞｰﾀ行の開始位置。未設定時は、-1
	 * @see	#useAutoCellSize( boolean )
	 */
	public void setDataStartRow( final int st ) {
		dataStartRow = st;
	}

	/**
	 * Sheet一覧を先頭Sheetに作成する場合のSheet名を指定します。
	 *
	 * これは、Workbook に含まれる Sheet 一覧を作成する場合に、利用可能です。
	 *
	 * この処理は、#saveFile( File ) 処理時に、実行されます。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 *
	 * @param shtName  Sheet一覧のSheet名
	 * @see	#makeAddTitleSheet()
	 */
	public void setAddTitleSheet( final String shtName ) {
		addTitleSheet = shtName;
	}

	/**
	 * 内部 Workbookの Sheet数を返します。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 *
	 * @return	ｼｰﾄ数
	 */
	public int getNumberOfSheets() {
		return wkbook.getNumberOfSheets();
	}

	/**
	 * 内部 Workbookより、雛形Sheetをｾｯﾄします。
	 *
	 * これは、雛形ｼｰﾄを使用する場合に、使います。このﾒｿｯﾄﾞが呼ばれると、
	 * 雛形ｼｰﾄを使用すると判定されます。
	 * 雛形ｼｰﾄ名が、内部 Workbook に存在しない場合は、ｴﾗｰになります。
	 * ただし、null をｾｯﾄした場合は、最初のｼｰﾄを雛形ｼｰﾄとして使用すると
	 * 判定します。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 *
	 * @param	refSheetName	参照ｼｰﾄ名(nullの場合、参照ｼｰﾄ使用する場合は、先頭のｼｰﾄ)
	 */
	public void setRefSheetName( final String refSheetName ) {
		// 参照ｼｰﾄ名の指定がない場合は、最初のｼｰﾄ
		refSheetIdx = ( refSheetName == null ) ? 0 : wkbook.getSheetIndex( refSheetName );

		if( refSheetIdx < 0 ) {		// 参照ｼｰﾄ名が存在しなかった。
			final String errMsg = "指定の参照ｼｰﾄ名は存在しませんでした。" + CR
							+ " inFilename=[" + inFilename + "] , refSheetName=[" + refSheetName + "]"  + CR ;
			throw new IllegalArgumentException( errMsg );
		}
	}

	/**
	 * 内部 Workbookより、新しいSheetを作ります。
	 *
	 * 先に雛形ｼｰﾄを指定している場合は、その雛形ｼｰﾄから作成します。
	 * 指定していない場合は、新しいｼｰﾄを作成します。
	 * 雛形ｼｰﾄを参照する場合は、雛形ｼｰﾄそのものを返します。
	 * また、雛形ｼｰﾄの枚数を超える場合は、前の雛形ｼｰﾄをｺﾋﾟｰします。
	 * 雛形ｼｰﾄが存在しない場合は、新しいｼｰﾄを作成します。
	 *
	 * ｼｰﾄ名は、重複ﾁｪｯｸを行い、同じ名前のｼｰﾄの場合は、(1),(2)が付けられます。
	 * shtName が null の場合は、"Sheet" が割り振られます。
	 *
	 * この処理を行うと、内部の Sheet にも、ここで作成された Sheet が設定されます。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 * @og.rev 6.2.2.3 (2015/04/10) 雛形ｼｰﾄにそのままﾃﾞｰﾀを書き込んでいく。
	 * @og.rev 6.5.0.0 (2016/09/30) 雛形ｼｰﾄ名をそのまま使用する場合は、isOverwrite に、true を指定します。
	 *
	 * @param	shtName	ｼｰﾄ名 (重複する場合は、(2)、(3)のような文字列を追加 、nullの場合は、"Sheet")
	 * @param	isOverwrite	雛形ｼｰﾄ名をそのまま使用する場合は、true を指定します。
	 */
	public void createSheet( final String shtName , final boolean isOverwrite ) {
		// 参照ｼｰﾄを使う場合(整合性の問題で、両方ともﾁｪｯｸしておきます)

		// 6.2.2.3 (2015/04/10) 雛形ｼｰﾄにそのままﾃﾞｰﾀを書き込んでいく。
		final int shtNo ;
		if( refSheetIdx < 0 ) {									// 雛形ｼｰﾄを使用しない。
			sheet = wkbook.createSheet();
			shtNo = wkbook.getNumberOfSheets() - 1;
		}
		else if( refSheetIdx >= wkbook.getNumberOfSheets() ) {	// ｼｰﾄ数が雛形より超えている。
			sheet = wkbook.cloneSheet( refSheetIdx-1 );			// 最後の雛形ｼｰﾄをｺﾋﾟｰします。
			shtNo = wkbook.getNumberOfSheets() - 1;
			refSheetIdx++ ;
		}
		else {
			sheet = wkbook.getSheetAt( refSheetIdx );			// 雛形ｼｰﾄをそのまま使用
			shtNo = refSheetIdx;
			refSheetIdx++ ;
		}

		// 6.5.0.0 (2016/09/30) 雛形ｼｰﾄ名をそのまま使用する場合。
		if( !isOverwrite ) {
			setSheetName( shtNo , shtName );
		}
	}

	/**
	 * 内部 Workbook の指定のｼｰﾄ番号の Sheet の名前を設定します。
	 *
	 * 指定のｼｰﾄ名が、既存のｼｰﾄになければ、そのまま設定します。
	 * すでに、同じ名前のｼｰﾄが存在する場合は、そのｼｰﾄ名の後に
	 * (1)、(2)、(3)のような文字列を追加します。
	 * shtName が null の場合は、"Sheet" が割り振られます。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 * @og.rev 6.2.5.1 (2015/06/12) ｼｰﾄ名重複が自分自身の場合は、(1)等の追加は行わない。
	 *
	 * @param	shtNo		ｼｰﾄ番号
	 * @param	shtName	ｼｰﾄ名 (重複する場合は、(1)、(2)のような文字列を追加 、nullの場合は、"Sheet")
	 */
	public void setSheetName( final int shtNo, final String shtName ) {
		String tempName = ( shtName == null ) ? DEF_SHEET_NAME : WorkbookUtil.createSafeSheetName( shtName ) ;
		int cnt = 1;

		// 6.2.5.1 (2015/06/12) ｼｰﾄ名重複が自分自身の場合は、(1)等の追加は行わない。
		// ※ EXCELのｼｰﾄ名は、大文字、小文字だけでなく、全角半角の区別もしない。
		final String nowName = wkbook.getSheetName( shtNo );
		if( tempName != null && !tempName.equals( nowName ) ) {			// 全く同一の場合は、何もしない。
			if( shtNo == wkbook.getSheetIndex( tempName ) ) {			// ｼｰﾄ名判定が、自身の場合
				wkbook.setSheetName( shtNo,tempName );
			}
			else {
				while( wkbook.getSheetIndex( tempName ) >= 0 ) {		// ｼｰﾄ名が存在している場合
					tempName = WorkbookUtil.createSafeSheetName( shtName + "(" + cnt + ")" );
					if( tempName.length() >= 31 ) {						// 重複時の追加文字分を減らす。
						tempName = tempName.substring( 0,26 ) + "(" + cnt + ")" ;	// cnt３桁まで可能
					}
					cnt++;
				}
				wkbook.setSheetName( shtNo,tempName );
			}
		}
	}

	/**
	 * 内部 Workbook の 指定のSheet番号のｼｰﾄ名前を返します。
	 *
	 * ｼｰﾄが存在しない場合は、null を返します。
	 *
	 * この処理を行うと、内部の Sheet にも、ここで見つけた Sheet が設定されます。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 *
	 * @param	 shtNo		ｼｰﾄ番号
	 *
	 * @return	shtName	ｼｰﾄ名
	 */
	public String getSheetName( final int shtNo ) {
		final int shLen = wkbook.getNumberOfSheets();

		String shtName = null;
		if( shtNo < shLen ) {
			sheet = wkbook.getSheetAt( shtNo );		// 現在の sheet に設定する。
			shtName = sheet.getSheetName();
		}

		return shtName ;
	}

	/**
	 * 内部 Workbook の 指定のSheet名のｼｰﾄ番号を返します。
	 *
	 * ｼｰﾄが存在しない場合は、-1 を返します。
	 * この処理を行うと、内部の Sheet にも、ここで見つけた Sheet が設定されます。
	 * ｼｰﾄが存在しない場合、内部の Sheet ｵﾌﾞｼﾞｪｸﾄも null がｾｯﾄされますのでご注意ください。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 *
	 * @param	 shtName		ｼｰﾄ名
	 *
	 * @return	ｼｰﾄ番号(名前のｼｰﾄがなければ、-1)
	 */
	public int getSheetNo( final String shtName ) {
		sheet = wkbook.getSheet( shtName );					// ｼｰﾄ名がﾏｯﾁしなければ、null

		return wkbook.getSheetIndex( shtName ) ;			// ｼｰﾄ名がﾏｯﾁしなければ、-1
	}

	/**
	 * Excelの指定Sheetｵﾌﾞｼﾞｪｸﾄを削除します。
	 *
	 * 削除するｼｰﾄは、ｼｰﾄ番号でFrom-To形式で指定します。
	 * Fromも Toも、削除するｼｰﾄ番号を含みます。
	 * 例えば、0,3 と指定すると、0,1,2,3 の ４ｼｰﾄ分を削除します。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 *
	 * @param	 fromNo		削除する開始ｼｰﾄ番号(含む)
	 * @param	 toNo		削除する終了ｼｰﾄ番号(含む)
	 */
	public void removeSheet( final int fromNo,final int toNo ) {
		for( int shtNo=toNo; shtNo>=fromNo; shtNo-- ) {			// 逆順に処理します。
			wkbook.removeSheetAt( shtNo );
		}
	}

	/**
	 * 内部 Workbookの 現在Sheet の最初の行番号を返します。
	 *
	 * 行は、0 から始まります。
	 * この処理は、内部Sheetが作成されているか、null でない場合のみ実行できます。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 *
	 * @return	最初の行番号
	 */
	public int getFirstRowNum() {
		return sheet.getFirstRowNum();
	}

	/**
	 * 内部 Workbookの 現在Sheet の最後の行番号を返します。
	 *
	 * 最終行は、含みます。よって、行数は、getLastRowNum()＋１になります。
	 * この処理は、内部Sheetが作成されているか、null でない場合のみ実行できます。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 *
	 * @return	最後の行番号
	 */
	public int getLastRowNum() {
		return sheet.getLastRowNum();
	}

	/**
	 * Excelの指定行のRowｵﾌﾞｼﾞｪｸﾄを作成します。
	 *
	 * 指定行の Row ｵﾌﾞｼﾞｪｸﾄが存在しない場合は、新規作成します。
	 * この処理を実行すると、指定行の Rowｵﾌﾞｼﾞｪｸﾄが内部 Row に設定されます。
	 * この処理は、内部Sheetが作成されているか、null でない場合のみ実行できます。
	 *
	 * この処理を行うと、内部の Rowｵﾌﾞｼﾞｪｸﾄが設定されます。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 *
	 * @param	 rowNo		行の番号
	 */
	public void createRow( final int rowNo ) {
		rowObj = sheet.getRow( rowNo );
		if( rowObj == null ) { rowObj = sheet.createRow( rowNo ); }
	}

	/**
	 * Excelの指定行以降の余計なRowｵﾌﾞｼﾞｪｸﾄを削除します。
	 *
	 * 指定行の Row ｵﾌﾞｼﾞｪｸﾄから、getLastRowNum() までの行を、削除します。
	 * この処理は、内部Sheetが作成されているか、null でない場合のみ実行できます。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 *
	 * @param	 startRowNum		指定以降の余計な行を削除
	 */
	public void removeRow( final int startRowNum ) {
		final int stR = startRowNum;
		final int edR = sheet.getLastRowNum();

		for( int rowNo=edR; rowNo>=stR && rowNo>=0; rowNo-- ) {			// 逆順に処理します。
			final Row rowObj = sheet.getRow( rowNo );
			if( rowObj != null ) { sheet.removeRow( rowObj ); }
		}
	}

	/**
	 * Excelの処理中のRowｵﾌﾞｼﾞｪｸﾄの指定ｶﾗﾑ以降の余計なCellｵﾌﾞｼﾞｪｸﾄを削除します。
	 *
	 * 指定行の Row ｵﾌﾞｼﾞｪｸﾄから、getLastCellNum() までのｶﾗﾑを、削除します。
	 * この処理は、内部Rowが作成されているか、null でない場合のみ実行できます。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 *
	 * @param	 startCellNum		指定以降の余計なｶﾗﾑを削除
	 */
	public void removeCell( final int startCellNum ) {
		final int stC = startCellNum;
		final int edC = rowObj.getLastCellNum();

		for( int colNo=edC; colNo>=stC; colNo-- ) {			// 逆順に処理します。
			final Cell colObj = rowObj.getCell( colNo );
			if( colObj != null ) { rowObj.removeCell( colObj ); }
		}
	}

	/**
	 * row にあるｾﾙのｵﾌﾞｼﾞｪｸﾄ値を設定します。
	 *
	 * 行が存在しない場合、行を追加します。
	 * この処理を行うと、内部の Rowｵﾌﾞｼﾞｪｸﾄがなければ新規作成されます。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 *
	 * @param   vals  新しい配列値。
	 * @param   rowNo   値が変更される行(無視されます)
	 */
	public void setValues( final String[] vals,final int rowNo ) {
		if( rowObj == null ) { createRow( rowNo ); }

		if( vals != null ) {
			for( int colNo=0; colNo<vals.length; colNo++ ) {
				setCellValue( vals[colNo],colNo );
			}
		}
	}

	/**
	 * row にあるｾﾙのｵﾌﾞｼﾞｪｸﾄ値を設定します。
	 *
	 * 行が存在しない場合、行を追加します。
	 * 引数に、ｶﾗﾑがNUMBER型かどうかを指定することが出来ます。
	 * この処理を行うと、内部の Rowｵﾌﾞｼﾞｪｸﾄがなければ新規作成されます。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 *
	 * @param   vals  新しい配列値。
	 * @param   rowNo   値が変更される行(無視されます)
	 * @param	isNums	ｾﾙが、NUMBER型の場合は、true/それ以外は、false
	 */
	public void setValues( final String[] vals,final int rowNo,final boolean[] isNums ) {
		if( rowObj == null ) { createRow( rowNo ); }

		if( vals != null ) {
			for( int colNo=0; colNo<vals.length; colNo++ ) {
				setCellValue( vals[colNo],colNo,isNums[colNo] );
			}
		}
	}

	/**
	 * Excelの指定ｾﾙにﾃﾞｰﾀを設定します。
	 *
	 * ここで設定する行は、現在の内部 Row です。
	 * Row を切り替えたい場合は、#createRow( int ) を呼び出してください。
	 * このﾒｿｯﾄﾞでは、ﾃﾞｰﾀを文字列型として設定します。
	 * この処理は、内部Rowが作成されているか、null でない場合のみ実行できます。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 *
	 * @param	dataVal    String文字列
	 * @param	colNo		ｾﾙの番号(0,1,2････)
	 * @see		#setCellValue( String,int,boolean )
	 */
	public void setCellValue( final String dataVal , final int colNo ) {
		setCellValue( dataVal,colNo,false );
	}

	/**
	 * Excelの指定ｾﾙにﾃﾞｰﾀを設定します。
	 *
	 * ここで設定する行は、現在の内部 Row です。
	 * Row を切り替えたい場合は、#createRow( int ) を呼び出してください。
	 * このﾒｿｯﾄﾞでは、引数のﾃﾞｰﾀ型をNUMBER型の場合は、doubleに変換して、
	 * それ以外は文字列としてとして設定します。
	 * この処理は、内部Rowが作成されているか、null でない場合のみ実行できます。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 *
	 * @param	dataVal		String文字列
	 * @param	colNo		ｾﾙの番号(0,1,2････)
	 * @param	isNumber	ｾﾙが、NUMBER型の場合は、true/それ以外は、false
	 * @see		#createRow( int )
	 * @see		#setCellValue( String,int )
	 */
	public void setCellValue( final String dataVal , final int colNo , final boolean isNumber ) {
		Cell colObj = rowObj.getCell( colNo );
		if( colObj == null ) { colObj = rowObj.createCell( colNo ); }

		if( style != null ) { colObj.setCellStyle(style); }

		// CELL_TYPE_NUMERIC 以外は、String扱いします。
		if( isNumber ) {
			final Double dbl = parseDouble( dataVal );
			if( dbl != null ) {
				colObj.setCellValue( dbl.doubleValue() );
				return ;		// Double 変換できた場合は、即抜けます。
			}
		}

		final RichTextString richText = createHelper.createRichTextString( dataVal );
		colObj.setCellValue( richText );
	}

	/**
	 * Excelの指定ｾﾙにHyperlinkを設定します。
	 *
	 * ここで設定する行は、現在の内部 Row です。
	 * Row を切り替えたい場合は、#createRow( int ) を呼び出してください。
	 * このﾒｿｯﾄﾞで設定するHyperlinkは、Sheetに対する LINK_DOCUMENT です。
	 * 先に、ｾﾙに対する値をｾｯﾄしておいてください。
	 * Hyperlinkは、文字に対して、下線 と 青字 のｽﾀｲﾙ設定を行います。
	 *
	 * Link文字列(ｼｰﾄ名) が、null や ｾﾞﾛ文字列の場合は、処理を行いません。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 * @og.rev 6.5.0.0 (2016/09/30) poi-3.15 対応(Hyperlink.LINK_XXXX → HyperlinkType.XXXX)
	 *
	 * @param	linkVal		Link文字列(ｼｰﾄ名)
	 * @param	colNo		ｾﾙの番号(0,1,2････)
	 * @see		#setCellValue( String,int )
	 */
	public void setCellLink( final String linkVal , final int colNo ) {
		if( linkVal == null || linkVal.isEmpty() ) { return; }

		Cell colObj = rowObj.getCell( colNo );
		if( colObj == null ) { colObj = rowObj.createCell( colNo ); }

		if( hLinkStyle == null ) {
			hLinkStyle = wkbook.createCellStyle();
			if( style != null ) { hLinkStyle.cloneStyleFrom(style); }

			final Font font = wkbook.createFont();
			font.setColor( IndexedColors.BLUE.getIndex() );		// ﾘﾝｸは青文字
			font.setUnderline( Font.U_SINGLE );					// 下線付

			hLinkStyle.setFont( font );
		}
		colObj.setCellStyle(hLinkStyle);

	//	final Hyperlink hLink = createHelper.createHyperlink( Hyperlink.LINK_DOCUMENT );		// 6.5.0.0 (2016/09/30) poi-3.12
		final Hyperlink hLink = createHelper.createHyperlink( HyperlinkType.DOCUMENT );			// 6.5.0.0 (2016/09/30) poi-3.15
		hLink.setAddress( "'" + linkVal + "'!A1" );
		colObj.setHyperlink( hLink );
	}

	/**
	 * 現在のRow にあるｾﾙの属性値を配列で返します。
	 *
	 * Rowｵﾌﾞｼﾞｪｸﾄが存在しない場合は、長さ０の配列を返します。
	 * また、Rowｵﾌﾞｼﾞｪｸﾄの中の ｾﾙｵﾌﾞｼﾞｪｸﾄが存在しない場合は、
	 * null がｾｯﾄされます。
	 *
	 * この処理は、内部Sheetが作成されているか、null でない場合のみ実行できます。
	 * この処理を実行すると、指定行の Rowｵﾌﾞｼﾞｪｸﾄが内部 Row に設定されます。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 * @og.rev 6.3.9.0 (2015/11/06) ExcelModel#getValues(int) では、nullは返さない。
	 * @og.rev 6.3.9.1 (2015/11/27) ﾒｿｯﾄﾞの出口は、最後の１か所にすべきです(PMD)。
	 *
	 * @param	rowNo		行の番号
	 * @return	指定されたｾﾙの属性値。Rowがnullの場合は、長さ０の配列を返します。
	 * @og.rtnNotNull
	 */
	public String[] getValues( final int rowNo ) {
		rowObj = sheet.getRow( rowNo );

		final int len = rowObj == null ? 0 : rowObj.getLastCellNum();		// 含まないので、length と同じ意味になる。
		final String[] vals = new String[len];				// 6.3.9.1 (2015/11/27) ﾒｿｯﾄﾞの出口

		for( int colNo=0; colNo<len; colNo++ ) {
			final Cell colObj = rowObj.getCell( colNo );
			vals[colNo] = POIUtil.getValue( colObj );
		}

		return vals ;
	}

	/**
	 * 現在のrow にあるｾﾙの属性値を返します。
	 *
	 * ｾﾙｵﾌﾞｼﾞｪｸﾄが存在しない場合は、null を返します。
	 *
	 * この処理は、内部Sheetが作成されているか、null でない場合のみ実行できます。
	 * この処理を実行すると、指定行の Rowｵﾌﾞｼﾞｪｸﾄが内部 Row に設定されます。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 * @og.rev 6.3.9.1 (2015/11/27) ﾒｿｯﾄﾞの出口は、最後の１か所にすべきです(PMD)。
	 *
	 * @param   rowNo     値が参照される行
	 * @param   colNo     値が参照される列
	 *
	 * @return  指定されたｾﾙの値 T
	 */
	public String getValue( final int rowNo, final int colNo ) {
		rowObj = sheet.getRow( rowNo );

		return rowObj == null ? null : POIUtil.getValue( rowObj.getCell( colNo ) );
	}

	/**
	 * 指定のｼｰﾄの行・列の箇所に、ｲﾒｰｼﾞﾌｧｲﾙを挿入します。
	 *
	 * ここでは、ｾﾙ範囲ではなく、指定の行列の箇所に、ｱﾝｶｰを設定して、画像ﾌｧｲﾙを
	 * 挿入します。一応、ﾘｻｲｽﾞして、元の大きさ近くに戻しますが、縦横比が変わってしまいます。
	 * 正確に挿入する場合は、ｾﾙ範囲の指定と、ﾏｰｼﾞﾝを指定しなければなりませんが、
	 * 微調整が必要です。
	 *
	 * この処理で使用される Sheetｵﾌﾞｼﾞｪｸﾄは一時的に作成されます。(ｷｬｯｼｭされません)
	 * 一連処理のどのﾀｲﾐﾝｸﾞで実行しても、内部の状態には影響はありません。
	 *
	 * @og.rev 6.0.2.3 (2014/10/10) 新規作成
	 *
	 * @param   imgFile   挿入するｲﾒｰｼﾞﾌｧｲﾙ名
	 * @param   shtNo     ｼｰﾄ番号
	 * @param   rowNo     挿入する行
	 * @param   colNo     挿入する列
	 */
	public void addImageFile( final String imgFile, final int shtNo, final int rowNo, final int colNo ) {
		addImageFile( imgFile,shtNo,rowNo,colNo,rowNo,colNo,0,0,0,0 );
	}

	/**
	 * 指定のｼｰﾄの行・列の箇所に、ｲﾒｰｼﾞﾌｧｲﾙを挿入します。
	 *
	 * ここでは、ｾﾙ範囲ではなく、指定の行列の箇所に、ｱﾝｶｰを設定して、画像ﾌｧｲﾙを
	 * 挿入します。一応、ﾘｻｲｽﾞして、元の大きさ近くに戻しますが、縦横比が変わってしまいます。
	 * 正確に挿入する場合は、ｾﾙ範囲の指定と、ﾏｰｼﾞﾝを指定しなければなりませんが、
	 * 微調整が必要です。
	 *
	 * この処理で使用される Sheetｵﾌﾞｼﾞｪｸﾄは一時的に作成されます。(ｷｬｯｼｭされません)
	 * 一連処理のどのﾀｲﾐﾝｸﾞで実行しても、内部の状態には影響はありません。
	 *
	 * @og.rev 6.0.2.3 (2014/10/10) 新規作成
	 * @og.rev 6.4.6.0 (2016/05/27) poi-3.15 準備
	 * @og.rev 6.8.2.4 (2017/11/20) poi-3.17 で、警告: [rawtypes] raw型が見つかりました対応
	 * @og.rev 7.2.9.0 (2020/10/12) ClientAnchorのｵﾌｾｯﾄ指定は、Units.EMU_PER_PIXEL が単位
	 *
	 * @param   imgFile   挿入するｲﾒｰｼﾞﾌｧｲﾙ名
	 * @param   shtNo     ｼｰﾄ番号
	 * @param   row1      挿入する行(開始)
	 * @param   col1      挿入する列(開始)
	 * @param   row2      挿入する行(終了-含まず)
	 * @param   col2      挿入する列(終了-含まず)
	 * @param   dx1       開始ｾﾙのX軸座標のｵﾌｾｯﾄ(ﾋﾟｸｾﾙ)
	 * @param   dy1       開始ｾﾙのY軸座標のｵﾌｾｯﾄ(ﾋﾟｸｾﾙ)
	 * @param   dx2       終了ｾﾙのX軸座標のｵﾌｾｯﾄ(ﾋﾟｸｾﾙ)
	 * @param   dy2       終了ｾﾙのY軸座標のｵﾌｾｯﾄ(ﾋﾟｸｾﾙ)
	 */
	public void addImageFile( final String imgFile , final int shtNo ,
								final int row1 , final int col1 , final int row2 , final int col2 ,
								final int dx1  , final int dy1  , final int dx2  , final int dy2   ) {
		final String suffix   = ImageUtil.getSuffix( imgFile );
		final Integer picType = PICTURE_TYPE.get( suffix );

		// 実験した結果、bmp,gif,tif については、PICTURE_TYPE_PNG で、挿入できた。
		// 6.4.1.1 (2016/01/16) PMD refactoring. Avoid if (x != y) ..; else ..;
		final int pictureType = picType == null ? Workbook.PICTURE_TYPE_PNG : picType.intValue() ;

		final byte[] imgs = ImageUtil.byteImage( imgFile );

		final int pictureIdx = wkbook.addPicture( imgs, pictureType );

		final Sheet sheet = wkbook.getSheetAt( shtNo );
		// 6.8.2.4 (2017/11/20) poi-3.17 で、警告: [rawtypes] raw型が見つかりました対応
		final Drawing<?> patriarch = sheet.createDrawingPatriarch();		// 昔は一度しか実行できなかったようです。
	//	final Drawing patriarch = sheet.createDrawingPatriarch();			// 昔は一度しか実行できなかったようです。

//		final ClientAnchor anchor = patriarch.createAnchor( dx1,dy1,dx2,dy2,col1,row1,col2,row2 );
		final int px = Units.EMU_PER_PIXEL;									// 7.2.9.0 (2020/10/12)
		final ClientAnchor anchor = patriarch.createAnchor( px*dx1,px*dy1,px*dx2,px*dy2,col1,row1,col2,row2 );

		// ClientAnchor anchor = createHelper.createClientAnchor();	でも作成可能。

		// MOVE_AND_RESIZE, MOVE_DONT_RESIZE, DONT_MOVE_AND_RESIZE から、決め打ち。
	//	anchor.setAnchorType( ClientAnchor.MOVE_DONT_RESIZE );					// 6.4.6.0 (2016/05/27) poi-3.12
		anchor.setAnchorType( ClientAnchor.AnchorType.MOVE_DONT_RESIZE );		// 6.4.6.0 (2016/05/27) poi-3.15

		final Picture pic = patriarch.createPicture( anchor, pictureIdx );
		// ｾﾙの範囲指定がｾﾞﾛの場合、画像ｻｲｽﾞもｾﾞﾛになる為、ﾘｻｲｽﾞしておく。
		if( row1 == row2 || col1 == col2 ) { pic.resize(); }	// resize すると、anchor のﾏｰｼﾞﾝが無視されるようです。
	}

	/**
	 * 内部 Workbook ｵﾌﾞｼﾞｪｸﾄをﾌｧｲﾙに書き出します。
	 *
	 * Excelの形式は、ここで指定する出力ﾌｧｲﾙの拡張子ではなく、ｺﾝｽﾄﾗｸﾀで
	 * 指定したﾌｧｲﾙの拡張子で決まります。
	 * 異なる形式の拡張子を持つﾌｧｲﾙを指定した場合、強制的に、ｵｰﾌﾟﾝした
	 * Workbook の形式の拡張子を追加します。
	 *
	 * 拡張子は、Excel 2007以降の形式(.xlsx)か、Excel 2003以前の形式(.xls) が指定できます。
	 * 拡張子が未設定の場合は、ｵｰﾌﾟﾝした Workbook の形式に合わせた拡張子を付与します。
	 *
	 * isAutoCellSize=true の場合は、ここで全Sheetに対してCell幅の自動調整が行われます。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 * @og.rev 6.1.0.0 (2014/12/26) 入力ﾌｧｲﾙの拡張子判定の対応
	 * @og.rev 6.2.0.0 (2015/02/27) ﾌｧｲﾙ引数を、String → File に変更
	 * @og.rev 6.5.0.0 (2016/09/30) ｾﾙの計算式の再計算をさせる recalcSheetNames 属性の追加。
	 *
	 * @param	file	ｾｰﾌﾞするﾌｧｲﾙ
	 */
	public void saveFile( final File file ) {
		final File saveFile ;
		String fname = file.getName();
		if( fname.toLowerCase(Locale.JAPAN).endsWith( sufix ) ) {
			saveFile = file;
		}
		else {
			final int idx = fname.lastIndexOf( '.' );
			if( idx >= 0 ) { fname = fname.substring( 0,idx ); }
			saveFile = new File( file.getParent() , fname + sufix );
		}

		if( isAutoCellSize ) { POIUtil.autoCellSize( wkbook, maxColCount, dataStartRow ); }

		// 6.5.0.0 (2016/09/30) ｾﾙの計算式の再計算をさせる recalcSheetNames 属性の追加。
		if( recalcSheetNames != null && recalcSheetNames.length > 0 ) {
			for( final String shtName : recalcSheetNames ) {
				final Sheet sht = wkbook.getSheet( shtName );			// ｼｰﾄ名がﾏｯﾁしなければ、null
				if( sht != null ) { sht.setForceFormulaRecalculation(true); }
			}
		}

		// こちらの都合で、TitleSheet は、autoCellSize ではなく、Sheet#autoSizeColumn(int) を使用して、自動計算させる。
		if( addTitleSheet != null ) { makeAddTitleSheet(); }

		OutputStream fileOut = null ;
		try {
			fileOut = new BufferedOutputStream( new FileOutputStream( saveFile ) );		// 6.1.0.0 (2014/12/26)
			wkbook.write( fileOut );
			wkbook.close();
		}
		catch( final IOException ex ) {
			final String errMsg = "ﾌｧｲﾙへ書込み中にｴﾗｰが発生しました。" + CR
							+ "  File=" + saveFile + CR
							+ ex.getMessage() ;
			throw new OgRuntimeException( errMsg,ex );
		}
		finally {
			Closer.ioClose( fileOut );
		}
	}

	/**
	 * 内部 Workbook ｵﾌﾞｼﾞｪｸﾄのSheet一覧のSheetを、先頭に追加します。
	 *
	 * これは、Workbook に含まれる Sheet 一覧を作成する場合に、利用可能です。
	 *
	 * この処理は、内部のWorkbook、Sheetｵﾌﾞｼﾞｪｸﾄに依存して実行されます。
	 * また、単独ではなく、#saveFile( File ) 実行時に、addTitleSheet が
	 * 設定されている場合のみ、実行されます。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 *
	 * @see		#saveFile( File )
	 * @see		#setAddTitleSheet( String )
	 */
	private void makeAddTitleSheet() {
		sheet = wkbook.createSheet();
		final String shtNm = sheet.getSheetName();				// Sheet名の取得
		wkbook.setSheetOrder( shtNm,0 );					// そのSheetを先頭に移動
		setSheetName( 0,addTitleSheet );					// そのSheet名を変更 → これが、TitleSheet

		int rowNo = 0;
		createRow( rowNo++ );								// 先頭行(ｲﾝｽﾀﾝｽ共通のRowｵﾌﾞｼﾞｪｸﾄ)作成
		setCellValue( "No"	 , 0 );
		setCellValue( "Sheet", 1 );

		final int shCnt = wkbook.getNumberOfSheets();
		for( int shtNo=1; shtNo<shCnt; shtNo++,rowNo++ ) {
			final String nm = wkbook.getSheetName( shtNo );

			createRow( rowNo );									// 行の追加作成
			setCellValue( String.valueOf( rowNo ),0,true );		// 行番号として、数字型で登録
			setCellValue( nm , 1 );								// ｼｰﾄの値を書き込む
			setCellLink(  nm , 1 );								// ｼｰﾄへのﾘﾝｸを作成する。
		}

		sheet.autoSizeColumn( 0 );
		sheet.autoSizeColumn( 1 );
	}

//	/**
//	 * 指定の Workbook の全Sheetを対象に、実際の有効行と有効ｶﾗﾑを取得します。
//	 *
//	 * ※ 現在、唯一LibreOfficeでのみ、xslx 変換できますが、有効行とｶﾗﾑが
//	 *    ｼｭﾘﾝｸされず、無駄な行とｶﾗﾑが存在します。
//	 *    これは、xsl で出力されたﾌｧｲﾙから有効な値を取得して、xslxに適用させるための
//	 *    機能で、本来きちんとした有効範囲の xslx が生成されれば、不要な処理です。
//	 *
//	 * 配列は、[0]=行の最大値(Sheet#getLastRowNum())と、[1]は有効行の中の列の
//	 * 最大値(Row#getLastCellNum())を、ｼｰﾄごとにListに追加していきます。
//	 *
//	 * @og.rev 8.0.1.0 (2021/10/29) 全Sheetを対象に、実際の有効行と有効ｶﾗﾑを取得
//	 * @og.rev 8.0.3.0 (2021/12/17) 処理が中途半端だったので、廃止します。
//	 *
//	 * @return	ｼｰﾄごとの有効行の配列ﾘｽﾄ
//	 * @see		#activeWorkbook( List )
//	 */
//	public List<int[]> getLastRowCellNum() {
//		return POIUtil.getLastRowCellNum( wkbook );
//	}

	/**
	 * Workbook の全Sheetを対象に、空行を取り除き、全体をｼｭﾘﾝｸします。
	 *
	 * この処理は、#saveFile( File ) の直前に行うのがよいでしょう。
	 *
	 * ここでは、Row を逆順にｽｷｬﾝし、Cellが 存在しない間は、行を削除します。
	 * 途中の空行の削除ではなく、最終行からの連続した空行の削除です。
	 *
	 * isCellDel=true を指定すると、Cellの末尾削除を行います。
	 * 有効行の最後のCellから空ｾﾙを削除していきます。
	 * 表形式などの場合は、Cellのあるなしで、ﾚｲｱｳﾄが崩れる場合がありますので
	 * 処理が不要な場合は、isCellDel=false を指定してください。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 *
	 * @param	isCellDel	Cellの末尾削除を行うかどうか(true:行う/false:行わない)
	 */
	public void activeWorkbook( final boolean isCellDel ) {
		POIUtil.activeWorkbook( wkbook, isCellDel );
	}

	/**
	 * 指定の Workbook の全Sheetを対象に、実際の有効行と有効ｶﾗﾑを元に全体をｼｭﾘﾝｸします。
	 *
	 * ※ 現在、唯一LibreOfficeでのみ、xslx 変換できますが、有効行とｶﾗﾑが
	 *    ｼｭﾘﾝｸされず、無駄な行とｶﾗﾑが存在します。
	 *    これは、xsl で出力されたﾌｧｲﾙから有効な値を取得して、xslxに適用させるための
	 *    機能で、本来きちんとした有効範囲の xslx が生成されれば、不要な処理です。
	 *
	 * 引数のListｵﾌﾞｼﾞｪｸﾄに従って、無条件に処理を行います。
	 *
	 * @og.rev 8.0.1.0 (2021/10/29) 全Sheetを対象に、実際の有効行と有効ｶﾗﾑを取得
	 * @og.rev 8.0.3.0 (2021/12/17) ｼｰﾄ毎の行数Listに変更。
	 *
//	 * @param	rcList		ｼｰﾄごとの有効行の配列ﾘｽﾄ
	 * @param	rowCntList		ｼｰﾄごとの有効行の配列ﾘｽﾄ
//	 * @see		#getLastRowCellNum()
	 * @see		#activeWorkbook( boolean )
	 */
//	public void activeWorkbook( final List<int[]> rcList ) {
	public void activeWorkbook( final List<Integer> rowCntList ) {
		POIUtil.activeWorkbook( wkbook, rowCntList );
	}

	/**
	 * Workbook の全Sheetを対象に、ﾃｷｽﾄ変換処理を行います(XSLX限定)。
	 *
	 * この処理は、#saveFile( File ) の直前に行うのがよいでしょう。
	 * #activeWorkbook( boolean ) との順番は構いません。
	 *
	 * ・ｼｰﾄ名の一覧をﾋﾟｯｸｱｯﾌﾟします。
	 * ・ｾﾙ値を、ｾﾙ単位にﾋﾟｯｸｱｯﾌﾟします。
	 * ・ｵﾌﾞｼﾞｪｸﾄ文字列を、改行単位にﾋﾟｯｸｱｯﾌﾟし、結果を合成します。
	 *
	 * ここでは、内部的に、TextConverterｲﾝﾀｰﾌｪｰｽを作成して処理します。
	 *
	 * @og.rev 6.2.4.2 (2015/05/29) ﾃｷｽﾄ変換処理
	 * @og.rev 6.3.1.0 (2015/06/28) TextConverterに、引数(cmnt)を追加
	 * @og.rev 6.3.9.0 (2015/11/06) Java 8 ﾗﾑﾀﾞ式に変更
	 *
	 * @param	convMap	変換対象を管理するMapｵﾌﾞｼﾞｪｸﾄ
	 * @see		#textConverter( TextConverter )
	 */
	public void textConverter( final Map<String,String> convMap ) {
		textConverter(
			( val,cmnt ) -> convMap.get( val )
		);

	//	textConverter(
	//		new TextConverter<String,String>() {
	//			/**
	//			 * 入力文字列を、変換します。
	//			 *
	//			 * @param	val  入力文字列
	//			 * @param	cmnt ｺﾒﾝﾄ
	//			 * @return	変換文字列(変換されない場合は、null)
	//			 */
	//			@Override
	//			public String change( final String val , final String cmnt ) {
	//				return convMap.get( val );
	//			}
	//		}
	//	);
	}

	/**
	 * Workbook の全Sheetを対象に、ﾃｷｽﾄ変換処理を行います(XSLX限定)。
	 *
	 * この処理は、#saveFile( File ) の直前に行うのがよいでしょう。
	 * #activeWorkbook( boolean ) との順番は構いません。
	 *
	 * ・ｼｰﾄ名の一覧をﾋﾟｯｸｱｯﾌﾟします。
	 * ・ｾﾙ値を、ｾﾙ単位内の改行単位にﾋﾟｯｸｱｯﾌﾟし、結果を合成ます。
	 * ・ｵﾌﾞｼﾞｪｸﾄ文字列を、改行単位にﾋﾟｯｸｱｯﾌﾟし、結果を合成します。
	 *
	 * ここでは、ｼｰﾄ名、ｾﾙﾃｷｽﾄ、SimpleShapeｵﾌﾞｼﾞｪｸﾄのﾃｷｽﾄを
	 * input に、TextConverterｲﾝﾀｰﾌｪｰｽ の change ﾒｿｯﾄﾞを呼び出します。
	 * 戻り値が、null でないなら、元のﾃﾞｰﾀと置き換えます。
	 * 戻り値が、null の場合は、そのまま読み飛ばします。(なにもしません)
	 * EXCELへの書き戻しが発生しますので、万一、ﾌｧｲﾙ破損で、開けなくなる場合を
	 * 想定して、ﾊﾞｯｸｱｯﾌﾟﾌｧｲﾙは、各自で準備してください。
	 *
	 * @og.rev 6.2.4.2 (2015/05/29) ﾃｷｽﾄ変換処理
	 * @og.rev 6.2.5.0 (2015/06/05) xsl形式のｵﾌﾞｼﾞｪｸﾄ取得…はできなかった。
	 * @og.rev 6.3.1.0 (2015/06/28) TextConverterに、引数(cmnt)を追加
	 * @og.rev 6.3.9.0 (2015/11/06) ｾﾙに値をｾｯﾄするときに、ｾﾙﾀｲﾌﾟを考慮する。
	 * @og.rev 8.0.3.1 (2021/12/28) ﾃｷｽﾄ取得処理にgetShapeTextを使用する。
	 * @og.rev 8.1.2.3 (2022/05/20) ｵﾌﾞｼﾞｪｸﾄ文字列の変換で、drawing の null 対応
	 * @og.rev 8.4.0.0 (2023/01/30) ｵﾌﾞｼﾞｪｸﾄ文字列の変換ﾐｽ対応とﾒｿｯﾄﾞ化
	 *
	 * @param	conv	TextConverterｲﾝﾀｰﾌｪｰｽ
	 * @see		#textConverter( Map )
	 */
//	@SuppressWarnings(value={"deprecation"})	// poi-3.15
	public void textConverter( final TextConverter<String,String> conv ) {
	//	if( ".xlsx".equals( sufix ) || ".xlsm".equals( sufix ) ) {
			final int shCnt = wkbook.getNumberOfSheets();
			for( int shtNo=0; shtNo<shCnt; shtNo++ ) {
				final Sheet sht = wkbook.getSheetAt( shtNo );
				// ｼｰﾄ名の変換
	//			final String shtNm = conv.change( sht.getSheetName() , "Sheet" + shtNo + ":" );
				final String shtNm = conv.change( sht.getSheetName() , "Sheet" + shtNo + ":Name" );	// 8.5.0.0 (2023/04/21)
				if( shtNm != null ) {
					setSheetName( shtNo,shtNm );			// 同一ｼｰﾄ対策済みのﾒｿｯﾄﾞを呼び出す。
				}

				// ｾﾙ値の変換
				final int stR = Math.max( sht.getFirstRowNum(),0 );		// stR が、ﾏｲﾅｽのｹｰｽがある。
				final int edR = sht.getLastRowNum();

				for( int rowNo=stR; rowNo<=edR; rowNo++ ) {
					final Row rowObj = sht.getRow( rowNo );
					if( rowObj != null ) {
						final int stC = Math.max( rowObj.getFirstCellNum(),0 );		// stC が、ﾏｲﾅｽのｹｰｽがある。
						final int edC = rowObj.getLastCellNum();
						for( int colNo=stC; colNo<=edC; colNo++ ) {
							final Cell colObj = rowObj.getCell( colNo );
//							if( colObj != null && colObj.getCellType() != Cell.CELL_TYPE_BLANK ) {		// 6.5.0.0 (2016/09/30) poi-3.12
//							if( colObj != null && colObj.getCellTypeEnum() != CellType.BLANK ) {		// 6.5.0.0 (2016/09/30) poi-3.15
							if( colObj != null && colObj.getCellType() != CellType.BLANK ) {			// 8.0.0.0 (2021/07/31) poi-4.1.2.jar → poi-5.0.0.jar
								final String cmnt= "Sheet" + shtNo + ":" + POIUtil.getCelKigo( rowNo,colNo );
								final String val = crConv( conv, POIUtil.getValue( colObj ),cmnt );		// 改行対応
								if( val != null ) {
									POIUtil.setValue( colObj,val );		// 6.3.9.0 (2015/11/06)
						//			colObj.setCellValue( val );

								}
							}
						}
					}
				}

				// 8.0.3.1 (2021/12/28) ｵﾌﾞｼﾞｪｸﾄ文字列の変換
				final Drawing<?> drawing = sht.getDrawingPatriarch();
				// 8.1.2.3 (2022/05/20) ｵﾌﾞｼﾞｪｸﾄ文字列の変換で、drawing の null 対応
				if( drawing instanceof XSSFDrawing ) {
	//			if( drawing != null ) {
					for (final XSSFShape shape : ((XSSFDrawing)drawing).getShapes() ) {
						// 8.4.0.0 (2023/01/30) ｵﾌﾞｼﾞｪｸﾄ文字列の変換ﾐｽ対応とﾒｿｯﾄﾞ化
						shapeConvert( shtNo,shape,conv );

				//		final String shpNm = shape.getShapeName();
				//		final String cmnt = "Sheet" + shtNo + ":" + shpNm ;
				//		conv.change( getShapeText( (XSSFShape)shape,null ),cmnt );
					}
				}
				// 8.1.2.3 (2022/05/20) ｵﾌﾞｼﾞｪｸﾄ文字列の変換で、drawing の null 対応
				else {
					// 8.1.2.3 (2022/05/20) ｵﾌﾞｼﾞｪｸﾄ文字列の変換 復活
					if( sht instanceof POIXMLDocumentPart ) {
						for( final POIXMLDocumentPart pxdp : ((POIXMLDocumentPart)sht).getRelations() ) {
							if( pxdp instanceof XSSFDrawing ) {
								for( final XSSFShape shape : ((XSSFDrawing)pxdp).getShapes() ) {
									// 8.4.0.0 (2023/01/30) ｵﾌﾞｼﾞｪｸﾄ文字列の変換ﾐｽ対応とﾒｿｯﾄﾞ化
									shapeConvert( shtNo,shape,conv );

					//				// 8.0.3.1 (2021/12/28) ﾃｷｽﾄ取得処理にgetShapeTextを使用する。
					//				final String shpNm = shape.getShapeName();
					//				final String cmnt = "Sheet" + shtNo + ":" + shpNm ;
					//				conv.change( getShapeText( shape ),cmnt );

									// 8.1.2.3 (2022/05/20) ｵﾌﾞｼﾞｪｸﾄ文字列の変換 復活
									// 8.4.0.0 (2023/01/30) shapeConvert のﾒｿｯﾄﾞ化
							//		final XSSFAnchor anc = shape.getAnchor();
							//		final String ancSt = "XY(" + anc.getDx1() + "-" + anc.getDy1() + ")" ;
							//		int cnt = 0;
							//		if( shape instanceof XSSFSimpleShape ) {
							//			for( final XSSFTextParagraph para : ((XSSFSimpleShape)shape).getTextParagraphs() ) {
							//				for( final XSSFTextRun text : para.getTextRuns() ) {
							//					final String cmnt= "Sheet" + shtNo + ":" + ancSt + ":(" + cnt++ + ")" ;
							//					final String val = crConv( conv,text.getText() , cmnt );
							//					if( val != null ) {
							//						text.setText( val );
							//					}
							//				}
							//			}
							//		}
								}
							}
						}
					}
				}

			// 6.2.5.0 (2015/06/05) xsl形式のｵﾌﾞｼﾞｪｸﾄ取得…はできなかった。
			//	else if( sht instanceof HSSFSheet ) {
			//		HSSFPatriarch patri = ((HSSFSheet)sht).getDrawingPatriarch();
			//		for( final HSSFShape shape : patri.getChildren() ) {
			//			if( shape instanceof HSSFTextbox ) {
			//				HSSFRichTextString rts = ((HSSFSimpleShape)shape).getString();
			//				if( rts != null ) {
			//					final String val = crConv( conv,rts.getString() );
			//					if( val != null ) {
			//						HSSFRichTextString rts2 = new HSSFRichTextString( val );
			//						((HSSFSimpleShape)shape).setString( rts2 );
			//					}
			//				}
			//			}
			//		}
			//	}
			}
	//	}
	}

	/**
	 * XSSFShape を引数に、XSSFSimpleShape の場合に、変換処理を行います。
	 *
	 * @og.rev 8.4.0.0 (2023/01/30) ｵﾌﾞｼﾞｪｸﾄ文字列の変換ﾐｽ対応とﾒｿｯﾄﾞ化
	 * @og.rev 8.5.0.0 (2023/04/21) XSSFAnchor ｵﾌﾞｼﾞｪｸﾄの NullPointerException 対策
	 *
	 * @param	shtNo	ｼｰﾄ番号
	 * @param	shape	XSSFShapeｲﾝﾀｰﾌｪｰｽ
	 * @param	conv	TextConverterｲﾝﾀｰﾌｪｰｽ
	 * @see		#textConverter( Map )
	 */
	private void shapeConvert( final int shtNo , final XSSFShape shape , final TextConverter<String,String> conv ) {
		final XSSFAnchor anc = shape.getAnchor();

		// 8.5.0.0 (2023/04/21) XSSFAnchor ｵﾌﾞｼﾞｪｸﾄの NullPointerException 対策
		final String ancSt ;
		if( anc instanceof XSSFClientAnchor ) {
			final XSSFClientAnchor anc2 = (XSSFClientAnchor)anc;
	//		if( ClientAnchor.AnchorType.DONT_MOVE_AND_RESIZE.equals(anc2.getAnchorType()) ) {
	//			ancSt = "XY1(" + anc2.getPosition()  + ")" ;

				final String kigo = POIUtil.getCelKigo( anc2.getRow1(),anc2.getCol1() )
							+ "-" + POIUtil.getCelKigo( anc2.getRow2(),anc2.getCol2() ) ;
				ancSt = "(" + kigo  + ")" ;
	//		}
	//		else {
	//			ancSt = "XY2(" + anc2.getDx1() + "-" + anc2.getDy1() + ")" ;
	//		}
		}
		else {
			ancSt = "(" + anc.getDx1() + "-" + anc.getDy1() + ")" ;
		}

//		final String ancSt = "XY(" + anc.getDx1() + "-" + anc.getDy1() + ")" ;
	//	int cnt = 0;
		if( shape instanceof XSSFSimpleShape ) {
			for( final XSSFTextParagraph para : ((XSSFSimpleShape)shape).getTextParagraphs() ) {
				for( final XSSFTextRun text : para.getTextRuns() ) {
	//				final String cmnt= "Sheet" + shtNo + ":" + ancSt + ":(" + cnt++ + ")" ;
					final String cmnt= "Sheet" + shtNo + ":Shape " + ancSt ;
					final String val = crConv( conv,text.getText() , cmnt );
					if( val != null ) {
						text.setText( val );
					}
				}
			}
		}
	}

	/**
	 * Workbook の全SheetのShapeを対象に、ﾃｷｽﾄをｽｷｬﾝします(XSLX限定)。
	 *
	 * 引数のBiConsumerは、ﾗﾑﾀﾞ式として適用できます。
	 * ｼｰﾄ毎のShapeから、#getShapeText(XSSFShape,BiConsumer) を呼び出して、
	 * ﾃｷｽﾄが存在した場合に、その時のXSSFSimpleShapeとﾃｷｽﾄを引数のﾗﾑﾀﾞ式に渡します。
	 *
	 * @og.rev 8.1.0.1 (2022/01/07) ﾃｷｽﾄﾍﾞｰｽのﾘﾝｸ作成
	 *
	 * @param	bicon	BiConsumer関数型ｲﾝﾀｰﾌｪｰｽ
	 */
	public void xssfShapeScan( final BiConsumer<XSSFSimpleShape,String> bicon ) {
		final int shCnt = wkbook.getNumberOfSheets();
		for( int shtNo=0; shtNo<shCnt; shtNo++ ) {
			final Sheet sht = wkbook.getSheetAt( shtNo );

			// 8.0.3.1 (2021/12/28) ｵﾌﾞｼﾞｪｸﾄ文字列の変換
			final Drawing<?> drawing = sht.getDrawingPatriarch();
			for (final Shape shape : drawing) {
				if( shape instanceof XSSFShape ) {
					getShapeText( (XSSFShape)shape, bicon );
				}
			}
		}
	}

	/**
	 * 現在のｼｰﾄを選択済み(true)か、非選択済み(false)に設定します。
	 *
	 * 通常は、ｼｰﾄは、先頭ｼｰﾄ以外は、非選択状態になっています。
	 * ｼｰﾄを選択済みにすることで、印刷範囲を指定する事ができます。
	 *
	 * @og.rev 6.3.9.0 (2015/11/06) 新規追加
	 *
	 * @param	isSelect	true:ｼｰﾄ選択/false:非選択
	 */
	public void sheetSelected( final boolean isSelect ) {
		sheet.setSelected( isSelect );
	}

	/**
	 * Workbook の雛形ｼｰﾄのTextConverter した、新しいSheetを作成します。
	 *
	 * 正確には、
	 *   1.雛形ｼｰﾄを、ｺﾋﾟｰして、新しいSheet(shtName)を、作成します。
	 *   2.雛形ｼｰﾄが指定されていない場合は、一番最後のｼｰﾄをｺﾋﾟｰします。
	 *   3.そのｼｰﾄに対して、TextConverter を行い、文字列変換します。
	 *
	 * @og.rev 6.3.9.0 (2015/11/06) 新規追加
	 * @og.rev 6.5.0.0 (2016/09/30) poi-3.15 対応(Cell.CELL_TYPE_XXXX → CellType.XXXX)
	 * @og.rev 8.0.3.1 (2021/12/28) ﾃｷｽﾄ取得処理にgetShapeTextを使用する。
	 *
	 * @param	conv	TextConverterｲﾝﾀｰﾌｪｰｽ
	 * @param	shtName		ｼｰﾄ名
	 * @see		#textConverter( Map )
	 */
//	@SuppressWarnings(value={"deprecation"})	// poi-3.15
	public void sheetCopy( final TextConverter<String,String> conv , final String shtName ) {
		int shtNo = wkbook.getNumberOfSheets() - 1;
		if( refSheetIdx >= 0 && refSheetIdx < shtNo ) {		// 雛形ｼｰﾄをｺﾋﾟｰする。
			sheet = wkbook.cloneSheet( refSheetIdx );
		}
		else {
			sheet = wkbook.cloneSheet( shtNo );				// 最後のｼｰﾄをｺﾋﾟｰします。
		}
		shtNo++ ;											// ｼｰﾄ番号を増やしておく。

		// ｼｰﾄ名の変換
		setSheetName( shtNo,shtName );						// 同一ｼｰﾄ対策済みのﾒｿｯﾄﾞを呼び出す。

		// ｾﾙ値の変換
		final int stR = Math.max( sheet.getFirstRowNum(),0 );		// stR が、ﾏｲﾅｽのｹｰｽがある。
		final int edR = sheet.getLastRowNum();

		for( int rowNo=stR; rowNo<=edR; rowNo++ ) {
			final Row rowObj = sheet.getRow( rowNo );
			if( rowObj != null ) {
				final int stC = Math.max( rowObj.getFirstCellNum(),0 );		// stC が、ﾏｲﾅｽのｹｰｽがある。
				final int edC = rowObj.getLastCellNum();
				for( int colNo=stC; colNo<=edC; colNo++ ) {
					final Cell colObj = rowObj.getCell( colNo );
//					if( colObj != null && colObj.getCellType() != Cell.CELL_TYPE_BLANK ) {		// 6.5.0.0 (2016/09/30) poi-3.12
//					if( colObj != null && colObj.getCellTypeEnum() != CellType.BLANK ) {		// 6.5.0.0 (2016/09/30) poi-3.15
					if( colObj != null && colObj.getCellType() != CellType.BLANK ) {			// 8.0.0.0 (2021/07/31) poi-4.1.2.jar → poi-5.0.0.jar
						final String cmnt= "Sheet" + shtNo + ":" + POIUtil.getCelKigo( rowNo,colNo );	//
	//					final String val = crConv( conv, POIUtil.getValue( colObj ),null );		// 改行対応
						final String val = crConv( conv, POIUtil.getValue( colObj ),cmnt );		// 8.5.0.0 (2023/04/21)
						if( val != null ) {
							POIUtil.setValue( colObj,val );
				//			colObj.setCellValue( val );
						}
					}
				}
			}
		}

		// 8.0.3.1 (2021/12/28) ｵﾌﾞｼﾞｪｸﾄ文字列の変換
		final Drawing<?> drawing = sheet.getDrawingPatriarch();
		for (final Shape shape : drawing) {
			if( shape instanceof XSSFShape ) {
				shapeConvert( shtNo,(XSSFShape)shape,conv );	// 8.5.0.0 (2023/04/21) shapeConvert のﾒｿｯﾄﾞ化

	//			final String shpNm = shape.getShapeName();
	//			final String cmnt = "Sheet" + shtNo + ":" + shpNm ;
	//			conv.change( getShapeText( (XSSFShape)shape,null ),cmnt );
			}
		}

	//	// ｵﾌﾞｼﾞｪｸﾄ文字列の変換
	//	if( sheet instanceof POIXMLDocumentPart ) {
	//		for( final POIXMLDocumentPart pxdp : ((POIXMLDocumentPart)sheet).getRelations() ) {
	//			if( pxdp instanceof XSSFDrawing ) {
	//				for( final XSSFShape shape : ((XSSFDrawing)pxdp).getShapes() ) {
	//					// 8.0.3.1 (2021/12/28) ﾃｷｽﾄ取得処理にgetShapeTextを使用する。
	//					conv.change( getShapeText( shape ),null );

				//		final org.apache.poi.xssf.usermodel.XSSFAnchor anc = shape.getAnchor();
				//		if( shape instanceof XSSFSimpleShape ) {
				//			for( final XSSFTextParagraph para : ((XSSFSimpleShape)shape).getTextParagraphs() ) {
				//				for( final XSSFTextRun text : para.getTextRuns() ) {
				//					final String val = crConv( conv,text.getText() , null );
				//					if( val != null ) {
				//						text.setText( val );
				//					}
				//				}
				//			}
				//		}
	//				}
	//			}
	//		}
	//	}
	}

	/**
	 * XSSFShapeから、ﾃｷｽﾄ文字列を取得します(XSLX限定)。
	 *
	 * XSSFSimpleShapeの場合は、そのまま#getText()を実行します。
	 * XSSFShapeGroupの場合は、XSSFSimpleShapeに順次分解して文字列を連結していきます。
	 * 途中に存在する改行ｺｰﾄﾞは削除しておきます。
	 *
	 * @og.rev 8.0.3.1 (2021/12/28) ﾃｷｽﾄ取得処理にgetShapeTextを使用する。
	 * @og.rev 8.1.0.1 (2022/01/07) BiConsumerの引数付きﾒｿｯﾄﾞに修正
	 *
	 * @param	shape	XSSFShapeｵﾌﾞｼﾞｪｸﾄ
	 * @param	bicon	BiConsumer関数ｵﾌﾞｼﾞｪｸﾄ
	 * @return	ｼｪｰﾌﾟから取得した文字列
	 */
//	private String getShapeText( final XSSFShape shape ) {
	private String getShapeText( final XSSFShape shape, final BiConsumer<XSSFSimpleShape,String> bicon ) {
		if( shape instanceof XSSFSimpleShape ) {
			final String txt = ((XSSFSimpleShape)shape).getText().replace("\n","");
			if( bicon != null && !txt.isEmpty() ) {
				bicon.accept( (XSSFSimpleShape)shape,txt );
			}
			return txt;
		}
		else if( shape instanceof XSSFShapeGroup ) {
	//		final StringBuilder buf = new StringBuilder();
			for( final XSSFShape shape2 : (XSSFShapeGroup)shape ) {
				final String txt = getShapeText( shape2,bicon );
				if( !txt.isEmpty() ) {		// 見つかった時点で終了
					return txt;
				}
	//			buf.append( getShapeText( shape2 ) );
			}
	//		return buf.toString();
		}
		return "";
	}

	/**
	 * Workbook の全Sheetを対象に、ﾃｷｽﾄ変換処理を行います(XSLX限定)。
	 *
	 * この処理は、#saveFile( File ) の直前に行うのがよいでしょう。
	 * #activeWorkbook( boolean ) との順番は構いません。
	 *
	 * ・ｼｰﾄ名の一覧をﾋﾟｯｸｱｯﾌﾟします。
	 * ・ｾﾙ値を、ｾﾙ単位内の改行単位にﾋﾟｯｸｱｯﾌﾟし、結果を合成ます。
	 * ・ｵﾌﾞｼﾞｪｸﾄ文字列を、改行単位にﾋﾟｯｸｱｯﾌﾟし、結果を合成します。
	 *
	 * ここでは、ｼｰﾄ名、ｾﾙﾃｷｽﾄ、SimpleShapeｵﾌﾞｼﾞｪｸﾄのﾃｷｽﾄを
	 * input に、TextConverterｲﾝﾀｰﾌｪｰｽ の change ﾒｿｯﾄﾞを呼び出します。
	 * 戻り値が、null でないなら、元のﾃﾞｰﾀと置き換えます。
	 * 戻り値が、null の場合は、そのまま読み飛ばします。(なにもしません)
	 * EXCELへの書き戻しが発生しますので、万一、ﾌｧｲﾙ破損で、開けなくなる場合を
	 * 想定して、ﾊﾞｯｸｱｯﾌﾟﾌｧｲﾙは、各自で準備してください。
	 *
	 * @og.rev 6.2.4.2 (2015/05/29) ﾃｷｽﾄ変換処理
	 * @og.rev 6.3.1.0 (2015/06/28) TextConverterに、引数(cmnt)を追加
	 *
	 * @param	conv	TextConverterｲﾝﾀｰﾌｪｰｽ
	 * @param	val		改行処理を行う元の値
	 * @param	cmnt	ｺﾒﾝﾄ
	 * @return	改行処理の結果の値(対象が無ければ、null)
	 * @see		#textConverter( Map )
	 */
	private String crConv( final TextConverter<String,String> conv , final String val , final String cmnt ) {
		String rtn = null;
		if( val != null ) {
			if( val.contains( "\n" ) ) {						// 改行がある場合(EXCEL のｾﾙ内改行ｺｰﾄﾞは、LF(0A)=\n のみ。
				final String[] val2 = val.split( "\\n" );		// 改行で分割する。
				boolean flag = false;
				for( int i=0; i<val2.length; i++ ) {
					final String val3 = conv.change( val2[i],cmnt );	// 6.3.1.0 (2015/06/28)
					if( val3 != null ) { val2[i] = val3; flag = true; }
				}
				if( flag ) {
					final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
					buf.append( val2[0] );
					for( int i=1; i<val2.length; i++ ) {
						buf.append( '\n' ).append( val2[i] );		// LF(\n)で、ｾﾊﾟﾚｰﾄしているので、LF のみ追加する。
					}
					rtn = buf.toString();
				}
			}
			else {												// 改行がない場合
				rtn = conv.change( val,cmnt );					// 6.3.1.0 (2015/06/28)
			}
		}
		return rtn;
	}

	/**
	 * ｼｰﾄ一覧を、内部の Workbook から取得します。
	 *
	 * 取得元が、Workbook なので、xls , xlsx どちらの形式でも取り出せます。
	 *
	 * EXCEL上のｼｰﾄ名を、配列で返します。
	 *
	 * @og.rev 6.2.6.0 (2015/06/19) 新規作成
	 *
	 * @return	ｼｰﾄ名の配列
	 * @see		POIUtil#getSheetNames( Workbook )
	 */
	public String[] getSheetNames() {
		return POIUtil.getSheetNames( wkbook );
	}

	/**
	 * 名前定義一覧を内部の Workbook から取得します。
	 *
	 * EXCEL上に定義された名前を、配列で返します。
	 * ここでは、名前とFormulaをﾀﾌﾞで連結した文字列を配列で返します。
	 * Name ｵﾌﾞｼﾞｪｸﾄを削除すると、EXCELが開かなくなったりするので、
	 * 取りあえず一覧を作成して、手動で削除してください。
	 * なお、名前定義には、非表示というのがありますので、ご注意ください。
	 *
	 * @og.rev 6.2.6.0 (2015/06/19) 新規作成
	 *
	 * @return	名前定義(名前+TAB+Formula)の配列
	 * @see		POIUtil#getNames( Workbook )
	 * @og.rtnNotNull
	 */
	public String[] getNames() {
		return POIUtil.getNames( wkbook );
	}

	/**
	 * 書式のｽﾀｲﾙ一覧を内部の Workbook から取得します。
	 *
	 * EXCEL上に定義された書式のｽﾀｲﾙを、配列で返します。
	 * 書式のｽﾀｲﾙの名称は、CellStyle にﾒｿｯﾄﾞが定義されていません。
	 * 実ｸﾗｽである HSSFCellStyle にｷｬｽﾄして使用する
	 * 必要があります。(XSSFCellStyle にも名称を取得するﾒｿｯﾄﾞがありません。)
	 *
	 * ※ EXCEL2010 ﾎｰﾑﾀﾌﾞ→ｾﾙのｽﾀｲﾙ は、一つづつしか削除できません。
	 *    ﾏｸﾛは、開発ﾀﾌﾞ→Visual Basic で、挿入→標準ﾓｼﾞｭｰﾙ を開き
	 *    ﾃｷｽﾄを張り付けてください。
	 *    実行は、開発ﾀﾌﾞ→ﾏｸﾛ で、ﾏｸﾛ名を選択して、実行します。
	 *    最後は、削除してください。
	 *
	 * @og.rev 6.2.6.0 (2015/06/19) 新規作成
	 *
	 * @return	書式のｽﾀｲﾙ一覧
	 * @see		POIUtil#getStyleNames( Workbook )
	 * @og.rtnNotNull
	 */
	public String[] getStyleNames() {
		return POIUtil.getStyleNames( wkbook );
	}

	/**
	 * 文字列を Double ｵﾌﾞｼﾞｪｸﾄに変換します。
	 *
	 * これは、引数の ｶﾝﾏ(,) を削除した文字列から、Double ｵﾌﾞｼﾞｪｸﾄを生成します。
	 * 処理中に、文字列が解析可能な double を含まない場合(NumberFormatException)
	 * また、引数が、null,ｾﾞﾛ文字列,'_', ｴﾗｰ の時には、null を返します。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 * @og.rev 6.3.9.0 (2015/11/06) もう少し判りやすくする。(処理速度は落ちてます。)
	 *
	 * @param	value	Doubleに変換する元の文字列
	 *
	 * @return	変換後のDoubleｵﾌﾞｼﾞｪｸﾄ(ｴﾗｰ発生時や変換不可の場合は、null)
	 */
	private Double parseDouble( final String value ) {
		Double rtn = null ;

		try {
			if( value == null || value.isEmpty() || value.equals( "_" ) ) {
				rtn = null;
			}
			else if( value.indexOf( ',' ) < 0 ) {
				rtn = Double.valueOf( value );		// 6.0.2.4 (2014/10/17) ﾒｿｯﾄﾞが非効率だった。
			}
			else {
				// 6.3.9.0 (2015/11/06) もう少し判りやすくする。(処理速度は落ちてます。)
				rtn = Double.valueOf( value.replaceAll( ",","" ) );
			}
		}
		catch( final NumberFormatException ex ) {		// 文字列が解析可能な数値を含まない場合
			final String errMsg = "Double変換できませんでした。" + CR
								+ ex.getMessage() + CR
								+ "  value=" + value;
			System.err.println( errMsg );
			rtn = null;
		}

		return rtn ;
	}

	/**
	 * ｱﾌﾟﾘｹｰｼｮﾝのｻﾝﾌﾟﾙです。
	 *
	 * Usage: java org.opengion.fukurou.model.ExcelModel 入力ﾌｧｲﾙ名 [出力ﾌｧｲﾙ名] ･･･
	 *  通常は標準出力に行単位に、ｾﾙをﾀﾌﾞ区切り出力します。
	 *  出力ﾌｧｲﾙ名 を指定すると、EXCEL ﾌｧｲﾙとしてｾｰﾌﾞし直します。
	 *  その場合は、以下のﾊﾟﾗﾒｰﾀも使用できます。
	 *   -CS      CellStyleを 設定します。
	 *   -AS      useAutoCellSizeを 設定します。
	 *   -FN=***  FontNameを 設定します。
	 *   -FP=**   FontPointを 設定します。
	 *   -IMG     画像ﾌｧｲﾙを挿入します。(-IMG 画像ﾌｧｲﾙ名 ｼｰﾄ番号 行 列)をｽﾍﾟｰｽ区切りで続けます。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 *
	 * @param	args	ｺﾏﾝﾄﾞ引数配列
	 */
	public static void main( final String[] args ) {
		if( args.length == 0 ) {
			final String usage = "Usage: java org.opengion.fukurou.model.ExcelModel 入力ﾌｧｲﾙ名 [出力ﾌｧｲﾙ名] ･･･\n" +
						"\t-CS      CellStyleを 設定します。        \n" +
						"\t-TC      TextConverterを実行します。     \n" +
						"\t-AS      useAutoCellSizeを 設定します。  \n" +
						"\t-FN=***  FontNameを 設定します。         \n" +
						"\t-FP=**   FontPointを 設定します。        \n" +
						"\t-IMG     画像ﾌｧｲﾙを挿入します。      \n" +
						"\t     (-IMG ﾌｧｲﾙ名 ｼｰﾄ番号 行 列)  \n" ;
			System.err.println( usage );
			return ;
		}

		final ExcelModel excel = new ExcelModel( new File( args[0] ) , true );

		excel.activeWorkbook( true );			// 余計な行を削除します。

		if( args.length > 1 ) {
			final File outFile = new File( args[1] );			// 6.2.0.0 (2015/02/27)
			boolean isCS = false;
			boolean isAS = false;
			boolean isTC = false;				// 6.2.4.2 (2015/05/29) ﾃｷｽﾄ変換処理
			String  fn   = null;
			short   fp   = -1;

			for( int i=2; i<args.length; i++ ) {
				final String prm = args[i];

				if( "-CS".equalsIgnoreCase( prm ) ) { isCS = true; }	// 6.4.1.1 (2016/01/16) PMD refactoring. Position literals first in String comparisons for EqualsIgnoreCase.
				if( "-AS".equalsIgnoreCase( prm ) ) { isAS = true; }	// 6.4.1.1 (2016/01/16) PMD refactoring. Position literals first in String comparisons for EqualsIgnoreCase.
				if( "-TC".equalsIgnoreCase( prm ) ) { isTC = true; }	// 6.4.1.1 (2016/01/16) PMD refactoring. Position literals first in String comparisons for EqualsIgnoreCase.
				if( prm.startsWith( "-FN" ) ) { fn   = prm.substring( 3 ); }
				if( prm.startsWith( "-FP" ) ) { fp   = Short.parseShort( prm.substring( 3 ) ); }
				if( "-IMG".equalsIgnoreCase( prm ) ) {					// 6.4.1.1 (2016/01/16) PMD refactoring. Position literals first in String comparisons for EqualsIgnoreCase.
					final String img = args[++i];
					final int  shtNo = Integer.parseInt( args[++i] );
					final int  rowNo = Integer.parseInt( args[++i] );
					final int  colNo = Integer.parseInt( args[++i] );

					excel.addImageFile( img,shtNo,rowNo,colNo );
				}
			}

			if( isCS ) { excel.setCellStyle(); }
			excel.useAutoCellSize( isAS );
			excel.setFont( fn,fp );

			// 6.2.4.2 (2015/05/29) ﾃｷｽﾄ変換処理
			if( isTC ) {
				// 6.3.9.0 (2015/11/06) Java 8 ﾗﾑﾀﾞ式に変更
				// 処理が複数行に別れるのは判りにくいので良くない。
				excel.textConverter(
					( val,cmnt ) -> {
						System.out.println( val );			// すべてのﾃｷｽﾄを読み取る。
						return null;						// 変換せず。
					}
				);
			}

			excel.saveFile( outFile );
		}
		else {
			final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );

			final int shLen = excel.getNumberOfSheets();
			for( int shtNo=0; shtNo<shLen; shtNo++ ) {
				final String shtName = excel.getSheetName( shtNo );

				final int stRow = excel.getFirstRowNum();
				final int edRow = excel.getLastRowNum();
				for( int rowNo=stRow; rowNo<=edRow; rowNo++ ) {
					buf.setLength(0);		// Clearの事
					buf.append( shtName ).append( '\t' ).append( rowNo );
					final String[] vals = excel.getValues( rowNo );
					if( vals != null ) {
						for( int colNo=0; colNo<vals.length; colNo++ ) {
							final String val = vals[colNo] == null ? "" : vals[colNo];
							buf.append( '\t' ).append( val );
						}
					}
					System.out.println( buf );
				}
				System.out.println();
			}
		}
	}
}
