/* * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software * Foundation. * * You should have received a copy of the GNU Lesser General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * Copyright (c) 2001 - 2016 Object Refinery Ltd, Pentaho Corporation and Contributors.. All rights reserved. */ package org.pentaho.reporting.engine.classic.core.modules.output.table.xls.helper; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.util.CellRangeAddress; import org.pentaho.reporting.engine.classic.core.AttributeNames; import org.pentaho.reporting.engine.classic.core.DefaultImageReference; import org.pentaho.reporting.engine.classic.core.ImageContainer; import org.pentaho.reporting.engine.classic.core.InvalidReportStateException; import org.pentaho.reporting.engine.classic.core.layout.model.LogicalPageBox; import org.pentaho.reporting.engine.classic.core.layout.model.RenderBox; import org.pentaho.reporting.engine.classic.core.layout.model.RenderNode; import org.pentaho.reporting.engine.classic.core.layout.output.LogicalPageKey; import org.pentaho.reporting.engine.classic.core.layout.output.OutputProcessorFeature; import org.pentaho.reporting.engine.classic.core.layout.output.OutputProcessorMetaData; import org.pentaho.reporting.engine.classic.core.layout.output.RenderUtility; import org.pentaho.reporting.engine.classic.core.modules.output.table.base.AbstractTableOutputProcessor; import org.pentaho.reporting.engine.classic.core.modules.output.table.base.CellBackground; import org.pentaho.reporting.engine.classic.core.modules.output.table.base.CellBackgroundProducer; import org.pentaho.reporting.engine.classic.core.modules.output.table.base.CellMarker; import org.pentaho.reporting.engine.classic.core.modules.output.table.base.SheetLayout; import org.pentaho.reporting.engine.classic.core.modules.output.table.base.TableContentProducer; import org.pentaho.reporting.engine.classic.core.modules.output.table.base.TableRectangle; import org.pentaho.reporting.engine.classic.core.style.ElementStyleKeys; import org.pentaho.reporting.engine.classic.core.style.StyleSheet; import org.pentaho.reporting.engine.classic.core.util.RotatedTextDrawable; import org.pentaho.reporting.engine.classic.core.util.geom.StrictBounds; import org.pentaho.reporting.engine.classic.core.util.geom.StrictGeomUtility; import org.pentaho.reporting.libraries.resourceloader.ResourceManager; import org.pentaho.reporting.libraries.resourceloader.factory.drawable.DrawableWrapper; import java.awt.Image; import java.awt.Shape; import java.io.IOException; import java.io.OutputStream; public class ExcelPrinter extends ExcelPrinterBase { private static final Log logger = LogFactory.getLog( ExcelPrinter.class ); private OutputStream outputStream; private Workbook workbook; private Sheet sheet; private CellBackgroundProducer cellBackgroundProducer; private ExcelTextExtractor textExtractor; private ResourceManager resourceManager; public ExcelPrinter( final OutputStream outputStream, final ResourceManager resourceManager ) { if ( outputStream == null ) { throw new NullPointerException(); } if ( resourceManager == null ) { throw new NullPointerException(); } this.resourceManager = resourceManager; this.outputStream = outputStream; } public void init( final OutputProcessorMetaData metaData ) { if ( metaData == null ) { throw new NullPointerException(); } super.init( metaData, resourceManager ); this.cellBackgroundProducer = new CellBackgroundProducer( metaData .isFeatureSupported( AbstractTableOutputProcessor.TREAT_ELLIPSE_AS_RECTANGLE ), metaData .isFeatureSupported( OutputProcessorFeature.UNALIGNED_PAGEBANDS ) ); } public Sheet getSheet() { return sheet; } public void print( final LogicalPageKey logicalPageKey, final LogicalPageBox logicalPage, final TableContentProducer contentProducer, final boolean incremental ) { if ( workbook == null ) { workbook = createWorkbook(); initializeStyleProducers( workbook ); this.textExtractor = new ExcelTextExtractor( getMetaData(), getFontColorProducer(), workbook.getCreationHelper(), getCellStyleProducer().getFontFactory() ); } final int startRow = contentProducer.getFinishedRows(); final int finishRow = contentProducer.getFilledRows(); if ( incremental && startRow == finishRow ) { return; } if ( sheet == null ) { // make sure a new patriarch is created if needed. sheet = openSheet( contentProducer.getSheetName() ); final SheetPropertySource excelTableContentProducer = (SheetPropertySource) contentProducer; configureSheetProperties( sheet, excelTableContentProducer ); // Start a new page. configureSheetPaperSize( sheet, logicalPage.getPageGrid().getPage( 0, 0 ) ); configureSheetColumnWidths( sheet, contentProducer.getSheetLayout(), contentProducer.getColumnCount() ); } // and finally the content .. final SheetLayout sheetLayout = contentProducer.getSheetLayout(); final int colCount = sheetLayout.getColumnCount(); for ( int row = startRow; row < finishRow; row++ ) { final Row hssfRow = getRowAt( row ); final double lastRowHeight = StrictGeomUtility.toExternalValue( sheetLayout.getRowHeight( row ) ); hssfRow.setHeightInPoints( (float) ( lastRowHeight ) ); for ( int col = 0; col < colCount; col++ ) { final CellMarker.SectionType sectionType = contentProducer.getSectionType( row, col ); final RenderBox content = contentProducer.getContent( row, col ); if ( content == null ) { final RenderBox backgroundBox = contentProducer.getBackground( row, col ); final CellBackground background; if ( backgroundBox != null ) { background = cellBackgroundProducer.getBackgroundForBox( logicalPage, sheetLayout, col, row, 1, 1, true, sectionType, backgroundBox ); } else { background = cellBackgroundProducer.getBackgroundAt( logicalPage, sheetLayout, col, row, true, sectionType ); } if ( background == null ) { if ( row == 0 && col == 0 ) { // create a single cell, so that we dont run into nullpointer inside POI.. getCellAt( col, row ); } // An empty cell .. ignore continue; } // A empty cell with a defined background .. final Cell cell = getCellAt( col, row ); final CellStyle style = getCellStyleProducer().createCellStyle( null, null, background ); if ( style != null ) { cell.setCellStyle( style ); } continue; } if ( content.isCommited() == false ) { throw new InvalidReportStateException( "Uncommited content encountered" ); } final long contentOffset = contentProducer.getContentOffset( row, col ); final TableRectangle rectangle = sheetLayout.getTableBounds( content.getX(), content.getY() + contentOffset, content.getWidth(), content .getHeight(), null ); if ( rectangle.isOrigin( col, row ) == false ) { // A spanned cell .. continue; } final CellBackground fastBackground = cellBackgroundProducer.getBackgroundForBox( logicalPage, sheetLayout, rectangle.getX1(), rectangle.getY1(), rectangle.getColumnSpan(), rectangle.getRowSpan(), false, sectionType, content ); // export the cell and all content .. final Cell cell = getCellAt( col, row ); final CellStyle style = getCellStyleProducer().createCellStyle( content.getInstanceId(), content.getStyleSheet(), fastBackground ); if ( style != null ) { cell.setCellStyle( style ); } if ( applyCellValue( content, cell, sheetLayout, rectangle, contentOffset ) ) { mergeCellRegion( rectangle, row, col, sheetLayout, logicalPage, content, contentProducer ); } content.setFinishedTable( true ); } } if ( incremental == false ) { // cleanup .. sheet = null; } } private void mergeCellRegion( final TableRectangle rectangle, final int row, final int col, final SheetLayout sheetLayout, final LogicalPageBox logicalPage, final RenderBox content, final TableContentProducer contentProducer ) { if ( content == null ) { throw new NullPointerException(); } final int rowSpan = rectangle.getRowSpan(); final int columnSpan = rectangle.getColumnSpan(); if ( rowSpan <= 1 && columnSpan <= 1 ) { return; } sheet.addMergedRegion( new CellRangeAddress( row, ( row + rowSpan - 1 ), col, ( col + columnSpan - 1 ) ) ); final int rectX = rectangle.getX1(); final int rectY = rectangle.getY1(); for ( int spannedRow = 0; spannedRow < rowSpan; spannedRow += 1 ) { for ( int spannedCol = 0; spannedCol < columnSpan; spannedCol += 1 ) { final CellMarker.SectionType sectionType = contentProducer.getSectionType( row, col ); final CellBackground bg = cellBackgroundProducer.getBackgroundForBox( logicalPage, sheetLayout, rectX + spannedCol, rectY + spannedRow, 1, 1, false, sectionType, content ); final Cell regionCell = getCellAt( ( col + spannedCol ), row + spannedRow ); final CellStyle spannedStyle = getCellStyleProducer().createCellStyle( content.getInstanceId(), content.getStyleSheet(), bg ); if ( spannedStyle != null ) { regionCell.setCellStyle( spannedStyle ); } } } } /** * Applies the cell value and determines whether the cell should be merged. Merging will only take place if the cell * has a row or colspan greater than one. Images will never be merged, as image content is rendered into an anchored * frame on top of the cells. * * @param content * @param cell * @param sheetLayout * @param rectangle * @return true, if the cell may to be put into a merged region, false otherwise. */ private boolean applyCellValue( final RenderBox content, final Cell cell, final SheetLayout sheetLayout, final TableRectangle rectangle, final long contentOffset ) { final Object value = textExtractor.compute( content ); if ( handleImageValues( content, sheetLayout, rectangle, contentOffset, value ) ) { return false; } final String linkTarget = (String) content.getStyleSheet().getStyleProperty( ElementStyleKeys.HREF_TARGET ); if ( linkTarget != null ) { // this may be wrong if we have quotes inside. We should escape them .. final RotatedTextDrawable extracted = RotatedTextDrawable.extract( value ); final String linkText = extracted == null ? textExtractor.getText() : extracted.getText(); final String formula = "HYPERLINK(" + splitAndQuoteExcelFormula( linkTarget ) + "," + splitAndQuoteExcelFormula( linkText ) + ")"; if ( formula.length() < 1024 ) { cell.setCellFormula( formula ); return true; } ExcelPrinter.logger .warn( "Excel-Cells cannot contain formulas longer than 1023 characters. Converting hyperlink into plain text" ); } final Object attr1 = content.getAttributes().getAttribute( AttributeNames.Excel.NAMESPACE, AttributeNames.Excel.FIELD_FORMULA ); if ( attr1 != null ) { final String formula = String.valueOf( attr1 ); if ( formula.length() < 1024 ) { cell.setCellFormula( formula ); return true; } ExcelPrinter.logger .warn( "Excel-Cells cannot contain formulas longer than 1023 characters. Converting excel formula into plain text" ); } handleValueType( cell, value, workbook ); return true; } private boolean handleImageValues( final RenderBox content, final SheetLayout sheetLayout, final TableRectangle rectangle, final long contentOffset, final Object value ) { if ( value instanceof RotatedTextDrawable ) { return false; } if ( value instanceof DrawableWrapper && ( (DrawableWrapper) value ).getBackend() instanceof RotatedTextDrawable ) { return false; } if ( value instanceof Image ) { try { final ImageContainer imageContainer = new DefaultImageReference( (Image) value ); final StyleSheet rawSource = textExtractor.getRawSource().getStyleSheet(); final StrictBounds contentBounds = new StrictBounds( content.getX(), content.getY() + contentOffset, content.getWidth(), content.getHeight() ); createImageCell( rawSource, imageContainer, sheetLayout, rectangle, contentBounds ); } catch ( IOException ioe ) { // Should not happen. ExcelPrinter.logger.warn( "Failed to process AWT-Image in Excel-Export", ioe ); } return true; } else if ( value instanceof ImageContainer ) { final ImageContainer imageContainer = (ImageContainer) value; // todo: this is wrong .. final StyleSheet rawSource = textExtractor.getRawSource().getStyleSheet(); final StrictBounds contentBounds = new StrictBounds( content.getX(), content.getY() + contentOffset, content.getWidth(), content.getHeight() ); createImageCell( rawSource, imageContainer, sheetLayout, rectangle, contentBounds ); return true; } else if ( value instanceof DrawableWrapper ) { final DrawableWrapper drawable = (DrawableWrapper) value; final RenderNode rawSource = textExtractor.getRawSource(); final StrictBounds contentBounds = new StrictBounds( rawSource.getX(), rawSource.getY() + contentOffset, rawSource.getWidth(), rawSource .getHeight() ); final ImageContainer imageFromDrawable = RenderUtility.createImageFromDrawable( drawable, contentBounds, content, getMetaData() ); createImageCell( rawSource.getStyleSheet(), imageFromDrawable, sheetLayout, rectangle, contentBounds ); return true; } else if ( value instanceof Shape ) { // We *could* do this as well ... but for now we dont. return true; } return false; } public void close() { final long start = System.currentTimeMillis(); logger.info( "Closing workbook and writing content to disk." ); if ( workbook != null ) { try { workbook.write( outputStream ); // cleanup.. sheet = null; outputStream.flush(); } catch ( IOException e ) { ExcelPrinter.logger.warn( "could not write xls data. Message:", e ); } finally { workbook = null; } } final long end = System.currentTimeMillis(); logger.info( "Closing workbook finished in " + ( ( end - start ) / 1000f ) + "s" ); } public Workbook getWorkbook() { return workbook; } }