package org.freehep.graphicsio.emf; import java.io.IOException; import java.awt.Graphics2D; import java.awt.Dimension; import java.awt.Point; import java.awt.BasicStroke; import java.awt.Stroke; import java.awt.Paint; import java.awt.Color; import java.awt.Shape; import java.awt.AlphaComposite; import java.awt.Image; import java.awt.RenderingHints; import java.awt.Font; import java.awt.image.BufferedImage; import java.awt.font.TextLayout; import java.awt.geom.AffineTransform; import java.awt.geom.GeneralPath; import java.util.logging.Logger; import java.util.Stack; import java.util.Vector; import java.util.Map; import org.freehep.util.io.Tag; import org.freehep.graphicsio.emf.gdi.GDIObject; /** * Standalone EMF renderer. * * @author Daniel Noll (daniel@nuix.com) * @version $Id$ */ public class EMFRenderer { private static final Logger logger = Logger.getLogger("org.freehep.graphicsio.emf"); /** * Header read from the EMFInputStream */ private EMFHeader header; /** * Each logical unit is mapped to one twentieth of a * printer's point (1/1440 inch, also called a twip). */ public static double TWIP_SCALE = 1d / 1440 * 254; /** * affect by all XXXTo methods, e.g. LinTo. ExtMoveTo creates the * starting point. CloseFigure closes the figure. */ private GeneralPath figure = null; /** * AffineTransform which is the base for all rendering * operations. */ private AffineTransform initialTransform; /** * origin of the emf window, set by SetWindowOrgEx */ private Point windowOrigin = null; /** * origin of the emf viewport, set By SetViewportOrgEx */ private Point viewportOrigin = null; /** * size of the emf window, set by SetWindowExtEx */ private Dimension windowSize = null; /** * size of the emf viewport, set by SetViewportExtEx */ private Dimension viewportSize = null; /** * The MM_ISOTROPIC mode ensures a 1:1 aspect ratio. * The MM_ANISOTROPIC mode allows the x-coordinates * and y-coordinates to be adjusted independently. */ private boolean mapModeIsotropic = false; /** * AffineTransform defined by SetMapMode. Used for * resizing the emf to propper device bounds. */ private AffineTransform mapModeTransform = AffineTransform.getScaleInstance(TWIP_SCALE, TWIP_SCALE); /** * clipping area which is the base for all rendering * operations. */ private Shape initialClip; /** * current Graphics2D to paint on. It is set during * {@link #paint(java.awt.Graphics2D)} */ private Graphics2D g2; /** * objects used by {@link org.freehep.graphicsio.emf.gdi.SelectObject}. * The array is filled by CreateXXX functions, e.g. * {@link org.freehep.graphicsio.emf.gdi.CreatePen} */ private GDIObject[] gdiObjects = new GDIObject[256]; // TODO: Make this more flexible. // Rendering state. private Paint brushPaint = new Color(0, 0, 0, 0); private Paint penPaint = Color.BLACK; private Stroke penStroke = new BasicStroke(); private int textAlignMode = 0; /** * color for simple text rendering */ private Color textColor = Color.BLACK; /** * written by {@link org.freehep.graphicsio.emf.gdi.SetPolyFillMode} used by * e.g. {@link org.freehep.graphicsio.emf.gdi.PolyPolygon16} */ private int windingRule = GeneralPath.WIND_EVEN_ODD; /** * Defined by SetBkModes, either {@link EMFConstants#BKG_OPAQUE} or * {@link EMFConstants#BKG_TRANSPARENT}. Used in * {@link #fillAndDrawOrAppend(java.awt.Graphics2D, java.awt.Shape)} */ private int bkMode = EMFConstants.BKG_OPAQUE; /** * The SetBkMode function affects the line styles for lines drawn using a * pen created by the CreatePen function. SetBkMode does not affect lines * drawn using a pen created by the ExtCreatePen function. */ private boolean useCreatePen = true; /** * The miter length is defined as the distance from the intersection * of the line walls on the inside of the join to the intersection of * the line walls on the outside of the join. The miter limit is the * maximum allowed ratio of the miter length to the line width. */ private int meterLimit = 10; /** * The SetROP2 function sets the current foreground mix mode. * Default is to use the pen. */ private int rop2 = EMFConstants.R2_COPYPEN; /** * e.g. {@link Image#SCALE_SMOOTH} for rendering images */ private int scaleMode = Image.SCALE_SMOOTH; /** * The brush origin is a pair of coordinates specifying the location of one * pixel in the bitmap. The default brush origin coordinates are (0,0). For * horizontal coordinates, the value 0 corresponds to the leftmost column * of pixels; the width corresponds to the rightmost column. For vertical * coordinates, the value 0 corresponds to the uppermost row of pixels; * the height corresponds to the lowermost row. */ private Point brushOrigin = new Point(0, 0); /** * stores the parsed tags. Filled by the constructor. Read by * {@link #paint(java.awt.Graphics2D)} */ private Vector tags = new Vector(0); /** * Created by BeginPath and closed by EndPath. */ private GeneralPath path = null; /** * The transformations set by ModifyWorldTransform are redirected to * that AffineTransform. They do not affect the current paint context, * after BeginPath is called. Only the figures appended to path * are transformed by this AffineTransform. * BeginPath clears the transformation, ModifyWorldTransform changes ist. */ private AffineTransform pathTransform = new AffineTransform(); /** * {@link org.freehep.graphicsio.emf.gdi.SaveDC} stores * an Instance of DC if saveDC is read. RestoreDC pops an object. */ private Stack dcStack = new Stack(); /** * default direction is counterclockwise */ private int arcDirection = EMFConstants.AD_COUNTERCLOCKWISE; /** * Class the encapsulate the state of a Graphics2D object. * Instances are store in dcStack by * {@link org.freehep.graphicsio.emf.EMFRenderer#paint(java.awt.Graphics2D)} */ private class DC { private Paint paint; private Stroke stroke; private AffineTransform transform; private Shape clip; public GeneralPath path; public int bkMode; public int windingRule; public int meterLimit; public boolean useCreatePen; public int scaleMode; public AffineTransform pathTransform; } /** * Constructs the renderer. * * @param is the input stream to read the EMF records from. * @throws IOException if an error occurs reading the header. */ public EMFRenderer(EMFInputStream is) throws IOException { this.header = is.readHeader(); // read all tags Tag tag; while ((tag = is.readTag()) != null) { tags.add(tag); } is.close(); } /** * Gets the size of a canvas which would be required to render the EMF. * * @return the size. */ public Dimension getSize() { return header.getBounds().getSize(); // TODO see the mapModePart of resetTransformation() // if uncommented size is too small /* Dimension bounds = header.getBounds().getSize(); return new Dimension( (int)Math.ceil(bounds.width * TWIP_SCALE), (int)Math.ceil(bounds.height * TWIP_SCALE));*/ } /** * Paints the EMF onto the provided graphics context. * * @param g2 the graphics context to paint onto. */ public void paint(Graphics2D g2) { this.g2 = g2; // store at leat clip and transformation Shape clip = g2.getClip(); AffineTransform at = g2.getTransform(); Map hints = g2.getRenderingHints(); // some quality settings g2.setRenderingHint( RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g2.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2.setRenderingHint( RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); g2.setRenderingHint( RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); g2.setRenderingHint( RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); // used by SetWorldTransform to reset transformation initialTransform = g2.getTransform(); // set the initial value, defaults for EMF path = null; figure = null; meterLimit = 10; windingRule = GeneralPath.WIND_EVEN_ODD; bkMode = EMFConstants.BKG_OPAQUE; useCreatePen = true; scaleMode = Image.SCALE_SMOOTH; windowOrigin = null; viewportOrigin = null; windowSize = null; viewportSize = null; mapModeIsotropic = false; mapModeTransform = AffineTransform.getScaleInstance( TWIP_SCALE, TWIP_SCALE); // apply all default settings resetTransformation(g2); // determin initial clip after all basic transformations initialClip = g2.getClip(); // iterate and render all tags Tag tag; for (int i = 0; i < tags.size(); i++) { tag = (Tag) tags.get(i); if (tag instanceof EMFTag) { ((EMFTag) tags.get(i)).render(this); } else { logger.warning("unknown tag: " + tag); } } // reset Transform and clip g2.setRenderingHints(hints); g2.setTransform(at); g2.setClip(clip); } // --------------------------------------------------------------------- // complex drawing methods for EMFTags // --------------------------------------------------------------------- /** * set the initial transform, the windowOrigin and viewportOrigin, * scales by viewportSize and windowSize * @param g2 Context to apply transformations */ private void resetTransformation(Graphics2D g2) { // rest to device configuration if (initialTransform != null) { g2.setTransform(initialTransform); } else { g2.setTransform(new AffineTransform()); } /* TODO mapModeTransform dows not work correctly if (mapModeTransform != null) { g2.transform(mapModeTransform); }*/ // move to window origin if (windowOrigin != null) { g2.translate( - windowOrigin.getX(), - windowOrigin.getY()); } // move to window origin if (viewportOrigin != null) { g2.translate( - viewportOrigin.getX(), - viewportOrigin.getY()); } // TWIP_SCALE by window and viewport size if (viewportSize != null && windowSize != null) { double scaleX = viewportSize.getWidth() / windowSize.getWidth(); double scaleY = viewportSize.getHeight() / windowSize.getHeight(); g2.scale(scaleX, scaleY); } } /** * Stores the current state. Used by * {@link org.freehep.graphicsio.emf.gdi.SaveDC} */ public void saveDC() { // create a DC instance with current settings DC dc = new DC(); dc.paint = g2.getPaint(); dc.stroke = g2.getStroke(); dc.transform = g2.getTransform(); dc.pathTransform = pathTransform; dc.clip = g2.getClip(); dc.path = path; dc.meterLimit = meterLimit; dc.windingRule = windingRule; dc.bkMode = bkMode; dc.useCreatePen = useCreatePen; dc.scaleMode = scaleMode; // push it on top of the stack dcStack.push(dc); } /** * Retores a saved state. Used by * {@link org.freehep.graphicsio.emf.gdi.RestoreDC} */ public void retoreDC() { // is somethoing stored? if (!dcStack.empty()) { // read it DC dc = (DC) dcStack.pop(); // use it meterLimit = dc.meterLimit; windingRule = dc.windingRule; path = dc.path; bkMode = dc.bkMode; useCreatePen = dc.useCreatePen; scaleMode = dc.scaleMode; pathTransform = dc.pathTransform; g2.setPaint(dc.paint); g2.setStroke(dc.stroke); g2.setTransform(dc.transform); g2.setClip(dc.clip); } else { // set the default values } } /** * closes and appends the current open figure to the * path */ public void closeFigure() { if (figure == null) { return; } try { figure.closePath(); appendToPath(figure); figure = null; } catch (java.awt.geom.IllegalPathStateException e) { logger.warning("no figure to close"); } } /** * Logical units are mapped to arbitrary units with equally scaled axes; * that is, one unit along the x-axis is equal to one unit along the y-axis. * Use the SetWindowExtEx and SetViewportExtEx functions to specify the * units and the orientation of the axes. Graphics device interface (GDI) * makes adjustments as necessary to ensure the x and y units remain the * same size (When the window extent is set, the viewport will be adjusted * to keep the units isotropic). */ public void fixViewportSize() { if (mapModeIsotropic && (windowSize != null && viewportSize != null)) { viewportSize.setSize( viewportSize.getWidth(), viewportSize.getWidth() * (windowSize.getHeight() / windowSize.getWidth()) ); } } /** * fills a shape using the brushPaint, penPaint and penStroke * @param g2 Painting context * @param s Shape to fill with current brush */ private void fillAndDrawOrAppend(Graphics2D g2, Shape s) { // don't draw, just append the shape if BeginPath // has opened the path if (!appendToPath(s)) { // The SetBkMode function affects the line styles for lines drawn using a // pen created by the CreatePen function. SetBkMode does not affect lines // drawn using a pen created by the ExtCreatePen function. if (useCreatePen) { // OPAQUE Background is filled with the current background // color before the text, hatched brush, or pen is drawn. if (bkMode == EMFConstants.BKG_OPAQUE) { fillShape(g2, s); } else { // TRANSPARENT Background remains untouched. // TODO: if we really do nothing some drawings are incomplete // this needs definitly a fix fillShape(g2, s); } } else { // always fill the background if ExtCreatePen is set fillShape(g2, s); } drawShape(g2, s); } } /** * draws a shape using the penPaint and penStroke * @param g2 Painting context * @param s Shape to draw with current paen */ private void drawOrAppend(Graphics2D g2, Shape s) { // don't draw, just append the shape if BeginPath // opens a GeneralPath if (!appendToPath(s)) { drawShape(g2, s); } } /** * draws the text * * @param text Text * @param x x-Position * @param y y-Position */ public void drawOrAppendText(String text, double x, double y) { // TODO: Use explicit widths to pixel-position each character, if present. // TODO: Implement alignment properly. What we have already seems to work well enough. // FontRenderContext frc = g2.getFontRenderContext(); // TextLayout layout = new TextLayout(str, g2.getFont(), frc); // if ((textAlignMode & EMFConstants.TA_CENTER) != 0) { // layout.draw(g2, x + (width - textWidth) / 2, y); // } else if ((textAlignMode & EMFConstants.TA_RIGHT) != 0) { // layout.draw(g2, x + width - textWidth, y); // } else { // layout.draw(g2, x, y); // } if (path != null) { // do not use g2.drawString(str, x, y) to be aware of path TextLayout tl = new TextLayout( text, g2.getFont(), g2.getFontRenderContext()); path.append(tl.getOutline(null), false); } else { g2.setPaint(textColor); g2.drawString(text, (int)x, (int)y); } } /** * Append the shape to the current path * * @param s Shape to fill with current brush * @return true, if path was changed */ private boolean appendToPath(Shape s) { // don't draw, just append the shape if BeginPath // opens a GeneralPath if (path != null) { // aplly transformation if set if (pathTransform != null) { s = pathTransform.createTransformedShape(s); } // append the shape path.append(s, false); // current path set return true; } // current path not set return false; } /** * closes the path opened by {@link org.freehep.graphicsio.emf.gdi.BeginPath} */ public void closePath() { if (path != null) { try { path.closePath(); } catch (java.awt.geom.IllegalPathStateException e) { logger.warning("no figure to close"); } } } /** * fills a shape using the brushPaint, penPaint and penStroke. * This method should only be called for path painting. It doesn't check for a * current path. * * @param g2 Painting context * @param s Shape to fill with current brush */ private void fillShape(Graphics2D g2, Shape s) { g2.setPaint(brushPaint); g2.fill(s); } /** * draws a shape using the penPaint and penStroke * This method should only be called for path drawing. It doesn't check for a * current path. * * @param g2 Painting context * @param s Shape to draw with current pen */ private void drawShape(Graphics2D g2, Shape s) { g2.setStroke(penStroke); // R2_BLACK Pixel is always 0. if (rop2 == EMFConstants.R2_BLACK) { g2.setComposite(AlphaComposite.SrcOver); g2.setPaint(Color.black); } // R2_COPYPEN Pixel is the pen color. else if (rop2 == EMFConstants.R2_COPYPEN) { g2.setComposite(AlphaComposite.SrcOver); g2.setPaint(penPaint); } // R2_NOP Pixel remains unchanged. else if (rop2 == EMFConstants.R2_NOP) { g2.setComposite(AlphaComposite.SrcOver); g2.setPaint(penPaint); } // R2_WHITE Pixel is always 1. else if (rop2 == EMFConstants.R2_WHITE) { g2.setComposite(AlphaComposite.SrcOver); g2.setPaint(Color.white); } // R2_NOTCOPYPEN Pixel is the inverse of the pen color. else if (rop2 == EMFConstants.R2_NOTCOPYPEN) { g2.setComposite(AlphaComposite.SrcOver); // TODO: set at least inverted color if paint is a color } // R2_XORPEN Pixel is a combination of the colors // in the pen and in the screen, but not in both. else if (rop2 == EMFConstants.R2_XORPEN) { g2.setComposite(AlphaComposite.Xor); } else { logger.warning("got unsupported ROP" + rop2); // TODO: //R2_MASKNOTPEN Pixel is a combination of the colors common to both the screen and the inverse of the pen. //R2_MASKPEN Pixel is a combination of the colors common to both the pen and the screen. //R2_MASKPENNOT Pixel is a combination of the colors common to both the pen and the inverse of the screen. //R2_MERGENOTPEN Pixel is a combination of the screen color and the inverse of the pen color. //R2_MERGEPEN Pixel is a combination of the pen color and the screen color. //R2_MERGEPENNOT Pixel is a combination of the pen color and the inverse of the screen color. //R2_NOT Pixel is the inverse of the screen color. //R2_NOTCOPYPEN Pixel is the inverse of the pen color. //R2_NOTMASKPEN Pixel is the inverse of the R2_MASKPEN color. //R2_NOTMERGEPEN Pixel is the inverse of the R2_MERGEPEN color. //R2_NOTXORPEN Pixel is the inverse of the R2_XORPEN color. } g2.draw(s); } // --------------------------------------------------------------------- // simple wrapping methods to the painting context // --------------------------------------------------------------------- public void setFont(Font font) { g2.setFont(font); } public AffineTransform getTransform() { return g2.getTransform(); } public void transform(AffineTransform transform) { g2.transform(transform); } public void resetTransformation() { resetTransformation(g2); } public void setTransform(AffineTransform at) { g2.setTransform(at); } public void setClip(Shape shape) { g2.setClip(shape); } public void clip(Shape shape) { g2.clip(shape); } public Shape getClip() { return g2.getClip(); } public void drawImage(BufferedImage image, AffineTransform transform) { g2.drawImage(image, transform, null); } public void drawImage(BufferedImage image, int x, int y, int width, int height) { g2.drawImage(image, x, y, width, height, null); } public void drawShape(Shape shape) { drawShape(g2, shape); } public void fillShape(Shape shape) { fillShape(g2, shape); } public void fillAndDrawOrAppend(Shape s) { fillAndDrawOrAppend(g2, s); } public void drawOrAppend(Shape s) { drawOrAppend(g2, s); } // --------------------------------------------------------------------- // simple getter / setter methods // --------------------------------------------------------------------- public int getWindingRule() { return windingRule; } public GeneralPath getFigure() { return figure; } public void setFigure(GeneralPath figure) { this.figure = figure; } public GeneralPath getPath() { return path; } public void setPath(GeneralPath path) { this.path = path; } public Shape getInitialClip() { return initialClip; } public AffineTransform getPathTransform() { return pathTransform; } public void setPathTransform(AffineTransform pathTransform) { this.pathTransform = pathTransform; } public void setWindingRule(int windingRule) { this.windingRule = windingRule; } public void setMapModeIsotropic(boolean mapModeIsotropic) { this.mapModeIsotropic = mapModeIsotropic; } public AffineTransform getMapModeTransform() { return mapModeTransform; } public void setMapModeTransform(AffineTransform mapModeTransform) { this.mapModeTransform = mapModeTransform; } public void setWindowOrigin(Point windowOrigin) { this.windowOrigin = windowOrigin; } public void setViewportOrigin(Point viewportOrigin) { this.viewportOrigin = viewportOrigin; } public void setViewportSize(Dimension viewportSize) { this.viewportSize = viewportSize; fixViewportSize(); resetTransformation(); } public void setWindowSize(Dimension windowSize) { this.windowSize = windowSize; fixViewportSize(); resetTransformation(); } public GDIObject getGDIObject(int index) { return gdiObjects[index]; } public void storeGDIObject(int index, GDIObject tag) { gdiObjects[index] = tag; } public void setUseCreatePen(boolean useCreatePen) { this.useCreatePen = useCreatePen; } public void setPenPaint(Paint penPaint) { this.penPaint = penPaint; } public Stroke getPenStroke() { return penStroke; } public void setPenStroke(Stroke penStroke) { this.penStroke = penStroke; } public void setBrushPaint(Paint brushPaint) { this.brushPaint = brushPaint; } public float getMeterLimit() { return meterLimit; } public void setMeterLimit(int meterLimit) { this.meterLimit = meterLimit; } public void setTextColor(Color textColor) { this.textColor = textColor; } public void setRop2(int rop2) { this.rop2 = rop2; } public void setBkMode(int bkMode) { this.bkMode = bkMode; } public int getTextAlignMode() { return textAlignMode; } public void setTextAlignMode(int textAlignMode) { this.textAlignMode = textAlignMode; } public void setScaleMode(int scaleMode) { this.scaleMode = scaleMode; } public Point getBrushOrigin() { return brushOrigin; } public void setBrushOrigin(Point brushOrigin) { this.brushOrigin = brushOrigin; } public void setArcDirection(int arcDirection) { this.arcDirection = arcDirection; } public int getArcDirection() { return arcDirection; } }