package com.mxgraph.canvas; import java.awt.AlphaComposite; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Composite; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.GradientPaint; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Paint; import java.awt.Rectangle; import java.awt.Stroke; import java.awt.font.TextAttribute; import java.awt.geom.AffineTransform; import java.awt.geom.Ellipse2D; import java.awt.geom.GeneralPath; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.text.AttributedString; import java.util.LinkedHashMap; import java.util.Map; import java.util.Stack; import javax.swing.CellRendererPane; import javax.swing.JLabel; import com.mxgraph.util.mxConstants; import com.mxgraph.util.mxLightweightLabel; import com.mxgraph.util.mxUtils; /** * Used for exporting images. To render to an image from a given XML string, * graph size and background color, the following code is used: * * <code> * BufferedImage image = mxUtils.createBufferedImage(width, height, background); * Graphics2D g2 = image.createGraphics(); * mxUtils.setAntiAlias(g2, true, true); * XMLReader reader = SAXParserFactory.newInstance().newSAXParser().getXMLReader(); * reader.setContentHandler(new mxSaxOutputHandler(new mxGraphicsCanvas2D(g2))); * reader.parse(new InputSource(new StringReader(xml))); * </code> * * Text rendering is available for plain text and HTML markup, the latter with optional * word wrapping. CSS support is limited to the following: * http://docs.oracle.com/javase/6/docs/api/index.html?javax/swing/text/html/CSS.html */ public class mxGraphicsCanvas2D implements mxICanvas2D { /** * Specifies the image scaling quality. Default is Image.SCALE_SMOOTH. * See {@link #scaleImage(Image, int, int)} */ public static int IMAGE_SCALING = Image.SCALE_SMOOTH; /** * Specifies the additional pixels when computing the text width for HTML labels. * Default is 2. */ public static int JAVA_TEXT_WIDTH_DELTA = 2; /** * Specifies the size of the cache used to store parsed colors */ public static int COLOR_CACHE_SIZE = 100; /** * Reference to the graphics instance for painting. */ protected Graphics2D graphics; /** * Specifies if text output should be rendered. Default is true. */ protected boolean textEnabled = true; /** * Represents the current state of the canvas. */ protected transient CanvasState state = new CanvasState(); /** * Stack of states for save/restore. */ protected transient Stack<CanvasState> stack = new Stack<CanvasState>(); /** * Holds the current path. */ protected transient GeneralPath currentPath; /** * Optional renderer pane to be used for HTML label rendering. */ protected CellRendererPane rendererPane; /** * Font caching. */ protected transient Font lastFont = null; /** * Font caching. */ protected transient int lastFontStyle = 0; /** * Font caching. */ protected transient int lastFontSize = 0; /** * Font caching. */ protected transient String lastFontFamily = ""; /** * Stroke caching. */ protected transient Stroke lastStroke = null; /** * Stroke caching. */ protected transient float lastStrokeWidth = 0; /** * Stroke caching. */ protected transient int lastCap = 0; /** * Stroke caching. */ protected transient int lastJoin = 0; /** * Stroke caching. */ protected transient float lastMiterLimit = 0; /** * Stroke caching. */ protected transient boolean lastDashed = false; /** * Stroke caching. */ protected transient Object lastDashPattern = ""; /** * Caches parsed colors. */ @SuppressWarnings("serial") protected transient LinkedHashMap<String, Color> colorCache = new LinkedHashMap<String, Color>() { @Override protected boolean removeEldestEntry(Map.Entry<String, Color> eldest) { return size() > COLOR_CACHE_SIZE; } }; /** * Constructs a new graphics export canvas. */ public mxGraphicsCanvas2D(Graphics2D g) { setGraphics(g); state.g = g; // Initializes the cell renderer pane for drawing HTML markup try { rendererPane = new CellRendererPane(); } catch (Exception e) { // ignore } } /** * Sets the graphics instance. */ public void setGraphics(Graphics2D value) { graphics = value; } /** * Returns the graphics instance. */ public Graphics2D getGraphics() { return graphics; } /** * Returns true if text should be rendered. */ public boolean isTextEnabled() { return textEnabled; } /** * Disables or enables text rendering. */ public void setTextEnabled(boolean value) { textEnabled = value; } /** * Saves the current canvas state. */ public void save() { stack.push(state); state = cloneState(state); state.g = (Graphics2D) state.g.create(); } /** * Restores the last canvas state. */ public void restore() { state = stack.pop(); } /** * Returns a clone of thec given state. */ protected CanvasState cloneState(CanvasState state) { try { return (CanvasState) state.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; } /** * */ public void scale(double value) { // This implementation uses custom scale/translate and built-in rotation state.scale = state.scale * value; } /** * */ public void translate(double dx, double dy) { // This implementation uses custom scale/translate and built-in rotation state.dx += dx; state.dy += dy; } /** * */ public void rotate(double theta, boolean flipH, boolean flipV, double cx, double cy) { cx += state.dx; cy += state.dy; cx *= state.scale; cy *= state.scale; state.g.rotate(Math.toRadians(theta), cx, cy); // This implementation uses custom scale/translate and built-in rotation // Rotation state is part of the AffineTransform in state.transform if (flipH && flipV) { theta += 180; } else if (flipH ^ flipV) { double tx = (flipH) ? cx : 0; int sx = (flipH) ? -1 : 1; double ty = (flipV) ? cy : 0; int sy = (flipV) ? -1 : 1; state.g.translate(tx, ty); state.g.scale(sx, sy); state.g.translate(-tx, -ty); } state.theta = theta; state.rotationCx = cx; state.rotationCy = cy; state.flipH = flipH; state.flipV = flipV; } /** * */ public void setStrokeWidth(double value) { // Lazy and cached instantiation strategy for all stroke properties if (value != state.strokeWidth) { state.strokeWidth = value; } } /** * Caches color conversion as it is expensive. */ public void setStrokeColor(String value) { // Lazy and cached instantiation strategy for all stroke properties if (state.strokeColorValue == null || !state.strokeColorValue.equals(value)) { state.strokeColorValue = value; state.strokeColor = null; } } /** * */ public void setDashed(boolean value) { // Lazy and cached instantiation strategy for all stroke properties if (value != state.dashed) { state.dashed = value; } } /** * */ public void setDashPattern(String value) { if (value != null && value.length() > 0) { String[] tokens = value.split(" "); float[] dashpattern = new float[tokens.length]; for (int i = 0; i < tokens.length; i++) { dashpattern[i] = (float) (Float.parseFloat(tokens[i])); } state.dashPattern = dashpattern; } } /** * */ public void setLineCap(String value) { if (!state.lineCap.equals(value)) { state.lineCap = value; } } /** * */ public void setLineJoin(String value) { if (!state.lineJoin.equals(value)) { state.lineJoin = value; } } /** * */ public void setMiterLimit(double value) { if (value != state.miterLimit) { state.miterLimit = value; } } /** * */ public void setFontSize(double value) { if (value != state.fontSize) { state.fontSize = value; } } /** * */ public void setFontColor(String value) { if (state.fontColorValue == null || !state.fontColorValue.equals(value)) { state.fontColorValue = value; state.fontColor = null; } } /** * */ public void setFontBackgroundColor(String value) { if (state.fontBackgroundColorValue == null || !state.fontBackgroundColorValue.equals(value)) { state.fontBackgroundColorValue = value; state.fontBackgroundColor = null; } } /** * */ public void setFontBorderColor(String value) { if (state.fontBorderColorValue == null || !state.fontBorderColorValue.equals(value)) { state.fontBorderColorValue = value; state.fontBorderColor = null; } } /** * */ public void setFontFamily(String value) { if (!state.fontFamily.equals(value)) { state.fontFamily = value; } } /** * */ public void setFontStyle(int value) { if (value != state.fontStyle) { state.fontStyle = value; } } /** * */ public void setAlpha(double value) { if (state.alpha != value) { state.g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) (value))); state.alpha = value; } } /** * */ public void setFillColor(String value) { if (state.fillColorValue == null || !state.fillColorValue.equals(value)) { state.fillColorValue = value; state.fillColor = null; // Setting fill color resets gradient paint state.gradientPaint = null; } } /** * */ public void setGradient(String color1, String color2, double x, double y, double w, double h, String direction, double alpha1, double alpha2) { // LATER: Add lazy instantiation and check if paint already created float x1 = (float) ((state.dx + x) * state.scale); float y1 = (float) ((state.dy + y) * state.scale); float x2 = (float) x1; float y2 = (float) y1; h *= state.scale; w *= state.scale; if (direction == null || direction.length() == 0 || direction.equals(mxConstants.DIRECTION_SOUTH)) { y2 = (float) (y1 + h); } else if (direction.equals(mxConstants.DIRECTION_EAST)) { x2 = (float) (x1 + w); } else if (direction.equals(mxConstants.DIRECTION_NORTH)) { y1 = (float) (y1 + h); } else if (direction.equals(mxConstants.DIRECTION_WEST)) { x1 = (float) (x1 + w); } Color c1 = parseColor(color1); if (alpha1 != 1) { c1 = new Color(c1.getRed(), c1.getGreen(), c1.getBlue(), (int) (alpha1 * 255)); } Color c2 = parseColor(color2); if (alpha2 != 1) { c2 = new Color(c2.getRed(), c2.getGreen(), c2.getBlue(), (int) (alpha2 * 255)); } state.gradientPaint = new GradientPaint(x1, y1, c1, x2, y2, c2, true); // Resets fill color state.fillColorValue = null; } /** * Helper method that uses {@link mxUtils#parseColor(String)}. */ protected Color parseColor(String hex) { Color result = colorCache.get(hex); if (result == null) { result = mxUtils.parseColor(hex); colorCache.put(hex, result); } return result; } /** * */ public void rect(double x, double y, double w, double h) { currentPath = new GeneralPath(); currentPath.append(new Rectangle2D.Double((state.dx + x) * state.scale, (state.dy + y) * state.scale, w * state.scale, h * state.scale), false); } /** * Implements a rounded rectangle using a path. */ public void roundrect(double x, double y, double w, double h, double dx, double dy) { // LATER: Use arc here or quad in VML/SVG for exact match begin(); moveTo(x + dx, y); lineTo(x + w - dx, y); quadTo(x + w, y, x + w, y + dy); lineTo(x + w, y + h - dy); quadTo(x + w, y + h, x + w - dx, y + h); lineTo(x + dx, y + h); quadTo(x, y + h, x, y + h - dy); lineTo(x, y + dy); quadTo(x, y, x + dx, y); } /** * */ public void ellipse(double x, double y, double w, double h) { currentPath = new GeneralPath(); currentPath.append(new Ellipse2D.Double((state.dx + x) * state.scale, (state.dy + y) * state.scale, w * state.scale, h * state.scale), false); } /** * */ public void image(double x, double y, double w, double h, String src, boolean aspect, boolean flipH, boolean flipV) { if (src != null && w > 0 && h > 0) { Image img = loadImage(src); if (img != null) { Rectangle bounds = getImageBounds(img, x, y, w, h, aspect); img = scaleImage(img, bounds.width, bounds.height); if (img != null) { drawImage(createImageGraphics(bounds.x, bounds.y, bounds.width, bounds.height, flipH, flipV), img, bounds.x, bounds.y); } } } } /** * */ protected void drawImage(Graphics2D graphics, Image image, int x, int y) { graphics.drawImage(image, x, y, null); } /** * Hook for image caching. */ protected Image loadImage(String src) { return mxUtils.loadImage(src); } /** * */ protected final Rectangle getImageBounds(Image img, double x, double y, double w, double h, boolean aspect) { x = (state.dx + x) * state.scale; y = (state.dy + y) * state.scale; w *= state.scale; h *= state.scale; if (aspect) { Dimension size = getImageSize(img); double s = Math.min(w / size.width, h / size.height); int sw = (int) Math.round(size.width * s); int sh = (int) Math.round(size.height * s); x += (w - sw) / 2; y += (h - sh) / 2; w = sw; h = sh; } else { w = Math.round(w); h = Math.round(h); } return new Rectangle((int) x, (int) y, (int) w, (int) h); } /** * Returns the size for the given image. */ protected Dimension getImageSize(Image image) { return new Dimension(image.getWidth(null), image.getHeight(null)); } /** * Uses {@link #IMAGE_SCALING} to scale the given image. */ protected Image scaleImage(Image img, int w, int h) { Dimension size = getImageSize(img); if (w == size.width && h == size.height) { return img; } else { return img.getScaledInstance(w, h, IMAGE_SCALING); } } /** * Creates a graphic instance for rendering an image. */ protected final Graphics2D createImageGraphics(double x, double y, double w, double h, boolean flipH, boolean flipV) { Graphics2D g2 = state.g; if (flipH || flipV) { g2 = (Graphics2D) g2.create(); if (flipV && flipH) { g2.rotate(Math.toRadians(180), x + w / 2, y + h / 2); } else { int sx = 1; int sy = 1; int dx = 0; int dy = 0; if (flipH) { sx = -1; dx = (int) (-w - 2 * x); } if (flipV) { sy = -1; dy = (int) (-h - 2 * y); } g2.scale(sx, sy); g2.translate(dx, dy); } } return g2; } /** * Creates a HTML document around the given markup. */ protected String createHtmlDocument(String text, String align, String valign, int w, int h, boolean wrap, String overflow, boolean clip) { StringBuffer css = new StringBuffer(); css.append("display:inline;"); css.append("font-family:" + state.fontFamily + ";"); css.append("font-size:" + Math.round(state.fontSize) + " pt;"); css.append("color:" + state.fontColorValue + ";"); // KNOWN: Line-height ignored in JLabel css.append("line-height:" + ((mxConstants.ABSOLUTE_LINE_HEIGHT) ? Math.round(state.fontSize * mxConstants.LINE_HEIGHT) + " pt" : mxConstants.LINE_HEIGHT) + ";"); boolean setWidth = false; if ((state.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD) { css.append("font-weight:bold;"); } if ((state.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC) { css.append("font-style:italic;"); } if ((state.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE) { css.append("text-decoration:underline;"); } if (align != null) { if (align.equals(mxConstants.ALIGN_CENTER)) { css.append("text-align:center;"); } else if (align.equals(mxConstants.ALIGN_RIGHT)) { css.append("text-align:right;"); } } if (state.fontBackgroundColorValue != null) { css.append("background-color:" + state.fontBackgroundColorValue + ";"); } // KNOWN: Border ignored in JLabel if (state.fontBorderColorValue != null) { css.append("border:1pt solid " + state.fontBorderColorValue + ";"); } // KNOWN: max-width/-height ignored in JLabel if (clip) { css.append("overflow:hidden;"); setWidth = true; } else if (overflow != null) { if (overflow.equals("fill")) { css.append("height:" + Math.round(h) + "pt;"); setWidth = true; } else if (overflow.equals("width")) { setWidth = true; if (h > 0) { css.append("height:" + Math.round(h) + "pt;"); } } } if (wrap) { if (!clip) { // NOTE: Max-width not available in Java setWidth = true; } css.append("white-space:normal;"); } else { css.append("white-space:nowrap;"); } if (setWidth && w > 0) { css.append("width:" + Math.round(w) + "pt;"); } return "<html><div style=\"" + css.toString() + "\">" + text + "</div></html>"; } /** * Hook to return the renderer for HTML formatted text. This implementation returns * the shared instance of mxLighweightLabel. */ protected JLabel getTextRenderer() { return mxLightweightLabel.getSharedInstance(); } /** * */ protected Point2D getMargin(String align, String valign) { double dx = 0; double dy = 0; if (align != null) { if (align.equals(mxConstants.ALIGN_CENTER)) { dx = -0.5; } else if (align.equals(mxConstants.ALIGN_RIGHT)) { dx = -1; } } if (valign != null) { if (valign.equals(mxConstants.ALIGN_MIDDLE)) { dy = -0.5; } else if (valign.equals(mxConstants.ALIGN_BOTTOM)) { dy = -1; } } return new Point2D.Double(dx, dy); } /** * Draws the given HTML text. */ protected void htmlText(double x, double y, double w, double h, String str, String align, String valign, boolean wrap, String format, String overflow, boolean clip, double rotation) { x += state.dx; y += state.dy; JLabel textRenderer = getTextRenderer(); if (textRenderer != null && rendererPane != null) { // Use native scaling for HTML AffineTransform previous = state.g.getTransform(); state.g.scale(state.scale, state.scale); double rad = rotation * (Math.PI / 180); state.g.rotate(rad, x, y); // Renders the scaled text with a correction factor of // PX_PER_PIXEL for px in HTML vs pixels in the bitmap boolean widthFill = false; boolean fill = false; String original = str; if (overflow != null) { widthFill = overflow.equals("width"); fill = overflow.equals("fill"); } str = createHtmlDocument(str, align, valign, (widthFill || fill) ? (int) Math.round(w) : 0, (fill) ? (int) Math.round(h) : 0, wrap, overflow, clip); textRenderer.setText(str); Dimension pref = textRenderer.getPreferredSize(); int prefWidth = pref.width; int prefHeight = pref.height; // Poor man's max-width // TODO: Is this still needed? if (((clip || wrap) && prefWidth > w && w > 0) || (clip && prefHeight > h && h > 0)) { // TextWidthDelta is workaround for inconsistent word wrapping in Java int cw = (int) Math.round(w + ((wrap) ? JAVA_TEXT_WIDTH_DELTA : 0)); int ch = (int) Math.round(h); str = createHtmlDocument(original, align, valign, cw, ch, wrap, overflow, clip); textRenderer.setText(str); pref = textRenderer.getPreferredSize(); prefWidth = pref.width; prefHeight = pref.height + 2; } // Matches HTML output if (clip && w > 0 && h > 0) { prefWidth = Math.min(pref.width, (int) w); prefHeight = Math.min(prefHeight, (int) h); h = prefHeight; } else if (!clip && wrap && w > 0 && h > 0) { prefWidth = pref.width; w = Math.max(pref.width, (int) w); h = prefHeight; prefHeight = Math.max(prefHeight, (int) h); } else if (!clip && !wrap) { if (w > 0 && w < prefWidth) { w = prefWidth; } if (h > 0 && h < prefHeight) { h = prefHeight; } } Point2D margin = getMargin(align, valign); x += margin.getX() * prefWidth; y += margin.getY() * prefHeight; if (w == 0) { w = prefWidth; } if (h == 0) { h = prefHeight; } rendererPane.paintComponent(state.g, textRenderer, rendererPane, (int) Math.round(x), (int) Math.round(y), (int) Math.round(w), (int) Math.round(h), true); state.g.setTransform(previous); } } /** * Draws the given text. */ public void text(double x, double y, double w, double h, String str, String align, String valign, boolean wrap, String format, String overflow, boolean clip, double rotation) { if (format != null && format.equals("html")) { htmlText(x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation); } else { plainText(x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation); } } /** * Draws the given text. */ public void plainText(double x, double y, double w, double h, String str, String align, String valign, boolean wrap, String format, String overflow, boolean clip, double rotation) { if (state.fontColor == null) { state.fontColor = parseColor(state.fontColorValue); } if (state.fontColor != null) { x = (state.dx + x) * state.scale; y = (state.dy + y) * state.scale; w *= state.scale; h *= state.scale; // Font-metrics needed below this line Graphics2D g2 = createTextGraphics(x, y, w, h, rotation, clip, align, valign); FontMetrics fm = g2.getFontMetrics(); String[] lines = str.split("\n"); int[] stringWidths = new int[lines.length]; int textWidth = 0; for (int i = 0; i < lines.length; i++) { stringWidths[i] = fm.stringWidth(lines[i]); textWidth = Math.max(textWidth, stringWidths[i]); } int textHeight = (int) Math.round(lines.length * (fm.getFont().getSize() * mxConstants.LINE_HEIGHT)); if (clip && textHeight > h && h > 0) { textHeight = (int) h; } Point2D margin = getMargin(align, valign); x += margin.getX() * textWidth; y += margin.getY() * textHeight; if (state.fontBackgroundColorValue != null) { if (state.fontBackgroundColor == null) { state.fontBackgroundColor = parseColor(state.fontBackgroundColorValue); } if (state.fontBackgroundColor != null) { g2.setColor(state.fontBackgroundColor); g2.fillRect((int) Math.round(x), (int) Math.round(y - 1), textWidth + 1, textHeight + 2); } } if (state.fontBorderColorValue != null) { if (state.fontBorderColor == null) { state.fontBorderColor = parseColor(state.fontBorderColorValue); } if (state.fontBorderColor != null) { g2.setColor(state.fontBorderColor); g2.drawRect((int) Math.round(x), (int) Math.round(y - 1), textWidth + 1, textHeight + 2); } } g2.setColor(state.fontColor); y += fm.getHeight() - fm.getDescent() - (margin.getY() + 0.5); for (int i = 0; i < lines.length; i++) { double dx = 0; if (align != null) { if (align.equals(mxConstants.ALIGN_CENTER)) { dx = (textWidth - stringWidths[i]) / 2; } else if (align.equals(mxConstants.ALIGN_RIGHT)) { dx = textWidth - stringWidths[i]; } } // Adds support for underlined text via attributed character iterator if (!lines[i].isEmpty()) { if ((state.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE) { AttributedString as = new AttributedString(lines[i]); as.addAttribute(TextAttribute.FONT, g2.getFont()); as.addAttribute(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON); g2.drawString(as.getIterator(), (int) Math.round(x + dx), (int) Math.round(y)); } else { g2.drawString(lines[i], (int) Math.round(x + dx), (int) Math.round(y)); } } y += (int) Math.round(fm.getFont().getSize() * mxConstants.LINE_HEIGHT); } } } /** * Returns a new graphics instance with the correct color and font for * text rendering. */ protected final Graphics2D createTextGraphics(double x, double y, double w, double h, double rotation, boolean clip, String align, String valign) { Graphics2D g2 = state.g; updateFont(); if (rotation != 0) { g2 = (Graphics2D) state.g.create(); double rad = rotation * (Math.PI / 180); g2.rotate(rad, x, y); } if (clip && w > 0 && h > 0) { if (g2 == state.g) { g2 = (Graphics2D) state.g.create(); } Point2D margin = getMargin(align, valign); x += margin.getX() * w; y += margin.getY() * h; g2.clip(new Rectangle2D.Double(x, y, w, h)); } return g2; } /** * */ public void begin() { currentPath = new GeneralPath(); } /** * */ public void moveTo(double x, double y) { if (currentPath != null) { currentPath.moveTo((float) ((state.dx + x) * state.scale), (float) ((state.dy + y) * state.scale)); } } /** * */ public void lineTo(double x, double y) { if (currentPath != null) { currentPath.lineTo((float) ((state.dx + x) * state.scale), (float) ((state.dy + y) * state.scale)); } } /** * */ public void quadTo(double x1, double y1, double x2, double y2) { if (currentPath != null) { currentPath.quadTo((float) ((state.dx + x1) * state.scale), (float) ((state.dy + y1) * state.scale), (float) ((state.dx + x2) * state.scale), (float) ((state.dy + y2) * state.scale)); } } /** * */ public void curveTo(double x1, double y1, double x2, double y2, double x3, double y3) { if (currentPath != null) { currentPath.curveTo((float) ((state.dx + x1) * state.scale), (float) ((state.dy + y1) * state.scale), (float) ((state.dx + x2) * state.scale), (float) ((state.dy + y2) * state.scale), (float) ((state.dx + x3) * state.scale), (float) ((state.dy + y3) * state.scale)); } } /** * Closes the current path. */ public void close() { if (currentPath != null) { currentPath.closePath(); } } /** * */ public void stroke() { paintCurrentPath(false, true); } /** * */ public void fill() { paintCurrentPath(true, false); } /** * */ public void fillAndStroke() { paintCurrentPath(true, true); } /** * */ protected void paintCurrentPath(boolean filled, boolean stroked) { if (currentPath != null) { if (stroked) { if (state.strokeColor == null) { state.strokeColor = parseColor(state.strokeColorValue); } if (state.strokeColor != null) { updateStroke(); } } if (filled) { if (state.gradientPaint == null && state.fillColor == null) { state.fillColor = parseColor(state.fillColorValue); } } if (state.shadow) { paintShadow(filled, stroked); } if (filled) { if (state.gradientPaint != null) { state.g.setPaint(state.gradientPaint); state.g.fill(currentPath); } else { if (state.fillColor == null) { state.fillColor = parseColor(state.fillColorValue); } if (state.fillColor != null) { state.g.setColor(state.fillColor); state.g.setPaint(null); state.g.fill(currentPath); } } } if (stroked && state.strokeColor != null) { state.g.setColor(state.strokeColor); state.g.draw(currentPath); } } } /** * */ protected void paintShadow(boolean filled, boolean stroked) { if (state.shadowColor == null) { state.shadowColor = parseColor(state.shadowColorValue); } if (state.shadowColor != null) { double rad = -state.theta * (Math.PI / 180); double cos = Math.cos(rad); double sin = Math.sin(rad); double dx = state.shadowOffsetX * state.scale; double dy = state.shadowOffsetY * state.scale; if (state.flipH) { dx *= -1; } if (state.flipV) { dy *= -1; } double tx = dx * cos - dy * sin; double ty = dx * sin + dy * cos; state.g.setColor(state.shadowColor); state.g.translate(tx, ty); double alpha = state.alpha * state.shadowAlpha; Composite comp = state.g.getComposite(); state.g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) (alpha))); if (filled && (state.gradientPaint != null || state.fillColor != null)) { state.g.fill(currentPath); } // FIXME: Overlaps with fill in composide mode if (stroked && state.strokeColor != null) { state.g.draw(currentPath); } state.g.translate(-tx, -ty); state.g.setComposite(comp); } } /** * */ public void setShadow(boolean value) { state.shadow = value; } /** * */ public void setShadowColor(String value) { state.shadowColorValue = value; } /** * */ public void setShadowAlpha(double value) { state.shadowAlpha = value; } /** * */ public void setShadowOffset(double dx, double dy) { state.shadowOffsetX = dx; state.shadowOffsetY = dy; } /** * */ protected void updateFont() { int size = (int) Math.round(state.fontSize * state.scale); int style = ((state.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD) ? Font.BOLD : Font.PLAIN; style += ((state.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC) ? Font.ITALIC : Font.PLAIN; if (lastFont == null || !lastFontFamily.equals(state.fontFamily) || size != lastFontSize || style != lastFontStyle) { lastFont = createFont(state.fontFamily, style, size); lastFontFamily = state.fontFamily; lastFontStyle = style; lastFontSize = size; } state.g.setFont(lastFont); } /** * Hook for subclassers to implement font caching. */ protected Font createFont(String family, int style, int size) { return new Font(getFontName(family), style, size); } /** * Returns a font name for the given CSS values for font-family. * This implementation returns the first entry for comma-separated * lists of entries. */ protected String getFontName(String family) { if (family != null) { int comma = family.indexOf(','); if (comma >= 0) { family = family.substring(0, comma); } } return family; } /** * */ protected void updateStroke() { float sw = (float) Math.max(1, state.strokeWidth * state.scale); int cap = BasicStroke.CAP_BUTT; if (state.lineCap.equals("round")) { cap = BasicStroke.CAP_ROUND; } else if (state.lineCap.equals("square")) { cap = BasicStroke.CAP_SQUARE; } int join = BasicStroke.JOIN_MITER; if (state.lineJoin.equals("round")) { join = BasicStroke.JOIN_ROUND; } else if (state.lineJoin.equals("bevel")) { join = BasicStroke.JOIN_BEVEL; } float miterlimit = (float) state.miterLimit; if (lastStroke == null || lastStrokeWidth != sw || lastCap != cap || lastJoin != join || lastMiterLimit != miterlimit || lastDashed != state.dashed || (state.dashed && lastDashPattern != state.dashPattern)) { float[] dash = null; if (state.dashed) { dash = new float[state.dashPattern.length]; for (int i = 0; i < dash.length; i++) { dash[i] = (float) (state.dashPattern[i] * sw); } } lastStroke = new BasicStroke(sw, cap, join, miterlimit, dash, 0); lastStrokeWidth = sw; lastCap = cap; lastJoin = join; lastMiterLimit = miterlimit; lastDashed = state.dashed; lastDashPattern = state.dashPattern; } state.g.setStroke(lastStroke); } /** * */ protected class CanvasState implements Cloneable { /** * */ protected double alpha = 1; /** * */ protected double scale = 1; /** * */ protected double dx = 0; /** * */ protected double dy = 0; /** * */ protected double theta = 0; /** * */ protected double rotationCx = 0; /** * */ protected double rotationCy = 0; /** * */ protected boolean flipV = false; /** * */ protected boolean flipH = false; /** * */ protected double miterLimit = 10; /** * */ protected int fontStyle = 0; /** * */ protected double fontSize = mxConstants.DEFAULT_FONTSIZE; /** * */ protected String fontFamily = mxConstants.DEFAULT_FONTFAMILIES; /** * */ protected String fontColorValue = "#000000"; /** * */ protected Color fontColor; /** * */ protected String fontBackgroundColorValue; /** * */ protected Color fontBackgroundColor; /** * */ protected String fontBorderColorValue; /** * */ protected Color fontBorderColor; /** * */ protected String lineCap = "flat"; /** * */ protected String lineJoin = "miter"; /** * */ protected double strokeWidth = 1; /** * */ protected String strokeColorValue; /** * */ protected Color strokeColor; /** * */ protected String fillColorValue; /** * */ protected Color fillColor; /** * */ protected Paint gradientPaint; /** * */ protected boolean dashed = false; /** * */ protected float[] dashPattern = { 3, 3 }; /** * */ protected boolean shadow = false; /** * */ protected String shadowColorValue = mxConstants.W3C_SHADOWCOLOR; /** * */ protected Color shadowColor; /** * */ protected double shadowAlpha = 1; /** * */ protected double shadowOffsetX = mxConstants.SHADOW_OFFSETX; /** * */ protected double shadowOffsetY = mxConstants.SHADOW_OFFSETY; /** * Stores the actual state. */ protected transient Graphics2D g; /** * */ public Object clone() throws CloneNotSupportedException { return super.clone(); } } }