/*
* 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) 2006 - 2013 Pentaho Corporation.. All rights reserved.
*/
package org.pentaho.reporting.engine.classic.core.modules.output.table.xls.helper;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.poi.ss.usermodel.ClientAnchor;
import org.apache.poi.ss.usermodel.Drawing;
import org.apache.poi.ss.usermodel.Picture;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFShape;
import org.pentaho.reporting.engine.classic.core.ElementAlignment;
import org.pentaho.reporting.engine.classic.core.ImageContainer;
import org.pentaho.reporting.engine.classic.core.LocalImageContainer;
import org.pentaho.reporting.engine.classic.core.URLImageContainer;
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.SlimSheetLayout;
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.ImageUtils;
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.base.encoder.UnsupportedEncoderException;
import org.pentaho.reporting.libraries.base.util.ArgumentNullException;
import org.pentaho.reporting.libraries.base.util.StringUtils;
import org.pentaho.reporting.libraries.base.util.WaitingImageObserver;
import org.pentaho.reporting.libraries.resourceloader.Resource;
import org.pentaho.reporting.libraries.resourceloader.ResourceData;
import org.pentaho.reporting.libraries.resourceloader.ResourceException;
import org.pentaho.reporting.libraries.resourceloader.ResourceKey;
import org.pentaho.reporting.libraries.resourceloader.ResourceManager;
/**
* A specialized class containing all image handling functionality for Excel exports.
*/
public class ExcelImageHandler {
private static final Log logger = LogFactory.getLog( ExcelPrinter.class );
private ResourceManager resourceManager;
private ExcelPrinterBase printerBase;
public ExcelImageHandler( final ResourceManager resourceManager, final ExcelPrinterBase printerBase ) {
ArgumentNullException.validate( "resourceManager", resourceManager ); // NON-NLS
ArgumentNullException.validate( "printerBase", printerBase ); // NON-NLS
this.resourceManager = resourceManager;
this.printerBase = printerBase;
}
/**
* Produces the content for image or drawable cells. Excel does not support image-content in cells. Images are
* rendered to an embedded OLE canvas instead, which is then positioned over the cell that would contain the image.
*
* @param layoutContext
* the stylesheet of the render node that produced the image.
* @param image
* the image object
* @param currentLayout
* the current sheet layout containing all row and column breaks
* @param rectangle
* the current cell in grid-coordinates
* @param cellBounds
* the bounds of the cell.
*/
public void createImageCell( final StyleSheet layoutContext, final ImageContainer image,
final SlimSheetLayout currentLayout, TableRectangle rectangle, final StrictBounds cellBounds ) {
try {
if ( rectangle == null ) {
// there was an error while computing the grid-position for this
// element. Evil me...
logger.debug( "Invalid reference: I was not able to compute the rectangle for the content." ); // NON-NLS
return;
}
final boolean shouldScale = layoutContext.getBooleanStyleProperty( ElementStyleKeys.SCALE );
final int imageWidth = image.getImageWidth();
final int imageHeight = image.getImageHeight();
if ( imageWidth < 1 || imageHeight < 1 ) {
return;
}
final double scaleFactor = computeImageScaleFactor();
final ElementAlignment horizontalAlignment =
(ElementAlignment) layoutContext.getStyleProperty( ElementStyleKeys.ALIGNMENT );
final ElementAlignment verticalAlignment =
(ElementAlignment) layoutContext.getStyleProperty( ElementStyleKeys.VALIGNMENT );
final long internalImageWidth = StrictGeomUtility.toInternalValue( scaleFactor * imageWidth );
final long internalImageHeight = StrictGeomUtility.toInternalValue( scaleFactor * imageHeight );
final long cellWidth = cellBounds.getWidth();
final long cellHeight = cellBounds.getHeight();
final StrictBounds cb;
final int pictureId;
try {
if ( shouldScale ) {
final double scaleX;
final double scaleY;
final boolean keepAspectRatio = layoutContext.getBooleanStyleProperty( ElementStyleKeys.KEEP_ASPECT_RATIO );
if ( keepAspectRatio ) {
final double imgScaleFactor =
Math.min( cellWidth / (double) internalImageWidth, cellHeight / (double) internalImageHeight );
scaleX = imgScaleFactor;
scaleY = imgScaleFactor;
} else {
scaleX = cellWidth / (double) internalImageWidth;
scaleY = cellHeight / (double) internalImageHeight;
}
final long clipWidth = (long) ( scaleX * internalImageWidth );
final long clipHeight = (long) ( scaleY * internalImageHeight );
final long alignmentX = RenderUtility.computeHorizontalAlignment( horizontalAlignment, cellWidth, clipWidth );
final long alignmentY = RenderUtility.computeVerticalAlignment( verticalAlignment, cellHeight, clipHeight );
cb =
new StrictBounds( cellBounds.getX() + alignmentX, cellBounds.getY() + alignmentY, Math.min( clipWidth,
cellWidth ), Math.min( clipHeight, cellHeight ) );
// Recompute the cells that this image will cover (now that it has been resized)
rectangle = currentLayout.getTableBounds( cb, rectangle );
pictureId = loadImage( image );
if ( printerBase.isUseXlsxFormat() ) {
if ( pictureId < 0 ) {
return;
}
} else if ( pictureId <= 0 ) {
return;
}
} else {
// unscaled ..
if ( internalImageWidth <= cellWidth && internalImageHeight <= cellHeight ) {
// No clipping needed.
final long alignmentX =
RenderUtility.computeHorizontalAlignment( horizontalAlignment, cellBounds.getWidth(),
internalImageWidth );
final long alignmentY =
RenderUtility.computeVerticalAlignment( verticalAlignment, cellBounds.getHeight(), internalImageHeight );
cb =
new StrictBounds( cellBounds.getX() + alignmentX, cellBounds.getY() + alignmentY, internalImageWidth,
internalImageHeight );
// Recompute the cells that this image will cover (now that it has been resized)
rectangle = currentLayout.getTableBounds( cb, rectangle );
pictureId = loadImage( image );
if ( printerBase.isUseXlsxFormat() ) {
if ( pictureId < 0 ) {
return;
}
} else if ( pictureId <= 0 ) {
return;
}
} else {
// at least somewhere there is clipping needed.
final long clipWidth = Math.min( cellWidth, internalImageWidth );
final long clipHeight = Math.min( cellHeight, internalImageHeight );
final long alignmentX =
RenderUtility.computeHorizontalAlignment( horizontalAlignment, cellBounds.getWidth(), clipWidth );
final long alignmentY =
RenderUtility.computeVerticalAlignment( verticalAlignment, cellBounds.getHeight(), clipHeight );
cb =
new StrictBounds( cellBounds.getX() + alignmentX, cellBounds.getY() + alignmentY, clipWidth, clipHeight );
// Recompute the cells that this image will cover (now that it has been resized)
rectangle = currentLayout.getTableBounds( cb, rectangle );
pictureId = loadImageWithClipping( image, clipWidth, clipHeight, scaleFactor );
if ( printerBase.isUseXlsxFormat() ) {
if ( pictureId < 0 ) {
return;
}
} else if ( pictureId <= 0 ) {
return;
}
}
}
} catch ( final UnsupportedEncoderException uee ) {
// should not happen, as PNG is always supported.
logger.warn( "Assertation-Failure: PNG encoding failed.", uee ); // NON-NLS
return;
}
final ClientAnchor anchor = computeClientAnchor( currentLayout, rectangle, cb );
Drawing patriarch = printerBase.getDrawingPatriarch();
final Picture picture = patriarch.createPicture( anchor, pictureId );
logger.info( String.format( "Created image: %d => %s", pictureId, picture ) ); // NON-NLS
} catch ( final IOException e ) {
logger.warn( "Failed to add image. Ignoring.", e ); // NON-NLS
}
}
private double computeImageScaleFactor() {
OutputProcessorMetaData metaData = printerBase.getMetaData();
final double scaleFactor;
final double devResolution = metaData.getNumericFeatureValue( OutputProcessorFeature.DEVICE_RESOLUTION );
if ( metaData.isFeatureSupported( OutputProcessorFeature.IMAGE_RESOLUTION_MAPPING ) ) {
if ( devResolution != 72.0 && devResolution > 0 ) {
// Need to scale the device to its native resolution before attempting to draw the image..
scaleFactor = ( 72.0 / devResolution );
} else {
scaleFactor = 1;
}
} else {
scaleFactor = 1;
}
return scaleFactor;
}
protected ClientAnchor computeClientAnchor( final SlimSheetLayout currentLayout, final TableRectangle rectangle,
final StrictBounds cb ) {
if ( printerBase.isUseXlsxFormat() ) {
return computeExcel2003ClientAnchor( currentLayout, rectangle, cb );
} else {
return computeExcel97ClientAnchor( currentLayout, rectangle, cb );
}
}
protected ClientAnchor computeExcel97ClientAnchor( final SlimSheetLayout currentLayout,
final TableRectangle rectangle, final StrictBounds cb ) {
final int cell1x = rectangle.getX1();
final int cell1y = rectangle.getY1();
final int cell2x = Math.max( cell1x, rectangle.getX2() - 1 );
final int cell2y = Math.max( cell1y, rectangle.getY2() - 1 );
final long cell1width = currentLayout.getCellWidth( cell1x );
final long cell1height = currentLayout.getRowHeight( cell1y );
final long cell2width = currentLayout.getCellWidth( cell2x );
final long cell2height = currentLayout.getRowHeight( cell2y );
final long cell1xPos = currentLayout.getXPosition( cell1x );
final long cell1yPos = currentLayout.getYPosition( cell1y );
final long cell2xPos = currentLayout.getXPosition( cell2x );
final long cell2yPos = currentLayout.getYPosition( cell2y );
final int dx1 = (int) ( 1023 * ( ( cb.getX() - cell1xPos ) / (double) cell1width ) );
final int dy1 = (int) ( 255 * ( ( cb.getY() - cell1yPos ) / (double) cell1height ) );
final int dx2 = (int) ( 1023 * ( ( cb.getX() + cb.getWidth() - cell2xPos ) / (double) cell2width ) );
final int dy2 = (int) ( 255 * ( ( cb.getY() + cb.getHeight() - cell2yPos ) / (double) cell2height ) );
final ClientAnchor anchor = printerBase.getWorkbook().getCreationHelper().createClientAnchor();
anchor.setDx1( dx1 );
anchor.setDy1( dy1 );
anchor.setDx2( dx2 );
anchor.setDy2( dy2 );
anchor.setCol1( cell1x );
anchor.setRow1( cell1y );
anchor.setCol2( cell2x );
anchor.setRow2( cell2y );
anchor.setAnchorType( ClientAnchor.MOVE_DONT_RESIZE );
return anchor;
}
protected ClientAnchor computeExcel2003ClientAnchor( final SlimSheetLayout currentLayout,
final TableRectangle rectangle, final StrictBounds cb ) {
final int cell1x = rectangle.getX1();
final int cell1y = rectangle.getY1();
final int cell2x = Math.max( cell1x, rectangle.getX2() - 1 );
final int cell2y = Math.max( cell1y, rectangle.getY2() - 1 );
final long cell1xPos = currentLayout.getXPosition( cell1x );
final long cell1yPos = currentLayout.getYPosition( cell1y );
final long cell2xPos = currentLayout.getXPosition( cell2x );
final long cell2yPos = currentLayout.getYPosition( cell2y );
final int dx1 = (int) StrictGeomUtility.toExternalValue( ( cb.getX() - cell1xPos ) * XSSFShape.EMU_PER_POINT );
final int dy1 = (int) StrictGeomUtility.toExternalValue( ( cb.getY() - cell1yPos ) * XSSFShape.EMU_PER_POINT );
final int dx2 =
(int) Math.max( 0, StrictGeomUtility.toExternalValue( ( cb.getX() + cb.getWidth() - cell2xPos )
* XSSFShape.EMU_PER_POINT ) );
final int dy2 =
(int) Math.max( 0, StrictGeomUtility.toExternalValue( ( cb.getY() + cb.getHeight() - cell2yPos )
* XSSFShape.EMU_PER_POINT ) );
final ClientAnchor anchor = printerBase.getWorkbook().getCreationHelper().createClientAnchor();
anchor.setDx1( dx1 );
anchor.setDy1( dy1 );
anchor.setDx2( dx2 );
anchor.setDy2( dy2 );
anchor.setCol1( cell1x );
anchor.setRow1( cell1y );
anchor.setCol2( cell2x );
anchor.setRow2( cell2y );
anchor.setAnchorType( ClientAnchor.MOVE_DONT_RESIZE );
return anchor;
}
private int getImageFormat( final ResourceKey key ) {
final URL url = resourceManager.toURL( key );
if ( url == null ) {
return -1;
}
final String file = url.getFile();
if ( StringUtils.endsWithIgnoreCase( file, ".png" ) ) { // NON-NLS
return Workbook.PICTURE_TYPE_PNG;
}
if ( StringUtils.endsWithIgnoreCase( file, ".jpg" ) || // NON-NLS
StringUtils.endsWithIgnoreCase( file, ".jpeg" ) ) { // NON-NLS
return Workbook.PICTURE_TYPE_JPEG;
}
if ( StringUtils.endsWithIgnoreCase( file, ".bmp" ) || // NON-NLS
StringUtils.endsWithIgnoreCase( file, ".ico" ) ) { // NON-NLS
return Workbook.PICTURE_TYPE_DIB;
}
return -1;
}
private int loadImageWithClipping( final ImageContainer reference, final long clipWidth, final long clipHeight,
final double deviceScaleFactor ) throws IOException, UnsupportedEncoderException {
Image image = null;
// The image has an assigned URL ...
if ( reference instanceof URLImageContainer ) {
final URLImageContainer urlImage = (URLImageContainer) reference;
final ResourceKey url = urlImage.getResourceKey();
// if we have an source to load the image data from ..
if ( url != null && urlImage.isLoadable() ) {
if ( reference instanceof LocalImageContainer ) {
final LocalImageContainer li = (LocalImageContainer) reference;
image = li.getImage();
}
if ( image == null ) {
try {
final Resource resource = resourceManager.create( url, null, Image.class );
image = (Image) resource.getResource();
} catch ( final ResourceException e ) {
// ignore.
}
}
}
}
if ( reference instanceof LocalImageContainer ) {
// Check, whether the imagereference contains an AWT image.
// if so, then we can use that image instance for the recoding
final LocalImageContainer li = (LocalImageContainer) reference;
if ( image == null ) {
image = li.getImage();
}
}
if ( image != null ) {
// now encode the image. We don't need to create digest data
// for the image contents, as the image is perfectly identifyable
// by its URL
return clipAndEncodeImage( image, clipWidth, clipHeight, deviceScaleFactor );
}
return -1;
}
private int
clipAndEncodeImage( final Image image, final long width, final long height, final double deviceScaleFactor )
throws UnsupportedEncoderException, IOException {
final int imageWidth = (int) StrictGeomUtility.toExternalValue( width );
final int imageHeight = (int) StrictGeomUtility.toExternalValue( height );
// first clip.
final BufferedImage bi = ImageUtils.createTransparentImage( imageWidth, imageHeight );
final Graphics2D graphics = (Graphics2D) bi.getGraphics();
graphics.scale( deviceScaleFactor, deviceScaleFactor );
if ( image instanceof BufferedImage ) {
if ( graphics.drawImage( image, null, null ) == false ) {
logger.debug( "Failed to render the image. This should not happen for BufferedImages" ); // NON-NLS
}
} else {
final WaitingImageObserver obs = new WaitingImageObserver( image );
obs.waitImageLoaded();
while ( graphics.drawImage( image, null, obs ) == false ) {
obs.waitImageLoaded();
if ( obs.isError() ) {
logger.warn( "Error while loading the image during the rendering." ); // NON-NLS
break;
}
}
}
graphics.dispose();
final byte[] data = RenderUtility.encodeImage( bi );
return printerBase.getWorkbook().addPicture( data, Workbook.PICTURE_TYPE_PNG );
}
private int loadImage( final ImageContainer reference ) throws IOException, UnsupportedEncoderException {
final Workbook workbook = printerBase.getWorkbook();
Image image = null;
// The image has an assigned URL ...
if ( reference instanceof URLImageContainer ) {
final URLImageContainer urlImage = (URLImageContainer) reference;
final ResourceKey url = urlImage.getResourceKey();
// if we have an source to load the image data from ..
if ( url != null && urlImage.isLoadable() ) {
// and the image is one of the supported image formats ...
// we we can embedd it directly ...
final int format = getImageFormat( url );
if ( format == -1 ) {
// This is a unsupported image format.
if ( reference instanceof LocalImageContainer ) {
final LocalImageContainer li = (LocalImageContainer) reference;
image = li.getImage();
}
if ( image == null ) {
try {
final Resource resource = resourceManager.create( url, null, Image.class );
image = (Image) resource.getResource();
} catch ( final ResourceException re ) {
logger.info( "Failed to load image from URL " + url, re ); // NON-NLS
}
}
} else {
try {
final ResourceData data = resourceManager.load( url );
// create the image
return workbook.addPicture( data.getResource( resourceManager ), format );
} catch ( final ResourceException re ) {
logger.info( "Failed to load image from URL " + url, re ); // NON-NLS
}
}
}
}
if ( reference instanceof LocalImageContainer ) {
// Check, whether the imagereference contains an AWT image.
// if so, then we can use that image instance for the recoding
final LocalImageContainer li = (LocalImageContainer) reference;
if ( image == null ) {
image = li.getImage();
}
}
if ( image != null ) {
// now encode the image. We don't need to create digest data
// for the image contents, as the image is perfectly identifyable
// by its URL
final byte[] data = RenderUtility.encodeImage( image );
return workbook.addPicture( data, Workbook.PICTURE_TYPE_PNG );
}
return -1;
}
}