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