org.freehep.graphicsio.emf.EMFRenderer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of freehep-graphicsio-emf Show documentation
Show all versions of freehep-graphicsio-emf Show documentation
FreeHEP Enhanced Metafile Format Driver
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 ([email protected])
* @version $Id: freehep-graphicsio-emf/src/main/java/org/freehep/graphicsio/emf/EMFRenderer.java 10ec7516e3ce 2007/02/06 18:42:34 duns $
*/
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 = 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 = 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;
}
}