/*
* 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 - 2013 Object Refinery Ltd, Pentaho Corporation and Contributors.. All rights reserved.
*/
package org.pentaho.reporting.libraries.pixie.wmf;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.pentaho.reporting.libraries.base.util.FastStack;
import org.pentaho.reporting.libraries.pixie.wmf.records.CommandFactory;
import org.pentaho.reporting.libraries.pixie.wmf.records.MfCmd;
import org.pentaho.reporting.libraries.pixie.wmf.records.MfCmdSetWindowExt;
import org.pentaho.reporting.libraries.pixie.wmf.records.MfCmdSetWindowOrg;
import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
/**
* Parses and replays the WmfFile.
*/
public class WmfFile {
private static final Log logger = LogFactory.getLog( WmfFile.class );
public static final int QUALITY_NO = 0; // Can't convert.
public static final int QUALITY_MAYBE = 1; // Might be able to convert.
public static final int QUALITY_YES = 2; // Can convert.
// Maximal picture size is 1200x1200. A average wmf file scales easily
// to 20000 and more, so we have to limit the pixel image's size.
private static final int MAX_PICTURE_SIZE = getMaxPictureSize();
private static int getMaxPictureSize() {
return 1200;
}
private WmfObject[] objects;
private FastStack dcStack;
private MfPalette palette;
//private String inName;
private InputStream in;
private MfHeader header;
private int fileSize;
private int filePos;
private ArrayList records;
private Graphics2D graphics;
private int maxWidth;
private int maxHeight;
private int imageWidth;
private int imageHeight;
private int minX;
private int minY;
private int imageX;
private int imageY;
/**
* Initialize metafile for reading from an URL. Width and height will be computed automatically.
*
* @param input the URL from where to read.
* @throws IOException if any other error occured.
*/
public WmfFile( final URL input )
throws IOException {
this( input, -1, -1 );
}
/**
* Initialize metafile for reading from file. Width and height will be computed automatically.
*
* @param input the name of the file from where to read.
* @throws IOException if any other error occured.
*/
public WmfFile( final String input )
throws IOException {
this( input, -1, -1 );
}
/**
* Initialize metafile for reading from an URL.
*
* @param imageWidth the target width of the image or -1 for automatic mode.
* @param imageHeight the target height of the image or -1 for automatic mode.
* @param input the URL from where to read.
* @throws IOException if any other error occured.
*/
public WmfFile( final URL input, final int imageWidth, final int imageHeight )
throws IOException {
this( new BufferedInputStream( input.openStream() ), imageWidth, imageHeight );
}
/**
* Initialize metafile for reading from filename.
*
* @param imageWidth the target width of the image or -1 for automatic mode.
* @param imageHeight the target height of the image or -1 for automatic mode.
* @param inName the file name from where to read.
* @throws FileNotFoundException if the file was not found.
* @throws IOException if any other error occured.
*/
public WmfFile( final String inName, final int imageWidth, final int imageHeight )
throws FileNotFoundException, IOException {
this( new BufferedInputStream( new FileInputStream( inName ) ), imageWidth, imageHeight );
}
/**
* Initialize metafile for reading from the given input stream.
*
* @param imageWidth the target width of the image or -1 for automatic mode.
* @param imageHeight the target height of the image or -1 for automatic mode.
* @param in the stream from where to read.
* @throws IOException if any other error occured.
*/
public WmfFile( final InputStream in, final int imageWidth,
final int imageHeight )
throws IOException {
this.in = in;
this.imageWidth = imageWidth;
this.imageHeight = imageHeight;
records = new ArrayList();
dcStack = new FastStack( 100 );
palette = new MfPalette();
readHeader();
parseRecords();
resetStates();
}
public Dimension getImageSize() {
return new Dimension( maxWidth, maxHeight );
}
private void resetStates() {
Arrays.fill( objects, null );
dcStack.clear();
dcStack.push( new MfDcState( this ) );
}
public MfPalette getPalette() {
return palette;
}
/**
* Return Placeable and Windows headers that were read earlier.
*
* @return the meta-file header.
*/
public MfHeader getHeader() {
return header;
}
public Graphics2D getGraphics2D() {
return graphics;
}
/**
* Check class invariant.
*/
private void assertValid() {
if ( filePos < 0 || filePos > fileSize ) {
throw new IllegalStateException( "WmfFile is not valid" );
}
}
/**
* Read Placeable and Windows headers.
*/
private MfHeader readHeader()
throws IOException {
header = new MfHeader();
header.read( in );
if ( header.isValid() ) {
fileSize = header.getFileSize();
objects = new WmfObject[ header.getObjectsSize() ];
filePos = header.getHeaderSize();
return header;
} else {
throw new IOException( "The given file is not a real metafile" );
}
}
/**
* Fetch a record.
*
* @return the next record read or null, if the end-of-file has been reached.
* @throws IOException if an IO-Error occurs.
*/
private MfRecord readNextRecord()
throws IOException {
if ( filePos >= fileSize ) {
return null;
}
assertValid();
final MfRecord record = new MfRecord( in );
filePos += record.getLength();
return record;
}
/**
* Read and interpret the body of the metafile.
*
* @throws IOException if an IO-Error occurs.
*/
private void parseRecords() throws IOException {
minX = Integer.MAX_VALUE;
minY = Integer.MAX_VALUE;
maxWidth = 0;
maxHeight = 0;
final CommandFactory cmdFactory = CommandFactory.getInstance();
MfRecord mf;
while ( ( mf = readNextRecord() ) != null ) {
final MfCmd cmd = cmdFactory.getCommand( mf.getType() );
if ( cmd == null ) {
logger.info( "Failed to parse record " + mf.getType() );
} else {
cmd.setRecord( mf );
if ( cmd.getFunction() == MfType.SET_WINDOW_ORG ) {
final MfCmdSetWindowOrg worg = (MfCmdSetWindowOrg) cmd;
final Point p = worg.getTarget();
minX = Math.min( p.x, minX );
minY = Math.min( p.y, minY );
} else if ( cmd.getFunction() == MfType.SET_WINDOW_EXT ) {
final MfCmdSetWindowExt worg = (MfCmdSetWindowExt) cmd;
final Dimension d = worg.getDimension();
maxWidth = Math.max( maxWidth, d.width );
maxHeight = Math.max( maxHeight, d.height );
}
records.add( cmd );
}
}
in.close();
in = null;
// make sure that we don't have invalid values in case no
// setWindow records were found ...
if ( minX == Integer.MAX_VALUE ) {
minX = 0;
}
if ( minY == Integer.MAX_VALUE ) {
minY = 0;
}
//System.out.println(records.size() + " records read");
//System.out.println("Image Extends: " + maxWidth + " " + maxHeight);
if ( imageWidth < 1 || imageHeight < 1 ) {
scaleToFit( MAX_PICTURE_SIZE, MAX_PICTURE_SIZE );
} else {
scaleToFit( imageWidth, imageHeight );
}
}
/**
* Scales the WMF-image to the given width and height while preserving the aspect ration.
*
* @param fitWidth the target width.
* @param fitHeight the target height.
*/
public void scaleToFit( final float fitWidth, final float fitHeight ) {
final float percentX = ( fitWidth * 100 ) / maxWidth;
final float percentY = ( fitHeight * 100 ) / maxHeight;
scalePercent( percentX < percentY ? percentX : percentY );
}
/**
* Scale the image to a certain percentage.
*
* @param percent the scaling percentage <!-- Yes, this is from iText lib -->
*/
public void scalePercent( final float percent ) {
scalePercent( percent, percent );
}
/**
* Scale the width and height of an image to a certain percentage.
*
* @param percentX the scaling percentage of the width
* @param percentY the scaling percentage of the height <!-- Yes, this is from iText lib -->
*/
public void scalePercent( final float percentX, final float percentY ) {
imageWidth = (int) ( ( maxWidth * percentX ) / 100f );
imageHeight = (int) ( ( maxHeight * percentY ) / 100f );
imageX = (int) ( ( minX * percentX ) / 100f );
imageY = (int) ( ( minY * percentY ) / 100f );
}
public static void main( final String[] args )
throws Exception {
final WmfFile wmf = new WmfFile( "./head/pixie/res/a0.wmf", 800, 600 );
wmf.replay();
// System.out.println(wmf.imageWidth + ", " + wmf.imageHeight);
}
public MfDcState getCurrentState() {
return (MfDcState) dcStack.peek();
}
// pushes a state on the stack
public void saveDCState() {
final MfDcState currentState = getCurrentState();
dcStack.push( new MfDcState( currentState ) );
}
public int getStateCount() {
return dcStack.size();
}
/**
* Restores a state. The stateCount specifies the number of states to discard to find the correct one.
*
* @param stateCount the state count.
*/
public void restoreDCState( final int stateCount ) {
if ( ( stateCount > 0 ) == false ) {
throw new IllegalArgumentException();
}
// this is contrary to Caolans description of the WMF file format, but
// Batik also ignores the stateCount parameter.
dcStack.pop();
getCurrentState().restoredState();
}
/**
* Return the next free slot from the objects table.
*
* @return the next new free slot in the objects-registry
*/
protected int findFreeSlot() {
for ( int slot = 0; slot < objects.length; slot++ ) {
if ( objects[ slot ] == null ) {
return slot;
}
}
throw new IllegalStateException( "No free slot" );
}
public void storeObject( final WmfObject o ) {
final int idx = findFreeSlot();
objects[ idx ] = o;
}
public void deleteObject( final int slot ) {
if ( ( slot < 0 ) || ( slot >= objects.length ) ) {
throw new IllegalArgumentException( "Range violation" );
}
objects[ slot ] = null;
}
public WmfObject getObject( final int slot ) {
if ( ( slot < 0 ) || ( slot >= objects.length ) ) {
throw new IllegalStateException( "Range violation" );
}
return objects[ slot ];
}
public MfLogBrush getBrushObject( final int slot ) {
final WmfObject obj = getObject( slot );
if ( obj.getType() == WmfObject.OBJ_BRUSH ) {
return (MfLogBrush) obj;
}
throw new IllegalStateException( "Object " + slot + " was no brush" );
}
public MfLogPen getPenObject( final int slot ) {
final WmfObject obj = getObject( slot );
if ( obj.getType() == WmfObject.OBJ_PEN ) {
return (MfLogPen) obj;
}
throw new IllegalStateException( "Object " + slot + " was no pen" );
}
public MfLogRegion getRegionObject( final int slot ) {
final WmfObject obj = getObject( slot );
if ( obj.getType() == WmfObject.OBJ_REGION ) {
return (MfLogRegion) obj;
}
throw new IllegalStateException( "Object " + slot + " was no region" );
}
public synchronized BufferedImage replay() {
return replay( imageWidth, imageHeight );
}
public synchronized BufferedImage replay( final int imageX, final int imageY ) {
final BufferedImage image = new BufferedImage( imageX, imageY, BufferedImage.TYPE_INT_ARGB );
final Graphics2D graphics = image.createGraphics();
// clear the image area ...
graphics.setPaint( new Color( 0, 0, 0, 0 ) );
graphics.fill( new Rectangle( 0, 0, imageX, imageY ) );
draw( graphics, new Rectangle2D.Float( 0, 0, imageX, imageY ) );
graphics.dispose();
return image;
}
public synchronized void draw( final Graphics2D graphics, final Rectangle2D bounds ) {
// this adjusts imageWidth and imageHeight
scaleToFit( (float) bounds.getWidth(), (float) bounds.getHeight() );
// adjust translation if needed ...
graphics.translate( bounds.getX(), bounds.getY() );
// adjust to the image origin
graphics.translate( -imageX, -imageY );
this.graphics = graphics;
for ( int i = 0; i < records.size(); i++ ) {
try {
final MfCmd command = (MfCmd) records.get( i );
command.setScale( (float) imageWidth / (float) maxWidth, (float) imageHeight / (float) maxHeight );
command.replay( this );
} catch ( Exception e ) {
logger.warn( "Error while processing image record #" + i, e );
}
}
resetStates();
}
/**
* Returns the preferred size of the drawable. If the drawable is aspect ratio aware, these bounds should be used to
* compute the preferred aspect ratio for this drawable.
*
* @return the preferred size.
*/
public Dimension getPreferredSize() {
return new Dimension( imageWidth, imageHeight );
}
/**
* Returns true, if this drawable will preserve an aspect ratio during the drawing.
*
* @return true, if an aspect ratio is preserved, false otherwise.
*/
public boolean isPreserveAspectRatio() {
return true;
}
public String toString() {
final StringBuffer bo = new StringBuffer( 500 );
bo.append( "WmfFile={width=" );
bo.append( imageWidth );
bo.append( ", height=" );
bo.append( imageHeight );
bo.append( ", recordCount=" );
bo.append( records.size() );
bo.append( ", records={\n" );
for ( int i = 0; i < records.size(); i++ ) {
final MfCmd cmd = (MfCmd) records.get( i );
bo.append( i );
bo.append( ',' );
bo.append( cmd.toString() );
bo.append( '\n' );
}
bo.append( "}\n" );
return bo.toString();
}
}