// Copyright 2000-2003, FreeHEP package org.freehep.graphicsio; import java.awt.*; import java.awt.font.*; import java.awt.geom.*; import java.awt.image.*; import java.awt.image.renderable.*; import java.io.*; import java.text.*; import java.util.*; import org.freehep.graphics2d.GenericTagHandler; import org.freehep.graphics2d.TagString; import org.freehep.util.UserProperties; import org.freehep.util.images.ImageUtilities; /** * This class provides an abstract VectorGraphicsIO class for specific output drivers. * * @author Charles Loomis * @author Mark Donszelmann * @version $Id: AbstractVectorGraphicsIO.java,v 1.14 2003/12/01 16:54:17 duns Exp $ */ public abstract class AbstractVectorGraphicsIO extends VectorGraphicsIO { private static final String rootKey = AbstractVectorGraphicsIO.class.getName(); public static final String EMIT_WARNINGS = rootKey+".EMIT_WARNINGS"; public static final String EMIT_ERRORS = rootKey+".EMIT_ERRORS"; /*================================================================================ * Table of Contents: * ------------------ * 1. Constructors & Factory Methods * 2. Document Settings * 3. Header, Trailer, Multipage & Comments * 3.1 Header & Trailer * 3.2 MultipageDocument methods * 4. Create & Dispose * 5. Drawing Methods * 5.1. shapes (draw/fill) * 5.1.1. lines, rectangles, round rectangles * 5.1.2. polylines, polygons * 5.1.3. ovals, arcs * 5.1.4. shapes * 5.2. Images * 5.3. Strings * 6. Transformations * 7. Clipping * 8. Graphics State / Settings * 8.1. stroke/linewidth * 8.2. paint/color * 8.3. font * 8.4. rendering hints * 9. Auxiliary * 10. Private/Utility Methods *================================================================================*/ private Dimension size; private Component component; private boolean doRestoreOnDispose; private Rectangle deviceClip; private Area userClip; private AffineTransform currentTransform; private Composite currentComposite; private Stroke currentStroke; private RenderingHints hints; /*================================================================================ * 1. Constructors & Factory Methods *================================================================================*/ /** * Constructs a Graphics context with the following graphics state: * * * @param size rectangle specifying the bounds of the image * @param doRestoreOnDispose true if writeGraphicsRestore() should be called when * this graphics context is disposed of. */ protected AbstractVectorGraphicsIO(Dimension size, boolean doRestoreOnDispose) { super(); this.size = size; this.component = null; this.doRestoreOnDispose = doRestoreOnDispose; deviceClip = (size != null ? new Rectangle(0, 0, size.width, size.height) : null); userClip = null; currentTransform = new AffineTransform(); currentComposite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER); currentStroke = new BasicStroke(1.0f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 10.0f, null, 0.0f); super.setColor(Color.BLACK); super.setBackground(Color.BLACK); super.setFont(new Font("Dialog", Font.PLAIN, 12)); // Initialize the rendering hints. hints = new RenderingHints(null); } /** * Constructs a Graphics context with the following graphics state: * * * @param component to be used to initialize the values of the graphics state * @param doRestoreOnDispose true if writeGraphicsRestore() should be called when * this graphics context is disposed of. */ protected AbstractVectorGraphicsIO(Component component, boolean doRestoreOnDispose) { super(); this.size = component.getSize(); this.component = component; this.doRestoreOnDispose = doRestoreOnDispose; deviceClip = (size != null ? new Rectangle(0, 0, size.width, size.height) : null); userClip = null; GraphicsConfiguration gc = component.getGraphicsConfiguration(); currentTransform = (gc != null) ? gc.getDefaultTransform() : new AffineTransform(); currentComposite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER); currentStroke = new BasicStroke(1.0f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 10.0f, null, 0.0f); super.setFont(component.getFont()); super.setBackground(component.getBackground()); super.setColor(component.getForeground()); // Initialize the rendering hints. hints = new RenderingHints(null); } /** * Constructs a subgraphics context. * * @param graphics context to clone from * @param doRestoreOnDispose true if writeGraphicsRestore() should be called when * this graphics context is disposed of. */ protected AbstractVectorGraphicsIO(AbstractVectorGraphicsIO graphics, boolean doRestoreOnDispose) { super(graphics); this.doRestoreOnDispose = doRestoreOnDispose; size = new Dimension(graphics.size); component = graphics.component; deviceClip = new Rectangle(graphics.deviceClip); userClip = (graphics.userClip != null) ? new Area(graphics.userClip) : null; currentTransform = new AffineTransform(graphics.currentTransform); currentComposite = graphics.currentComposite; currentStroke = graphics.currentStroke; hints = graphics.hints; } /*================================================================================ | 2. Document Settings *================================================================================*/ public Dimension getSize() { return size; } public Component getComponent() { return component; } /*================================================================================ | 3. Header, Trailer, Multipage & Comments *================================================================================*/ /* 3.1 Header & Trailer */ public void startExport() { try { writeHeader(); // delegate this to openPage if it is a MultiPage document if (!(this instanceof MultiPageDocument)) { writeGraphicsState(); writeBackground(); } } catch (IOException e) { handleException(e); } } public void endExport() { try { dispose(); writeTrailer(); closeStream(); } catch (IOException e) { handleException(e); } } /** * Called to write the header part of the output. */ public abstract void writeHeader() throws IOException; /** * Called to write the initial graphics state. */ public void writeGraphicsState() throws IOException { Color color = super.getColor(); super.setColor(null); setColor(color); Shape clip = userClip; setClip(clip); AffineTransform transform = currentTransform; currentTransform = new AffineTransform(); setTransform(transform); Font font = getFont(); super.setFont(null); setFont(font); Composite composite = currentComposite; currentComposite = null; setComposite(composite); Stroke stroke = currentStroke; currentStroke = null; setStroke(stroke); } public abstract void writeBackground() throws IOException; /** * Called to write the trailing part of the output. */ public abstract void writeTrailer() throws IOException; /** * Called to close the stream you are writing to. */ public abstract void closeStream() throws IOException; public void printComment(String comment) { try { writeComment(comment); } catch (IOException e) { handleException(e); } } /** * Called to Write out a comment. * * @param comment to be written */ public abstract void writeComment(String comment) throws IOException; /* 3.2 MultipageDocument methods */ protected void resetClip(Rectangle clip) { deviceClip = clip; userClip = null; } /*================================================================================ * 4. Create & Dispose *================================================================================*/ /** * Disposes of the graphics context. If on creation restoreOnDispose was true, * writeGraphicsRestore() will be called. */ public void dispose() { try { // Swing sometimes calls dispose several times for a given // graphics object. Ensure that the grestore is only written // once if this happens. if (doRestoreOnDispose) { writeGraphicsRestore(); doRestoreOnDispose = false; } } catch (IOException e) { handleException(e); } } /** * Writes out the save of a graphics context for a later restore. * Some implementations keep track of this by hand if the output * format does not support it. */ protected abstract void writeGraphicsSave() throws IOException; /** * Writes out the restore of a graphics context. * Some implementations keep track of this by hand if the output * format does not support it. */ protected abstract void writeGraphicsRestore() throws IOException; /*================================================================================ | 5. Drawing Methods *================================================================================*/ /* 5.3. Images */ public boolean drawImage(Image image, int x, int y, ImageObserver observer) { int imageWidth = image.getWidth(observer); int imageHeight = image.getHeight(observer); return drawImage(image, x, y, x+imageWidth, y+imageHeight, 0, 0, imageWidth, imageHeight, null, observer); } public boolean drawImage(Image image, int x, int y, int width, int height, ImageObserver observer) { int imageWidth = image.getWidth(observer); int imageHeight = image.getHeight(observer); return drawImage(image, x, y, x+width, y+height, 0, 0, imageWidth, imageHeight, null, observer); } public boolean drawImage(Image image, int x, int y, int width, int height, Color bgColor, ImageObserver observer) { int imageWidth = image.getWidth(observer); int imageHeight = image.getHeight(observer); return drawImage(image, x, y, x+width, y+height, 0, 0, imageWidth, imageHeight, bgColor, observer); } public boolean drawImage(Image image, int x, int y, Color bgColor, ImageObserver observer) { int imageWidth = image.getWidth(observer); int imageHeight = image.getHeight(observer); return drawImage(image, x, y, x+imageWidth, y+imageHeight, 0, 0, imageWidth, imageHeight, bgColor, observer); } public boolean drawImage(Image image, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, ImageObserver observer) { return drawImage(image, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, null, observer); } public boolean drawImage(Image image, AffineTransform xform, ImageObserver observer) { drawRenderedImage(ImageUtilities.createRenderedImage(image, observer, null), xform); return true; } public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) { drawImage(op.filter(img, null), x, y, null); } // NOTE: not tested yet!!! public void drawRenderableImage(RenderableImage image, AffineTransform xform) { drawRenderedImage(image.createRendering(new RenderContext(new AffineTransform(), getRenderingHints())), xform); } /** * Draw and resizes (transparent) image. Calls writeImage(...). * * @param image image to be drawn * @param dx1, dy1, dx2, dy2 destination image bounds * @param sx1, sy1, sx2, sy2 source image bounds * @param bgColor background color * @param observer for updates if image still incomplete * @return true if successful */ public boolean drawImage(Image image, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, Color bgColor, ImageObserver observer) { try { int srcX = Math.min(sx1, sx2); int srcY = Math.min(sy1, sy2); int srcWidth = Math.abs(sx2 - sx1); int srcHeight = Math.abs(sy2 - sy1); int width = Math.abs(dx2 - dx1); int height = Math.abs(dy2 - dy1); if ((srcX != 0) || (srcY != 0) || (srcWidth != image.getWidth(observer)) || (srcHeight != image.getHeight(observer))) { // crop the source image ImageFilter crop = new CropImageFilter(srcX, srcY, srcWidth, srcHeight); image = Toolkit.getDefaultToolkit().createImage(new FilteredImageSource(image.getSource(), crop)); MediaTracker mediaTracker = new MediaTracker(new Panel()); mediaTracker.addImage(image, 0); try { mediaTracker.waitForAll(); } catch (InterruptedException e) { handleException(e); } } boolean flipHorizontal = (dx2 source flipped XOR dest flipped double tx = (flipHorizontal) ? (double)dx2 : (double)dx1; double ty = (flipVertical) ? (double)dy2 : (double)dy1; double sx = (double)width/srcWidth; sx = flipHorizontal ? -1*sx : sx; double sy = (double)height/srcHeight; sy = flipVertical ? -1*sy : sy; writeImage(ImageUtilities.createRenderedImage(image, observer, bgColor), new AffineTransform(sx, 0, 0, sy, tx, ty), bgColor); return true; } catch (IOException e) { return false; } } /* // first use the original orientation int clippingWidth = Math.abs(sx2 - sx1); int clippingHeight = Math.abs(sy2 - sy1); int sulX = Math.min(sx1, sx2); int sulY = Math.min(sy1, sy2); Image background = null; if (bgColor != null) { // Draw the image on the background color // maybe we could crop it and fill the transparent pixels in one go // by means of a filter. background = new BufferedImage(clippingWidth, clippingHeight, BufferedImage.TYPE_INT_ARGB); Graphics bgGfx = background.getGraphics(); bgGfx.drawImage(image, 0, 0, clippingWidth, clippingWidth, sulX, sulY, sulX+clippingWidth, sulY+clippingHeight, getPrintColor(bgColor), observer); } else { // crop the source image ImageFilter crop = new CropImageFilter(sulX, sulY, clippingWidth, clippingHeight); background = Toolkit.getDefaultToolkit().createImage(new FilteredImageSource(image.getSource(), crop)); MediaTracker mediaTracker = new MediaTracker(new Panel()); mediaTracker.addImage(background, 0); try { mediaTracker.waitForAll(); } catch (InterruptedException e) { handleException(e); } } // now flip the image if necessary boolean flipHorizontal = (dx2 source flipped XOR dest flipped int destWidth = Math.abs(dx2-dx1); int destHeight = Math.abs(dy2-dy1); try { return writeImage(background, flipHorizontal ? dx2 : dx1, flipVertical ? dy2 : dy1, flipHorizontal ? -destWidth : destWidth, flipVertical ? -destHeight : destHeight, (bgColor == null), observer); } catch (IOException e) { return false; } } */ /** * Draws a rendered image using a transform. * * @param image to be drawn * @param transform transform to be used on the image */ public void drawRenderedImage(RenderedImage image, AffineTransform xform) { try { writeImage(image, xform, null); } catch (Exception e) { handleException(e); } } protected abstract void writeImage(RenderedImage image, AffineTransform xform, Color bkg) throws IOException; /** * Clears rectangle by painting it with the backgroundColor. * * @param x, y, width, height rectangle to be cleared. */ public void clearRect(double x, double y, double width, double height) { Paint temp = getPaint(); setPaint(getBackground()); fillRect(x, y, width, height); setPaint(temp); } public void drawString(String string, double x, double y) { try { writeString(string, x, y); } catch (IOException e) { handleException(e); } } // FIXME, maybe can move up more public void drawString(String str, double x, double y, int horizontal, int vertical, boolean framed, Color frameColor, double frameWidth, boolean banner, Color bannerColor) { try { LineMetrics metrics = getFont().getLineMetrics(str, getFontRenderContext()); double width = getFont().getStringBounds(str, getFontRenderContext()).getWidth(); double height = metrics.getHeight(); double descent = metrics.getDescent(); Rectangle2D textSize = new Rectangle2D.Double(0, descent-height, width, height); double adjustment = (getFont().getSize2D()*2)/10; Point2D textUL = drawFrameAndBanner(x, y, textSize, adjustment, framed, frameColor, frameWidth, banner, bannerColor, horizontal, vertical); drawString(str, textUL.getX(), textUL.getY()); } catch (IOException e) { handleException(e); } } // FIXME, maybe can move up more public void drawString(TagString str, double x, double y, int horizontal, int vertical, boolean framed, Color frameColor, double frameWidth, boolean banner, Color bannerColor) { try { GenericTagHandler tagHandler = new GenericTagHandler(this); Rectangle2D r = tagHandler.stringSize(str); double adjustment = (getFont().getSize2D()*2)/10; Point2D textUL = drawFrameAndBanner(x, y, r, adjustment, framed, frameColor, frameWidth, banner, bannerColor, horizontal, vertical); tagHandler.print(str, textUL.getX(), textUL.getY()); } catch (IOException e) { handleException(e); } } protected abstract void writeString(String string, double x, double y) throws IOException; /*================================================================================ | 6. Transformations *================================================================================*/ /** * Get the current transform. * * @return current transform */ public AffineTransform getTransform() { return new AffineTransform(currentTransform); } /** * Set the current transform. Since most output formats do not * implement this functionality, the inverse transform of the * currentTransform is calculated and multiplied by the * transform to be set. The result is then forwarded by a call * to writeTransform(Transform). * * @param transform to be set */ public void setTransform(AffineTransform transform) { try { AffineTransform difference = currentTransform.createInverse(); difference.concatenate(transform); writeTransform(difference); } catch (NoninvertibleTransformException e) { writeWarning("VectorGraphics2D.setTransform(AffineTransform): current transformation matrix not invertible"); } catch (IOException e) { handleException(e); } currentTransform = transform; } /** * Transforms the current transform. Calls writeTransform(Transform) * * @param transform to be applied */ public void transform(AffineTransform transform) { currentTransform.concatenate(transform); try { writeTransform(transform); } catch (IOException e) { handleException(e); } } /** * Translates the current transform. Calls writeTransform(Transform) * * @param x, y amount by which to translate */ public void translate(double x, double y) { currentTransform.translate(x, y); try { writeTransform(new AffineTransform(1, 0, 0, 1, x, y)); } catch (IOException e) { handleException(e); } } /** * Rotate the current transform over the Z-axis. Calls writeTransform(Transform). * Rotating with a positive angle theta rotates points on the positive x axis * toward the positive y axis. * * @param theta radians over which to rotate */ public void rotate(double theta) { currentTransform.rotate(theta); try { writeTransform(new AffineTransform(Math.cos(theta), Math.sin(theta), -Math.sin(theta), Math.cos(theta), 0, 0)); } catch (IOException e) { handleException(e); } } /** * Scales the current transform. Calls writeTransform(Transform). * * @param sx, sy amount used for scaling */ public void scale(double sx, double sy) { currentTransform.scale(sx, sy); try { writeTransform(new AffineTransform(sx, 0, 0, sy, 0, 0)); } catch (IOException e) { handleException(e); } } /** * Shears the current transform. Calls writeTransform(Transform). * * @param shx, shy amount for shearing */ public void shear(double shx, double shy) { currentTransform.shear(shx, shy); try { writeTransform(new AffineTransform(1, shy, shx, 1, 0, 0)); } catch (IOException e) { handleException(e); } } /** * Writes out the transform as it needs to be concatenated to the * internal transform of the output format. If there is no implementation * of an internal transform, then this method needs to do nothing, BUT * all coordinates need to be transformed by the currentTransform before * being written out. * * @param transform to be written */ protected abstract void writeTransform(AffineTransform transform) throws IOException; /*================================================================================ | 7. Clipping *================================================================================*/ /** * Gets the current clip in form of a Shape (Rectangle). * * @return current clip */ public Shape getClip() { return (userClip != null) ? new Area(untransformShape(userClip)) : null; } /** * Gets the current clip in form of a Rectangle. * * @return current clip */ public Rectangle getClipBounds() { Shape clip = getClip(); return (clip != null) ? getClip().getBounds() : null; } /** * Gets the current clip in form of a Rectangle. * * @return current clip */ public Rectangle getClipBounds(Rectangle r) { Rectangle bounds = getClipBounds(); if (bounds != null) r.setBounds(bounds); return r; } /** * Clips rectangle. Calls clip(Rectangle). * * @param x, y, width, height rectangle for clipping */ public void clipRect(int x, int y, int width, int height) { clip(new Rectangle(x, y, width, height)); } /** * Clips rectangle. Calls clip(Rectangle2D). * * @param x, y, width, height rectangle for clipping */ public void clipRect(double x, double y, double width, double height) { clip(new Rectangle2D.Double(x, y, width, height)); } /** * Clips rectangle. Calls clip(Rectangle). * * @param x, y, width, height rectangle for clipping */ public void setClip(int x, int y, int width, int height) { setClip(new Rectangle(x, y, width, height)); } /** * Clips rectangle. Calls clip(Rectangle2D). * * @param x, y, width, height rectangle for clipping */ public void setClip(double x, double y, double width, double height) { setClip(new Rectangle2D.Double(x, y, width, height)); } /** * Clips shape. Sets the userClip to shape and calls clip(Shape). * * @param shape used for clipping */ public void setClip(Shape shape) { userClip = (shape != null) ? new Area(transformShape(shape)) : null; try { writeSetClip(shape); } catch (IOException e) { handleException(e); } } /** * Called to set a clip, no intersection made. * Calls clip(shape). */ protected void writeSetClip(Shape shape) throws IOException { clip(shape); } /** * Clips using given shape. Dispatches to writeClip(Rectangle), * writeClip(Rectangle2D) or writeClip(Shape). * * @param shape used for clipping */ public void clip(Shape s) { Shape ts = transformShape(s); if (userClip != null) { if (ts != null) { userClip.intersect(new Area(ts)); } else { userClip = null; } } else { userClip = (ts != null) ? new Area(ts) : null; } try { if (s instanceof Rectangle) { writeClip((Rectangle)s); } else if (s instanceof Rectangle2D) { writeClip((Rectangle2D)s); } else { writeClip(s); } } catch (IOException e) { handleException(e); } } /** * Write out Rectangle clip. Calls writeClip(Rectangle2D). * * @param rectangle to be used for clipping */ protected void writeClip(Rectangle rectangle) throws IOException { writeClip((Rectangle2D)rectangle); } /** * Write out Rectangle2D clip. * * @param rectangle to be used for clipping */ protected abstract void writeClip(Rectangle2D rectangle) throws IOException; /** * Write out Shape clip. * * @param shape to be used for clipping */ protected abstract void writeClip(Shape shape) throws IOException; /*================================================================================ | 8. Graphics State *================================================================================*/ /* 8.1. stroke/linewidth */ /** * Get the current stroke. * * @return current stroke */ public Stroke getStroke() { return currentStroke; } /** * Sets the current stroke. * Calls writeStroke if stroke is unequal to the current stroke. * * @param stroke to be set */ public void setStroke(Stroke stroke) { if (stroke.equals(currentStroke)) { return; } try { writeStroke(stroke); } catch (IOException e) { handleException(e); } currentStroke = stroke; } /** * Writes the current stroke. If stroke is an instance of BasicStroke it will * call writeWidth, writeCap, writeJoin, writeMiterLimit and writeDash, * if any were different than the current stroke. */ protected void writeStroke(Stroke stroke) throws IOException { if (stroke instanceof BasicStroke) { BasicStroke ns = (BasicStroke) stroke; // get the current values for comparison if available, // otherwise set them to -1="undefined" int currentCap = -1, currentJoin = -1; float currentWidth = -1, currentLimit = -1, currentDashPhase = -1; float[] currentDashArray = null; if ((currentStroke != null) && (currentStroke instanceof BasicStroke)) { BasicStroke cs = (BasicStroke) currentStroke; currentCap = cs.getEndCap(); currentJoin = cs.getLineJoin(); currentWidth = cs.getLineWidth(); currentLimit = cs.getMiterLimit(); currentDashArray = cs.getDashArray(); currentDashPhase = cs.getDashPhase(); } // Check the linewidth. float width = ns.getLineWidth(); if (currentWidth != width) { writeWidth(width); } // Check the line caps. int cap = ns.getEndCap(); if (currentCap != cap) { writeCap(cap); } // Check the line joins. int join = ns.getLineJoin(); if (currentJoin != join) { writeJoin(join); } // Check the miter limit and validity of value float limit = ns.getMiterLimit(); if ((currentLimit != limit) && (limit >= 1.0f)) { writeMiterLimit(limit); } // Get the dashing parameters. float phase = ns.getDashPhase(); float[] newDashArray = ns.getDashArray(); // Check to see if there are differences in the phase or // length of the array. boolean different = false; if (currentDashPhase!=phase) different = true; if ((newDashArray==null && currentDashArray!=null) || (newDashArray!=null && currentDashArray==null)) different = true; if (newDashArray!=null && currentDashArray!=null && newDashArray.length!=currentDashArray.length) different = true; // Check the individual dashing entries if necessary. if (!different && newDashArray!=null && currentDashArray!=null) { for (int i=0; i 0) { path.moveTo((float)xPoints[0], (float)yPoints[0]); for (int i = 1; i < nPoints; i++) { path.lineTo((float)xPoints[i], (float)yPoints[i]); } if (close) path.closePath(); } return path; } private Shape transformShape(AffineTransform at, Shape s) { if (s == null) return null; return at.createTransformedShape(s); } private Shape transformShape(Shape s) { return transformShape(getTransform(), s); } private Shape untransformShape(Shape s) { if (s == null) return null; try { return transformShape(getTransform().createInverse(), s); } catch (NoninvertibleTransformException e) { return null; } } private Point2D drawFrameAndBanner(double x, double y, Rectangle2D textSize, double adjustment, boolean framed, Color frameColor, double frameWidth, boolean banner, Color bannerColor, int horizontal, int vertical) throws IOException { double descent = textSize.getY() + textSize.getHeight(); x = getXalignment(x, textSize.getWidth(), horizontal); y = getYalignment(y, textSize.getHeight(), descent, vertical); double rx = x - adjustment; double ry = y - textSize.getHeight() + descent - adjustment; double rw = textSize.getWidth() + 2*adjustment; double rh = textSize.getHeight() + 2*adjustment; if (banner) { Paint paint = getPaint(); setColor(bannerColor); fillRect(rx, ry, rw, rh); setPaint(paint); } if (framed) { Paint paint = getPaint(); Stroke stroke = getStroke(); setColor(frameColor); setLineWidth(frameWidth); drawRect(rx, ry, rw, rh); setPaint(paint); setStroke(stroke); } return new Point2D.Double(x, y); } }