/* PdfExporter.java

	Purpose:
		
	Description:
		
	History:
		August 23, 5:53:16 PM     2010, Created by Ashish Dasnurkar
		January 3, 5:09:00 PM     2014, Modified for ZSS 3.5 by Kuro Chung

Copyright (C) 2013 Potix Corporation. All Rights Reserved.
 */
package org.zkoss.zss.model.impl.pdf;

import java.awt.Color;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import org.jfree.chart.encoders.EncoderUtil;
import org.jfree.chart.encoders.ImageFormat;
import org.zkoss.lang.Library;
import org.zkoss.poi.ss.usermodel.ZssContext;
import org.zkoss.zss.model.CellRegion;
import org.zkoss.zss.model.SBook;
import org.zkoss.zss.model.SCell;
import org.zkoss.zss.model.SCellStyle;
import org.zkoss.zss.model.SCellStyle.Alignment;
import org.zkoss.zss.model.SCellStyle.VerticalAlignment;
import org.zkoss.zss.model.SChart;
import org.zkoss.zss.model.SColor;
import org.zkoss.zss.model.SColumn;
import org.zkoss.zss.model.SFont;
import org.zkoss.zss.model.SFont.Boldweight;
import org.zkoss.zss.model.SFooter;
import org.zkoss.zss.model.SHeader;
import org.zkoss.zss.model.SPicture;
import org.zkoss.zss.model.SPrintSetup;
import org.zkoss.zss.model.SRow;
import org.zkoss.zss.model.SSheet;
import org.zkoss.zss.model.SheetRegion;
import org.zkoss.zss.model.ViewAnchor;
import org.zkoss.zss.model.SCell.CellType;
import org.zkoss.zss.model.SFont.Underline;
import org.zkoss.zss.model.SRichText.Segment;
import org.zkoss.zss.model.sys.EngineFactory;
import org.zkoss.zss.model.sys.dependency.Ref;
import org.zkoss.zss.model.sys.format.FormatContext;
import org.zkoss.zss.model.sys.format.FormatEngine;
import org.zkoss.zss.model.sys.format.FormatResult;
import org.zkoss.zss.model.sys.formula.FormulaExpression;
import org.zkoss.zss.model.sys.formula.FormulaParseContext;
import org.zkoss.zss.model.util.RichTextHelper;
import org.zkoss.zss.range.SExporter;
import org.zkoss.zss.range.impl.imexp.*;
import org.zkoss.zss.model.impl.AbstractRowAdv;
import org.zkoss.zssex.util.ChartDrawer;

import com.lowagie.text.BadElementException;
import com.lowagie.text.Chunk;
import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Element;
import com.lowagie.text.ExceptionConverter;
import com.lowagie.text.Font;
import com.lowagie.text.FontFactory;
import com.lowagie.text.Image;
import com.lowagie.text.Phrase;
import com.lowagie.text.Rectangle;
import com.lowagie.text.Utilities;
import com.lowagie.text.html.WebColors;
import com.lowagie.text.pdf.BaseFont;
import com.lowagie.text.pdf.FontSelector;
import com.lowagie.text.pdf.PdfContentByte;
import com.lowagie.text.pdf.PdfImportedPage;
import com.lowagie.text.pdf.PdfPCell;
import com.lowagie.text.pdf.PdfPTable;
import com.lowagie.text.pdf.PdfPageEventHelper;
import com.lowagie.text.pdf.PdfReader;
import com.lowagie.text.pdf.PdfTemplate;
import com.lowagie.text.pdf.PdfWriter;

/**
 * Implementation of {@link Exporter} interface that converts Excel data
 * model {@link org.zkoss.zss.model.Book} into a PDF format written to a 
 * {@link OutputStream}
 * 
 * @author ashish, henri, kuro
 * 
 */
public class PdfExporter implements SExporter, Serializable {
	private static final long serialVersionUID = 671162544116213200L;

	// cell padding
	protected static final float PAD_TOP = 0f;
	protected static final float PAD_BOTTOM = 1.40f;
	protected static final float PAD_LEFT = 1.5f;
	protected static final float PAD_RIGHT = 1.5f;
	
	// outline line width
	protected static final float OUTLINE_WIDTH = 0.8f;
	// gridline line width
	protected static final float GRIDLINE_WIDTH = 0.4f;
	
	//ZSS-776
	// double border line width
	// 20140926, henrichen:  double border is implemented as 2 thin parallel 
	//    lines; total width == DOUBLE_LINE_WIDTH * 2 + DOUBLE_LINE_SPACE
	// see StyleConversionUtil#getBorderStyle(BorderStyle)
	protected static final float DOUBLE_LINE_SPACE = 1.4f;
	protected static final float DOUBLE_LINE_WIDTH = 0.6f;

	//ZSS-789
	protected static final float UNDERLINE_WIDTH = 0.6f; // line width of underline
	protected static final float UNDERLINE_OFFSET = 0.6f; //from font baseline down
	protected static final float ACCOUNT_UNDERLINE_OFFSET = 1.0f; //from font bottom-line down

	protected SBook _wb;
	protected PdfWriter _writer;

	protected int[] _colBreaks; // column index is the 1st column of the page; page break is to the left of the column
	protected int[] _rowBreaks; // row index is the 1st row of the page; page break is to the top of the row

	//data boundary
	protected int _firstCol;
	protected int _endCol;
	protected int _firstRow;
	protected int _endRow;

	//print boundary
	protected int _printCol1;
	protected int _printCol2;
	protected int _printRow1;
	protected int _printRow2;
	
	// footer header info
	protected int _pageCount;
	protected int _pageNumber;
	protected String _sheetName;
	protected String _bookName;
	
	protected Font _docStandardFont;

	protected SPrintSetup _printSetup;
	
	protected Rectangle _paperSize; //paper size
	protected double _totalHeaderFooterWidth;
	protected double _leftMargin;
	protected double _rightMargin;
	protected double _topMargin;
	protected double _bottomMargin;
	protected double _headerMargin; //y offset to print header from top of the paper(top margin is irrelevant)
	protected double _footerMargin; //y offset to print footer from bottom of the paper(bottom margin is irrelevant)
	
	protected double _totalPrintWidth; //geometric width that can print content(paper size - left margin - right margin)
	protected double _totalPrintHeight; //geometric height htat can print content(paper size - top margin - bottom margin) 
	
	protected double[] _repeatColLeft; //x offset from left to right in PDF points for repeat column titles
	protected double[] _repeatRowTop; //y offset from top to bottom in PDF points for repeat row titles
	protected double[] _colLeft; //x offset from left to right in PDF points
	protected double[] _rowTop; //y offset from top to bottom in PDF points
	
	protected int[] _repeatColLeftPx; //x offset from left to right in pixel for repeat column titles
	protected int[] _repeatRowTopPx; //y offset from top to bottom in pixel for repeat row titles
	protected int[] _colLeftPx; //x offset from left to right in pixel
	protected int[] _rowTopPx; //y offset from top to bottom in pixel
	
	protected int _currentSheetIndex = 0;
	
	protected List<SPicture> _pictures;
	
	//ZSS-549
	//Page Setup control parameters
	protected boolean _firstPage; //whether first page of this current sheet
	protected SSheet _currentSheet; //what current printing sheet is
	protected int _titleCol1 = -1; //first column of repeat column titles; -1 means no repeat column titles 
	protected int _titleCol2 = -1; //last column of repeat column titles
	protected int _titleRow1 = -1; //first row of repeat row titles; -1 means no repeat row titles
	protected int _titleRow2 = -1; //last row of repeat row titles
	protected boolean _printGridlines; //whethe want to print gridlines
	protected boolean _printHeadings; //whether user want to print headings
	protected boolean _hcenter; //whether user want to print horizontally centered page  
	protected boolean _vcenter; //whether user want to print vertically centered page
	
	//ZSS-772
	//keep the overflow long text region so we can print across page boundary
	protected TreeSet<CellRegion> _extendRegions = new TreeSet<CellRegion>(new Comparator<CellRegion>() {
		@Override
		public int compare(CellRegion arg0, CellRegion arg1) {
			return arg0.getLastColumn() - arg1.getLastColumn();
		}
	});

	//ZSS-775
	protected Rectangle _customPaperSize;
	protected int _scale; // scale factor
	
	// row/column indexes for this page
	protected int _pageRow1, _pageRow2, _pageCol1, _pageCol2;
	
	protected Map<SChart, BufferedImage> imageCacher = new HashMap<SChart, BufferedImage>();
	
	protected static final String IGNORE_COLUMN_BREAK_PROPERTY_KEY = "org.zkoss.zss.pdf.ignoreColumnBreak";
	protected static final String IGNORE_ROW_BREAK_PROPERTY_KEY = "org.zkoss.zss.pdf.ignoreRowBreak";

	public PdfExporter() {
		FontLoader.getInstance(); // register fonts from OS; needed.
	}

	@Override
	public void export(SBook book, File file) throws IOException {
		OutputStream os = null;
		try{
			os = new FileOutputStream(file);
			export(book,os);
		}finally {
			if(os!=null){
				try{
					os.close();
				} catch (ExceptionConverter ex) { //ZSS-1028
					throwIOException(ex);
				}
			}
		}
	}

	/**
	 * Exports ZK Spreadsheet {@link Book} into another format written to a 
	 * {@link OutputStream}. Note that it exports entire workbook. 
	 * @param workbook
	 * @param outputStream
	 */
	public void export(final SBook workbook, OutputStream outputStream) throws IOException{

		Document doc = new Document();
		try {
			this._wb = workbook;
			_bookName = workbook.getBookName();
			SSheet sheet0 = _wb.getSheet(0); 
			setupExporter(sheet0, outputStream, doc);
			doc.open();
			_currentSheetIndex = 0;
			exportSheet(sheet0, 0, doc);

			int numSheets = workbook.getNumOfSheet();
			for (int i = 1; i < numSheets; i++) {
				final SSheet sheet = workbook.getSheet(i);
				initDocumentForSheet(sheet, doc);
				_currentSheetIndex = i;
				exportSheet(sheet, i, doc);
			}
		} catch (DocumentException e) {
			throw new IOException(e);
		} finally {
			try  { //ZSS-1028
				doc.close();
			} catch (ExceptionConverter ex) {
				throwIOException(ex);
			}
		}
	}

	/**
	 * Exports ZK Spreadsheet {@link SSheet} into another format written to a 
	 * @param worksheet sheet instance that contains selected area
	 * @param outputStream outoutStream to which exported contents to be written
	 */
	public void export(SSheet worksheet, OutputStream outputStream) throws IOException{
		Document doc = new Document(); 
		try {
			this._wb = worksheet.getBook();
			_bookName = worksheet.getBook().getBookName();
			setupExporter(worksheet, outputStream, doc);
			doc.open();
			
			_currentSheetIndex = _wb.getSheetIndex(worksheet);
			exportSheet(worksheet, _currentSheetIndex, doc);
		} catch (DocumentException e) {
			throw new IOException(e);
		} finally {
			try { //ZSS-1028
				doc.close();
			} catch (ExceptionConverter ex) {
				throwIOException(ex);
			}
		}
	}

	/**
	 * Exports selected area of ZK Spreadsheet active sheet represented by 
	 * {@link org.zkoss.zss.model.sys.XRange}
	 * @param worksheet sheet instance that contains selected area
	 * @param area area representing selected area to be exported
	 * @param outputStream outoutStream to which exported contents to be written
	 */
	public void export(SheetRegion sheetRegion, OutputStream outputStream) throws IOException{
		Document doc = new Document(); 
		try {
			SSheet worksheet = sheetRegion.getSheet();
			CellRegion area = sheetRegion.getRegion();
			this._wb = worksheet.getBook();
			_bookName = worksheet.getBook().getBookName();
			setupExporter(worksheet, outputStream, doc);
			doc.open();
			
			_currentSheetIndex = _wb.getSheetIndex(worksheet);
			exportSheetSelection(worksheet, _currentSheetIndex, area, doc);
		} catch (DocumentException e) {
			throw new IOException(e);
		} finally {
			try { //ZSS-1028
				doc.close();
			} catch (ExceptionConverter ex) {
				throwIOException(ex);
			}
		}
	}

	//ZSS-1028
	protected void throwIOException(ExceptionConverter ex) throws IOException {
		throw (IOException) ex.getException();
	}
	
	/**
	 * @param outputStream
	 * @throws DocumentException
	 */
	protected Document setupExporter(SSheet sheet, OutputStream outputStream, Document document)
			throws DocumentException {
		_writer = PdfWriter.getInstance(document, outputStream);
		initDocument(sheet, document);

		PdfHeaderFooter phf = new PdfHeaderFooter();
		_writer.setPageEvent(phf);
		
		return document;
	}

	//ZSS-775
	protected void printScalePages(float scale, Document doc, byte[] pdfIn) throws IOException {
		PdfReader reader = new PdfReader(pdfIn);
	    PdfContentByte cb = _writer.getDirectContent();
		final float x = (float) _leftMargin;
		final float y = (float) _bottomMargin;
		_pageCount = reader.getNumberOfPages();
		try {
		    for(int i = 1; i <= reader.getNumberOfPages(); i++){
		        PdfImportedPage page = _writer.getImportedPage(reader, i);
		        cb.addTemplate(page, scale, 0f, 0f, scale, x, y);
		        doc.newPage();
		    }
		} catch (ExceptionConverter ex) { // handle IOException from #onEndPage
			throwIOException(ex); //ZSS-1028
		}
		//ZSS-798
	    //doc.close();
	}
	
	//ZSS-892
	protected int normalizeScale(int scale) {
		return scale < 10 ? 10 : scale > 400 ? 400 : scale;
	}

	//ZSS-775
	protected void printScaleSheet(SSheet sheet, Document doc, SPrintSetup ps) throws IOException {
		final int scale = normalizeScale(_scale); //ZSS-892
		final float newWidth = (float) (_totalPrintWidth * 100d / scale);
		final float newHeight = (float) (_totalPrintHeight * 100d / scale);
		final ByteArrayOutputStream os = new ByteArrayOutputStream();  
		PdfExporter scaleExporter = new PdfExporter();
		scaleExporter.setCustomPaperSize(newWidth, newHeight);
		SPrintSetup scalePs = scaleExporter.getPrintSetup();
		
		//Give proper printSetup
		scalePs.setLeftMargin(0d);
		scalePs.setTopMargin(0d);
		scalePs.setRightMargin(0d);
		scalePs.setBottomMargin(0d);
		scalePs.setHeaderMargin(0d);
		scalePs.setFooterMargin(0d);
		
		scalePs.setDifferentFirstPage(false);
		scalePs.setDifferentOddEvenPage(false);
		scalePs.setHeader(null);
		scalePs.setFooter(null);
		
		scalePs.setLandscape(false);
		scalePs.setScale(100);
		scalePs.setFitHeight(0);
		scalePs.setFitWidth(0);

		//ZSS-928
		scalePs.setFirstHeader(null);
		scalePs.setFirstFooter(null);
		scalePs.setEvenHeader(null);
		scalePs.setEvenFooter(null);
		scalePs.setScaleWithDoc(false);
		scalePs.setAlignWithMargins(false);
		
		scalePs.setHCenter(ps.isHCenter());
		scalePs.setVCenter(ps.isVCenter());
		scalePs.setPageStart(ps.getPageStart());
		scalePs.setPrintGridlines(ps.isPrintGridlines());
		scalePs.setPrintArea(ps.getPrintArea());
		final CellRegion rowRgn = ps.getRepeatingRowsTitle();
		if (rowRgn != null) {
			scalePs.setRepeatingRowsTitle(rowRgn.getRow(), rowRgn.getLastRow());
		}
		final CellRegion colRgn = ps.getRepeatingColumnsTitle();
		if (colRgn != null) {
			scalePs.setRepeatingColumnsTitle(colRgn.getColumn(), colRgn.getLastColumn());
		}
		scalePs.setPrintHeadings(ps.isPrintHeadings());
		scalePs.setCommentsMode(ps.getCommentsMode());
		scalePs.setErrorPrintMode(ps.getErrorPrintMode());
		scalePs.setLeftToRight(ps.isLeftToRight());
		
		scaleExporter.export(sheet, os);
		// scale each page from the scaleDoc and print onto this doc
		printScalePages((float) (scale/100d), doc, os.toByteArray());
	}

	//ZSS-695
	protected int prepareFitPageScale(SSheet sheet, int fitWidth, int fitHeight) {
		int scaleX = 100;
		int scaleY = 100;
		if (fitHeight > 0) {
			scaleY = prepareScaleY(sheet, fitHeight);
		}
		if (fitWidth > 0) {
			scaleX = prepareScaleX(sheet, fitWidth);
		}
		return Math.min(scaleX,  scaleY);
	}
	
	//ZSS-695
	//fitHeight is the number of pages the sheet should fit in Y axis
	protected int prepareScaleY(SSheet sheet, int fitHeight) {
		double total = 0d;
		final double defaultRowHeight = LayoutUnitConversionHelper.pxToPdfHeightPoints(sheet.getDefaultRowHeight());
		if (_printHeadings) { //total fitHeight pages
			total = defaultRowHeight * fitHeight; 
		}
		if (isUseRowTitles()) {
			if (_titleRow2 < _firstRow) { //total fitHeight pages
				total += _repeatRowTop[_titleRow2 + 1 - _titleRow1] * fitHeight;
			} else if (_titleRow1 < _firstRow) { //first page + the rest of (fitHeight - 1) pages
				total += _repeatRowTop[_firstRow - _titleRow1] + _repeatRowTop[_titleRow2 + 1 - _titleRow1] * (fitHeight - 1);
			}
		}
		total += _rowTop[_endRow - _firstRow];
		
		// average page height
		double avgHeight = total / fitHeight;
		double pageHeight = Math.max(_totalPrintHeight, avgHeight);
		
		// try to break pages in rows
		double headHeight = 0d;
		if(_printHeadings) {
			headHeight = defaultRowHeight;
		}
		double currentPageHeight = headHeight;
		
		// repeat title rows in each top-to-down page
		// A1. if 1st row in a page is to the top of the repeat row range; irrelevant so ignore it.
		// A2. if 1st row in a page is to the bottom of the repeat row range; must print first the repeat row range.
		// A3. if 1st row in a page is inside the repeat row range; print first the part of the repeat row range that are before the 1st row.
		double titleHeight = getTitleHeight();
		
		boolean firstRowInPage = true;
		int pageCount = 0;
		// if need to print repeat title
		if (isUseRowTitles()) {
			if (_titleRow2 < _firstRow) { // A2
				currentPageHeight += titleHeight;
			} else if (_titleRow1 < _firstRow) { // A3
				currentPageHeight += _repeatRowTop[_firstRow - _titleRow1];
			}
		}
		// 20141003, henrichen: we use the following algorithm to decide how to 
		//  break pages: 
		// 	1. When a row across page border, assign the row to the page(this 
		//     page or next page) that will need less extra page size.
		//  2. As long as the page size grows, use the larger page size to 
		//     layout next page
		//  2A. if row assigned to next page, the rest of total row height can
		//      calculate a a new average page height; if that average page 
		//      height is greater than current page height, use the new average 
		//      page height as the current page height.
		//	2B. if row assigned to this page, page size grows to accommodate the 
		//      whole row height.
		for(int i = _firstRow; i <= _endRow; i++) {
			double rowHeight = _rowTop[i + 1 - _firstRow] - _rowTop[i - _firstRow]; 
			currentPageHeight += rowHeight;
			
			boolean exceedEdge = currentPageHeight > pageHeight;
			if(exceedEdge) { // next page
				double partNextPage = currentPageHeight - pageHeight;
				double partThisPage = rowHeight - partNextPage;
				//Assign row to next page 
				//if this page is NOT the last page AND this row is not the first row of the page AND
				// 	1A. if next page is the last page and the page size can 
				//      accommodate the rest of rows; OR
				// 	1B. assign row to next page will use less extra space
				
				// this page is NOT the last page and this row is not the first row of the page
				final double rest = total - currentPageHeight + rowHeight;
				if (pageCount < fitHeight - 1 && !firstRowInPage
						&& ((pageCount == fitHeight - 2 && rest <= pageHeight) // 1A
								|| partNextPage > partThisPage)) { // 1B
					i = i - 1;
					total = rest;
					//2A
					avgHeight = total / (fitHeight - pageCount - 1);
					if (pageHeight < avgHeight) {
						pageHeight = avgHeight;
					}
				} else { //keep row in this page; make page size to bigger one
					pageHeight = currentPageHeight;
					total -= currentPageHeight;
				}
				
				++pageCount;
				
				currentPageHeight = headHeight;
				
				// if need to print repeat title
				if (isUseRowTitles()) {
					if (_titleRow2 < i) {
						currentPageHeight += titleHeight;
					} else if (_titleRow1 < i) {
						currentPageHeight += _repeatRowTop[i - _titleRow1];
					}
				}
				firstRowInPage = true;
			} else {
				firstRowInPage = false;
			}
		}
		if (!firstRowInPage) { // handle last page
			++pageCount;
		}
		return pageHeight > _totalPrintHeight ?
				(int) (_totalPrintHeight * 100 / pageHeight) : 100; 
	}
	
	//ZSS-695
	protected int prepareScaleX(SSheet sheet, int fitWidth) {
		double total = 0d;
		final double defaultColWidth = StyleConversionUtil.DEFAULT_ROW_HEADER_COLUMN_WIDTH;
		if (_printHeadings) { //total fitWidth pages
			total = defaultColWidth * fitWidth; 
		}
		if (isUseColTitles()) {
			if (_titleCol2 < _printCol1) { //total fitWidth pages
				total += _repeatColLeft[_titleCol2 + 1 - _titleCol1] * fitWidth;
			} else if (_titleCol1 < _printCol1) { //first page + the rest of (fitWidth - 1) pages
				total += _repeatColLeft[_printCol1 - _titleCol1] + _repeatColLeft[_titleCol2 + 1 - _titleCol1] * (fitWidth - 1);
			}
		}
		total += _colLeft[_printCol2 - _firstCol];
		
		// average page width
		double avgWidth = total / fitWidth;
		double pageWidth = Math.max(_totalPrintWidth, avgWidth);
		
		// try to break pages in columns
		double headWidth = 0d;
		if(_printHeadings) {
			headWidth = defaultColWidth;
		}
		double currentPageWidth = headWidth;
		double titleWidth = getTitleWidth();
		
		// repeat title columns in each left-to-right page
		// A1. if 1st column in a page is to the left of the repeat column range; irrelevant so ignore it.
		// A2. if 1st column in a page is to the right of the repeat column range; must print the repeat column range first.
		// A3. if 1st column in a page is inside the repeat column range; print the part of the repeat column range that are before the 1st column first.
		
		boolean firstColInPage = true;
		int pageCount = 0;
		// if need to print repeat title
		if (isUseColTitles()) {
			if (_titleCol2 < _printCol1) { // A2
				currentPageWidth += titleWidth;
			} else if (_titleCol1 < _printCol1) { // A3
				currentPageWidth += _repeatColLeft[_printCol1 - _titleCol1];
			}
		}
		// 20141003, henrichen: we use the following algorithm to decide how to 
		//  break pages: 
		// 	1. When a column across page border, assign the column to the page(this 
		//     page or next page) that will need less extra page size.
		//  2. As long as the page size grows, use the larger page size to 
		//     layout next page
		//  2A. if column assigned to next page, the rest of total column width can
		//      calculate a a new average page width; if that average page 
		//      width is greater than current page width, use the new average 
		//      page width as the current page width.
		//	2B. if column assigned to this page, page size grows to accommodate the 
		//      whole column width.
		for(int i = _printCol1; i <= _printCol2; i++) {
			double colWidth = _colLeft[i + 1 - _firstCol] - _colLeft[i - _firstCol];
			if (colWidth <= 0) continue; //ZSS-973
			currentPageWidth += colWidth;
			
			boolean exceedEdge = currentPageWidth > pageWidth;
			if(exceedEdge) { // next page
				double partNextPage = currentPageWidth - pageWidth;
				double partThisPage = colWidth - partNextPage;
				//Assign column to next page 
				//if this page is NOT the last page AND this column is not the first column of the page AND
				// 	1A. if next page is the last page and the page size can 
				//      accommodate the rest of columns; OR
				// 	1B. assign column to next page will use less extra space
				
				// this page is NOT the last page and this column is not the first column of the page
				final double rest = total - currentPageWidth + colWidth;
				if (pageCount < fitWidth - 1 && !firstColInPage
						&& ((pageCount == fitWidth - 2 && rest <= pageWidth) // 1A
								|| partNextPage > partThisPage)) { // 1B
					i = i - 1;
					total = rest;
					//2A
					avgWidth = total / (fitWidth - pageCount - 1);
					if (pageWidth < avgWidth) {
						pageWidth = avgWidth;
					}
				} else { //keep column in this page; make page size to bigger one
					pageWidth = currentPageWidth;
					total -= currentPageWidth;
				}
				
				++pageCount;
				
				currentPageWidth = headWidth;
				
				// if need to print repeat title
				if (isUseColTitles()) {
					if (_titleCol2 < i) {
						currentPageWidth += titleWidth;
					} else if (_titleCol1 < i) {
						currentPageWidth += _repeatColLeft[i - _titleCol1];
					}
				}
				firstColInPage = true;
			} else {
				firstColInPage = false;
			}
		}
		if (!firstColInPage) { // handle last page
			++pageCount;
		}
		return pageWidth > _totalPrintWidth ?
				(int) (_totalPrintWidth * 100 / pageWidth) : 100; 
	}
	
	/**
	 * converts POI {@link SSheet} to Pdf page(s)
	 * 
	 * @param sheet
	 * @throws DocumentException
	 */
	protected void exportSheetSelection(final SSheet sheet, final int sheetIndex, CellRegion area, Document doc)
			throws DocumentException, IOException {
		
		// initialize
		SPrintSetup ps = getCurrentPrintSetup(sheet);
		_scale = ps.getScale();
		
		int fitHeight = ps.getFitHeight();
		int fitWidth = ps.getFitWidth();
		boolean useFitPages = fitHeight > 0 || fitWidth > 0;
		
		//ZSS-775
		if (_scale != 100) {
			printScaleSheet(sheet, doc, ps);
			return;
		}
		
		CellRegion crgn = ps.getRepeatingColumnsTitle();
		if (crgn != null) {
			_titleCol1 = crgn.getColumn();
			_titleCol2 = crgn.getLastColumn();
		} else {
			_titleCol1 = -1;
			_titleCol2 = -1;
		}
		CellRegion rrgn = ps.getRepeatingRowsTitle();
		if (rrgn != null) {
			_titleRow1 = rrgn.getRow();
			_titleRow2 = rrgn.getLastRow();
		} else {
			_titleRow1 = -1;
			_titleRow2 = -1;
		}
		_printGridlines = ps.isPrintGridlines();
		_printHeadings = ps.isPrintHeadings();
		_hcenter = ps.isHCenter();
		_vcenter = ps.isVCenter();

		_currentSheet = sheet;

		//decide real print area (in firstColumn, firstRow, lastColumn, lastRow)
		initPrintArea(sheet, area);
		
		//if !empty page
		if (_endCol >= 0 && _endRow >= 0) {
			//calculate absolute position in points
			initRowColumnPositions(sheet);
			
			//ZSS-695
			if (useFitPages) {
				_scale = prepareFitPageScale(sheet, fitWidth, fitHeight);
				if (_scale != 100) {
					printScaleSheet(sheet, doc, ps);
					return;
				}
			}
			
			//determine column and row page breaks
			final boolean ignoreRowManualBreaks = 
					"true".equalsIgnoreCase(Library.getProperty(IGNORE_ROW_BREAK_PROPERTY_KEY));
			final boolean ignoreColManualBreaks = 
					"true".equalsIgnoreCase(Library.getProperty(IGNORE_COLUMN_BREAK_PROPERTY_KEY));
			prepareRowBreaks(sheet, ignoreRowManualBreaks );
			prepareColBreaks(sheet, ignoreColManualBreaks );
			
			//get pictures that show in this sheet
	        getPictureData(sheet);
			
	        //start printing the sheet
	        printSheet(sheet, doc);
		}
	}

	protected void initRowColumnPositions(SSheet sheet) {
		double defaultRowHeight = LayoutUnitConversionHelper.pxToPdfHeightPoints(sheet.getDefaultRowHeight());
		int defaultRowHeightPx = sheet.getDefaultRowHeight();

		//ZSS-770
		// Boundary for repeat column title; will need extra column position when render a cell
		if (isUseColTitles()) {
			// find extendedTitleCol2
			int extendedTitleCol2 = _titleCol2;
			Iterator<SRow> rowIterX = sheet.getRowIterator();
			while (rowIterX.hasNext()) {
				SRow row = rowIterX.next();
				int rowIdx = row.getIndex();
				CellRegion mergedRegion = getMergedRegionIfAny(sheet, rowIdx, _titleCol2);
				if (mergedRegion != null) {
					 int col = mergedRegion.getLastColumn();
					 if (col > extendedTitleCol2) {
						 extendedTitleCol2 = col;
					 }
				} else { //ZSS-772: could be a long text that extends the end title column 
					SCell zssCell = sheet.getCell(rowIdx, _titleCol2);
					final int col = getExtendedEndColumn(sheet, zssCell, _titleCol2);
					if (col > extendedTitleCol2) {
						extendedTitleCol2 = col;
					}
				}
			}

			// columns of repeat column titles
			double titleWidth = 0d;
			_repeatColLeft = new double[extendedTitleCol2 - _titleCol1 + 2];
			
			//ZSS-974
			int titleWidthPx = 0; 
			_repeatColLeftPx = new int[extendedTitleCol2 - _titleCol1 + 2];
			
			for (int j = _titleCol1; j <= extendedTitleCol2; ++j) {
				_repeatColLeft[j - _titleCol1] = titleWidth;
				_repeatColLeftPx[j - _titleCol1] = titleWidthPx; //ZSS-974
				final SColumn columnj = sheet.getColumn(j); //ZSS-973
				titleWidth += LayoutUnitConversionHelper.pxToPdfWidthPoints(columnj.isHidden() ? 0 : columnj.getWidth()); //ZSS-973
				titleWidthPx += columnj.isHidden() ? 0 : columnj.getWidth(); //ZSS-974
			}
			_repeatColLeft[extendedTitleCol2 + 1 - _titleCol1] = titleWidth;
			_repeatColLeftPx[extendedTitleCol2 + 1 - _titleCol1] = titleWidthPx; //ZSS-974
		}
		
		// rows of repeat row titles
		if (isUseRowTitles()) {
			double titleHeight = 0d;
			_repeatRowTop = new double[_titleRow2 - _titleRow1 + 2];
			
			//ZSS-974
			int titleHeightPx = 0;
			_repeatRowTopPx = new int[_titleRow2 - _titleRow1 + 2];
			
			for (int j = _titleRow1; j <= _titleRow2; ++j) {
				_repeatRowTop[j - _titleRow1] = titleHeight;
				_repeatRowTopPx[j - _titleRow1] = titleHeightPx; //ZSS-974
				SRow currentRow = sheet.getRow(j);
				if (currentRow != null) {
					titleHeight += LayoutUnitConversionHelper.pxToPdfHeightPoints(currentRow.isHidden() ? 0 : currentRow.getHeight()); //ZSS-973
					titleHeightPx += currentRow.isHidden() ? 0 : currentRow.getHeight(); //ZSS-974
				} else {
					titleHeight += defaultRowHeight;
					titleHeightPx += defaultRowHeightPx; //ZSS-974
				}
			}
			_repeatRowTop[_titleRow2 + 1 - _titleRow1] = titleHeight;
			_repeatRowTopPx[_titleRow2 + 1 - _titleRow1] = titleHeightPx; //ZSS-974
		}

		// columns of data area
		double titleWidth = 0f;
		_colLeft = new double[_endCol - _firstCol + 2];
		
		//ZSS-974
		int titleWidthPx = 0;
		_colLeftPx = new int[_endCol - _firstCol + 2];

		for (int j = _firstCol; j <= _endCol; ++j) {
			_colLeft[j - _firstCol] = titleWidth;
			_colLeftPx[j - _firstCol] = titleWidthPx; //ZSS-974
			final SColumn columnj = sheet.getColumn(j); //ZSS-973
			titleWidth += LayoutUnitConversionHelper.pxToPdfWidthPoints(columnj.isHidden() ? 0 : columnj.getWidth()); //ZSS-973
			titleWidthPx += columnj.isHidden() ? 0 : columnj.getWidth(); //ZSS-974
		}
		_colLeft[_endCol + 1 - _firstCol] = titleWidth;
		_colLeftPx[_endCol + 1 - _firstCol] = titleWidthPx;
		
		// rows of print area
		double titleHeight = 0f;
		_rowTop = new double[_endRow - _firstRow + 2];
		
		//ZSS-974
		int titleHeightPx = 0;
		_rowTopPx = new int[_endRow - _firstRow + 2];

		for (int j = _firstRow; j <= _endRow; ++j) {
			_rowTop[j - _firstRow] = titleHeight;
			_rowTopPx[j - _firstRow] = titleHeightPx; //ZSS-974
			SRow currentRow = sheet.getRow(j);
			if (currentRow != null) {
				titleHeight += LayoutUnitConversionHelper.pxToPdfHeightPoints(currentRow.isHidden() ? 0 : currentRow.getHeight()); //ZSS-973
				titleHeightPx += currentRow.isHidden() ? 0 : currentRow.getHeight(); //ZSS-974
			} else {
				titleHeight += defaultRowHeight;
				titleHeightPx += defaultRowHeightPx; //ZSS-974
			}
		}
		_rowTop[_endRow + 1 - _firstRow] = titleHeight;
		_rowTopPx[_endRow + 1 - _firstRow] = titleHeightPx; //ZSS-974
	}
	
	/**
	 * converts POI {@link SSheet} to Pdf page(s)
	 * 
	 * @param sheet
	 * @throws DocumentException
	 */
	protected void exportSheet(final SSheet sheet, final int sheetIndex, Document doc)
			throws DocumentException, IOException {
		
		_currentSheet = sheet;
		_sheetName = sheet.getSheetName();
		SPrintSetup ps = getCurrentPrintSetup(sheet);
		CellRegion[] regions = getPrintAreas(ps, sheet); //ZSS-195
		if (regions == null) {
			exportSheetSelection(sheet, sheetIndex, null, doc);
		} else {
			for (CellRegion rgn : regions) {
				exportSheetSelection(sheet, sheetIndex, rgn, doc); //ZSS-195
			}
		}
	}

	protected CellRegion[] getPrintAreas(SPrintSetup ps, SSheet sheet) {
		if (ps.getPrintArea() == null) {
			return null;
		}
		
		FormulaExpression fexpr = 
			EngineFactory.getInstance().createFormulaEngine()
				.parse(ps.getPrintArea(), new FormulaParseContext(sheet, null));
		
		Ref[] refs = fexpr.getAreaRefs();
		CellRegion[] regions = new CellRegion[refs.length];
		for (int j = 0; j < refs.length; ++j) {
			final Ref ref = refs[j];
			regions[j++] = new CellRegion(ref.getRow(), ref.getColumn(), ref.getLastRow(), ref.getLastColumn());
		}
		return regions;
	}
	
	/**
	 * retrieve all zkpictures for given sheet
	 * @param sheet
	 */
	protected void getPictureData(SSheet sheet) {
		_pictures = sheet.getPictures();
	}

	/**
	 * Sets column/row breaks if not defined already
	 * @param sheet
	 */
	protected void prepareRowBreaks(final SSheet sheet, boolean ignoreManualBreaks) {
		if(ignoreManualBreaks) {
			_rowBreaks = new int[0];
		} else {
			_rowBreaks = sheet.getViewInfo().getRowBreaks();
		}
		if(_rowBreaks == null) {
			_rowBreaks = new int[0];
		}
		setupRowBreaks(sheet);  //ZSS-529
	}
	
	protected void prepareColBreaks(final SSheet sheet, boolean ignoreManualBreaks) {
		if(ignoreManualBreaks) {
			_colBreaks = new int[0];
		} else {
			_colBreaks = sheet.getViewInfo().getColumnBreaks();
		}
		if(_colBreaks == null) {
			_colBreaks = new int[0];
		}
		setupColumnBreaks(sheet); //ZSS-529
	}

	/**
	 * sets auto row breaks based on row heights and available total page height
	 * @param sheet
	 */
	protected void setupRowBreaks(SSheet sheet) {
		double headHeight = 0d;
		double defaultRowHeight = LayoutUnitConversionHelper.pxToPdfHeightPoints(sheet.getDefaultRowHeight());
		if(_printHeadings) {
			headHeight = defaultRowHeight;
		}
		double currentPageHeight = headHeight;
		
		// repeat title rows in each top-to-down page
		// A1. if 1st row in a page is to the top of the repeat row range; irrelevant so ignore it.
		// A2. if 1st row in a page is to the bottom of the repeat row range; must print first the repeat row range.
		// A3. if 1st row in a page is inside the repeat row range; print first the part of the repeat row range that are before the 1st row.
		double titleHeight = getTitleHeight();
		
		boolean firstRowInPage = true;
		ArrayList<Integer> autoRowBreaks = new ArrayList<Integer>();
		// if need to print repeat title
		if (isUseRowTitles()) {
			if (_titleRow2 < _printRow1) { // A2
				currentPageHeight += titleHeight;
			} else if (_titleRow1 < _printRow1) { // A3
				currentPageHeight += _repeatRowTop[_printRow1 - _titleRow1];
			}
		}
		for(int i = _printRow1; i <= _printRow2; i++) {
			double rowHeight = _rowTop[i + 1 - _firstRow] - _rowTop[i - _firstRow]; 
			currentPageHeight += rowHeight;
			
			//ZSS-529
			boolean manualBreak = isRowPageBreak(i); 
			boolean exceedEdge = currentPageHeight > _totalPrintHeight; 
			if(exceedEdge || manualBreak) { // next page
				// move row index back as first row in next page
				if (exceedEdge && !firstRowInPage) { //ZSS-529
					i = i - 1;
				}
				
				autoRowBreaks.add(i + 1);
				
				currentPageHeight = headHeight;
				
				// if need to print repeat title
				if (isUseRowTitles()) {
					if (_titleRow2 < i) {
						currentPageHeight += titleHeight;
					} else if (_titleRow1 < i) {
						currentPageHeight += _repeatRowTop[i - _titleRow1];
					}
				}
				firstRowInPage = true;
			} else {
				firstRowInPage = false;
			}
		}
		if (!firstRowInPage) { // handle last page
			autoRowBreaks.add(_printRow2 + 1);
		}
		if(autoRowBreaks.size() > 0) {
			_rowBreaks = PdfExporter.convertIntegers(autoRowBreaks);
		}
	}

	/**
	 * sets auto column breaks based on column width and available page width
	 * @param sheet
	 */
	protected void setupColumnBreaks(SSheet sheet) {
		final SPrintSetup ps = getCurrentPrintSetup(sheet);
		final boolean printHeadings = ps.isPrintHeadings();
		double headWidth = 0d;
		if(printHeadings) {
			headWidth = StyleConversionUtil.DEFAULT_ROW_HEADER_COLUMN_WIDTH;
		}
		double currentPageWidth = headWidth;
		double titleWidth = getTitleWidth();
		
		// repeat title columns in each left-to-right page
		// A1. if 1st column in a page is to the left of the repeat column range; irrelevant so ignore it.
		// A2. if 1st column in a page is to the right of the repeat column range; must print the repeat column range first.
		// A3. if 1st column in a page is inside the repeat column range; print the part of the repeat column range that are before the 1st column first.
		
		boolean firstColInPage = true;
		ArrayList<Integer> autoColumnBreaks = new ArrayList<Integer>();
		// if need to print repeat title
		if (isUseColTitles()) {
			if (_titleCol2 < _printCol1) { // A2
				currentPageWidth += titleWidth;
			} else if (_titleCol1 < _printCol1) { // A3
				currentPageWidth += _repeatColLeft[_printCol1 - _titleCol1];
			}
		}
		for(int i = _printCol1; i <= _printCol2; i++) {
			final double colWidth = _colLeft[i + 1 - _firstCol] - _colLeft[i - _firstCol];
			if (colWidth <= 0) continue; //ZSS-973
			currentPageWidth += colWidth;
			
			//ZSS-529
			boolean manualBreak = isColPageBreak(i); 
			boolean exceedEdge = currentPageWidth > _totalPrintWidth; 
			if(exceedEdge || manualBreak) { // next page
				// move column index back as first column in next page
				if (!firstColInPage && exceedEdge) { //ZSS-529
					i = i - 1;
				}
				
				autoColumnBreaks.add(i + 1);
				
				currentPageWidth = headWidth;
				
				// if need to print repeat title
				if (isUseColTitles()) {
					if (_titleCol2 < i) {
						currentPageWidth += titleWidth;
					} else if (_titleCol1 < i) {
						currentPageWidth += _repeatColLeft[i - _titleCol1]; 
					}
				}
				firstColInPage = true;
			} else {
				firstColInPage = false;
			}
		}
		if (!firstColInPage) { // handle last page
			autoColumnBreaks.add(_printCol2 + 1);
		}
		if(autoColumnBreaks.size() > 0) {
			_colBreaks = PdfExporter.convertIntegers(autoColumnBreaks);
		}
	}
	
	protected boolean isColPageBreak(int col) {
		return Arrays.binarySearch(_colBreaks, col) >= 0;
	}
	
	protected boolean isRowPageBreak(int row) {
		return Arrays.binarySearch(_rowBreaks, row) >= 0;
	}
	
	/**
	 * converts List<Integer> to primitive int[]
	 * @param integers
	 * @return
	 */
	protected static int[] convertIntegers(List<Integer> integers)
	{
	    int[] ret = new int[integers.size()];
	    for (int i=0; i < ret.length; i++)
	    {
	        ret[i] = integers.get(i).intValue();
	    }
	    return ret;
	}

	//ZSS-775
	protected void setCustomPaperSize(float width, float height) {
		_customPaperSize = new Rectangle(0f, 0f, width, height);
	}
	
	/**
	 * Set pdf page common properties such as margins and document standard font
	 * @param SSheet
	 *            sheet
	 */
	protected void initDocument(SSheet sheet, Document doc) {
		SFont defFont = sheet.getBook().getDefaultFont();
		_docStandardFont = FontFactory.getFont(defFont.getName(), BaseFont.IDENTITY_H);
		_docStandardFont.setSize(11);
		_docStandardFont.setStyle(Font.NORMAL);
		
		initDocumentForSheet(sheet, doc);
	}
	
	protected void initDocumentForSheet(SSheet sheet, Document doc) {
		initSheetPrintSetup(sheet); //ZSS-827
		SPrintSetup ps = getCurrentPrintSetup(sheet);
		
		if (ps.getPageStart() > 0) { // if set a legal starting page number, use it!
			_pageNumber = ps.getPageStart() - 1;
		} else {
			_pageNumber = 0;
		}
		_firstPage = true;
		
		
		_leftMargin = Utilities.inchesToPoints((float) ps.getLeftMargin());
		_rightMargin = Utilities.inchesToPoints((float) ps.getRightMargin());
		_topMargin = Utilities.inchesToPoints((float) ps.getTopMargin());
		_bottomMargin = Utilities.inchesToPoints((float) ps.getBottomMargin());
		_headerMargin = Utilities.inchesToPoints((float) ps.getHeaderMargin());
		_footerMargin = Utilities.inchesToPoints((float) ps.getFooterMargin());
		
		_paperSize = _customPaperSize != null ? _customPaperSize : //ZSS-775 
			PageSizeConversionUtil.getPageSize(PoiEnumConversion.toPoiPaperSize(ps.getPaperSize()));
		
		if (ps.isLandscape()) {
			Rectangle r = _paperSize.rotate();
			_paperSize = r;
			doc.setPageSize(r);
			_totalHeaderFooterWidth = r.getWidth() - (_topMargin + _bottomMargin);
		} else {
			doc.setPageSize(_paperSize);
			_totalHeaderFooterWidth = _paperSize.getWidth() - (_leftMargin + _rightMargin);
		}
		
		_totalPrintWidth = (_paperSize.getRight() - _rightMargin) - (_paperSize.getLeft() + _leftMargin);
		_totalPrintHeight = (_paperSize.getTop() - _topMargin) - (_paperSize.getBottom() + _bottomMargin);

		// margin l,r,t,b
		doc.setMargins((float)_leftMargin, (float)_rightMargin, (float)_topMargin, (float)_bottomMargin);
	}
	
	// print a sheet
	protected void printSheet(SSheet sheet, Document doc) throws DocumentException, IOException {
		_extendRegions.clear(); // clear for each new sheet
		_pageCount = _rowBreaks.length * _colBreaks.length;
		SPrintSetup ps = getCurrentPrintSetup(sheet);
		try {
			if (ps.isLeftToRight()) { // ->, then v
				int bRow = _printRow1;
				for (int j = 0; j < _rowBreaks.length; ++j) {
					int eRow = _rowBreaks[j];
					int bCol = _printCol1;
					for (int k = 0; k < _colBreaks.length; ++k) {
						int eCol = _colBreaks[k];
						printPage(bRow, eRow, bCol, eCol, sheet, doc);
						doc.newPage();
						bCol = eCol;
					}
					bRow = eRow;
				}
			} else { // v, then ->
				int bCol = _printCol1;
				for (int j = 0; j < _colBreaks.length; ++j) {
					int eCol = _colBreaks[j];
					int bRow = _printRow1;
					for (int k = 0; k < _rowBreaks.length; ++k) {
						int eRow = _rowBreaks[k];
						printPage(bRow, eRow, bCol, eCol, sheet, doc);
						doc.newPage();
						bRow = eRow;
					}
					bCol = eCol;
				}
			}
		} catch (ExceptionConverter ex) { // handle IOException from #onEndPage
			throwIOException(ex); //ZSS-1028
		}
	}
		
	
	//print a page
	protected void printPage(int bRow, int eRow, int bCol, int eCol, SSheet sheet, Document doc) 
	throws DocumentException {
		PdfPTable pdfTable = createPdfTable(bCol, eCol, sheet);
		
		//print area page bound
		_pageRow1 = bRow;
		_pageRow2 = eRow;
		_pageCol1 = bCol;
		_pageCol2 = eCol;
		
		//outline page bound (used for print page outline borders) 
		int outlineRow1 = bRow;
		int outlineCol1 = bCol;
		int outlineRow2 = eRow - 1;
		int outlineCol2 = eCol - 1;
		
		if (isUseRowTitles() && bRow > _titleRow1) {
			outlineRow1 = _titleRow1;
		}
		if (isUseColTitles() && bCol > _titleCol1) {
			outlineCol1 = _titleCol1;
		}
		
		CellRegion outlineRegion = new CellRegion(outlineRow1, outlineCol1, outlineRow2, outlineCol2);
		
		if (isUseRowTitles()) {
			if (bRow > _titleRow2) {
				for (int j = _titleRow1; j <= _titleRow2; ++j) {
					float rowHeight = (float) (_repeatRowTop[j + 1 - _titleRow1] - _repeatRowTop[j - _titleRow1]);
					if (rowHeight <= 0) continue; //ZSS-973
					printRow(pdfTable, j, _titleRow1, _titleRow2+1, bCol, eCol, sheet, rowHeight, outlineRegion);
				}
			} else if (bRow > _titleRow1) { // && bRow <= row2
				for (int j = _titleRow1; j < bRow; ++j) {
					float rowHeight = (float) (_repeatRowTop[j + 1 - _titleRow1] - _repeatRowTop[j - _titleRow1]);
					if (rowHeight <= 0) continue; //ZSS-973
					printRow(pdfTable, j, _titleRow1, eRow, bCol, eCol, sheet, rowHeight, outlineRegion);
				}
			}
		}
		for (int j = bRow; j < eRow; ++j) {
			float rowHeight = (float) (_rowTop[j + 1 - _firstRow] - _rowTop[j - _firstRow]);
			if (rowHeight <= 0) continue; //ZSS-973
			printRow(pdfTable, j, bRow, eRow, bCol, eCol, sheet, rowHeight, outlineRegion);
		}
		
		//ZSS-768
		double offsetX = _leftMargin;
		double offsetY = _bottomMargin + _totalPrintHeight;
		if (_vcenter) {
			double tableHeight = pdfTable.getTotalHeight();
			double shift = (_totalPrintHeight - tableHeight) / 2;
			offsetY -= shift; // origin at left-bottom corner
		}
		if (_hcenter) {
			double tableWidth = pdfTable.getTotalWidth();
			double shift = (_totalPrintWidth - tableWidth) / 2;
			offsetX += shift;
		}
		//doc.add(pdfTable);
		pdfTable.writeSelectedRows(0, -1, (float)offsetX, (float)offsetY, _writer.getDirectContent()); // left-top corner
	}
	
	protected void printRow(PdfPTable pdfTable, int rowIdx, 
			int bRow, int eRow, int bCol, int eCol, 
			SSheet sheet, float rowHeight, CellRegion outlineRegion) {
		if (_printHeadings) {
			printRowHeadingCell(pdfTable, rowIdx, rowHeight);
		}
		if (isUseColTitles()) {
			if (bCol > _titleCol2) {
				for (int k = _titleCol1; k <= _titleCol2; ++k) {
					//ZSS-973
					final int j0 = k - _titleCol1;
					double width = _repeatColLeft[j0 + 1] - _repeatColLeft[j0];
					if (width <= 0) continue;
					
					k = printCell(pdfTable, rowIdx, k, sheet, rowHeight, bRow, eRow, _titleCol1, _titleCol2 + 1, outlineRegion);
				}
			} else if (bCol > _titleCol1) { // && bCol <= col2
				for (int k = _titleCol1; k < bCol; ++k) {
					//ZSS-973
					final int j0 = k - _titleCol1;
					double width = _repeatColLeft[j0 + 1] - _repeatColLeft[j0];
					if (width <= 0) continue;

					k = printCell(pdfTable, rowIdx, k, sheet, rowHeight, bRow, eRow, _titleCol1, eCol, outlineRegion);
				}
			}
		}
		for (int k = bCol; k < eCol; ++k) {
			//ZSS-973
			final int j0 = k - _firstCol;
			double width = _colLeft[j0 + 1] - _colLeft[j0];
			if (width <= 0) continue;

			k = printCell(pdfTable, rowIdx, k, sheet, rowHeight, bRow, eRow, bCol, eCol, outlineRegion);
		}
	}
	
	// return next column
	protected int printCell(PdfPTable pdfTable, int rowIdx, int colIdx, 
			SSheet sheet, double rowHeight, 
			int bRow, int eRow, int bCol, int eCol, CellRegion outlineRegion) {
		
		CellRegion renderRegion = null;
		CellRegion clipRegion = null;
		SCell sCell = null;
		boolean extended = false; //ZSS-789: accounting underline does not extend
		//ZSS-772
		//extendRegion is created in previous loop
		CellRegion extendRegion = getExtendRegion(rowIdx, colIdx);
		CellRegion mergedRegion = null;
		if (extendRegion != null) {
			final int rowIdx0 = extendRegion.getRow();
			final int colIdx0 = extendRegion.getColumn();
			sCell = sheet.getCell(rowIdx0, colIdx0);
			renderRegion = extendRegion;
			
			// split the virtual merged cell along the page border; return the associated corner
			clipRegion = splitRegionsInBoundaries(renderRegion, bRow, eRow, bCol, eCol);
			
		} else { // normal case
			sCell = sheet.getCell(rowIdx, colIdx);
			
			mergedRegion = getMergedRegionIfAny(sheet, rowIdx, colIdx);
			if (mergedRegion != null) {
				renderRegion = mergedRegion;
				
				// split the merged cell along the page border; return the proper corner
				clipRegion = splitRegionsInBoundaries(mergedRegion, bRow, eRow, bCol, eCol);
				
				// get data cell at left-top corner (need to render phrase)
				final int mt = mergedRegion.getRow();
				final int ml = mergedRegion.getColumn();
				if (clipRegion.getColumn() == colIdx && clipRegion.getRow() == rowIdx) {
					sCell = sheet.getCell(mt, ml);
				} else {
					//ZSS-973: major cell of a merged cell is hidden; must
					//  process the 1st non-hidden minor cells of a merged cell
					final int mb = mergedRegion.getLastRow();
					final int mr = mergedRegion.getLastColumn();
					int trow = mt;
					int tcol = ml;
					for (int r = mt; r <= mb; ++r) {
						if (!sheet.getRow(r).isHidden()) {
							trow = r;
							break;
						}
					}
					for (int c = ml; c <= mr; ++c) {
						if (!sheet.getColumn(c).isHidden()) {
							tcol = c;
							break;
						}
					}
					if ((tcol != ml || trow != mt)
							&& colIdx == tcol && rowIdx == trow) {
						sCell = sheet.getCell(mt, ml);
					}
					
					clipRegion = new CellRegion(rowIdx, colIdx);
				}
			} else {
				//ZSS-772
				//long text that overflow(extend) to sibling cells
				int colIdx2 = getExtendedEndColumn(sheet, sCell, colIdx);
				for (int j = colIdx + 1; j <= colIdx2; ++j) {
					SCell sibling = sheet.getCell(rowIdx, j); // next sibling to the right; we don't support string h-align to right
					if (sibling != null && !sibling.isNull() && sibling.getType() != SCell.CellType.BLANK) {
						colIdx2 = j - 1;
						break;
					}
				}
				if (colIdx2 > colIdx) {
					renderRegion = new CellRegion(rowIdx, colIdx, rowIdx, colIdx2);
					
					// split the virtual merged cell along the page border; return the proper corner
					clipRegion = splitRegionsInBoundaries(renderRegion, bRow, eRow, bCol, eCol);
					addExtendRegion(renderRegion); // keep extendRegion for across page boundary cases
					
					//ZSS-789
					SCellStyle style = sCell.getCellStyle();
					extended = style.getAlignment() != Alignment.CENTER_SELECTION;
				} else {
					renderRegion = clipRegion = new CellRegion(rowIdx, colIdx);
				}
			}
		}
		
		final boolean printOutline = _printGridlines || _printHeadings;
		PdfPCell cell = createPdfCell(clipRegion, renderRegion, sCell, outlineRegion, sheet, _printGridlines, printOutline, extended);
		
		cell.setFixedHeight((float)rowHeight);
		pdfTable.addCell(cell);

		final int colSpan = clipRegion.getColumnCount();
		if (colSpan > 1) {
			for (int j = 1; j < colSpan; ++j) {
				final int colIdx0 = j + colIdx;
				
				//ZSS-1170
				final int j0 = colIdx0 - _firstCol;
				double width = _colLeft[j0 + 1] - _colLeft[j0];
				if (width <= 0) continue;
				
				PdfPCell cell0 = createEmptyRowSpanCell(sheet, outlineRegion, rowHeight, renderRegion, rowIdx, colIdx0, printOutline, _printGridlines);
				pdfTable.addCell(cell0);

			}
		}
		
		return clipRegion.getLastColumn();
	}

	//ZSS-773
	protected PdfPCell createEmptyRowSpanCell(SSheet sheet, CellRegion outlineRegion, double rowHeight, CellRegion renderRegion, int rowIdx, int colIdx, boolean printOutline, boolean printGridline) {
		final SCell zssCell = sheet.getCell(rowIdx, colIdx); 
		final CellRegion clipRegion = new CellRegion(rowIdx, colIdx);
		final PdfPCell cell = createPdfCell(clipRegion, renderRegion, zssCell, outlineRegion, sheet, printGridline, printOutline, false);
		cell.setFixedHeight((float)rowHeight);
		return cell;
	}
	
	protected CellRegion splitRegionsInBoundaries(CellRegion region, int bRow, int eRow, int bCol, int eCol) {
		int r1 = region.getRow();
		int r2 = region.getLastRow();
		int c1 = region.getColumn();
		int c2 = region.getLastColumn();
		
		if (bRow <= r1) { // top side
			if (bCol <= c1) { // left side
				//top-left
				return new CellRegion(r1, c1, Math.min(r2, eRow - 1), Math.min(c2, eCol - 1));
			} else {
				//top-right or top-intersect
				return new CellRegion(r1, bCol, Math.min(r2, eRow - 1), Math.min(c2, eCol - 1));
			}
		} else { // bottom side
			if (bCol <= c1) { // left side
				//bottom-left
				return new CellRegion(bRow, c1, Math.min(r2, eRow - 1), Math.min(c2, eCol - 1));
			} else {
				//bottom-right or bottom-intersect
				return new CellRegion(bRow, bCol, Math.min(r2, eRow - 1), Math.min(c2, eCol - 1));
			}
		}
	}
	
	//ZSS-772
	protected CellRegion getExtendRegion(int rowIdx, int colIdx) {
		Set<CellRegion> toRemoved = new HashSet<CellRegion>();
		for (CellRegion range : _extendRegions) {
			if (rowIdx == range.getRow() && colIdx >= range.getColumn() && colIdx <= range.getLastColumn()) {
				return range;
			}
			if (rowIdx == range.getRow() && colIdx > range.getLastColumn()) {
				toRemoved.add(range);
			}
		}
		if (!toRemoved.isEmpty()) {
			_extendRegions.removeAll(toRemoved);
		}
		return null; 
	}
	//ZSS-772
	protected void addExtendRegion(CellRegion region) {
		_extendRegions.add(region);
	}
	
	
	//eCol is exclusive
	protected PdfPTable createPdfTable(int bCol, int eCol, SSheet sheet) 
	throws DocumentException {

		List<Float> columnWidthList = new ArrayList<Float>();
		if (_printHeadings) {
			columnWidthList.add(StyleConversionUtil.DEFAULT_ROW_HEADER_COLUMN_WIDTH);
		}
		
		// calculate column width for each column
		if (isUseColTitles()) {
			if (bCol > _titleCol2) {
				for (int j = 0, len = _titleCol2 - _titleCol1 + 1; j < len; ++j) {
					double width = _repeatColLeft[j + 1] - _repeatColLeft[j];
					if (width <= 0) continue; //ZSS-973
					columnWidthList.add(new Float(width));
				}
			} else if (bCol > _titleCol1) { // && bCol <= col2
				for (int j = 0, len = bCol - _titleCol1; j < len; ++j) {
					double width = _repeatColLeft[j + 1] - _repeatColLeft[j];
					if (width <= 0) continue; //ZSS-973
					columnWidthList.add(new Float(width));
				}
			}
		}
		
		for (int j = bCol - _firstCol, len = eCol - _firstCol; j < len; ++j) {
			double width = _colLeft[j + 1] - _colLeft[j];
			if (width <= 0) continue; //ZSS-973
			columnWidthList.add(new Float(width));
		}

		//ZSS-973
		int numOfColumns = columnWidthList.size();
		float[] columnWidths= new float[numOfColumns];
		for (int j = 0; j < numOfColumns; ++j) {
			columnWidths[j] = columnWidthList.get(j);
		}
		PdfPTable pdfTable = new PdfPTable(numOfColumns);
		pdfTable.setWidths(columnWidths);
		pdfTable.setLockedWidth(true);
		pdfTable.setTotalWidth(columnWidths);
		pdfTable.setSpacingBefore(0.0f);

		// print headings 
		if (_printHeadings) {
			final float rowHeight = (float) LayoutUnitConversionHelper.pxToPdfHeightPoints(sheet.getDefaultRowHeight()); 
			printColHeadings(pdfTable, bCol, eCol, rowHeight);
		}
		
		return pdfTable;
	}

	//ZSS-769
	protected void printColHeadings(PdfPTable pdfTable, int bCol, int eCol, float rowHeight) {
		// cell at 0,0 in this case will always be empty
		PdfPCell cell = new PdfPCell(new Phrase(" ", _docStandardFont));
		cell.setFixedHeight((float)rowHeight);
		cell.setBorderWidth(OUTLINE_WIDTH);
		pdfTable.addCell(cell);

		if (isUseColTitles()) {
			if (bCol > _titleCol2) {
				for (int j = _titleCol1; j <= _titleCol2; ++j) {
					//ZSS-973
					final int j0 = j - _titleCol1;
					double width = _repeatColLeft[j0 + 1] - _repeatColLeft[j0];
					if (width <= 0) continue;

					printColHeadingCell(pdfTable, j, rowHeight);
				}
			} else if (bCol > _titleCol1) { // && bCol <= col2
				for (int j = _titleCol1; j < bCol; ++j) {
					//ZSS-973
					final int j0 = j - _titleCol1;
					double width = _repeatColLeft[j0 + 1] - _repeatColLeft[j0];
					if (width <= 0) continue;
					
					printColHeadingCell(pdfTable, j, rowHeight);
				}
			}
		}
		
		for (int j = bCol; j < eCol; ++j) {
			//ZSS-973
			final int j0 = j - _firstCol;
			double width = _colLeft[j0 + 1] - _colLeft[j0];
			if (width <= 0) continue;
			
			printColHeadingCell(pdfTable, j, rowHeight);
		}
	}
	
	//ZSS-769
	protected void printColHeadingCell(PdfPTable pdfTable, int colIdx, float rowHeight) {
		final Phrase p = new Phrase(
				CellRegion.convertIndexToColumnString(colIdx), _docStandardFont);
		printHeadingCell(pdfTable, p, rowHeight); //ZSS-1033
	}
	
	//ZSS-769
	protected void printRowHeadingCell(PdfPTable pdfTable, int rowIdx, float rowHeight) {
		Phrase p = new Phrase("" + (rowIdx + 1), _docStandardFont);
		printHeadingCell(pdfTable, p, rowHeight); //ZSS-1033
	}
	
	//ZSS-1033
	protected void printHeadingCell(PdfPTable pdfTable, Phrase p, float rowHeight) {
		PdfPCell cell = new PdfPCell();
		cell.setFixedHeight(rowHeight);
//		cell.setPaddingTop(0f);
//		cell.setPaddingLeft(PAD_LEFT);
//		cell.setPaddingRight(PAD_RIGHT);
//		// shift up descender of 11 pts calibri font so heading aligns with 
//		// data cell
//		cell.setPaddingBottom(bottomPadding); 
		
		cell.setBorderWidth(OUTLINE_WIDTH);
		cell.setVerticalAlignment(PdfPCell.ALIGN_BOTTOM);
		cell.setHorizontalAlignment(PdfPCell.ALIGN_CENTER);
		cell.setCellEvent(CellRenderEvent.getCellRenderEvent(null, null, null, null, null, null, false, false, null, null, p, null));

		pdfTable.addCell(cell);
	}

	
	//Prepare information for repeat offset for each page
	//0: pageColumnX : x coordinate of the bCol(first column of this page) on the whole sheet
	//1: pageRowY: y coordinate of the bRow(first row of this page) on the whole sheet
	//2: clipOffsetX: fixed offset to draw the left-top cell in x direction
	//3: clipOffsetY: fixed offset to draw the left-top cell in y direction
	protected double[] preparePageXYInfo(SSheet sheet, int bRow,
			int eRow, int bCol, int eCol) {
		double pageColumnX = _colLeft[bCol - _firstCol];
		double pageRowY = _rowTop[bRow - _firstRow];

		double clipOffsetX = _leftMargin;
		double clipOffsetY = _topMargin;
		
		// handle headings
		if(_printHeadings) {
			double defaultRowHeight = LayoutUnitConversionHelper.pxToPdfHeightPoints(sheet.getDefaultRowHeight());
			clipOffsetY += defaultRowHeight;
			clipOffsetX += StyleConversionUtil.DEFAULT_ROW_HEADER_COLUMN_WIDTH;
		}

		// handle repeat column titles
		if (isUseColTitles()) {
			if (bCol > _titleCol2) {
				clipOffsetX += _repeatColLeft[_titleCol2 + 1 - _titleCol1] - _repeatColLeft[0];
			} else if (bCol > _titleCol1) { // && bCol <= col2
				clipOffsetX += _repeatColLeft[bCol - _titleCol1] - _repeatColLeft[0];
			}
		}
		
		// handle repeat row titles
		if (isUseRowTitles()) {
			if (bRow > _titleRow2) {
				clipOffsetY += _repeatRowTop[_titleRow2 + 1 - _titleRow1] - _repeatRowTop[0];
			} else if (bRow > _titleRow1) { // && bRow <= row2; ZSS-1031
				clipOffsetY += _repeatRowTop[bRow - _titleRow1] - _repeatRowTop[0];
			}
		}
		return new double[] {pageColumnX, pageRowY, clipOffsetX, clipOffsetY};
	}
	
	protected void printImage(PdfContentByte canvas, byte[] rawData, ViewAnchor anchor1, ViewAnchor anchor2, 
			CellRegion clipRegion, CellRegion renderRegion, 
			double pageColumnX, double pageRowY, double clipOffsetX, double clipOffsetY,
			boolean outline) throws IOException {
		// locate the clipRegion Rectangle
		double clipLeft = _colLeft[clipRegion.getColumn() - _firstCol] - pageColumnX;
		double clipTop = _rowTop[clipRegion.getRow() - _firstRow] - pageRowY;
		double clipRight = _colLeft[clipRegion.getLastColumn() + 1 - _firstCol] - pageColumnX;
		double clipBottom = _rowTop[clipRegion.getLastRow() + 1 - _firstRow] - pageRowY;
		
		RenderInfo renderInfo = prepareRenderInfo(clipRegion, renderRegion, false, false);
		
		double renderLeft = clipLeft + renderInfo.getOffsetX();
		double renderTop = clipTop + renderInfo.getOffsetY();
		
		try {
			Image image = Image.getInstance(rawData);
			
			double width = LayoutUnitConversionHelper.pxToPdfWidthPoints(anchor1.getWidth());
			double height = LayoutUnitConversionHelper.pxToPdfHeightPoints(anchor1.getHeight());
			double xOffset = LayoutUnitConversionHelper.pxToPdfWidthPoints(anchor1.getXOffset());
			double yOffset = LayoutUnitConversionHelper.pxToPdfHeightPoints(anchor1.getYOffset());

			double imageLeft = renderLeft + xOffset; 
			double imageTop = renderTop + yOffset;
			double imageRight = imageLeft + width;
			double imageBottom = imageTop + height;
			
			double cropLeft = Math.max(clipLeft, imageLeft);
			double cropTop = Math.max(clipTop, imageTop);
			double cropRight = Math.min(clipRight, imageRight);
			double cropBottom = Math.min(clipBottom, imageBottom);
			
			double cropOffsetX = cropLeft - imageLeft;
			// ZSS-831, PDF page origin is at left-bottom corner, Y up is positive
			double cropOffsetY = imageBottom - cropBottom; 
			double cropWidth = cropRight - cropLeft;
			double cropHeight = cropBottom - cropTop; 

			if (cropWidth != width || cropHeight != height) {
				image = cropImage(image, (float)width, (float)height, _writer, (float) cropOffsetX, (float) cropOffsetY, (float)cropWidth, (float) cropHeight);
			}
			
			image.scaleAbsolute((float)cropWidth, (float)cropHeight);
			
			double x = clipOffsetX + cropLeft;
			double y = _paperSize.getTop() - clipOffsetY - cropTop - cropHeight;
			image.setAbsolutePosition((float)x, (float)y); // left-bottom corner
				
			canvas.addImage(image);
	
			if (outline) {
				if (renderRegion.getColumn() == clipRegion.getColumn()) { // left border
					final double y2 = y + cropHeight;
					drawLine(canvas, (float)x, (float)y, (float)x, (float)y2); // vertical
				}
				if (renderRegion.getLastColumn() == clipRegion.getLastColumn()) { // right border
					final double x2 = x + cropWidth;
					final double y2 = y + cropHeight;
					drawLine(canvas, (float)x2, (float)y, (float)x2, (float)y2); // vertical
				}
				if (renderRegion.getRow() == clipRegion.getRow()) { // top border
					final double x2 = x + cropWidth;
					final double y2 = y + cropHeight;
					drawLine(canvas, (float)x, (float)y2, (float)x2, (float)y2); // vertical
				}
				if (renderRegion.getLastRow() == clipRegion.getLastRow()) { // bottom border
					final double x2 = x + cropWidth;
					drawLine(canvas, (float)x, (float)y, (float)x2, (float)y); // vertical
					
				}
			}
		} catch (BadElementException e) {
			throw new IOException(e);
		} catch (DocumentException e) {
			throw new IOException(e);
		}
	}
	
	protected void drawLine(PdfContentByte lineCanvas, float x1, float y1, float x2, float y2) {
		lineCanvas.saveState();
		lineCanvas.setLineWidth(PdfExporter.GRIDLINE_WIDTH);
		lineCanvas.setLineCap(PdfContentByte.LINE_CAP_BUTT);
		lineCanvas.setLineDash(new float[]{}, 0f);
		lineCanvas.setColorStroke(java.awt.Color.DARK_GRAY);
		
		lineCanvas.moveTo(x1, y1);
		lineCanvas.lineTo(x2, y2);
		
		lineCanvas.stroke();
		lineCanvas.restoreState();
	}
	
	/**
	 * lays images over PdfTable
	 * @param sheet
	 * @param bRow
	 * @param eRow
	 * @param bCol
	 * @param eCol
	 */
	protected void layoverPictureIfAny(SSheet sheet, int bRow,
			int eRow, int bCol, int eCol, PdfWriter writer) throws IOException {
		
		PdfContentByte canvas = writer.getDirectContent();
	
		double[] pageInfo = preparePageXYInfo(sheet, bRow, eRow, bCol, eCol);
		
		double pageColumnX = pageInfo[0];
		double pageRowY = pageInfo[1];
		double clipOffsetX = pageInfo[2];
		double clipOffsetY = pageInfo[3];

		for(SPicture picture : sheet.getPictures()) {
			if(picture == null) {
				continue;
			}
			ViewAnchor anchor1 = picture.getAnchor();
			ViewAnchor anchor2 = anchor1.getRightBottomAnchor(sheet);
			final int anchorLeft = anchor1.getColumnIndex();
			final int anchorTop = anchor1.getRowIndex();
			final int anchorRight = anchor2.getColumnIndex();
			final int anchorBottom = anchor2.getRowIndex();
			
			if(anchorLeft < eCol && anchorRight >= bCol //ZSS-829
					&& anchorTop < eRow && anchorBottom >= bRow) { //intersect with page
				
				CellRegion renderRegion = new CellRegion(anchorTop, anchorLeft, anchorBottom, anchorRight);
				// split the image render region along the page border; return the associated corner
				CellRegion clipRegion = splitRegionsInBoundaries(renderRegion, bRow, eRow, bCol, eCol);
				
				printImage(canvas, picture.getData(), anchor1, anchor2,
						clipRegion, renderRegion,
						pageColumnX, pageRowY, clipOffsetX, clipOffsetY, false);
			}
		}
	}

	protected Image cropImage(Image image, float origWidth, float origHeight, PdfWriter writer, float x, float y, float width, float height) throws DocumentException {
	    PdfContentByte cb = writer.getDirectContent();
	    PdfTemplate t = cb.createTemplate(width, height);
	    t.addImage(image, origWidth, 0, 0, origHeight, -x, -y);
	    return Image.getInstance(t);
	}
	
	protected void layoutCharts(SSheet sheet, int bRow,
			int eRow, int bCol, int eCol, PdfWriter writer) throws IOException {
		
		PdfContentByte canvas = writer.getDirectContent();
		List<SChart> charts = sheet.getCharts();
		
		if (charts == null || charts.size() == 0)
			return;
		
		double[] pageInfo = preparePageXYInfo(sheet, bRow, eRow, bCol, eCol);
		
		double pageColumnX = pageInfo[0];
		double pageRowY = pageInfo[1];
		double clipOffsetX = pageInfo[2];
		double clipOffsetY = pageInfo[3];

		for(SChart chart : charts) {
			ViewAnchor anchor1 = chart.getAnchor();
			ViewAnchor anchor2 = anchor1.getRightBottomAnchor(sheet);

			final int anchorLeft = anchor1.getColumnIndex();
			final int anchorTop = anchor1.getRowIndex();
			final int anchorRight = anchor2.getColumnIndex();
			final int anchorBottom = anchor2.getRowIndex();
			
			if(anchorLeft < eCol && anchorRight >= bCol //ZSS-829
					&& anchorTop < eRow && anchorBottom >= bRow) { //intersect with page
				
				BufferedImage bufferedImage = null;
				if(imageCacher.containsKey(chart)) {
					bufferedImage = imageCacher.get(chart);
				} else {
					bufferedImage = ChartDrawer.drawChartImage(chart, Transparency.TRANSLUCENT);
					imageCacher.put(chart, bufferedImage);
				}
				
				if(bufferedImage == null)
					continue;
				
				byte[] rawData = null;
				//encode into png image format byte array
				rawData = EncoderUtil.encode(bufferedImage, ImageFormat.PNG, true);
				
				CellRegion renderRegion = new CellRegion(anchorTop, anchorLeft, anchorBottom, anchorRight);
				// split the image render region along the page border; return the associated corner
				CellRegion clipRegion = splitRegionsInBoundaries(renderRegion, bRow, eRow, bCol, eCol);
				
				printImage(canvas, rawData, anchor1, anchor2,
						clipRegion, renderRegion,
						pageColumnX, pageRowY, clipOffsetX, clipOffsetY, true);
			}
		} // End foreach chart
	}

	/**
	 * returns merged region cell range for a given cell
	 * @param sheet
	 * @param cell
	 * @return
	 */
	protected CellRegion getMergedRegionIfAny(SSheet sheet, int rowIdx, int colIdx) {
		CellRegion partOfRange = null;

		for(CellRegion range : sheet.getMergedRegions()) {
			if (colIdx >= range.getColumn() && colIdx <= range.getLastColumn()
				&& rowIdx >= range.getRow() && rowIdx <= range.getLastRow()) {
				partOfRange = range;
				break;
			}
		}

		return partOfRange;
	}

	/**
	 * checks if POI {@link Cell} is part of any merged regions on the passed
	 * {@link SSheet}
	 * 
	 * @param SSheet
	 *            sheet
	 * @param Cell
	 *            cell
	 * @return boolean
	 */
	protected boolean isPartOfMergedRegion(final SSheet sheet, final SCell cell) {
		boolean partOfMergedRegion = false;

		for(CellRegion range : sheet.getMergedRegions()) {
			if ((cell.getColumnIndex() >= range.getColumn() && cell
					.getColumnIndex() <= range.getLastColumn())
					&& (cell.getRowIndex() >= range.getRow() && cell
							.getRowIndex() <= range.getLastRow())) {
				partOfMergedRegion = true;
				break;
			}
		}

		return partOfMergedRegion;
	}

	/**
	 * creates a PdfCell from POI {@link Cell}
	 * 
	 * @param zssCell
	 * @return PdfCell
	 */
	protected PdfPCell createPdfCell(CellRegion clipRegion, CellRegion renderRegion, 
			SCell zssCell, CellRegion outlineRegion, SSheet sheet, 
			boolean printGridLines, boolean printOutline, boolean extended) {
		PdfPCell cell = null;
		SCellStyle style = zssCell.getCellStyle();
		if (!zssCell.isNull() || !sheet.getBook().getDefaultCellStyle().equals(style)) { //ZSS-1029
			Object[] results = !zssCell.isNull() ? createPhrase(zssCell) : new Object[] {null, Boolean.FALSE}; //ZSS-1029
			Phrase p = (Phrase) results[0];
			Boolean isNumber = (Boolean) results[1];
				
			cell = new PdfPCell();
			cell.setBorder(PdfPCell.NO_BORDER);
			cell.setNoWrap(!style.isWrapText());
			
			//ZSS-34 cell background color does not show in excel
			SColor color = style.getBackColor();
			java.awt.Color baseColor = java.awt.Color.white;
			if (color != null) {
				String sColor = color.getHtmlColor();
				baseColor = WebColors.getRGBColor(sColor == null || BookHelper.AUTO_COLOR.equals(sColor) ? "WHITE" : sColor);
				cell.setBackgroundColor(baseColor);
			}
			RenderInfo renderInfo = prepareRenderInfo(clipRegion, renderRegion, extended, isNumber.booleanValue());
			RenderInfo clipInfo = clipRegion.getColumnCount() > 1  || clipRegion.getRowCount() > 1 ? prepareClipInfo(clipRegion) : null; // we want 
			cell.setCellEvent(CellRenderEvent.getCellRenderEvent(clipRegion, clipInfo, renderRegion, renderInfo, zssCell, style, printOutline, printGridLines, outlineRegion, getPixelInfo(clipRegion), p, sheet));
		} else {
			cell = createEmptyCell(CellRenderEvent.getCellRenderEvent(clipRegion, null, renderRegion, null, null, null, printOutline, printGridLines, outlineRegion, null, null, sheet));
		}
		return cell;
	}

	PdfPCell createEmptyCell(CellRenderEvent event) {
		PdfPCell cell = new PdfPCell();
		cell.setBorder(PdfPCell.NO_BORDER);
		cell.setPadding(0f);
		cell.setPaddingLeft(PAD_LEFT);
		cell.setHorizontalAlignment(Element.ALIGN_UNDEFINED);
		cell.setVerticalAlignment(Element.ALIGN_BASELINE);
		cell.setCellEvent(event);
		return cell;
	}
	
	//ZSS-773
	protected RenderInfo prepareClipInfo(CellRegion clipRegion) {
		final double width = getRegionWidth(clipRegion);
		final double height = getRegionHeight(clipRegion);
		return new RenderInfo(0d, 0d, width, height, false, false);
	}
	
	//ZSS-772
	protected RenderInfo prepareRenderInfo(CellRegion clipRegion, CellRegion renderRegion, boolean extended, boolean number) {
		double xOffset = 0f;
		double yOffset = 0f;
		double width = getRegionWidth(renderRegion);
		double height = getRegionHeight(renderRegion);
		if (!clipRegion.equals(renderRegion)) {
			final int clipCol1 = clipRegion.getColumn();
			final int clipRow1 = clipRegion.getRow();
			final int renderCol1 = renderRegion.getColumn();
			final int renderRow1 = renderRegion.getRow();
			if (isUseColTitles() && _titleCol1 <= renderCol1 && renderCol1 <= _titleCol2) {
				xOffset = _repeatColLeft[renderCol1 - _titleCol1] 
							- _repeatColLeft[clipCol1 - _titleCol1];
			} else {
				xOffset = _colLeft[renderCol1 - _firstCol] 
						- _colLeft[clipCol1 - _firstCol];
			}
			if (isUseRowTitles() && _titleRow1 <= renderRow1 && renderRow1 <= _titleRow2) {
				yOffset = _repeatRowTop[renderRow1 - _titleRow1] 
							- _repeatRowTop[clipRow1 - _titleRow1];
			} else {
				yOffset = _rowTop[renderRow1 - _firstRow] 
							- _rowTop[clipRow1 - _firstRow];
			}
		}
		return new RenderInfo(xOffset, yOffset, width, height, extended, number);
	}
	
	//ZSS-773
	protected double getRegionWidth(CellRegion region) {
		final int col1 = region.getColumn();
		final int col2 = region.getLastColumn();
		if (isUseColTitles()
				&& _titleCol1 <= col1 && col1 <= _titleCol2
				&& _titleCol1 <= col2 && col2 <= _titleCol2) {
			return _repeatColLeft[col2 + 1 - _titleCol1] 
				- _repeatColLeft[col1 - _titleCol1];
		} else {
			return _colLeft[col2 + 1 - _firstCol] 
				- _colLeft[col1 - _firstCol];
		}
	}
	
	//ZSS-773
	protected double getRegionHeight(CellRegion region) {
		final int row1 = region.getRow();
		final int row2 = region.getLastRow();
		if (isUseRowTitles() 
				&& _titleRow1 <= row1 && row1 <= _titleRow2 
				&& _titleRow1 <= row2 && row2 <= _titleRow2) {
			return _repeatRowTop[row2 + 1 - _titleRow1] 
				- _repeatRowTop[row1 - _titleRow1];
			
		} else {
			return _rowTop[row2 + 1 - _firstRow] 
					- _rowTop[row1 - _firstRow];
		}
	}

	/**
	 * creates Phrase for corresponding ZSS Cell. If contents are RichString 
	 * Phrase is constructed out of several Chunk object each representing 
	 * different sections of rich text.
	 * @param zssCell
	 * @return
	 */
	protected Object[] createPhrase(SCell zssCell) {
		
		Phrase p = null;
		boolean number = false;
		if(zssCell != null && !zssCell.isNull() && zssCell.getType() != CellType.BLANK) {
			SCellStyle style = zssCell.getCellStyle();
			SFont styleFont = style.getFont();
			FormatEngine fe = EngineFactory.getInstance().createFormatEngine();
			FormatResult result = fe.format(zssCell, new FormatContext(ZssContext.getCurrent().getLocale()));
			number = !result.isDateFormatted() 
					&& (zssCell.getType() == CellType.NUMBER 
						|| (zssCell.getType() == CellType.FORMULA && zssCell.getFormulaResultType() == CellType.NUMBER)); 
			final boolean accountingUnder = 
					styleFont.getUnderline() == Underline.SINGLE_ACCOUNTING 
					|| styleFont.getUnderline() == Underline.DOUBLE_ACCOUNTING;
			if(result.isRichText()) {
				p = new Phrase();
				for(Segment sg : result.getRichText().getSegments()) {
					SFont zssFont = RichTextHelper.getRichTextRealFont(sg.getFont(), zssCell); //ZSS-1138
					//ZSS-1077
					final List<Chunk> chunks = createPdfChunks(zssFont, sg.getText(), accountingUnder);
					p.addAll(chunks);
				}
			} else {
				//ZSS-1077
				p = new Phrase();
				final List<Chunk> chunks = createPdfChunks(styleFont, result.getText(), accountingUnder);
				p.addAll(chunks);
			}
		}

		return new Object[] {p, Boolean.valueOf(number)};
	}
	

	/**
	 * returns pdf font corresponding to ZSS cell font
	 * @param zssFont
	 * @return
	 */
	protected Font createPdfCellFont(SFont zssFont) {
		return createPdfCellFont(zssFont, zssFont.getName()); //ZSS-1077 
	}
	
	/**
	 * Returns pdf font corresponding to ZSS cell font with a supplimental font face name.
	 * @param zssFont
	 * @param fontName
	 * @return
	 */
	//ZSS-1077
	protected Font createPdfCellFont(SFont zssFont, String fontName) {
		SColor fontColor = zssFont.getColor();
		Font cellFont = FontFactory.getFont(fontName, BaseFont.IDENTITY_H); //ZSS-1077
		String htmlColor = fontColor == null ? null : fontColor.getHtmlColor();
		java.awt.Color fontBaseColor = 
				WebColors.getRGBColor(htmlColor == null || BookHelper.AUTO_COLOR.equals(htmlColor) ? "BLACK" : htmlColor);
		cellFont.setColor(fontBaseColor);		
		//ZSS-748
		int fontsize = zssFont.getHeightPoints();
		if (zssFont.getTypeOffset() != SFont.TypeOffset.NONE) {
			fontsize = (int) (0.7 * fontsize + 0.5);
		}
		cellFont.setSize(fontsize);
		if (zssFont.getBoldweight() == Boldweight.BOLD) {
			if (zssFont.isItalic()) {
				cellFont.setStyle(Font.BOLDITALIC);
			} else {
				cellFont.setStyle(Font.BOLD);
			}
		} else if (zssFont.isItalic()) {
			cellFont.setStyle(Font.ITALIC);
		}
		if (zssFont.isStrikeout())
			cellFont.setStyle(Font.STRIKETHRU);
		return cellFont;
	}
	
	/**
	 * Prepare a FontSelector which can apply eastern supplemental fonts to 
	 * text.
	 */
	//ZSS-1077
	protected FontSelector prepareFontSelector(SFont zssFont) {
		FontSelector selector = new FontSelector();
		Font f1 = createPdfCellFont(zssFont);
		selector.addFont(f1);
		
		//enforce supplemental Chinese font face
		Font f2 = createPdfCellFont(zssFont, "新細明體");
		selector.addFont(f2);
		
		//TODO: enforce other supplemental font faces? 
		return selector;
	}
	
	/**
	 * Create and returns pdf chunks per the provided ZSS font, text, and 
	 * whether contains a Accounting Underline (because it is handled the 
	 * other way; see {@link CellRenderEvent#drawAccountingUnderline})
	 */
	//ZSS-1077
	protected List<Chunk> createPdfChunks(SFont zssFont, String text, boolean accountingUnder) {
		FontSelector selector = prepareFontSelector(zssFont);
		Phrase ph = selector.process(text);
		@SuppressWarnings("unchecked")
		List<Chunk> chunks = ph.getChunks();
		for (Chunk chunk : chunks) {
			processPdfChunk(zssFont, chunk, accountingUnder);
		}
		return chunks;
	}
	
	//ZSS-1077
	protected Chunk processPdfChunk(SFont zssFont, Chunk chunk, boolean accountingUnder) {
		if (zssFont.getTypeOffset() == SFont.TypeOffset.SUB) {
			chunk.setTextRise(-5f);
		} else if (zssFont.getTypeOffset() == SFont.TypeOffset.SUPER) {
			chunk.setTextRise(5f);
		}
		//richText will ignore segment underline if the cell is set accounting underline
		if (!accountingUnder) {
			switch(zssFont.getUnderline()) {
			case SINGLE:
				chunk.setUnderline(UNDERLINE_WIDTH, -UNDERLINE_OFFSET);
				break;
			case DOUBLE:
				chunk.setUnderline(UNDERLINE_WIDTH, -UNDERLINE_OFFSET);
				chunk.setUnderline(UNDERLINE_WIDTH, -UNDERLINE_OFFSET-DOUBLE_LINE_SPACE);
				break;
			case NONE:
			default:
			}
		}
		return chunk;
	}

	/**
	 * Returns the last cell that contains value
	 * @return last cell column index of the row
	 */
	protected int searchNonBlankEndColumn(SSheet sheet, int rowIdx, int lastColIdx) {
		int last = -1;
		for (int i = lastColIdx; i >= 0; i--) {
			final SCell cell = sheet.getCell(rowIdx, i);
			if (!cell.isNull() && cell.getType() != SCell.CellType.BLANK) {
				last = i;
				break;
			}
		}
		return last;
	}

	protected void initSelectedPrintArea(SSheet sheet) {
		int minC = _firstCol;
		int maxC = _endCol;
		int minR = _firstRow;
		int maxR = _endRow;
		if (_firstRow > 0) {
			int minRowIdx = sheet.getStartRowIndex();
			if (_firstRow > minRowIdx) {
				int rowIdx = _firstRow;
				for (int colIdx = _firstCol; colIdx <= _endCol; ++colIdx) {
					CellRegion mergedRegion = getMergedRegionIfAny(sheet, rowIdx, colIdx);
					if (mergedRegion != null) {
						minC = Math.min(minC,  mergedRegion.getColumn());
						maxC = Math.max(maxC, mergedRegion.getLastColumn());
						minR = Math.min(minR, mergedRegion.getRow());
					}
				}
				rowIdx = _endRow;
				for (int colIdx = _firstCol; colIdx <= _endCol; ++colIdx) {
					CellRegion mergedRegion = getMergedRegionIfAny(sheet, rowIdx, colIdx);
					if (mergedRegion != null) {
						minC = Math.min(minC,  mergedRegion.getColumn());
						maxC = Math.max(maxC, mergedRegion.getLastColumn());
						maxR = Math.min(maxR, mergedRegion.getLastRow());
					}
				}
			}
		}
		if (_firstCol > 0 && minC > 0) {
			int colIdx = _firstCol;
			for (int rowIdx = _firstRow; rowIdx <= _endRow; ++rowIdx) {
				CellRegion mergedRegion = getMergedRegionIfAny(sheet, rowIdx, colIdx);
				if (mergedRegion != null) {
					minC = Math.min(minC,  mergedRegion.getColumn());
					maxC = Math.max(maxC, mergedRegion.getLastColumn());
					if (minC == 0) break;
				} else {
					SRow row = sheet.getRow(rowIdx);
					if (row != null && !row.isNull()) {
						Iterator<SCell> iter = ((AbstractRowAdv)row).getCellIterator(true, 0, colIdx - 1);
						if (iter.hasNext()) {
							SCell sCell = (SCell) iter.next();
							if (sCell.getType() != CellType.BLANK) {
								final int col0 = sCell.getColumnIndex();
								final int col = getExtendedEndColumn(sheet, sCell, col0);
								if (col >= _firstCol && col0 < minC) {
									minC = col0;
									if (minC == 0) break;
								}
							}
						}
					}
				}
			}
		}
		int colIdx = _endCol;
		for (int rowIdx = _firstRow; rowIdx <= _endRow; ++rowIdx) {
			CellRegion mergedRegion = getMergedRegionIfAny(sheet, rowIdx, colIdx);
			if (mergedRegion != null) {
				maxC = Math.max(maxC, mergedRegion.getLastColumn());
			} else {
				SCell sCell = sheet.getCell(rowIdx, colIdx);
				if (sCell != null && !sCell.isNull() && sCell.getType() != CellType.BLANK) {
					final int col = getExtendedEndColumn(sheet, sCell, colIdx);
					if (col > maxC) {
						maxC = col;
					}
				}
			}
		}

		// Boundary for pictures
		List<SPicture> pics = sheet.getPictures();
		if (pics != null && pics.size() != 0) {
			for(SPicture pic : pics) {
				ViewAnchor anchor1 = pic.getAnchor();
				ViewAnchor anchor2 = anchor1.getRightBottomAnchor(sheet);
				if (anchor2.getColumnIndex() < _firstCol || anchor1.getColumnIndex() > _endCol
					|| anchor2.getRowIndex() < _firstRow || anchor1.getRowIndex() > _endRow) {
					continue;
				}
				minC = Math.min(minC, anchor1.getColumnIndex());
				minR = Math.min(minR, anchor1.getRowIndex());
				maxC = Math.max(maxC, anchor2.getColumnIndex());
				maxR = Math.max(maxR, anchor2.getRowIndex());
			}
		}
		
		// Boundary for charts
		List<SChart> charts = sheet.getCharts();
		if (charts != null && charts.size() != 0) {
			for(SChart chart : charts) {
				ViewAnchor anchor1 = chart.getAnchor();
				ViewAnchor anchor2 = anchor1.getRightBottomAnchor(sheet);
				if (anchor2.getColumnIndex() < _firstCol || anchor1.getColumnIndex() > _endCol
					|| anchor2.getRowIndex() < _firstRow || anchor1.getRowIndex() > _endRow) {
					continue;
				}
				minC = Math.min(minC, anchor1.getColumnIndex());
				minR = Math.min(minR, anchor1.getRowIndex());
				maxC = Math.max(maxC, anchor2.getColumnIndex());
				maxR = Math.max(maxR, anchor2.getRowIndex());
			}
		}

		_firstCol = minC;
		_firstRow = minR;
		_endCol = maxC;
		_endRow = maxR;
	}
	
	/**
	 * sets the column bounds for passed {@link SSheet}
	 * 
	 * @param sheet
	 */
	protected void initPrintArea(SSheet sheet, CellRegion region) {
		// if user choose a print area then use it
		if (region != null) {
			_printRow1 = _firstRow = region.getRow();
			_printRow2 = _endRow = region.getLastRow();
			_printCol1 = _firstCol = region.getColumn();
			_printCol2 = _endCol = region.getLastColumn();
			initSelectedPrintArea(sheet);
			return;
		}
		
		// or find print area for real data
		_firstCol = 0;
		_endCol = -1;
		_firstRow = 0;
		_endRow = -1;

		// Boundary for cell data
		Iterator<SRow> rowIter = sheet.getRowIterator();
		while (rowIter.hasNext()) {
			SRow row = rowIter.next();
			int rowIdx = row.getIndex();
			int lastCol = sheet.getEndCellIndex(rowIdx);
			if (lastCol < 0) {
				//ZSS-1074: fill is different to default; should print it!
				if (row.getCellStyle(true) != null && !row.getCellStyle().getFill().equals(_wb.getDefaultCellStyle().getFill())) {
					_endRow = Math.max(_endRow, rowIdx);
				} else {
					continue; //skip blank row
				}
			}
			int lastNonBlankCol = searchNonBlankEndColumn(sheet, rowIdx, lastCol);
			//ZSS-772: could be a long merged cell that exceeds the endColumn
			//mergedcell cannot overflow to next sibling
			CellRegion mergedRegion = getMergedRegionIfAny(sheet, rowIdx, lastNonBlankCol);
			if (mergedRegion != null) {
				 int col = mergedRegion.getLastColumn();
				 if (col > lastCol) {
					 lastCol = col;
				 }
			} else { //ZSS-772: could be a long text that exceeds the endColumn 
				SCell zssCell = sheet.getCell(rowIdx, lastNonBlankCol);
				final int col = getExtendedEndColumn(sheet, zssCell, lastNonBlankCol);
				if (col > lastCol) {
					lastCol = col;
				}
			}
			_endCol = Math.max(_endCol, lastCol);
			_endRow = Math.max(_endRow, row.getIndex());
		}
		
		// Boundary for pictures
		List<SPicture> pics = sheet.getPictures();
		if (pics != null && pics.size() != 0) {
			for(SPicture pic : pics) {
				ViewAnchor anchor1 = pic.getAnchor();
				ViewAnchor anchor2 = anchor1.getRightBottomAnchor(sheet);
				if(anchor2.getColumnIndex() > _endCol) {
					_endCol = anchor2.getColumnIndex(); 
				}
				if(anchor2.getRowIndex() > _endRow) {
					_endRow = anchor2.getRowIndex();
				}
			}
		}
		
		// Boundary for charts
		List<SChart> charts = sheet.getCharts();
		if (charts != null && charts.size() != 0) {
			for(SChart chart : charts) {
				ViewAnchor anchor1 = chart.getAnchor();
				ViewAnchor anchor2 = anchor1.getRightBottomAnchor(sheet);
				if(anchor2.getColumnIndex() > _endCol) {
					_endCol = anchor2.getColumnIndex(); 
				}
				if(anchor2.getRowIndex() > _endRow) {
					_endRow = anchor2.getRowIndex();
				}
			}
		}
		
		_printRow1 = _firstRow;
		_printRow2 = _endRow;
		_printCol1 = _firstCol;
		_printCol2 = _endCol;
	}
	
	//ZSS-772: could be a long text that overflows to next cells to the right
	//ZSS-779: could be center across selection cells
	protected int getExtendedEndColumn(SSheet sheet, SCell zssCell, int colIdx) {
		final CellType type = zssCell.getType();
		if (type == CellType.STRING || (type == CellType.FORMULA && zssCell.getFormulaResultType() == CellType.STRING)) {
			SCellStyle style = zssCell.getCellStyle();
			if (!style.isWrapText()) {
				if (style.getAlignment() == Alignment.LEFT 
					|| style.getAlignment() == Alignment.GENERAL) {
					return getExtendedEndColumn0(sheet, zssCell, colIdx);
				} else if (style.getAlignment() == Alignment.CENTER_SELECTION) { //ZSS-779
					return getAcrossSelectionColumn0(sheet, zssCell.getRowIndex(), colIdx);
				}
			}
		}
		return colIdx;
	}
	//ZSS-779
	protected int getAcrossSelectionColumn0(SSheet sheet, int rowIdx, int colIdx) {
		int maxCol = _wb.getMaxColumnIndex();
		for (int c = colIdx + 1; c <= maxCol; ++c) {
			final SCell sCell = sheet.getCell(rowIdx, c);
			if (sCell != null && sCell.getType() == CellType.BLANK) {
				final SCellStyle style = sCell.getCellStyle();
				if (style != null && style.getAlignment() == Alignment.CENTER_SELECTION) {
					continue;
				}
			}
			return c - 1;
		}
		return colIdx;
	}
	//ZSS-772
	protected int getExtendedEndColumn0(SSheet sheet, SCell zssCell, int colIdx) {
		Object[] results = createPhrase(zssCell);
		Phrase p = (Phrase) results[0];
		double stringWidth = getStringWidth(p) + PAD_LEFT;
		double width = 0f;
		int maxCol = _wb.getMaxColumnIndex();
		for (int c = colIdx; c <= maxCol; ++c) {
			final SColumn scol = sheet.getColumn(c); 
			final int cwidth = scol.isHidden() ? 0 : scol.getWidth(); //ZSS-1170
			width += LayoutUnitConversionHelper.pxToPdfWidthPoints(cwidth);
			if (width >= stringWidth)
				return c;
		}
		return colIdx;
	}
	//ZSS-772
	@SuppressWarnings("rawtypes")
	/*package*/ static double getStringWidth(Phrase phrase) {
		double width = 0f; 
		for (final Iterator it = phrase.getChunks().iterator(); it.hasNext();) {
			Chunk chunk = (Chunk) it.next();
			width += chunk.getWidthPoint();
		}
		return width;
	}
	
	class PdfHeaderFooter extends PdfPageEventHelper {

		public void onOpenDocument(PdfWriter pdfWriter, Document doc) {
		}

		public void onStartPage(PdfWriter pdfWriter, Document doc) {
			_pageNumber++;
		}
		
		/**
		 * create a PDF cell for header or footer
		 * @param text
		 * @param alignment
		 * @return
		 */
		protected PdfPCell createHeaderFooterCell(String rawText, int alignment, int valign) {
			Phrase phrase = createHeaderFooterPhrase(rawText);
			PdfPCell cell = new PdfPCell(phrase);
			cell.setPaddingLeft(PAD_LEFT);
			cell.setHorizontalAlignment(alignment);
			cell.setVerticalAlignment(valign);
			cell.setBorder(PdfPCell.NO_BORDER);
			return cell;
		}
		
		//ZSS-778
		protected String[] getFontInfo(String rawText, int start, int[] index) {
			StringBuilder sb = new StringBuilder();
			String fontName = "-", fontStyle = "Regular";
			
			for (int j = start + 1, len = rawText.length(); j < len; ++j) {
				char ch = rawText.charAt(j);
				if (ch == ',') {
					fontName = sb.toString();
					sb = new StringBuilder();
					continue;
				} else if (ch == '"') {
					fontStyle = sb.toString();
					index[0] = j;
					break;
				}
				sb.append(ch);
			}
			return new String[] {fontName, fontStyle};
		}
		
		//ZSS-778
		protected Color getFontColor(String rawText, int start, int[] index) {
			char ch3th = rawText.charAt(start + 3);
			boolean isTheme = ch3th == '-' || ch3th == '+';
			//TODO: we don't support theme color yet; always return null
			if (isTheme) {
				index[0] = start + 5; // skip 5 characters
				return null;
			}
			
			StringBuilder rgbsb = new StringBuilder();
			rgbsb.append('#');
			for (int j = start + 1, len = start + 7; j < len; ++j) {
				char ch = rawText.charAt(j);
				rgbsb.append(ch);
			}
			
			index[0] = start + 6;
			return WebColors.getRGBColor(rgbsb.toString());
		}
		
		//ZSS-778
		protected int getFontSize(String rawText, int start, int[] index) {
			StringBuilder numsb = new StringBuilder();
			for (int j = start, len = rawText.length(); j < len; ++j) {
				char ch = rawText.charAt(j);
				if ('0' <= ch && ch <= '9') {
					numsb.append(ch);
				} else {
					index[0] = j - 1;
					break;
				}
			}
			return Integer.parseInt(numsb.toString());
		}
		
		//ZSS-778
		protected Phrase createHeaderFooterPhrase(String rawText) {
			if (rawText == null || rawText.length() == 0) {
				return new Phrase(rawText,_docStandardFont);
			}
			
			Phrase p = new Phrase();
			
			//toggle switch
			boolean strikeout = false;
			boolean underline = false;
			boolean doubleline = false;
			boolean superscript = false;
			boolean subscript = false;
			boolean outline = false;
			boolean shadow =false;
			boolean bold = false;
			boolean italic = false;
			
			final String defaultFontName = _docStandardFont.getFamilyname(); // default font name
			final float defaultFontSize = _docStandardFont.getSize(); //default font size
			final Color defaultFontColor = _docStandardFont.getColor();
			
			//font style
			String fontName = defaultFontName;
			float fontSize = defaultFontSize;
			Color fontColor = defaultFontColor;
			
			//date and time
			Date now = new Date();
			Locale locale = ZssContext.getCurrent().getLocale();
			DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, locale);
			String date = df.format(now);
			DateFormat tf = DateFormat.getTimeInstance(DateFormat.LONG, locale);
			String time = tf.format(now);

			boolean endOfChunk = false;
			StringBuilder sb = new StringBuilder();
			int[] index = new int[1]; //container to bring the index back
			for (int j = 0, len = rawText.length(); j < len; ++j) {
				char ch = rawText.charAt(j);
				if (ch == '&') { //control symbol
					j = j + 1;
					//check next char
					ch = rawText.charAt(j);
					
					switch(ch) {
					case 'U': //underline
						if (sb.length() > 0) {
							j = j - 2;
							endOfChunk = true;
							break;
						}
						underline = !underline;
						break;
					case 'S': //strikout
						if (sb.length() > 0) {
							j = j - 2;
							endOfChunk = true;
							break;
						}
						strikeout = !strikeout;
						break;
					case 'X': //superscript
						if (sb.length() > 0) {
							j = j - 2;
							endOfChunk = true;
							break;
						}
						superscript = !superscript;
						break;
					case 'E': //double underline
						if (sb.length() > 0) {
							j = j - 2;
							endOfChunk = true;
							break;
						}
						doubleline = !doubleline;
						break;
					case 'Y': //subscript
						if (sb.length() > 0) {
							j = j - 2;
							endOfChunk = true;
							break;
						}
						subscript = !subscript;
						break;
					case 'B': //bold
						if (sb.length() > 0) {
							j = j - 2;
							endOfChunk = true;
							break;
						}
						bold = !bold;
						break;
					case 'I': //italic
						if (sb.length() > 0) {
							j = j - 2;
							endOfChunk = true;
							break;
						}
						italic = !italic;
						break;
					case 'O': //outline
						if (sb.length() > 0) {
							j = j - 2;
							endOfChunk = true;
							break;
						}
						outline = !outline;
						break;
					case 'H': //shadow
						if (sb.length() > 0) {
							j = j - 2;
							endOfChunk = true;
							break;
						}
						shadow = !shadow;
						break;
					case '"': //font name, font type
						if (sb.length() > 0) {
							j = j - 2;
							endOfChunk = true;
							break;
						}
						//font name and font type
						String[] fontInfo = getFontInfo(rawText, j, index);
						j = index[0];
						String nm = fontInfo[0];
						String style = fontInfo[1];
						fontName = nm == "-" ? defaultFontName : nm;
						if ("Bold".equalsIgnoreCase(style)) {
							bold = true;
						} else if ("Italic".equalsIgnoreCase(style)) {
							italic = true;
						} else if ("Bold Italic".equalsIgnoreCase(style)) {
							bold = italic = true;
						} else if ("Italic Bold".equalsIgnoreCase(style)) {
							bold = italic = true;
						} else { //"Regular"
							bold = italic = false;
						}
						break;				
					case '0':
					case '1':
					case '2':
					case '3':
					case '4':
					case '5':
					case '6':
					case '7':
					case '8':
					case '9': //font size
						if (sb.length() > 0) {
							j = j - 2;
							endOfChunk = true;
							break;
						}
						fontSize = getFontSize(rawText, j, index);
						j = index[0];
						break;
					case 'K': //font color
						if (sb.length() > 0) {
							j = j - 2;
							endOfChunk = true;
							break;
						}
						fontColor = getFontColor(rawText, j, index);
						if (fontColor == null) fontColor = defaultFontColor;
						j = index[0];
						break;
					case 'P': //page number
						sb.append(_pageNumber);
						break;
					case 'N': //number of page count
						sb.append(_pageCount);
						break;
					case 'D': //date
						sb.append(date);
						break;
					case 'T': //time
						sb.append(time);
						break;
					case 'Z': //path
						//web does not have path at all; skip
						//use bookname
						sb.append(_bookName);
						break;
					case 'F': //file name
						// use bookname
						sb.append(_bookName);
						break;
					case 'A': //sheet name
						sb.append(_sheetName);
						break;
					case 'G': //back ground picture of the cell
						//TODO: don't know how to do it yet, skip
						break;
					case '&': // &&
						sb.append(ch);
						break;
					default: //unknown &; just skip it
						break;
					}
				} else {
					sb.append(ch);
				}

				if (endOfChunk) {
					//create chunk of previous text
					Font cellFont = FontFactory.getFont(fontName, BaseFont.IDENTITY_H);
					cellFont.setColor(fontColor);
					int fontsize = (int) fontSize;
					if (superscript || subscript) {
						fontsize = (int) (0.7 * fontsize + 0.5);
					}
					cellFont.setSize(fontsize);
					if (bold) {
						if (italic) {
							cellFont.setStyle(Font.BOLDITALIC);
						} else {
							cellFont.setStyle(Font.BOLD);
						}
					} else if (italic) {
						cellFont.setStyle(Font.ITALIC);
					}
					if (strikeout) {
						cellFont.setStyle(Font.STRIKETHRU);
					}
					Chunk chunk = new Chunk(sb.toString(), cellFont);
					if (subscript) {
						chunk.setTextRise(-5f);
					} else if (superscript) {
						chunk.setTextRise(5f);
					}
					if (doubleline) {
						chunk.setUnderline(UNDERLINE_WIDTH, -UNDERLINE_OFFSET);
						chunk.setUnderline(UNDERLINE_WIDTH, -UNDERLINE_OFFSET-DOUBLE_LINE_SPACE);
					} else if (underline) {
						chunk.setUnderline(UNDERLINE_WIDTH, -UNDERLINE_OFFSET);
					}
					p.add(chunk);
					
					sb = new StringBuilder();
					endOfChunk = false;
				}
			}
			
			//last chunk
			if (sb.length() > 0) {
				Font cellFont = FontFactory.getFont(fontName, BaseFont.IDENTITY_H);
				cellFont.setColor(fontColor);
				int fontsize = (int) fontSize;
				if (superscript || subscript) {
					fontsize = (int) (0.7 * fontsize + 0.5);
				}
				cellFont.setSize(fontsize);
				if (bold) {
					if (italic) {
						cellFont.setStyle(Font.BOLDITALIC);
					} else {
						cellFont.setStyle(Font.BOLD);
					}
				} else if (italic) {
					cellFont.setStyle(Font.ITALIC);
				}
				if (strikeout)
					cellFont.setStyle(Font.STRIKETHRU);
				Chunk chunk = new Chunk(sb.toString(), cellFont);
				if (subscript) {
					chunk.setTextRise(-5f);
				} else if (superscript) {
					chunk.setTextRise(5f);
				}
				if (doubleline) {
					chunk.setUnderline(UNDERLINE_WIDTH, -UNDERLINE_OFFSET);
					chunk.setUnderline(UNDERLINE_WIDTH, -UNDERLINE_OFFSET-DOUBLE_LINE_SPACE);
				} else if (underline) {
					chunk.setUnderline(UNDERLINE_WIDTH, -UNDERLINE_OFFSET);
				}
				p.add(chunk);
			}
			
			return p;
		}
		
		/**
		 * create a table for header or footer
		 * @param alignment
		 * @return
		 * @throws DocumentException 
		 */
		protected PdfPTable createHeaderFooter(int alignment) throws IOException {
			PdfPTable table = new PdfPTable(3);
			float width = (float)(_totalHeaderFooterWidth / 3);
			float[] widths = new float[] {width, width, width};
			try {
				table.setWidths(widths);
				table.setLockedWidth(true);
				table.setTotalWidth(widths);
				table.setHorizontalAlignment(alignment);
			} catch (DocumentException e) {
				throw new IOException(e);
			}
			return table;
		}

		/**
		 * this is a page level event to draw header footer on pdf page;
		 * doc.newPage() will trigger this synchronous event.
		 */
		@Override
		public void onEndPage(PdfWriter writer, Document doc) {
			//20141003, henrichen: when document.newPage() this method is called.
			//20141006, henrichen: onEndPage is an interface method of 
			//   PdfPageEventHelper in iText. Which does not throw any
			//   managed exception. That is why we need to convert the
			//   IOException to ExceptionConverter and recover it back
			//   in PdfExporter#printScalePages() and PdfExporter#printSheet()
			try {
				doEndPage(writer, doc);
			} catch (IOException e) {
				throw new ExceptionConverter(e);
			}
		}
		protected void doEndPage(PdfWriter writer, Document doc) throws IOException {
//			Rectangle rect = paperSize;
			PdfContentByte canvas = writer.getDirectContent();
			
			SHeader header = getCurrentHeader(_currentSheet);
			if (header != null) {
				PdfPTable pheader = createHeaderFooter(PdfPTable.ALIGN_CENTER);
				pheader.addCell(createHeaderFooterCell(header.getLeftText(), Element.ALIGN_LEFT, Element.ALIGN_TOP));
				pheader.addCell(createHeaderFooterCell(header.getCenterText(), Element.ALIGN_CENTER, Element.ALIGN_TOP));
				pheader.addCell(createHeaderFooterCell(header.getRightText(), Element.ALIGN_RIGHT, Element.ALIGN_TOP));
				float topPosition = (float) (_bottomMargin + _totalPrintHeight + (_topMargin - _headerMargin));
				pheader.writeSelectedRows(0, -1, (float)_leftMargin, topPosition, canvas);
			}
			
			SFooter footer = getCurrentFooter(_currentSheet);
			if (footer != null) {
				PdfPTable pfooter = createHeaderFooter(PdfPTable.ALIGN_CENTER);
				pfooter.addCell(createHeaderFooterCell(footer.getLeftText(), Element.ALIGN_LEFT, Element.ALIGN_BOTTOM));
				pfooter.addCell(createHeaderFooterCell(footer.getCenterText(), Element.ALIGN_CENTER, Element.ALIGN_BOTTOM));
				pfooter.addCell(createHeaderFooterCell(footer.getRightText(), Element.ALIGN_RIGHT, Element.ALIGN_BOTTOM));
				float bottomPosition  = (float) (_footerMargin + pfooter.getTotalHeight());
				pfooter.writeSelectedRows(0, -1, (float)_leftMargin, bottomPosition, canvas);
			}
			
			// reset _firstPage flag
			if (_firstPage) {
				_firstPage = false;
			}
			
			if (_scale == 100) { //ZSS-775 normal case
				layoverPictureIfAny(_wb.getSheet(_currentSheetIndex),
						_pageRow1, _pageRow2, _pageCol1, _pageCol2, writer);
				
				layoutCharts(_wb.getSheet(_currentSheetIndex), 
						_pageRow1, _pageRow2, _pageCol1, _pageCol2, writer);
			}
		}
	}
	
	/**
	 * Get this setup to setup the exporting parameters for "print" as a PDF
	 * document
	 * @return
	 */
	public SPrintSetup getPrintSetup() {
		if (_printSetup == null) {
			_printSetup = new OverrideSetup();
		}
		return _printSetup;
	}
	
	//ZSS-827
	protected void initSheetPrintSetup(SSheet sheet) {
		if (_printSetup != null) {
			((OverrideSetup)_printSetup).setInternal(sheet.getPrintSetup());
		}
	}
	
	protected SPrintSetup getCurrentPrintSetup(SSheet sheet) {
		return _printSetup == null ? sheet.getPrintSetup() : _printSetup; 
	}
	
	protected SHeader getCurrentHeader(SSheet sheet) { 
		final SPrintSetup ps = getCurrentPrintSetup(sheet);
		if (_firstPage) {
			if (ps.isDifferentFirstPage()) {
				return ps.getFirstHeader(); 
			}
		}
		if (isEvenPage(_pageNumber) && ps.isDifferentOddEvenPage()) {
			return ps.getEvenHeader();
		}
		return ps.getHeader();
	}
	
	protected SFooter getCurrentFooter(SSheet sheet) { 
		final SPrintSetup ps = getCurrentPrintSetup(sheet);
		if (_firstPage) {
			if (ps.isDifferentFirstPage()) {
				return ps.getFirstFooter(); 
			}
		}
		if (isEvenPage(_pageNumber) && ps.isDifferentOddEvenPage()) {
			return ps.getEvenFooter();
		}
		return ps.getFooter();
	}
	
	protected boolean isEvenPage(int no) {
		return (no & 1) == 0;
	}
	
	protected boolean isUseColTitles() {
		return _titleCol1 >= 0 && _titleCol2 >= _titleCol1;
	}
	
	protected boolean isUseRowTitles() {
		return _titleRow1 >= 0 && _titleRow2 >= _titleRow1;
	}
	
	protected int getCountOfColTitles() {
		return isUseColTitles() ? _titleCol2 - _titleCol1 + 1 : 0;
	}
	
	protected double getTitleWidth() {
		return isUseColTitles() ? 
				_repeatColLeft[_titleCol2 + 1 - _titleCol1] - _repeatColLeft[0] : 0f;
	}
	
	protected double getTitleHeight() {
		return isUseRowTitles() ?
				_repeatRowTop[_titleRow2 + 1 - _titleRow1] - _repeatRowTop[0] : 0f;
	}

	/*package*/ static Alignment getRealAlignment(SCell cell) {
		if (cell == null) {
			return Alignment.CENTER;
		}
		SCellStyle style = cell.getCellStyle();
		CellType type = cell.getType();
		org.zkoss.zss.model.SCellStyle.Alignment align = style.getAlignment();
		if (align == Alignment.GENERAL) {
			final String format = style.getDataFormat();
			if (format != null && format.startsWith("@")) //a text format
				type = CellType.STRING;
			else if (type == CellType.FORMULA)
				type = cell.getFormulaResultType();
			switch(type) {
			case BLANK:
				return align;
			case BOOLEAN:
				return Alignment.CENTER;
			case ERROR:
				return Alignment.CENTER;
			case NUMBER:
				return Alignment.RIGHT;
			case STRING:
			default:
				return Alignment.LEFT;
			}
		}
		return align;
	}
	
	/*package*/ static VerticalAlignment getRealVerticalAlignment(SCell cell) {
		if (cell == null) {
			//return VerticalAlignment.BOTTOM;
			return VerticalAlignment.CENTER; //ZSS-1033
		}
		SCellStyle style = cell.getCellStyle();
		org.zkoss.zss.model.SCellStyle.VerticalAlignment align = style.getVerticalAlignment();
		if (align == VerticalAlignment.JUSTIFY) {
			return VerticalAlignment.TOP;
		}
		return align;
	}
	
	//ZSS-974
	protected PixelInfo getPixelInfo(CellRegion clipRegion) {
		final int row1 = clipRegion.getRow();
		final int col1 = clipRegion.getColumn();
		
		int xOffset = 0;
		int yOffset = 0;
		int width = 0;
		int height = 0;
		
		if (_titleCol1 <= col1 && col1 <= _titleCol2) {
			final int j0 = col1 - _titleCol1;
			xOffset = _repeatColLeftPx[j0];
			width = _repeatColLeftPx[j0 + 1] - xOffset;
		} else {
			final int j0 = col1 - _firstCol;
			xOffset = _colLeftPx[j0];
			width = _colLeftPx[j0 + 1] - xOffset;
		}
		
		if (_titleRow1 <= row1 && row1 <= _titleRow2) {
			final int j0 = row1 - _titleRow1;
			yOffset = _repeatRowTopPx[j0];
			height =  _repeatRowTopPx[j0 + 1] - yOffset;
		} else {
			final int k0 = row1 - _firstRow;
			yOffset = _rowTopPx[k0];
			height = _rowTopPx[k0 + 1] - yOffset;
		}
		
		return new PixelInfo(xOffset, yOffset, width, height);
	}
}

//ZSS-772
/*package*/ class RenderInfo {
	protected final double offsetX, offsetY, width, height;
	protected final boolean extended, number;
	
	
	RenderInfo(double offsetX, double offsetY, double width, double height, boolean extended, boolean number) {
		this.offsetX = offsetX;
		this.offsetY = offsetY;
		this.width = width;
		this.height = height;
		this.extended = extended;
		this.number = number;
	}
	
	double getOffsetX() {
		return offsetX;
	}
	
	double getOffsetY() {
		return offsetY;
	}
	
	double getWidth() {
		return width;
	}
	
	double getHeight() {
		return height;
	}
	
	boolean isExtended() {
		return extended;
	}
	
	boolean isNumber() {
		return number;
	}
}

//ZSS-827
final class OverrideSetup implements SPrintSetup {
	SPrintSetup _internal;
	Map<String, Object> _data = new HashMap<String, Object>();
	
	public void setInternal(SPrintSetup internal) {
		_internal = internal;
	}
	
	protected SPrintSetup getInternal() {
		if (_internal == null) {
			throw new IllegalStateException("PrintSetup is not ready yet");
		}
		return _internal;
	}
	
	@Override
	public boolean isPrintGridlines() {
		return _data.containsKey("gl") ? 
				((Boolean)_data.get("gl")).booleanValue() 
					: getInternal().isPrintGridlines();
	}

	@Override
	public void setPrintGridlines(boolean enable) {
		_data.put("gl", Boolean.valueOf(enable));
	}

	@Override
	public double getHeaderMargin() {
		return _data.containsKey("hm") ? 
				((Double)_data.get("hm")).doubleValue() 
					: getInternal().getHeaderMargin();
	}

	@Override
	public void setHeaderMargin(double inches) {
		_data.put("hm", Double.valueOf(inches));
	}

	@Override
	public double getFooterMargin() {
		return _data.containsKey("fm") ? 
				((Double)_data.get("fm")).doubleValue() 
					: getInternal().getFooterMargin();
	}

	@Override
	public void setFooterMargin(double inches) {
		_data.put("fm", Double.valueOf(inches));
	}

	@Override
	public double getLeftMargin() {
		return _data.containsKey("lm") ? 
				((Double)_data.get("lm")).doubleValue() 
					: getInternal().getLeftMargin();
	}

	@Override
	public void setLeftMargin(double inches) {
		_data.put("lm", Double.valueOf(inches));
	}

	@Override
	public double getRightMargin() {
		return _data.containsKey("rm") ? 
				((Double)_data.get("rm")).doubleValue() 
					: getInternal().getRightMargin();
	}

	@Override
	public void setRightMargin(double inches) {
		_data.put("rm", Double.valueOf(inches));
	}

	@Override
	public double getTopMargin() {
		return _data.containsKey("tm") ? 
				((Double)_data.get("tm")).doubleValue() 
					: getInternal().getTopMargin();
	}

	@Override
	public void setTopMargin(double inches) {
		_data.put("tm", Double.valueOf(inches));
	}

	@Override
	public double getBottomMargin() {
		return _data.containsKey("bm") ? 
				((Double)_data.get("bm")).doubleValue() 
					: getInternal().getBottomMargin();
	}

	@Override
	public void setBottomMargin(double inches) {
		_data.put("bm", Double.valueOf(inches));
	}

	@Override
	public void setPaperSize(PaperSize size) {
		_data.put("ps", size);
	}

	@Override
	public PaperSize getPaperSize() {
		return _data.containsKey("ps") ? 
				((PaperSize)_data.get("ps")) 
					: getInternal().getPaperSize();
	}

	@Override
	public void setLandscape(boolean landscape) {
		_data.put("ls", Boolean.valueOf(landscape));
	}

	@Override
	public boolean isLandscape() {
		return _data.containsKey("ls") ? 
				((Boolean)_data.get("ls")).booleanValue() 
					: getInternal().isLandscape();
	}

	@Override
	public void setScale(int scale) {
		if (scale < 10 || scale > 400) scale = 100; //ZSS-892
		_data.put("sc", Integer.valueOf(scale));
	}

	@Override
	public int getScale() {
		return _data.containsKey("sc") ? 
				((Integer)_data.get("sc")).intValue() 
					: getInternal().getScale();
	}

	@Override
	public void setHeader(SHeader header) {
		_data.put("hd", header);
	}

	@Override
	public SHeader getHeader() {
		return _data.containsKey("hd") ? 
				((SHeader)_data.get("hd")) 
					: getInternal().getHeader();
	}

	@Override
	public void setEvenHeader(SHeader header) {
		_data.put("ehd", header);
	}

	@Override
	public SHeader getEvenHeader() {
		return _data.containsKey("ehd") ? 
				((SHeader)_data.get("ehd")) 
					: getInternal().getEvenHeader();

	}

	@Override
	public void setFirstHeader(SHeader header) {
		_data.put("fhd", header);
	}

	@Override
	public SHeader getFirstHeader() {
		return _data.containsKey("fhd") ? 
				((SHeader)_data.get("fhd")) 
					: getInternal().getFirstHeader();
	}

	@Override
	public void setFooter(SFooter footer) {
		_data.put("ft", footer);
	}

	@Override
	public SFooter getFooter() {
		return _data.containsKey("ft") ? 
				((SFooter)_data.get("ft")) 
					: getInternal().getFooter();
	}

	@Override
	public void setEvenFooter(SFooter footer) {
		_data.put("eft", footer);
	}

	@Override
	public SFooter getEvenFooter() {
		return _data.containsKey("eft") ? 
				((SFooter)_data.get("eft")) 
					: getInternal().getEvenFooter();
	}

	@Override
	public void setFirstFooter(SFooter footer) {
		_data.put("fft", footer);
	}

	@Override
	public SFooter getFirstFooter() {
		return _data.containsKey("fft") ? 
				((SFooter)_data.get("fft")) 
					: getInternal().getFirstFooter();
	}

	@Override
	public void setDifferentOddEvenPage(boolean flag) {
		_data.put("dp", Boolean.valueOf(flag));
	}

	@Override
	public boolean isDifferentOddEvenPage() {
		return _data.containsKey("dp") ? 
				((Boolean)_data.get("dp")).booleanValue() 
					: getInternal().isDifferentOddEvenPage();
	}

	@Override
	public void setDifferentFirstPage(boolean flag) {
		_data.put("dfp", Boolean.valueOf(flag));
	}

	@Override
	public boolean isDifferentFirstPage() {
		return _data.containsKey("dfp") ? 
				((Boolean)_data.get("dfp")).booleanValue() 
					: getInternal().isDifferentFirstPage();
	}

	@Override
	public void setScaleWithDoc(boolean flag) {
		_data.put("sw", Boolean.valueOf(flag));
	}

	@Override
	public boolean isScaleWithDoc() {
		return _data.containsKey("sw") ? 
				((Boolean)_data.get("sw")).booleanValue() 
					: getInternal().isScaleWithDoc();
	}

	@Override
	public void setAlignWithMargins(boolean flag) {
		_data.put("aw", Boolean.valueOf(flag));
	}

	@Override
	public boolean isAlignWithMargins() {
		return _data.containsKey("aw") ? 
				((Boolean)_data.get("aw")).booleanValue() 
					: getInternal().isAlignWithMargins();
	}

	@Override
	public void setHCenter(boolean center) {
		_data.put("hc", Boolean.valueOf(center));
	}

	@Override
	public boolean isHCenter() {
		return _data.containsKey("hc") ? 
				((Boolean)_data.get("hc")).booleanValue() 
					: getInternal().isHCenter();
	}

	@Override
	public void setVCenter(boolean vcenter) {
		_data.put("vc", Boolean.valueOf(vcenter));
	}

	@Override
	public boolean isVCenter() {
		return _data.containsKey("vc") ? 
				((Boolean)_data.get("vc")).booleanValue() 
					: getInternal().isVCenter();
	}

	@Override
	public void setPageStart(int start) {
		_data.put("pst", Integer.valueOf(start));
	}

	@Override
	public int getPageStart() {
		return _data.containsKey("pst") ? 
				((Integer)_data.get("pst")).intValue() 
					: getInternal().getPageStart();
	}

	@Override
	public void setFitWidth(int numPages) {
		_data.put("fw", Integer.valueOf(numPages));
	}

	@Override
	public int getFitWidth() {
		return _data.containsKey("fw") ? 
				((Integer)_data.get("fw")).intValue() 
					: getInternal().getFitWidth();
	}

	@Override
	public void setFitHeight(int numPages) {
		_data.put("fh", Integer.valueOf(numPages));
	}

	@Override
	public int getFitHeight() {
		return _data.containsKey("fh") ? 
				((Integer)_data.get("fh")).intValue() 
					: getInternal().getFitHeight();
	}

	@Override
	public void setPrintArea(String formula) {
		_data.put("pa", formula);
	}

	@Override
	public String getPrintArea() {
		return _data.containsKey("pa") ? 
				((String)_data.get("pa")) 
					: getInternal().getPrintArea();
	}

	@Override
	public void setRepeatingRowsTitle(int firstRow, int lastRow) {
		_data.put("rr1", Integer.valueOf(firstRow));
		_data.put("rr2", Integer.valueOf(lastRow));
	}

	@Override
	public CellRegion getRepeatingRowsTitle() {
		if (_data.containsKey("rr1")) {
			int rr1 = ((Integer)_data.get("rr1")).intValue();
			int rr2 = ((Integer)_data.get("rr2")).intValue();
			return rr1 >= 0 && rr2 >= rr1 ? 
					new CellRegion(rr1, 0, rr2, 0) : null;
		} else {
			return getInternal().getRepeatingRowsTitle();
		}
	}

	@Override
	public void setRepeatingColumnsTitle(int firstCol, int lastCol) {
		_data.put("rc1", Integer.valueOf(firstCol));
		_data.put("rc2", Integer.valueOf(lastCol));
	}

	@Override
	public CellRegion getRepeatingColumnsTitle() {
		if (_data.containsKey("rc1")) {
			int rc1 = ((Integer)_data.get("rc1")).intValue();
			int rc2 = ((Integer)_data.get("rc2")).intValue();
			return rc1 >= 0 && rc2 >= rc1 ? 
					new CellRegion(0, rc1, 0, rc2) : null; //ZSS-1035
		} else {
			return getInternal().getRepeatingColumnsTitle();
		}
	}

	@Override
	public void setPrintHeadings(boolean flag) {
		_data.put("ph", Boolean.valueOf(flag));
	}

	@Override
	public boolean isPrintHeadings() {
		return _data.containsKey("ph") ?
				((Boolean)_data.get("ph")).booleanValue()
					: getInternal().isPrintHeadings();
	}

	@Override
	public void setCommentsMode(int mode) {
		_data.put("cm", Integer.valueOf(mode));
	}

	@Override
	public int getCommentsMode() {
		return _data.containsKey("cm") ?
				((Integer)_data.get("cm")).intValue()
					: getInternal().getCommentsMode();
	}

	@Override
	public void setErrorPrintMode(int mode) {
		_data.put("ep", Integer.valueOf(mode));
	}

	@Override
	public int getErrorPrintMode() {
		return _data.containsKey("ep") ?
				((Integer)_data.get("ep")).intValue()
					: getInternal().getErrorPrintMode();
	}

	@Override
	public void setLeftToRight(boolean flag) {
		_data.put("lr", Boolean.valueOf(flag));
	}

	@Override
	public boolean isLeftToRight() {
		return _data.containsKey("lr") ?
				((Boolean)_data.get("lr")).booleanValue()
					: getInternal().isLeftToRight();
	}
}

//ZSS-974
//Keep rectangular information in pixels
/*package*/ class PixelInfo {
	public int getOffsetX() {
		return offsetX;
	}

	public int getOffsetY() {
		return offsetY;
	}

	public int getWidth() {
		return width;
	}

	public int getHeight() {
		return height;
	}

	protected final int offsetX, offsetY, width, height;
	
	PixelInfo(int offsetX, int offsetY, int width, int height) {
		this.offsetX = offsetX;
		this.offsetY = offsetY;
		this.width = width;
		this.height = height;
	}
}
