/*
* 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;
}
}