001/* =================================================== 002 * JFreeSVG : an SVG library for the Java(tm) platform 003 * =================================================== 004 * 005 * (C)opyright 2013-2015, by Object Refinery Limited. All rights reserved. 006 * 007 * Project Info: http://www.jfree.org/jfreesvg/index.html 008 * 009 * This program is free software: you can redistribute it and/or modify 010 * it under the terms of the GNU General Public License as published by 011 * the Free Software Foundation, either version 3 of the License, or 012 * (at your option) any later version. 013 * 014 * This program is distributed in the hope that it will be useful, 015 * but WITHOUT ANY WARRANTY; without even the implied warranty of 016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 017 * GNU General Public License for more details. 018 * 019 * You should have received a copy of the GNU General Public License 020 * along with this program. If not, see <http://www.gnu.org/licenses/>. 021 * 022 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 023 * Other names may be trademarks of their respective owners.] 024 * 025 * If you do not wish to be bound by the terms of the GPL, an alternative 026 * commercial license can be purchased. For details, please see visit the 027 * JFreeSVG home page: 028 * 029 * http://www.jfree.org/jfreesvg 030 */ 031 032package org.jfree.graphics2d.svg; 033 034import java.awt.AlphaComposite; 035import java.awt.BasicStroke; 036import java.awt.Color; 037import java.awt.Composite; 038import java.awt.Font; 039import java.awt.FontMetrics; 040import java.awt.GradientPaint; 041import java.awt.Graphics; 042import java.awt.Graphics2D; 043import java.awt.GraphicsConfiguration; 044import java.awt.Image; 045import java.awt.LinearGradientPaint; 046import java.awt.MultipleGradientPaint.CycleMethod; 047import java.awt.Paint; 048import java.awt.RadialGradientPaint; 049import java.awt.Rectangle; 050import java.awt.RenderingHints; 051import java.awt.Shape; 052import java.awt.Stroke; 053import java.awt.font.FontRenderContext; 054import java.awt.font.GlyphVector; 055import java.awt.font.TextLayout; 056import java.awt.geom.AffineTransform; 057import java.awt.geom.Arc2D; 058import java.awt.geom.Area; 059import java.awt.geom.Ellipse2D; 060import java.awt.geom.GeneralPath; 061import java.awt.geom.Line2D; 062import java.awt.geom.NoninvertibleTransformException; 063import java.awt.geom.Path2D; 064import java.awt.geom.PathIterator; 065import java.awt.geom.Point2D; 066import java.awt.geom.Rectangle2D; 067import java.awt.geom.RoundRectangle2D; 068import java.awt.image.BufferedImage; 069import java.awt.image.BufferedImageOp; 070import java.awt.image.ImageObserver; 071import java.awt.image.RenderedImage; 072import java.awt.image.renderable.RenderableImage; 073import java.io.ByteArrayOutputStream; 074import java.io.IOException; 075import java.text.AttributedCharacterIterator; 076import java.text.AttributedCharacterIterator.Attribute; 077import java.text.AttributedString; 078import java.text.DecimalFormat; 079import java.text.DecimalFormatSymbols; 080import java.util.ArrayList; 081import java.util.HashMap; 082import java.util.HashSet; 083import java.util.List; 084import java.util.Map; 085import java.util.Map.Entry; 086import java.util.Set; 087import java.util.logging.Level; 088import java.util.logging.Logger; 089import javax.imageio.ImageIO; 090import javax.xml.bind.DatatypeConverter; 091import org.jfree.graphics2d.Args; 092import org.jfree.graphics2d.GradientPaintKey; 093import org.jfree.graphics2d.GraphicsUtils; 094import org.jfree.graphics2d.LinearGradientPaintKey; 095import org.jfree.graphics2d.RadialGradientPaintKey; 096 097/** 098 * A {@code Graphics2D} implementation that creates SVG output. After 099 * rendering the graphics via the {@code SVGGraphics2D}, you can retrieve 100 * an SVG element (see {@link #getSVGElement()}) or an SVG document (see 101 * {@link #getSVGDocument()}) containing your content. 102 * <p> 103 * <b>Usage</b><br> 104 * Using the {@code SVGGraphics2D} class is straightforward. First, 105 * create an instance specifying the height and width of the SVG element that 106 * will be created. Then, use standard Java2D API calls to draw content 107 * into the element. Finally, retrieve the SVG element that has been 108 * accumulated. For example: 109 * <p> 110 * {@code SVGGraphics2D g2 = new SVGGraphics2D(300, 200);<br>} 111 * {@code g2.setPaint(Color.RED);<br>} 112 * {@code g2.draw(new Rectangle(10, 10, 280, 180);<br>} 113 * {@code String svgElement = g2.getSVGElement();<br>} 114 * <p> 115 * For the content generation step, you can make use of third party libraries, 116 * such as <a href="http://www.jfree.org/jfreechart/">JFreeChart</a> and 117 * <a href="http://www.object-refinery.com/orsoncharts/">Orson Charts</a>, that 118 * render output using standard Java2D API calls. 119 * <p> 120 * <b>Rendering Hints</b><br> 121 * The {@code SVGGraphics2D} supports a couple of custom rendering hints - 122 * for details, refer to the {@link SVGHints} class documentation. Also see 123 * the examples in this blog post: 124 * <a href="http://www.object-refinery.com/blog/blog-20140509.html"> 125 * Orson Charts 3D / Enhanced SVG Export</a>. 126 * <p> 127 * <b>Other Notes</b><br> 128 * Some additional notes: 129 * <ul> 130 * <li>Images are supported, but for methods with an {@code ImageObserver} 131 * parameter note that the observer is ignored completely. In any case, using 132 * images that are not fully loaded already would not be a good idea in the 133 * context of generating SVG data/files;</li> 134 * 135 * <li>the {@link #getFontMetrics(java.awt.Font)} and 136 * {@link #getFontRenderContext()} methods return values that come from an 137 * internal {@code BufferedImage}, this is a short-cut and we don't know 138 * if there are any negative consequences (if you know of any, please let us 139 * know and we'll add the info here or find a way to fix it);</li> 140 * 141 * <li>there are settings to control the number of decimal places used to 142 * write the coordinates for geometrical elements (default 2dp) and transform 143 * matrices (default 6dp). These defaults may change in a future release.</li> 144 * 145 * <li>when an HTML page contains multiple SVG elements, the items within 146 * the DEFS element for each SVG element must have IDs that are unique across 147 * <em>all</em> SVG elements in the page. We auto-populate the 148 * {@code defsKeyPrefix} attribute to help ensure that unique IDs are 149 * generated.</li> 150 * </ul> 151 * 152 * <p>For some demos showing how to use this class, look in the 153 * {@code org.jfree.graphics2d.demo} package in the{@code src} directory.</p> 154 */ 155public final class SVGGraphics2D extends Graphics2D { 156 157 /** The prefix for keys used to identify clip paths. */ 158 private static final String CLIP_KEY_PREFIX = "clip-"; 159 160 private final int width; 161 162 private final int height; 163 164 /** 165 * The shape rendering property to set for the SVG element. Permitted 166 * values are "auto", "crispEdges", "geometricPrecision" and 167 * "optimizeSpeed". 168 */ 169 private String shapeRendering = "auto"; 170 171 /** 172 * The text rendering property for the SVG element. Permitted values 173 * are "auto", "optimizeSpeed", "optimizeLegibility" and 174 * "geometricPrecision". 175 */ 176 private String textRendering = "auto"; 177 178 /** Rendering hints (see SVGHints). */ 179 private final RenderingHints hints; 180 181 /** 182 * A flag that controls whether or not the KEY_STROKE_CONTROL hint is 183 * checked. 184 */ 185 private boolean checkStrokeControlHint = true; 186 187 /** 188 * The number of decimal places to use when writing the matrix values 189 * for transformations. 190 */ 191 private int transformDP; 192 193 /** 194 * The decimal formatter for transform matrices. 195 */ 196 private DecimalFormat transformFormat; 197 198 /** 199 * The number of decimal places to use when writing coordinates for 200 * geometrical shapes. 201 */ 202 private int geometryDP; 203 204 /** 205 * The decimal formatter for coordinates of geometrical shapes. 206 */ 207 private DecimalFormat geometryFormat; 208 209 /** The buffer that accumulates the SVG output. */ 210 private StringBuilder sb; 211 212 /** 213 * A prefix for the keys used in the DEFS element. This can be used to 214 * ensure that the keys are unique when creating more than one SVG element 215 * for a single HTML page. 216 */ 217 private String defsKeyPrefix = ""; 218 219 /** 220 * A map of all the gradients used, and the corresponding id. When 221 * generating the SVG file, all the gradient paints used must be defined 222 * in the defs element. 223 */ 224 private Map<GradientPaintKey, String> gradientPaints 225 = new HashMap<GradientPaintKey, String>(); 226 227 /** 228 * A map of all the linear gradients used, and the corresponding id. When 229 * generating the SVG file, all the linear gradient paints used must be 230 * defined in the defs element. 231 */ 232 private Map<LinearGradientPaintKey, String> linearGradientPaints 233 = new HashMap<LinearGradientPaintKey, String>(); 234 235 /** 236 * A map of all the radial gradients used, and the corresponding id. When 237 * generating the SVG file, all the radial gradient paints used must be 238 * defined in the defs element. 239 */ 240 private Map<RadialGradientPaintKey, String> radialGradientPaints 241 = new HashMap<RadialGradientPaintKey, String>(); 242 243 /** 244 * A list of the registered clip regions. These will be written to the 245 * DEFS element. 246 */ 247 private List<String> clipPaths = new ArrayList<String>(); 248 249 /** 250 * The filename prefix for images that are referenced rather than 251 * embedded but don't have an {@code href} supplied via the 252 * {@link #KEY_IMAGE_HREF} hint. 253 */ 254 private String filePrefix; 255 256 /** 257 * The filename suffix for images that are referenced rather than 258 * embedded but don't have an {@code href} supplied via the 259 * {@link #KEY_IMAGE_HREF} hint. 260 */ 261 private String fileSuffix; 262 263 /** 264 * A list of images that are referenced but not embedded in the SVG. 265 * After the SVG is generated, the caller can make use of this list to 266 * write PNG files if they don't already exist. 267 */ 268 private List<ImageElement> imageElements; 269 270 /** The user clip (can be null). */ 271 private Shape clip; 272 273 /** The reference for the current clip. */ 274 private String clipRef; 275 276 /** The current transform. */ 277 private AffineTransform transform = new AffineTransform(); 278 279 private Paint paint = Color.BLACK; 280 281 private Color color = Color.BLACK; 282 283 private Composite composite = AlphaComposite.getInstance( 284 AlphaComposite.SRC_OVER, 1.0f); 285 286 /** The current stroke. */ 287 private Stroke stroke = new BasicStroke(1.0f); 288 289 /** 290 * The width of the SVG stroke to use when the user supplies a 291 * BasicStroke with a width of 0.0 (in this case the Java specification 292 * says "If width is set to 0.0f, the stroke is rendered as the thinnest 293 * possible line for the target device and the antialias hint setting.") 294 */ 295 private double zeroStrokeWidth; 296 297 /** The last font that was set. */ 298 private Font font; 299 300 /** 301 * The font render context. The fractional metrics flag solves the glyph 302 * positioning issue identified by Christoph Nahr: 303 * http://news.kynosarges.org/2014/06/28/glyph-positioning-in-jfreesvg-orsonpdf/ 304 */ 305 private final FontRenderContext fontRenderContext = new FontRenderContext( 306 null, false, true); 307 308 /** Maps font family names to alternates (or leaves them unchanged). */ 309 private FontMapper fontMapper; 310 311 /** The background color, used by clearRect(). */ 312 private Color background = Color.BLACK; 313 314 /** A hidden image used for font metrics. */ 315 private final BufferedImage fmImage = new BufferedImage(10, 10, 316 BufferedImage.TYPE_INT_RGB); 317 318 /** 319 * An instance that is lazily instantiated in drawLine and then 320 * subsequently reused to avoid creating a lot of garbage. 321 */ 322 private Line2D line; 323 324 /** 325 * An instance that is lazily instantiated in fillRect and then 326 * subsequently reused to avoid creating a lot of garbage. 327 */ 328 Rectangle2D rect; 329 330 /** 331 * An instance that is lazily instantiated in draw/fillRoundRect and then 332 * subsequently reused to avoid creating a lot of garbage. 333 */ 334 private RoundRectangle2D roundRect; 335 336 /** 337 * An instance that is lazily instantiated in draw/fillOval and then 338 * subsequently reused to avoid creating a lot of garbage. 339 */ 340 private Ellipse2D oval; 341 342 /** 343 * An instance that is lazily instantiated in draw/fillArc and then 344 * subsequently reused to avoid creating a lot of garbage. 345 */ 346 private Arc2D arc; 347 348 /** 349 * If the current paint is an instance of {@link GradientPaint}, this 350 * field will contain the reference id that is used in the DEFS element 351 * for that linear gradient. 352 */ 353 private String gradientPaintRef = null; 354 355 /** 356 * The device configuration (this is lazily instantiated in the 357 * getDeviceConfiguration() method). 358 */ 359 private GraphicsConfiguration deviceConfiguration; 360 361 /** A set of element IDs. */ 362 private final Set<String> elementIDs; 363 364 /** 365 * Creates a new instance with the specified width and height. 366 * 367 * @param width the width of the SVG element. 368 * @param height the height of the SVG element. 369 */ 370 public SVGGraphics2D(int width, int height) { 371 this(width, height, new StringBuilder()); 372 } 373 374 /** 375 * Creates a new instance with the specified width and height that will 376 * populate the supplied StringBuilder instance. This constructor is 377 * used by the {@link #create()} method, but won't normally be called 378 * directly by user code. 379 * 380 * @param width the width of the SVG element. 381 * @param height the height of the SVG element. 382 * @param sb the string builder ({@code null} not permitted). 383 * 384 * @since 2.0 385 */ 386 public SVGGraphics2D(int width, int height, StringBuilder sb) { 387 this.width = width; 388 this.height = height; 389 this.shapeRendering = "auto"; 390 this.textRendering = "auto"; 391 this.defsKeyPrefix = String.valueOf(System.nanoTime()); 392 this.clip = null; 393 this.imageElements = new ArrayList<ImageElement>(); 394 this.filePrefix = "image-"; 395 this.fileSuffix = ".png"; 396 this.font = new Font("SansSerif", Font.PLAIN, 12); 397 this.fontMapper = new StandardFontMapper(); 398 this.zeroStrokeWidth = 0.1; 399 this.sb = sb; 400 this.hints = new RenderingHints(SVGHints.KEY_IMAGE_HANDLING, 401 SVGHints.VALUE_IMAGE_HANDLING_EMBED); 402 // force the formatters to use a '.' for the decimal point 403 DecimalFormatSymbols dfs = new DecimalFormatSymbols(); 404 dfs.setDecimalSeparator('.'); 405 this.transformFormat = new DecimalFormat("0.######", dfs); 406 this.geometryFormat = new DecimalFormat("0.##", dfs); 407 this.elementIDs = new HashSet<String>(); 408 } 409 410 /** 411 * Creates a new instance that is a child of the supplied parent. 412 * 413 * @param parent 414 */ 415 private SVGGraphics2D(SVGGraphics2D parent) { 416 this(parent.width, parent.height, parent.sb); 417 this.shapeRendering = parent.shapeRendering; 418 this.textRendering = parent.textRendering; 419 getRenderingHints().add(parent.hints); 420 this.checkStrokeControlHint = parent.checkStrokeControlHint; 421 setTransformDP(parent.transformDP); 422 setGeometryDP(parent.geometryDP); 423 this.defsKeyPrefix = parent.defsKeyPrefix; 424 this.gradientPaints = parent.gradientPaints; 425 this.linearGradientPaints = parent.linearGradientPaints; 426 this.radialGradientPaints = parent.radialGradientPaints; 427 this.clipPaths = parent.clipPaths; 428 this.filePrefix = parent.filePrefix; 429 this.fileSuffix = parent.fileSuffix; 430 this.imageElements = parent.imageElements; 431 this.zeroStrokeWidth = parent.zeroStrokeWidth; 432 } 433 434 /** 435 * Returns the width for the SVG element, specified in the constructor. 436 * This value will be written to the SVG element returned by the 437 * {@link #getSVGElement()} method. 438 * 439 * @return The width for the SVG element. 440 */ 441 public int getWidth() { 442 return this.width; 443 } 444 445 /** 446 * Returns the height for the SVG element, specified in the constructor. 447 * This value will be written to the SVG element returned by the 448 * {@link #getSVGElement()} method. 449 * 450 * @return The height for the SVG element. 451 */ 452 public int getHeight() { 453 return this.height; 454 } 455 456 /** 457 * Returns the value of the 'shape-rendering' property that will be 458 * written to the SVG element. The default value is "auto". 459 * 460 * @return The shape rendering property. 461 * 462 * @since 2.0 463 */ 464 public String getShapeRendering() { 465 return this.shapeRendering; 466 } 467 468 /** 469 * Sets the value of the 'shape-rendering' property that will be written to 470 * the SVG element. Permitted values are "auto", "crispEdges", 471 * "geometricPrecision", "inherit" and "optimizeSpeed". 472 * 473 * @param value the new value. 474 * 475 * @since 2.0 476 */ 477 public void setShapeRendering(String value) { 478 if (!value.equals("auto") && !value.equals("crispEdges") 479 && !value.equals("geometricPrecision") 480 && !value.equals("optimizeSpeed")) { 481 throw new IllegalArgumentException("Unrecognised value: " + value); 482 } 483 this.shapeRendering = value; 484 } 485 486 /** 487 * Returns the value of the 'text-rendering' property that will be 488 * written to the SVG element. The default value is "auto". 489 * 490 * @return The text rendering property. 491 * 492 * @since 2.0 493 */ 494 public String getTextRendering() { 495 return this.textRendering; 496 } 497 498 /** 499 * Sets the value of the 'text-rendering' property that will be written to 500 * the SVG element. Permitted values are "auto", "optimizeSpeed", 501 * "optimizeLegibility" and "geometricPrecision". 502 * 503 * @param value the new value. 504 * 505 * @since 2.0 506 */ 507 public void setTextRendering(String value) { 508 if (!value.equals("auto") && !value.equals("optimizeSpeed") 509 && !value.equals("optimizeLegibility") 510 && !value.equals("geometricPrecision")) { 511 throw new IllegalArgumentException("Unrecognised value: " + value); 512 } 513 this.textRendering = value; 514 } 515 516 /** 517 * Returns the flag that controls whether or not this object will observe 518 * the {@code KEY_STROKE_CONTROL} rendering hint. The default value is 519 * {@code true}. 520 * 521 * @return A boolean. 522 * 523 * @see #setCheckStrokeControlHint(boolean) 524 * @since 2.0 525 */ 526 public boolean getCheckStrokeControlHint() { 527 return this.checkStrokeControlHint; 528 } 529 530 /** 531 * Sets the flag that controls whether or not this object will observe 532 * the {@code KEY_STROKE_CONTROL} rendering hint. When enabled (the 533 * default), a hint to normalise strokes will write a {@code stroke-style} 534 * attribute with the value {@code crispEdges}. 535 * 536 * @param check the new flag value. 537 * 538 * @see #getCheckStrokeControlHint() 539 * @since 2.0 540 */ 541 public void setCheckStrokeControlHint(boolean check) { 542 this.checkStrokeControlHint = check; 543 } 544 545 /** 546 * Returns the prefix used for all keys in the DEFS element. The default 547 * value is {@code String.valueOf(System.nanoTime())}. 548 * 549 * @return The prefix string (never {@code null}). 550 * 551 * @since 1.9 552 */ 553 public String getDefsKeyPrefix() { 554 return this.defsKeyPrefix; 555 } 556 557 /** 558 * Sets the prefix that will be used for all keys in the DEFS element. 559 * If required, this must be set immediately after construction (before any 560 * content generation methods have been called). 561 * 562 * @param prefix the prefix ({@code null} not permitted). 563 * 564 * @since 1.9 565 */ 566 public void setDefsKeyPrefix(String prefix) { 567 Args.nullNotPermitted(prefix, "prefix"); 568 this.defsKeyPrefix = prefix; 569 } 570 571 /** 572 * Returns the number of decimal places used to write the transformation 573 * matrices in the SVG output. The default value is 6. 574 * <p> 575 * Note that there is a separate attribute to control the number of decimal 576 * places for geometrical elements in the output (see 577 * {@link #getGeometryDP()}). 578 * 579 * @return The number of decimal places. 580 * 581 * @see #setTransformDP(int) 582 */ 583 public int getTransformDP() { 584 return this.transformDP; 585 } 586 587 /** 588 * Sets the number of decimal places used to write the transformation 589 * matrices in the SVG output. Values in the range 1 to 10 will be used 590 * to configure a formatter to that number of decimal places, for all other 591 * values we revert to the normal {@code String} conversion of 592 * {@code double} primitives (approximately 16 decimals places). 593 * <p> 594 * Note that there is a separate attribute to control the number of decimal 595 * places for geometrical elements in the output (see 596 * {@link #setGeometryDP(int)}). 597 * 598 * @param dp the number of decimal places (normally 1 to 10). 599 * 600 * @see #getTransformDP() 601 */ 602 public void setTransformDP(int dp) { 603 this.transformDP = dp; 604 if (dp < 1 || dp > 10) { 605 this.transformFormat = null; 606 return; 607 } 608 DecimalFormatSymbols dfs = new DecimalFormatSymbols(); 609 dfs.setDecimalSeparator('.'); 610 this.transformFormat = new DecimalFormat("0." 611 + "##########".substring(0, dp), dfs); 612 } 613 614 /** 615 * Returns the number of decimal places used to write the coordinates 616 * of geometrical shapes. The default value is 2. 617 * <p> 618 * Note that there is a separate attribute to control the number of decimal 619 * places for transform matrices in the output (see 620 * {@link #getTransformDP()}). 621 * 622 * @return The number of decimal places. 623 */ 624 public int getGeometryDP() { 625 return this.geometryDP; 626 } 627 628 /** 629 * Sets the number of decimal places used to write the coordinates of 630 * geometrical shapes in the SVG output. Values in the range 1 to 10 will 631 * be used to configure a formatter to that number of decimal places, for 632 * all other values we revert to the normal String conversion of double 633 * primitives (approximately 16 decimals places). 634 * <p> 635 * Note that there is a separate attribute to control the number of decimal 636 * places for transform matrices in the output (see 637 * {@link #setTransformDP(int)}). 638 * 639 * @param dp the number of decimal places (normally 1 to 10). 640 */ 641 public void setGeometryDP(int dp) { 642 this.geometryDP = dp; 643 if (dp < 1 || dp > 10) { 644 this.geometryFormat = null; 645 return; 646 } 647 DecimalFormatSymbols dfs = new DecimalFormatSymbols(); 648 dfs.setDecimalSeparator('.'); 649 this.geometryFormat = new DecimalFormat("0." 650 + "##########".substring(0, dp), dfs); 651 } 652 653 /** 654 * Returns the prefix used to generate a filename for an image that is 655 * referenced from, rather than embedded in, the SVG element. 656 * 657 * @return The file prefix (never {@code null}). 658 * 659 * @since 1.5 660 */ 661 public String getFilePrefix() { 662 return this.filePrefix; 663 } 664 665 /** 666 * Sets the prefix used to generate a filename for any image that is 667 * referenced from the SVG element. 668 * 669 * @param prefix the new prefix ({@code null} not permitted). 670 * 671 * @since 1.5 672 */ 673 public void setFilePrefix(String prefix) { 674 Args.nullNotPermitted(prefix, "prefix"); 675 this.filePrefix = prefix; 676 } 677 678 /** 679 * Returns the suffix used to generate a filename for an image that is 680 * referenced from, rather than embedded in, the SVG element. 681 * 682 * @return The file suffix (never {@code null}). 683 * 684 * @since 1.5 685 */ 686 public String getFileSuffix() { 687 return this.fileSuffix; 688 } 689 690 /** 691 * Sets the suffix used to generate a filename for any image that is 692 * referenced from the SVG element. 693 * 694 * @param suffix the new prefix ({@code null} not permitted). 695 * 696 * @since 1.5 697 */ 698 public void setFileSuffix(String suffix) { 699 Args.nullNotPermitted(suffix, "suffix"); 700 this.fileSuffix = suffix; 701 } 702 703 /** 704 * Returns the width to use for the SVG stroke when the AWT stroke 705 * specified has a zero width (the default value is {@code 0.1}). In 706 * the Java specification for {@code BasicStroke} it states "If width 707 * is set to 0.0f, the stroke is rendered as the thinnest possible 708 * line for the target device and the antialias hint setting." We don't 709 * have a means to implement that accurately since we must specify a fixed 710 * width. 711 * 712 * @return The width. 713 * 714 * @since 1.9 715 */ 716 public double getZeroStrokeWidth() { 717 return this.zeroStrokeWidth; 718 } 719 720 /** 721 * Sets the width to use for the SVG stroke when the current AWT stroke 722 * has a width of 0.0. 723 * 724 * @param width the new width (must be 0 or greater). 725 * 726 * @since 1.9 727 */ 728 public void setZeroStrokeWidth(double width) { 729 if (width < 0.0) { 730 throw new IllegalArgumentException("Width cannot be negative."); 731 } 732 this.zeroStrokeWidth = width; 733 } 734 735 /** 736 * Returns the device configuration associated with this 737 * {@code Graphics2D}. 738 * 739 * @return The graphics configuration. 740 */ 741 @Override 742 public GraphicsConfiguration getDeviceConfiguration() { 743 if (this.deviceConfiguration == null) { 744 this.deviceConfiguration = new SVGGraphicsConfiguration(this.width, 745 this.height); 746 } 747 return this.deviceConfiguration; 748 } 749 750 /** 751 * Creates a new graphics object that is a copy of this graphics object 752 * (except that it has not accumulated the drawing operations). Not sure 753 * yet when or why this would be useful when creating SVG output. 754 * 755 * @return A new graphics object. 756 */ 757 @Override 758 public Graphics create() { 759 SVGGraphics2D copy = new SVGGraphics2D(this); 760 copy.setRenderingHints(getRenderingHints()); 761 copy.setTransform(getTransform()); 762 copy.setClip(getClip()); 763 copy.setPaint(getPaint()); 764 copy.setColor(getColor()); 765 copy.setComposite(getComposite()); 766 copy.setStroke(getStroke()); 767 copy.setFont(getFont()); 768 copy.setBackground(getBackground()); 769 copy.setFilePrefix(getFilePrefix()); 770 copy.setFileSuffix(getFileSuffix()); 771 return copy; 772 } 773 774 /** 775 * Returns the paint used to draw or fill shapes (or text). The default 776 * value is {@link Color#BLACK}. 777 * 778 * @return The paint (never {@code null}). 779 * 780 * @see #setPaint(java.awt.Paint) 781 */ 782 @Override 783 public Paint getPaint() { 784 return this.paint; 785 } 786 787 /** 788 * Sets the paint used to draw or fill shapes (or text). If 789 * {@code paint} is an instance of {@code Color}, this method will 790 * also update the current color attribute (see {@link #getColor()}). If 791 * you pass {@code null} to this method, it does nothing (in 792 * accordance with the JDK specification). 793 * 794 * @param paint the paint ({@code null} is permitted but ignored). 795 * 796 * @see #getPaint() 797 */ 798 @Override 799 public void setPaint(Paint paint) { 800 if (paint == null) { 801 return; 802 } 803 this.paint = paint; 804 this.gradientPaintRef = null; 805 if (paint instanceof Color) { 806 setColor((Color) paint); 807 } else if (paint instanceof GradientPaint) { 808 GradientPaint gp = (GradientPaint) paint; 809 GradientPaintKey key = new GradientPaintKey(gp); 810 String ref = this.gradientPaints.get(key); 811 if (ref == null) { 812 int count = this.gradientPaints.keySet().size(); 813 String id = this.defsKeyPrefix + "gp" + count; 814 this.elementIDs.add(id); 815 this.gradientPaints.put(key, id); 816 this.gradientPaintRef = id; 817 } else { 818 this.gradientPaintRef = ref; 819 } 820 } else if (paint instanceof LinearGradientPaint) { 821 LinearGradientPaint lgp = (LinearGradientPaint) paint; 822 LinearGradientPaintKey key = new LinearGradientPaintKey(lgp); 823 String ref = this.linearGradientPaints.get(key); 824 if (ref == null) { 825 int count = this.linearGradientPaints.keySet().size(); 826 String id = this.defsKeyPrefix + "lgp" + count; 827 this.elementIDs.add(id); 828 this.linearGradientPaints.put(key, id); 829 this.gradientPaintRef = id; 830 } 831 } else if (paint instanceof RadialGradientPaint) { 832 RadialGradientPaint rgp = (RadialGradientPaint) paint; 833 RadialGradientPaintKey key = new RadialGradientPaintKey(rgp); 834 String ref = this.radialGradientPaints.get(key); 835 if (ref == null) { 836 int count = this.radialGradientPaints.keySet().size(); 837 String id = this.defsKeyPrefix + "rgp" + count; 838 this.elementIDs.add(id); 839 this.radialGradientPaints.put(key, id); 840 this.gradientPaintRef = id; 841 } 842 } 843 } 844 845 /** 846 * Returns the foreground color. This method exists for backwards 847 * compatibility in AWT, you should use the {@link #getPaint()} method. 848 * 849 * @return The foreground color (never {@code null}). 850 * 851 * @see #getPaint() 852 */ 853 @Override 854 public Color getColor() { 855 return this.color; 856 } 857 858 /** 859 * Sets the foreground color. This method exists for backwards 860 * compatibility in AWT, you should use the 861 * {@link #setPaint(java.awt.Paint)} method. 862 * 863 * @param c the color ({@code null} permitted but ignored). 864 * 865 * @see #setPaint(java.awt.Paint) 866 */ 867 @Override 868 public void setColor(Color c) { 869 if (c == null) { 870 return; 871 } 872 this.color = c; 873 this.paint = c; 874 } 875 876 /** 877 * Returns the background color. The default value is {@link Color#BLACK}. 878 * This is used by the {@link #clearRect(int, int, int, int)} method. 879 * 880 * @return The background color (possibly {@code null}). 881 * 882 * @see #setBackground(java.awt.Color) 883 */ 884 @Override 885 public Color getBackground() { 886 return this.background; 887 } 888 889 /** 890 * Sets the background color. This is used by the 891 * {@link #clearRect(int, int, int, int)} method. The reference 892 * implementation allows {@code null} for the background color so 893 * we allow that too (but for that case, the clearRect method will do 894 * nothing). 895 * 896 * @param color the color ({@code null} permitted). 897 * 898 * @see #getBackground() 899 */ 900 @Override 901 public void setBackground(Color color) { 902 this.background = color; 903 } 904 905 /** 906 * Returns the current composite. 907 * 908 * @return The current composite (never {@code null}). 909 * 910 * @see #setComposite(java.awt.Composite) 911 */ 912 @Override 913 public Composite getComposite() { 914 return this.composite; 915 } 916 917 /** 918 * Sets the composite (only {@code AlphaComposite} is handled). 919 * 920 * @param comp the composite ({@code null} not permitted). 921 * 922 * @see #getComposite() 923 */ 924 @Override 925 public void setComposite(Composite comp) { 926 if (comp == null) { 927 throw new IllegalArgumentException("Null 'comp' argument."); 928 } 929 this.composite = comp; 930 } 931 932 /** 933 * Returns the current stroke (used when drawing shapes). 934 * 935 * @return The current stroke (never {@code null}). 936 * 937 * @see #setStroke(java.awt.Stroke) 938 */ 939 @Override 940 public Stroke getStroke() { 941 return this.stroke; 942 } 943 944 /** 945 * Sets the stroke that will be used to draw shapes. 946 * 947 * @param s the stroke ({@code null} not permitted). 948 * 949 * @see #getStroke() 950 */ 951 @Override 952 public void setStroke(Stroke s) { 953 if (s == null) { 954 throw new IllegalArgumentException("Null 's' argument."); 955 } 956 this.stroke = s; 957 } 958 959 /** 960 * Returns the current value for the specified hint. See the 961 * {@link SVGHints} class for information about the hints that can be 962 * used with {@code SVGGraphics2D}. 963 * 964 * @param hintKey the hint key ({@code null} permitted, but the 965 * result will be {@code null} also). 966 * 967 * @return The current value for the specified hint 968 * (possibly {@code null}). 969 * 970 * @see #setRenderingHint(java.awt.RenderingHints.Key, java.lang.Object) 971 */ 972 @Override 973 public Object getRenderingHint(RenderingHints.Key hintKey) { 974 return this.hints.get(hintKey); 975 } 976 977 /** 978 * Sets the value for a hint. See the {@link SVGHints} class for 979 * information about the hints that can be used with this implementation. 980 * 981 * @param hintKey the hint key ({@code null} not permitted). 982 * @param hintValue the hint value. 983 * 984 * @see #getRenderingHint(java.awt.RenderingHints.Key) 985 */ 986 @Override 987 public void setRenderingHint(RenderingHints.Key hintKey, Object hintValue) { 988 if (hintKey == null) { 989 throw new NullPointerException("Null 'hintKey' not permitted."); 990 } 991 // KEY_BEGIN_GROUP and KEY_END_GROUP are handled as special cases that 992 // never get stored in the hints map... 993 if (SVGHints.isBeginGroupKey(hintKey)) { 994 String groupId = null; 995 String ref = null; 996 List<Entry> otherKeysAndValues = null; 997 if (hintValue instanceof String) { 998 groupId = (String) hintValue; 999 } else if (hintValue instanceof Map) { 1000 Map hintValueMap = (Map) hintValue; 1001 groupId = (String) hintValueMap.get("id"); 1002 ref = (String) hintValueMap.get("ref"); 1003 for (final Object obj: hintValueMap.entrySet()) { 1004 final Entry e = (Entry) obj; 1005 final Object key = e.getKey(); 1006 if ("id".equals(key) || "ref".equals(key)) { 1007 continue; 1008 } 1009 if (otherKeysAndValues == null) { 1010 otherKeysAndValues = new ArrayList<Entry>(); 1011 } 1012 otherKeysAndValues.add(e); 1013 } 1014 } 1015 this.sb.append("<g"); 1016 if (groupId != null) { 1017 if (this.elementIDs.contains(groupId)) { 1018 throw new IllegalArgumentException("The group id (" 1019 + groupId + ") is not unique."); 1020 } else { 1021 this.sb.append(" id=\"").append(groupId).append("\""); 1022 this.elementIDs.add(groupId); 1023 } 1024 } 1025 if (ref != null) { 1026 this.sb.append(" jfreesvg:ref=\""); 1027 this.sb.append(SVGUtils.escapeForXML(ref)).append("\""); 1028 } 1029 if (otherKeysAndValues != null) { 1030 for (final Entry e: otherKeysAndValues) { 1031 this.sb.append(" ").append(e.getKey()).append("=\""); 1032 this.sb.append(SVGUtils.escapeForXML(String.valueOf( 1033 e.getValue()))).append("\""); 1034 } 1035 } 1036 this.sb.append(">"); 1037 } else if (SVGHints.isEndGroupKey(hintKey)) { 1038 this.sb.append("</g>\n"); 1039 } else if (SVGHints.isElementTitleKey(hintKey) && (hintValue != null)) { 1040 this.sb.append("<title>"); 1041 this.sb.append(SVGUtils.escapeForXML(String.valueOf(hintValue))); 1042 this.sb.append("</title>"); 1043 } else { 1044 this.hints.put(hintKey, hintValue); 1045 } 1046 } 1047 1048 /** 1049 * Returns a copy of the rendering hints. Modifying the returned copy 1050 * will have no impact on the state of this {@code Graphics2D} instance. 1051 * 1052 * @return The rendering hints (never {@code null}). 1053 * 1054 * @see #setRenderingHints(java.util.Map) 1055 */ 1056 @Override 1057 public RenderingHints getRenderingHints() { 1058 return (RenderingHints) this.hints.clone(); 1059 } 1060 1061 /** 1062 * Sets the rendering hints to the specified collection. 1063 * 1064 * @param hints the new set of hints ({@code null} not permitted). 1065 * 1066 * @see #getRenderingHints() 1067 */ 1068 @Override 1069 public void setRenderingHints(Map<?, ?> hints) { 1070 this.hints.clear(); 1071 addRenderingHints(hints); 1072 } 1073 1074 /** 1075 * Adds all the supplied rendering hints. 1076 * 1077 * @param hints the hints ({@code null} not permitted). 1078 */ 1079 @Override 1080 public void addRenderingHints(Map<?, ?> hints) { 1081 this.hints.putAll(hints); 1082 } 1083 1084 /** 1085 * A utility method that appends an optional element id if one is 1086 * specified via the rendering hints. 1087 * 1088 * @param sb the string builder ({@code null} not permitted). 1089 */ 1090 private void appendOptionalElementIDFromHint(StringBuilder sb) { 1091 String elementID = (String) this.hints.get(SVGHints.KEY_ELEMENT_ID); 1092 if (elementID != null) { 1093 this.hints.put(SVGHints.KEY_ELEMENT_ID, null); // clear it 1094 if (this.elementIDs.contains(elementID)) { 1095 throw new IllegalStateException("The element id " 1096 + elementID + " is already used."); 1097 } else { 1098 this.elementIDs.add(elementID); 1099 } 1100 this.sb.append("id=\"").append(elementID).append("\" "); 1101 } 1102 } 1103 1104 /** 1105 * Draws the specified shape with the current {@code paint} and 1106 * {@code stroke}. There is direct handling for {@code Line2D}, 1107 * {@code Rectangle2D} and {@code Path2D}. All other shapes are 1108 * mapped to a {@code GeneralPath} and then drawn (effectively as 1109 * {@code Path2D} objects). 1110 * 1111 * @param s the shape ({@code null} not permitted). 1112 * 1113 * @see #fill(java.awt.Shape) 1114 */ 1115 @Override 1116 public void draw(Shape s) { 1117 // if the current stroke is not a BasicStroke then it is handled as 1118 // a special case 1119 if (!(this.stroke instanceof BasicStroke)) { 1120 fill(this.stroke.createStrokedShape(s)); 1121 return; 1122 } 1123 if (s instanceof Line2D) { 1124 Line2D l = (Line2D) s; 1125 this.sb.append("<line "); 1126 appendOptionalElementIDFromHint(this.sb); 1127 this.sb.append("x1=\"").append(geomDP(l.getX1())) 1128 .append("\" y1=\"").append(geomDP(l.getY1())) 1129 .append("\" x2=\"").append(geomDP(l.getX2())) 1130 .append("\" y2=\"").append(geomDP(l.getY2())) 1131 .append("\" "); 1132 this.sb.append("style=\"").append(strokeStyle()).append("\" "); 1133 this.sb.append("transform=\"").append(getSVGTransform( 1134 this.transform)).append("\" "); 1135 this.sb.append(getClipPathRef()); 1136 this.sb.append("/>"); 1137 } else if (s instanceof Rectangle2D) { 1138 Rectangle2D r = (Rectangle2D) s; 1139 this.sb.append("<rect "); 1140 appendOptionalElementIDFromHint(this.sb); 1141 this.sb.append("x=\"").append(geomDP(r.getX())) 1142 .append("\" y=\"").append(geomDP(r.getY())) 1143 .append("\" width=\"").append(geomDP(r.getWidth())) 1144 .append("\" height=\"").append(geomDP(r.getHeight())) 1145 .append("\" "); 1146 this.sb.append("style=\"").append(strokeStyle()) 1147 .append("; fill: none").append("\" "); 1148 this.sb.append("transform=\"").append(getSVGTransform( 1149 this.transform)).append("\" "); 1150 this.sb.append(getClipPathRef()); 1151 this.sb.append("/>"); 1152 } else if (s instanceof Path2D) { 1153 Path2D path = (Path2D) s; 1154 this.sb.append("<g "); 1155 appendOptionalElementIDFromHint(this.sb); 1156 this.sb.append("style=\"").append(strokeStyle()) 1157 .append("; fill: none").append("\" "); 1158 this.sb.append("transform=\"").append(getSVGTransform( 1159 this.transform)).append("\" "); 1160 this.sb.append(getClipPathRef()); 1161 this.sb.append(">"); 1162 this.sb.append("<path ").append(getSVGPathData(path)).append("/>"); 1163 this.sb.append("</g>"); 1164 } else { 1165 draw(new GeneralPath(s)); // handled as a Path2D next time through 1166 } 1167 } 1168 1169 /** 1170 * Fills the specified shape with the current {@code paint}. There is 1171 * direct handling for {@code Rectangle2D} and {@code Path2D}. 1172 * All other shapes are mapped to a {@code GeneralPath} and then 1173 * filled. 1174 * 1175 * @param s the shape ({@code null} not permitted). 1176 * 1177 * @see #draw(java.awt.Shape) 1178 */ 1179 @Override 1180 public void fill(Shape s) { 1181 if (s instanceof Rectangle2D) { 1182 Rectangle2D r = (Rectangle2D) s; 1183 if (r.isEmpty()) { 1184 return; 1185 } 1186 this.sb.append("<rect "); 1187 appendOptionalElementIDFromHint(this.sb); 1188 this.sb.append("x=\"").append(geomDP(r.getX())) 1189 .append("\" y=\"").append(geomDP(r.getY())) 1190 .append("\" width=\"").append(geomDP(r.getWidth())) 1191 .append("\" height=\"").append(geomDP(r.getHeight())) 1192 .append("\" "); 1193 this.sb.append("style=\"").append(getSVGFillStyle()).append("\" "); 1194 this.sb.append("transform=\"").append(getSVGTransform( 1195 this.transform)).append("\" "); 1196 this.sb.append(getClipPathRef()); 1197 this.sb.append("/>"); 1198 } else if (s instanceof Path2D) { 1199 Path2D path = (Path2D) s; 1200 this.sb.append("<g "); 1201 appendOptionalElementIDFromHint(this.sb); 1202 this.sb.append("style=\"").append(getSVGFillStyle()); 1203 this.sb.append("; stroke: none").append("\" "); 1204 this.sb.append("transform=\"").append(getSVGTransform( 1205 this.transform)).append("\" "); 1206 this.sb.append(getClipPathRef()); 1207 this.sb.append(">"); 1208 this.sb.append("<path ").append(getSVGPathData(path)).append("/>"); 1209 this.sb.append("</g>"); 1210 } else { 1211 fill(new GeneralPath(s)); // handled as a Path2D next time through 1212 } 1213 } 1214 1215 /** 1216 * Creates an SVG path string for the supplied Java2D path. 1217 * 1218 * @param path the path ({@code null} not permitted). 1219 * 1220 * @return An SVG path string. 1221 */ 1222 private String getSVGPathData(Path2D path) { 1223 StringBuilder b = new StringBuilder("d=\""); 1224 float[] coords = new float[6]; 1225 boolean first = true; 1226 PathIterator iterator = path.getPathIterator(null); 1227 while (!iterator.isDone()) { 1228 int type = iterator.currentSegment(coords); 1229 if (!first) { 1230 b.append(" "); 1231 } 1232 first = false; 1233 switch (type) { 1234 case (PathIterator.SEG_MOVETO): 1235 b.append("M ").append(geomDP(coords[0])).append(" ") 1236 .append(geomDP(coords[1])); 1237 break; 1238 case (PathIterator.SEG_LINETO): 1239 b.append("L ").append(geomDP(coords[0])).append(" ") 1240 .append(geomDP(coords[1])); 1241 break; 1242 case (PathIterator.SEG_QUADTO): 1243 b.append("Q ").append(geomDP(coords[0])) 1244 .append(" ").append(geomDP(coords[1])) 1245 .append(" ").append(geomDP(coords[2])) 1246 .append(" ").append(geomDP(coords[3])); 1247 break; 1248 case (PathIterator.SEG_CUBICTO): 1249 b.append("C ").append(geomDP(coords[0])).append(" ") 1250 .append(geomDP(coords[1])).append(" ") 1251 .append(geomDP(coords[2])).append(" ") 1252 .append(geomDP(coords[3])).append(" ") 1253 .append(geomDP(coords[4])).append(" ") 1254 .append(geomDP(coords[5])); 1255 break; 1256 case (PathIterator.SEG_CLOSE): 1257 b.append("Z "); 1258 break; 1259 default: 1260 break; 1261 } 1262 iterator.next(); 1263 } 1264 return b.append("\"").toString(); 1265 } 1266 1267 /** 1268 * Returns the current alpha (transparency) in the range 0.0 to 1.0. 1269 * If the current composite is an {@link AlphaComposite} we read the alpha 1270 * value from there, otherwise this method returns 1.0. 1271 * 1272 * @return The current alpha (transparency) in the range 0.0 to 1.0. 1273 */ 1274 private float getAlpha() { 1275 float alpha = 1.0f; 1276 if (this.composite instanceof AlphaComposite) { 1277 AlphaComposite ac = (AlphaComposite) this.composite; 1278 alpha = ac.getAlpha(); 1279 } 1280 return alpha; 1281 } 1282 1283 /** 1284 * Returns an SVG color string based on the current paint. To handle 1285 * {@code GradientPaint} we rely on the {@code setPaint()} method 1286 * having set the {@code gradientPaintRef} attribute. 1287 * 1288 * @return An SVG color string. 1289 */ 1290 private String svgColorStr() { 1291 String result = "black;"; 1292 if (this.paint instanceof Color) { 1293 return rgbColorStr((Color) this.paint); 1294 } else if (this.paint instanceof GradientPaint 1295 || this.paint instanceof LinearGradientPaint 1296 || this.paint instanceof RadialGradientPaint) { 1297 return "url(#" + this.gradientPaintRef + ")"; 1298 } 1299 return result; 1300 } 1301 1302 /** 1303 * Returns the SVG RGB color string for the specified color. 1304 * 1305 * @param c the color ({@code null} not permitted). 1306 * 1307 * @return The SVG RGB color string. 1308 */ 1309 private String rgbColorStr(Color c) { 1310 StringBuilder b = new StringBuilder("rgb("); 1311 b.append(c.getRed()).append(",").append(c.getGreen()).append(",") 1312 .append(c.getBlue()).append(")"); 1313 return b.toString(); 1314 } 1315 1316 /** 1317 * Returns a string representing the specified color in RGBA format. 1318 * 1319 * @param c the color ({@code null} not permitted). 1320 * 1321 * @return The SVG RGBA color string. 1322 */ 1323 private String rgbaColorStr(Color c) { 1324 StringBuilder b = new StringBuilder("rgba("); 1325 double alphaPercent = c.getAlpha() / 255.0; 1326 b.append(c.getRed()).append(",").append(c.getGreen()).append(",") 1327 .append(c.getBlue()); 1328 b.append(",").append(transformDP(alphaPercent)); 1329 b.append(")"); 1330 return b.toString(); 1331 } 1332 1333 private static final String DEFAULT_STROKE_CAP = "butt"; 1334 private static final String DEFAULT_STROKE_JOIN = "miter"; 1335 private static final float DEFAULT_MITER_LIMIT = 4.0f; 1336 1337 /** 1338 * Returns a stroke style string based on the current stroke and 1339 * alpha settings. 1340 * 1341 * @return A stroke style string. 1342 */ 1343 private String strokeStyle() { 1344 double strokeWidth = 1.0f; 1345 String strokeCap = DEFAULT_STROKE_CAP; 1346 String strokeJoin = DEFAULT_STROKE_JOIN; 1347 float miterLimit = DEFAULT_MITER_LIMIT; 1348 float[] dashArray = new float[0]; 1349 if (this.stroke instanceof BasicStroke) { 1350 BasicStroke bs = (BasicStroke) this.stroke; 1351 strokeWidth = bs.getLineWidth() > 0.0 ? bs.getLineWidth() 1352 : this.zeroStrokeWidth; 1353 switch (bs.getEndCap()) { 1354 case BasicStroke.CAP_ROUND: 1355 strokeCap = "round"; 1356 break; 1357 case BasicStroke.CAP_SQUARE: 1358 strokeCap = "square"; 1359 break; 1360 case BasicStroke.CAP_BUTT: 1361 default: 1362 // already set to "butt" 1363 } 1364 switch (bs.getLineJoin()) { 1365 case BasicStroke.JOIN_BEVEL: 1366 strokeJoin = "bevel"; 1367 break; 1368 case BasicStroke.JOIN_ROUND: 1369 strokeJoin = "round"; 1370 break; 1371 case BasicStroke.JOIN_MITER: 1372 default: 1373 // already set to "miter" 1374 } 1375 miterLimit = bs.getMiterLimit(); 1376 dashArray = bs.getDashArray(); 1377 } 1378 StringBuilder b = new StringBuilder(); 1379 b.append("stroke-width: ").append(strokeWidth).append(";"); 1380 b.append("stroke: ").append(svgColorStr()).append(";"); 1381 b.append("stroke-opacity: ").append(getColorAlpha() * getAlpha()) 1382 .append(";"); 1383 if (!strokeCap.equals(DEFAULT_STROKE_CAP)) { 1384 b.append("stroke-linecap: ").append(strokeCap).append(";"); 1385 } 1386 if (!strokeJoin.equals(DEFAULT_STROKE_JOIN)) { 1387 b.append("stroke-linejoin: ").append(strokeJoin).append(";"); 1388 } 1389 if (Math.abs(DEFAULT_MITER_LIMIT - miterLimit) < 0.001) { 1390 b.append("stroke-miterlimit: ").append(geomDP(miterLimit)); 1391 } 1392 if (dashArray != null && dashArray.length != 0) { 1393 b.append("stroke-dasharray: "); 1394 for (int i = 0; i < dashArray.length; i++) { 1395 if (i != 0) b.append(", "); 1396 b.append(dashArray[i]); 1397 } 1398 b.append(";"); 1399 } 1400 if (this.checkStrokeControlHint) { 1401 Object hint = getRenderingHint(RenderingHints.KEY_STROKE_CONTROL); 1402 if (RenderingHints.VALUE_STROKE_NORMALIZE.equals(hint) 1403 && !this.shapeRendering.equals("crispEdges")) { 1404 b.append("shape-rendering:crispEdges;"); 1405 } 1406 if (RenderingHints.VALUE_STROKE_PURE.equals(hint) 1407 && !this.shapeRendering.equals("geometricPrecision")) { 1408 b.append("shape-rendering:geometricPrecision;"); 1409 } 1410 } 1411 return b.toString(); 1412 } 1413 1414 /** 1415 * Returns the alpha value of the current {@code paint}, or {@code 1.0f} if 1416 * it is not an instance of {@code Color}. 1417 * 1418 * @return The alpha value (in the range {@code 0.0} to {@code 1.0}. 1419 */ 1420 private float getColorAlpha() { 1421 if (this.paint instanceof Color) { 1422 Color c = (Color) this.paint; 1423 return c.getAlpha() / 255.0f; 1424 } 1425 return 1f; 1426 } 1427 1428 /** 1429 * Returns a fill style string based on the current paint and 1430 * alpha settings. 1431 * 1432 * @return A fill style string. 1433 */ 1434 private String getSVGFillStyle() { 1435 StringBuilder b = new StringBuilder(); 1436 b.append("fill: ").append(svgColorStr()).append("; "); 1437 b.append("fill-opacity: ").append(getColorAlpha() * getAlpha()); 1438 return b.toString(); 1439 } 1440 1441 /** 1442 * Returns the current font used for drawing text. 1443 * 1444 * @return The current font (never {@code null}). 1445 * 1446 * @see #setFont(java.awt.Font) 1447 */ 1448 @Override 1449 public Font getFont() { 1450 return this.font; 1451 } 1452 1453 /** 1454 * Sets the font to be used for drawing text. 1455 * 1456 * @param font the font ({@code null} is permitted but ignored). 1457 * 1458 * @see #getFont() 1459 */ 1460 @Override 1461 public void setFont(Font font) { 1462 if (font == null) { 1463 return; 1464 } 1465 this.font = font; 1466 } 1467 1468 /** 1469 * Returns the font mapper (an object that optionally maps font family 1470 * names to alternates). The default mapper will convert Java logical 1471 * font names to the equivalent SVG generic font name, and leave all other 1472 * font names unchanged. 1473 * 1474 * @return The font mapper (never {@code null}). 1475 * 1476 * @see #setFontMapper(org.jfree.graphics2d.svg.FontMapper) 1477 * @since 1.5 1478 */ 1479 public FontMapper getFontMapper() { 1480 return this.fontMapper; 1481 } 1482 1483 /** 1484 * Sets the font mapper. 1485 * 1486 * @param mapper the font mapper ({@code null} not permitted). 1487 * 1488 * @since 1.5 1489 */ 1490 public void setFontMapper(FontMapper mapper) { 1491 Args.nullNotPermitted(mapper, "mapper"); 1492 this.fontMapper = mapper; 1493 } 1494 1495 /** 1496 * Returns a string containing font style info. 1497 * 1498 * @return A string containing font style info. 1499 */ 1500 private String getSVGFontStyle() { 1501 StringBuilder b = new StringBuilder(); 1502 b.append("fill: ").append(svgColorStr()).append("; "); 1503 b.append("fill-opacity: ").append(getColorAlpha() * getAlpha()) 1504 .append("; "); 1505 String fontFamily = this.fontMapper.mapFont(this.font.getFamily()); 1506 b.append("font-family: ").append(fontFamily).append("; "); 1507 b.append("font-size: ").append(this.font.getSize()).append("px; "); 1508 if (this.font.isBold()) { 1509 b.append("font-weight: bold; "); 1510 } 1511 if (this.font.isItalic()) { 1512 b.append("font-style: italic; "); 1513 } 1514 return b.toString(); 1515 } 1516 1517 /** 1518 * Returns the font metrics for the specified font. 1519 * 1520 * @param f the font. 1521 * 1522 * @return The font metrics. 1523 */ 1524 @Override 1525 public FontMetrics getFontMetrics(Font f) { 1526 return this.fmImage.createGraphics().getFontMetrics(f); 1527 } 1528 1529 /** 1530 * Returns the font render context. 1531 * 1532 * @return The font render context (never {@code null}). 1533 */ 1534 @Override 1535 public FontRenderContext getFontRenderContext() { 1536 return this.fontRenderContext; 1537 } 1538 1539 /** 1540 * Draws a string at {@code (x, y)}. The start of the text at the 1541 * baseline level will be aligned with the {@code (x, y)} point. 1542 * <br><br> 1543 * Note that you can make use of the {@link SVGHints#KEY_TEXT_RENDERING} 1544 * hint when drawing strings (this is completely optional though). 1545 * 1546 * @param str the string ({@code null} not permitted). 1547 * @param x the x-coordinate. 1548 * @param y the y-coordinate. 1549 * 1550 * @see #drawString(java.lang.String, float, float) 1551 */ 1552 @Override 1553 public void drawString(String str, int x, int y) { 1554 drawString(str, (float) x, (float) y); 1555 } 1556 1557 /** 1558 * Draws a string at {@code (x, y)}. The start of the text at the 1559 * baseline level will be aligned with the {@code (x, y)} point. 1560 * <br><br> 1561 * Note that you can make use of the {@link SVGHints#KEY_TEXT_RENDERING} 1562 * hint when drawing strings (this is completely optional though). 1563 * 1564 * @param str the string ({@code null} not permitted). 1565 * @param x the x-coordinate. 1566 * @param y the y-coordinate. 1567 */ 1568 @Override 1569 public void drawString(String str, float x, float y) { 1570 if (str == null) { 1571 throw new NullPointerException("Null 'str' argument."); 1572 } 1573 if (!SVGHints.VALUE_DRAW_STRING_TYPE_VECTOR.equals( 1574 this.hints.get(SVGHints.KEY_DRAW_STRING_TYPE))) { 1575 this.sb.append("<g "); 1576 appendOptionalElementIDFromHint(this.sb); 1577 this.sb.append("transform=\"").append(getSVGTransform( 1578 this.transform)).append("\">"); 1579 this.sb.append("<text x=\"").append(geomDP(x)) 1580 .append("\" y=\"").append(geomDP(y)) 1581 .append("\""); 1582 this.sb.append(" style=\"").append(getSVGFontStyle()).append("\""); 1583 Object hintValue = getRenderingHint(SVGHints.KEY_TEXT_RENDERING); 1584 if (hintValue != null) { 1585 String textRenderValue = hintValue.toString(); 1586 this.sb.append(" text-rendering=\"").append(textRenderValue) 1587 .append("\""); 1588 } 1589 this.sb.append(" ").append(getClipPathRef()); 1590 this.sb.append(">"); 1591 this.sb.append(SVGUtils.escapeForXML(str)).append("</text>"); 1592 this.sb.append("</g>"); 1593 } else { 1594 AttributedString as = new AttributedString(str, 1595 this.font.getAttributes()); 1596 drawString(as.getIterator(), x, y); 1597 } 1598 } 1599 1600 /** 1601 * Draws a string of attributed characters at {@code (x, y)}. The 1602 * call is delegated to 1603 * {@link #drawString(AttributedCharacterIterator, float, float)}. 1604 * 1605 * @param iterator an iterator for the characters. 1606 * @param x the x-coordinate. 1607 * @param y the x-coordinate. 1608 */ 1609 @Override 1610 public void drawString(AttributedCharacterIterator iterator, int x, int y) { 1611 drawString(iterator, (float) x, (float) y); 1612 } 1613 1614 /** 1615 * Draws a string of attributed characters at {@code (x, y)}. 1616 * 1617 * @param iterator an iterator over the characters ({@code null} not 1618 * permitted). 1619 * @param x the x-coordinate. 1620 * @param y the y-coordinate. 1621 */ 1622 @Override 1623 public void drawString(AttributedCharacterIterator iterator, float x, 1624 float y) { 1625 Set<Attribute> s = iterator.getAllAttributeKeys(); 1626 if (!s.isEmpty()) { 1627 TextLayout layout = new TextLayout(iterator, 1628 getFontRenderContext()); 1629 layout.draw(this, x, y); 1630 } else { 1631 StringBuilder strb = new StringBuilder(); 1632 iterator.first(); 1633 for (int i = iterator.getBeginIndex(); i < iterator.getEndIndex(); 1634 i++) { 1635 strb.append(iterator.current()); 1636 iterator.next(); 1637 } 1638 drawString(strb.toString(), x, y); 1639 } 1640 } 1641 1642 /** 1643 * Draws the specified glyph vector at the location {@code (x, y)}. 1644 * 1645 * @param g the glyph vector ({@code null} not permitted). 1646 * @param x the x-coordinate. 1647 * @param y the y-coordinate. 1648 */ 1649 @Override 1650 public void drawGlyphVector(GlyphVector g, float x, float y) { 1651 fill(g.getOutline(x, y)); 1652 } 1653 1654 /** 1655 * Applies the translation {@code (tx, ty)}. This call is delegated 1656 * to {@link #translate(double, double)}. 1657 * 1658 * @param tx the x-translation. 1659 * @param ty the y-translation. 1660 * 1661 * @see #translate(double, double) 1662 */ 1663 @Override 1664 public void translate(int tx, int ty) { 1665 translate((double) tx, (double) ty); 1666 } 1667 1668 /** 1669 * Applies the translation {@code (tx, ty)}. 1670 * 1671 * @param tx the x-translation. 1672 * @param ty the y-translation. 1673 */ 1674 @Override 1675 public void translate(double tx, double ty) { 1676 AffineTransform t = getTransform(); 1677 t.translate(tx, ty); 1678 setTransform(t); 1679 } 1680 1681 /** 1682 * Applies a rotation (anti-clockwise) about {@code (0, 0)}. 1683 * 1684 * @param theta the rotation angle (in radians). 1685 */ 1686 @Override 1687 public void rotate(double theta) { 1688 AffineTransform t = getTransform(); 1689 t.rotate(theta); 1690 setTransform(t); 1691 } 1692 1693 /** 1694 * Applies a rotation (anti-clockwise) about {@code (x, y)}. 1695 * 1696 * @param theta the rotation angle (in radians). 1697 * @param x the x-coordinate. 1698 * @param y the y-coordinate. 1699 */ 1700 @Override 1701 public void rotate(double theta, double x, double y) { 1702 translate(x, y); 1703 rotate(theta); 1704 translate(-x, -y); 1705 } 1706 1707 /** 1708 * Applies a scale transformation. 1709 * 1710 * @param sx the x-scaling factor. 1711 * @param sy the y-scaling factor. 1712 */ 1713 @Override 1714 public void scale(double sx, double sy) { 1715 AffineTransform t = getTransform(); 1716 t.scale(sx, sy); 1717 setTransform(t); 1718 } 1719 1720 /** 1721 * Applies a shear transformation. This is equivalent to the following 1722 * call to the {@code transform} method: 1723 * <br><br> 1724 * <ul><li> 1725 * {@code transform(AffineTransform.getShearInstance(shx, shy));} 1726 * </ul> 1727 * 1728 * @param shx the x-shear factor. 1729 * @param shy the y-shear factor. 1730 */ 1731 @Override 1732 public void shear(double shx, double shy) { 1733 transform(AffineTransform.getShearInstance(shx, shy)); 1734 } 1735 1736 /** 1737 * Applies this transform to the existing transform by concatenating it. 1738 * 1739 * @param t the transform ({@code null} not permitted). 1740 */ 1741 @Override 1742 public void transform(AffineTransform t) { 1743 AffineTransform tx = getTransform(); 1744 tx.concatenate(t); 1745 setTransform(tx); 1746 } 1747 1748 /** 1749 * Returns a copy of the current transform. 1750 * 1751 * @return A copy of the current transform (never {@code null}). 1752 * 1753 * @see #setTransform(java.awt.geom.AffineTransform) 1754 */ 1755 @Override 1756 public AffineTransform getTransform() { 1757 return (AffineTransform) this.transform.clone(); 1758 } 1759 1760 /** 1761 * Sets the transform. 1762 * 1763 * @param t the new transform ({@code null} permitted, resets to the 1764 * identity transform). 1765 * 1766 * @see #getTransform() 1767 */ 1768 @Override 1769 public void setTransform(AffineTransform t) { 1770 if (t == null) { 1771 this.transform = new AffineTransform(); 1772 } else { 1773 this.transform = new AffineTransform(t); 1774 } 1775 this.clipRef = null; 1776 } 1777 1778 /** 1779 * Returns {@code true} if the rectangle (in device space) intersects 1780 * with the shape (the interior, if {@code onStroke} is {@code false}, 1781 * otherwise the stroked outline of the shape). 1782 * 1783 * @param rect a rectangle (in device space). 1784 * @param s the shape. 1785 * @param onStroke test the stroked outline only? 1786 * 1787 * @return A boolean. 1788 */ 1789 @Override 1790 public boolean hit(Rectangle rect, Shape s, boolean onStroke) { 1791 Shape ts; 1792 if (onStroke) { 1793 ts = this.transform.createTransformedShape( 1794 this.stroke.createStrokedShape(s)); 1795 } else { 1796 ts = this.transform.createTransformedShape(s); 1797 } 1798 if (!rect.getBounds2D().intersects(ts.getBounds2D())) { 1799 return false; 1800 } 1801 Area a1 = new Area(rect); 1802 Area a2 = new Area(ts); 1803 a1.intersect(a2); 1804 return !a1.isEmpty(); 1805 } 1806 1807 /** 1808 * Does nothing in this {@code SVGGraphics2D} implementation. 1809 */ 1810 @Override 1811 public void setPaintMode() { 1812 // do nothing 1813 } 1814 1815 /** 1816 * Does nothing in this {@code SVGGraphics2D} implementation. 1817 * 1818 * @param c ignored 1819 */ 1820 @Override 1821 public void setXORMode(Color c) { 1822 // do nothing 1823 } 1824 1825 /** 1826 * Returns the bounds of the user clipping region. 1827 * 1828 * @return The clip bounds (possibly {@code null}). 1829 * 1830 * @see #getClip() 1831 */ 1832 @Override 1833 public Rectangle getClipBounds() { 1834 if (this.clip == null) { 1835 return null; 1836 } 1837 return getClip().getBounds(); 1838 } 1839 1840 /** 1841 * Returns the user clipping region. The initial default value is 1842 * {@code null}. 1843 * 1844 * @return The user clipping region (possibly {@code null}). 1845 * 1846 * @see #setClip(java.awt.Shape) 1847 */ 1848 @Override 1849 public Shape getClip() { 1850 if (this.clip == null) { 1851 return null; 1852 } 1853 AffineTransform inv; 1854 try { 1855 inv = this.transform.createInverse(); 1856 return inv.createTransformedShape(this.clip); 1857 } catch (NoninvertibleTransformException ex) { 1858 return null; 1859 } 1860 } 1861 1862 /** 1863 * Sets the user clipping region. 1864 * 1865 * @param shape the new user clipping region ({@code null} permitted). 1866 * 1867 * @see #getClip() 1868 */ 1869 @Override 1870 public void setClip(Shape shape) { 1871 // null is handled fine here... 1872 this.clip = this.transform.createTransformedShape(shape); 1873 this.clipRef = null; 1874 } 1875 1876 /** 1877 * Registers the clip so that we can later write out all the clip 1878 * definitions in the DEFS element. 1879 * 1880 * @param clip the clip (ignored if {@code null}) 1881 */ 1882 private String registerClip(Shape clip) { 1883 if (clip == null) { 1884 this.clipRef = null; 1885 return null; 1886 } 1887 // generate the path 1888 String pathStr = getSVGPathData(new Path2D.Double(clip)); 1889 int index = this.clipPaths.indexOf(pathStr); 1890 if (index < 0) { 1891 this.clipPaths.add(pathStr); 1892 index = this.clipPaths.size() - 1; 1893 } 1894 return this.defsKeyPrefix + CLIP_KEY_PREFIX + index; 1895 } 1896 1897 private String transformDP(double d) { 1898 if (this.transformFormat != null) { 1899 return transformFormat.format(d); 1900 } else { 1901 return String.valueOf(d); 1902 } 1903 } 1904 1905 private String geomDP(double d) { 1906 if (this.geometryFormat != null) { 1907 return geometryFormat.format(d); 1908 } else { 1909 return String.valueOf(d); 1910 } 1911 } 1912 1913 private String getSVGTransform(AffineTransform t) { 1914 StringBuilder b = new StringBuilder("matrix("); 1915 b.append(transformDP(t.getScaleX())).append(","); 1916 b.append(transformDP(t.getShearY())).append(","); 1917 b.append(transformDP(t.getShearX())).append(","); 1918 b.append(transformDP(t.getScaleY())).append(","); 1919 b.append(transformDP(t.getTranslateX())).append(","); 1920 b.append(transformDP(t.getTranslateY())).append(")"); 1921 return b.toString(); 1922 } 1923 1924 /** 1925 * Clips to the intersection of the current clipping region and the 1926 * specified shape. 1927 * 1928 * According to the Oracle API specification, this method will accept a 1929 * {@code null} argument, but there is an open bug report (since 2004) 1930 * that suggests this is wrong: 1931 * <p> 1932 * <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6206189"> 1933 * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6206189</a> 1934 * 1935 * @param s the clip shape ({@code null} not permitted). 1936 */ 1937 @Override 1938 public void clip(Shape s) { 1939 if (s instanceof Line2D) { 1940 s = s.getBounds2D(); 1941 } 1942 if (this.clip == null) { 1943 setClip(s); 1944 return; 1945 } 1946 Shape ts = this.transform.createTransformedShape(s); 1947 if (!ts.intersects(this.clip.getBounds2D())) { 1948 setClip(new Rectangle2D.Double()); 1949 } else { 1950 Area a1 = new Area(ts); 1951 Area a2 = new Area(this.clip); 1952 a1.intersect(a2); 1953 this.clip = new Path2D.Double(a1); 1954 } 1955 this.clipRef = null; 1956 } 1957 1958 /** 1959 * Clips to the intersection of the current clipping region and the 1960 * specified rectangle. 1961 * 1962 * @param x the x-coordinate. 1963 * @param y the y-coordinate. 1964 * @param width the width. 1965 * @param height the height. 1966 */ 1967 @Override 1968 public void clipRect(int x, int y, int width, int height) { 1969 setRect(x, y, width, height); 1970 clip(this.rect); 1971 } 1972 1973 /** 1974 * Sets the user clipping region to the specified rectangle. 1975 * 1976 * @param x the x-coordinate. 1977 * @param y the y-coordinate. 1978 * @param width the width. 1979 * @param height the height. 1980 * 1981 * @see #getClip() 1982 */ 1983 @Override 1984 public void setClip(int x, int y, int width, int height) { 1985 setRect(x, y, width, height); 1986 setClip(this.rect); 1987 } 1988 1989 /** 1990 * Draws a line from {@code (x1, y1)} to {@code (x2, y2)} using 1991 * the current {@code paint} and {@code stroke}. 1992 * 1993 * @param x1 the x-coordinate of the start point. 1994 * @param y1 the y-coordinate of the start point. 1995 * @param x2 the x-coordinate of the end point. 1996 * @param y2 the x-coordinate of the end point. 1997 */ 1998 @Override 1999 public void drawLine(int x1, int y1, int x2, int y2) { 2000 if (this.line == null) { 2001 this.line = new Line2D.Double(x1, y1, x2, y2); 2002 } else { 2003 this.line.setLine(x1, y1, x2, y2); 2004 } 2005 draw(this.line); 2006 } 2007 2008 /** 2009 * Fills the specified rectangle with the current {@code paint}. 2010 * 2011 * @param x the x-coordinate. 2012 * @param y the y-coordinate. 2013 * @param width the rectangle width. 2014 * @param height the rectangle height. 2015 */ 2016 @Override 2017 public void fillRect(int x, int y, int width, int height) { 2018 setRect(x, y, width, height); 2019 fill(this.rect); 2020 } 2021 2022 /** 2023 * Clears the specified rectangle by filling it with the current 2024 * background color. If the background color is {@code null}, this 2025 * method will do nothing. 2026 * 2027 * @param x the x-coordinate. 2028 * @param y the y-coordinate. 2029 * @param width the width. 2030 * @param height the height. 2031 * 2032 * @see #getBackground() 2033 */ 2034 @Override 2035 public void clearRect(int x, int y, int width, int height) { 2036 if (getBackground() == null) { 2037 return; // we can't do anything 2038 } 2039 Paint saved = getPaint(); 2040 setPaint(getBackground()); 2041 fillRect(x, y, width, height); 2042 setPaint(saved); 2043 } 2044 2045 /** 2046 * Draws a rectangle with rounded corners using the current 2047 * {@code paint} and {@code stroke}. 2048 * 2049 * @param x the x-coordinate. 2050 * @param y the y-coordinate. 2051 * @param width the width. 2052 * @param height the height. 2053 * @param arcWidth the arc-width. 2054 * @param arcHeight the arc-height. 2055 * 2056 * @see #fillRoundRect(int, int, int, int, int, int) 2057 */ 2058 @Override 2059 public void drawRoundRect(int x, int y, int width, int height, 2060 int arcWidth, int arcHeight) { 2061 setRoundRect(x, y, width, height, arcWidth, arcHeight); 2062 draw(this.roundRect); 2063 } 2064 2065 /** 2066 * Fills a rectangle with rounded corners using the current {@code paint}. 2067 * 2068 * @param x the x-coordinate. 2069 * @param y the y-coordinate. 2070 * @param width the width. 2071 * @param height the height. 2072 * @param arcWidth the arc-width. 2073 * @param arcHeight the arc-height. 2074 * 2075 * @see #drawRoundRect(int, int, int, int, int, int) 2076 */ 2077 @Override 2078 public void fillRoundRect(int x, int y, int width, int height, 2079 int arcWidth, int arcHeight) { 2080 setRoundRect(x, y, width, height, arcWidth, arcHeight); 2081 fill(this.roundRect); 2082 } 2083 2084 /** 2085 * Draws an oval framed by the rectangle {@code (x, y, width, height)} 2086 * using the current {@code paint} and {@code stroke}. 2087 * 2088 * @param x the x-coordinate. 2089 * @param y the y-coordinate. 2090 * @param width the width. 2091 * @param height the height. 2092 * 2093 * @see #fillOval(int, int, int, int) 2094 */ 2095 @Override 2096 public void drawOval(int x, int y, int width, int height) { 2097 setOval(x, y, width, height); 2098 draw(this.oval); 2099 } 2100 2101 /** 2102 * Fills an oval framed by the rectangle {@code (x, y, width, height)}. 2103 * 2104 * @param x the x-coordinate. 2105 * @param y the y-coordinate. 2106 * @param width the width. 2107 * @param height the height. 2108 * 2109 * @see #drawOval(int, int, int, int) 2110 */ 2111 @Override 2112 public void fillOval(int x, int y, int width, int height) { 2113 setOval(x, y, width, height); 2114 fill(this.oval); 2115 } 2116 2117 /** 2118 * Draws an arc contained within the rectangle 2119 * {@code (x, y, width, height)}, starting at {@code startAngle} 2120 * and continuing through {@code arcAngle} degrees using 2121 * the current {@code paint} and {@code stroke}. 2122 * 2123 * @param x the x-coordinate. 2124 * @param y the y-coordinate. 2125 * @param width the width. 2126 * @param height the height. 2127 * @param startAngle the start angle in degrees, 0 = 3 o'clock. 2128 * @param arcAngle the angle (anticlockwise) in degrees. 2129 * 2130 * @see #fillArc(int, int, int, int, int, int) 2131 */ 2132 @Override 2133 public void drawArc(int x, int y, int width, int height, int startAngle, 2134 int arcAngle) { 2135 setArc(x, y, width, height, startAngle, arcAngle); 2136 draw(this.arc); 2137 } 2138 2139 /** 2140 * Fills an arc contained within the rectangle 2141 * {@code (x, y, width, height)}, starting at {@code startAngle} 2142 * and continuing through {@code arcAngle} degrees, using 2143 * the current {@code paint} 2144 * 2145 * @param x the x-coordinate. 2146 * @param y the y-coordinate. 2147 * @param width the width. 2148 * @param height the height. 2149 * @param startAngle the start angle in degrees, 0 = 3 o'clock. 2150 * @param arcAngle the angle (anticlockwise) in degrees. 2151 * 2152 * @see #drawArc(int, int, int, int, int, int) 2153 */ 2154 @Override 2155 public void fillArc(int x, int y, int width, int height, int startAngle, 2156 int arcAngle) { 2157 setArc(x, y, width, height, startAngle, arcAngle); 2158 fill(this.arc); 2159 } 2160 2161 /** 2162 * Draws the specified multi-segment line using the current 2163 * {@code paint} and {@code stroke}. 2164 * 2165 * @param xPoints the x-points. 2166 * @param yPoints the y-points. 2167 * @param nPoints the number of points to use for the polyline. 2168 */ 2169 @Override 2170 public void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) { 2171 GeneralPath p = GraphicsUtils.createPolygon(xPoints, yPoints, nPoints, 2172 false); 2173 draw(p); 2174 } 2175 2176 /** 2177 * Draws the specified polygon using the current {@code paint} and 2178 * {@code stroke}. 2179 * 2180 * @param xPoints the x-points. 2181 * @param yPoints the y-points. 2182 * @param nPoints the number of points to use for the polygon. 2183 * 2184 * @see #fillPolygon(int[], int[], int) */ 2185 @Override 2186 public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) { 2187 GeneralPath p = GraphicsUtils.createPolygon(xPoints, yPoints, nPoints, 2188 true); 2189 draw(p); 2190 } 2191 2192 /** 2193 * Fills the specified polygon using the current {@code paint}. 2194 * 2195 * @param xPoints the x-points. 2196 * @param yPoints the y-points. 2197 * @param nPoints the number of points to use for the polygon. 2198 * 2199 * @see #drawPolygon(int[], int[], int) 2200 */ 2201 @Override 2202 public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) { 2203 GeneralPath p = GraphicsUtils.createPolygon(xPoints, yPoints, nPoints, 2204 true); 2205 fill(p); 2206 } 2207 2208 /** 2209 * Returns the bytes representing a PNG format image. 2210 * 2211 * @param img the image to encode. 2212 * 2213 * @return The bytes representing a PNG format image. 2214 */ 2215 private byte[] getPNGBytes(Image img) { 2216 RenderedImage ri; 2217 if (img instanceof RenderedImage) { 2218 ri = (RenderedImage) img; 2219 } else { 2220 BufferedImage bi = new BufferedImage(img.getWidth(null), 2221 img.getHeight(null), BufferedImage.TYPE_INT_ARGB); 2222 Graphics2D g2 = bi.createGraphics(); 2223 g2.drawImage(img, 0, 0, null); 2224 ri = bi; 2225 } 2226 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 2227 try { 2228 ImageIO.write(ri, "png", baos); 2229 } catch (IOException ex) { 2230 Logger.getLogger(SVGGraphics2D.class.getName()).log(Level.SEVERE, 2231 "IOException while writing PNG data.", ex); 2232 } 2233 return baos.toByteArray(); 2234 } 2235 2236 /** 2237 * Draws an image at the location {@code (x, y)}. Note that the 2238 * {@code observer} is ignored. 2239 * 2240 * @param img the image ({@code null} not permitted). 2241 * @param x the x-coordinate. 2242 * @param y the y-coordinate. 2243 * @param observer ignored. 2244 * 2245 * @return {@code true} if the image is drawn. 2246 */ 2247 @Override 2248 public boolean drawImage(Image img, int x, int y, ImageObserver observer) { 2249 int w = img.getWidth(observer); 2250 if (w < 0) { 2251 return false; 2252 } 2253 int h = img.getHeight(observer); 2254 if (h < 0) { 2255 return false; 2256 } 2257 return drawImage(img, x, y, w, h, observer); 2258 } 2259 2260 /** 2261 * Draws the image into the rectangle defined by {@code (x, y, w, h)}. 2262 * Note that the {@code observer} is ignored (it is not useful in this 2263 * context). 2264 * 2265 * @param img the image. 2266 * @param x the x-coordinate. 2267 * @param y the y-coordinate. 2268 * @param w the width. 2269 * @param h the height. 2270 * @param observer ignored. 2271 * 2272 * @return {@code true} if the image is drawn. 2273 */ 2274 @Override 2275 public boolean drawImage(Image img, int x, int y, int w, int h, 2276 ImageObserver observer) { 2277 2278 // the rendering hints control whether the image is embedded or 2279 // referenced... 2280 Object hint = getRenderingHint(SVGHints.KEY_IMAGE_HANDLING); 2281 if (SVGHints.VALUE_IMAGE_HANDLING_EMBED.equals(hint)) { 2282 this.sb.append("<image "); 2283 appendOptionalElementIDFromHint(this.sb); 2284 this.sb.append("preserveAspectRatio=\"none\" "); 2285 this.sb.append("xlink:href=\"data:image/png;base64,"); 2286 this.sb.append(DatatypeConverter.printBase64Binary(getPNGBytes( 2287 img))); 2288 this.sb.append("\" "); 2289 this.sb.append(getClipPathRef()).append(" "); 2290 this.sb.append("transform=\"").append(getSVGTransform( 2291 this.transform)).append("\" "); 2292 this.sb.append("x=\"").append(geomDP(x)) 2293 .append("\" y=\"").append(geomDP(y)) 2294 .append("\" "); 2295 this.sb.append("width=\"").append(geomDP(w)).append("\" height=\"") 2296 .append(geomDP(h)).append("\"/>\n"); 2297 return true; 2298 } else { // here for SVGHints.VALUE_IMAGE_HANDLING_REFERENCE 2299 int count = this.imageElements.size(); 2300 String href = (String) this.hints.get(SVGHints.KEY_IMAGE_HREF); 2301 if (href == null) { 2302 href = this.filePrefix + count + this.fileSuffix; 2303 } else { 2304 // KEY_IMAGE_HREF value is for a single use... 2305 this.hints.put(SVGHints.KEY_IMAGE_HREF, null); 2306 } 2307 ImageElement imageElement = new ImageElement(href, img); 2308 this.imageElements.add(imageElement); 2309 // write an SVG element for the img 2310 this.sb.append("<image "); 2311 appendOptionalElementIDFromHint(this.sb); 2312 this.sb.append("xlink:href=\""); 2313 this.sb.append(href).append("\" "); 2314 this.sb.append(getClipPathRef()).append(" "); 2315 this.sb.append("transform=\"").append(getSVGTransform( 2316 this.transform)).append("\" "); 2317 this.sb.append("x=\"").append(geomDP(x)) 2318 .append("\" y=\"").append(geomDP(y)) 2319 .append("\" "); 2320 this.sb.append("width=\"").append(geomDP(w)).append("\" height=\"") 2321 .append(geomDP(h)).append("\"/>\n"); 2322 return true; 2323 } 2324 } 2325 2326 /** 2327 * Draws an image at the location {@code (x, y)}. Note that the 2328 * {@code observer} is ignored. 2329 * 2330 * @param img the image ({@code null} not permitted). 2331 * @param x the x-coordinate. 2332 * @param y the y-coordinate. 2333 * @param bgcolor the background color ({@code null} permitted). 2334 * @param observer ignored. 2335 * 2336 * @return {@code true} if the image is drawn. 2337 */ 2338 @Override 2339 public boolean drawImage(Image img, int x, int y, Color bgcolor, 2340 ImageObserver observer) { 2341 int w = img.getWidth(null); 2342 if (w < 0) { 2343 return false; 2344 } 2345 int h = img.getHeight(null); 2346 if (h < 0) { 2347 return false; 2348 } 2349 return drawImage(img, x, y, w, h, bgcolor, observer); 2350 } 2351 2352 /** 2353 * Draws an image to the rectangle {@code (x, y, w, h)} (scaling it if 2354 * required), first filling the background with the specified color. Note 2355 * that the {@code observer} is ignored. 2356 * 2357 * @param img the image. 2358 * @param x the x-coordinate. 2359 * @param y the y-coordinate. 2360 * @param w the width. 2361 * @param h the height. 2362 * @param bgcolor the background color ({@code null} permitted). 2363 * @param observer ignored. 2364 * 2365 * @return {@code true} if the image is drawn. 2366 */ 2367 @Override 2368 public boolean drawImage(Image img, int x, int y, int w, int h, 2369 Color bgcolor, ImageObserver observer) { 2370 Paint saved = getPaint(); 2371 setPaint(bgcolor); 2372 fillRect(x, y, w, h); 2373 setPaint(saved); 2374 return drawImage(img, x, y, w, h, observer); 2375 } 2376 2377 /** 2378 * Draws part of an image (defined by the source rectangle 2379 * {@code (sx1, sy1, sx2, sy2)}) into the destination rectangle 2380 * {@code (dx1, dy1, dx2, dy2)}. Note that the {@code observer} is ignored. 2381 * 2382 * @param img the image. 2383 * @param dx1 the x-coordinate for the top left of the destination. 2384 * @param dy1 the y-coordinate for the top left of the destination. 2385 * @param dx2 the x-coordinate for the bottom right of the destination. 2386 * @param dy2 the y-coordinate for the bottom right of the destination. 2387 * @param sx1 the x-coordinate for the top left of the source. 2388 * @param sy1 the y-coordinate for the top left of the source. 2389 * @param sx2 the x-coordinate for the bottom right of the source. 2390 * @param sy2 the y-coordinate for the bottom right of the source. 2391 * 2392 * @return {@code true} if the image is drawn. 2393 */ 2394 @Override 2395 public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, 2396 int sx1, int sy1, int sx2, int sy2, ImageObserver observer) { 2397 int w = dx2 - dx1; 2398 int h = dy2 - dy1; 2399 BufferedImage img2 = new BufferedImage(w, h, 2400 BufferedImage.TYPE_INT_ARGB); 2401 Graphics2D g2 = img2.createGraphics(); 2402 g2.drawImage(img, 0, 0, w, h, sx1, sy1, sx2, sy2, null); 2403 return drawImage(img2, dx1, dy1, null); 2404 } 2405 2406 /** 2407 * Draws part of an image (defined by the source rectangle 2408 * {@code (sx1, sy1, sx2, sy2)}) into the destination rectangle 2409 * {@code (dx1, dy1, dx2, dy2)}. The destination rectangle is first 2410 * cleared by filling it with the specified {@code bgcolor}. Note that 2411 * the {@code observer} is ignored. 2412 * 2413 * @param img the image. 2414 * @param dx1 the x-coordinate for the top left of the destination. 2415 * @param dy1 the y-coordinate for the top left of the destination. 2416 * @param dx2 the x-coordinate for the bottom right of the destination. 2417 * @param dy2 the y-coordinate for the bottom right of the destination. 2418 * @param sx1 the x-coordinate for the top left of the source. 2419 * @param sy1 the y-coordinate for the top left of the source. 2420 * @param sx2 the x-coordinate for the bottom right of the source. 2421 * @param sy2 the y-coordinate for the bottom right of the source. 2422 * @param bgcolor the background color ({@code null} permitted). 2423 * @param observer ignored. 2424 * 2425 * @return {@code true} if the image is drawn. 2426 */ 2427 @Override 2428 public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, 2429 int sx1, int sy1, int sx2, int sy2, Color bgcolor, 2430 ImageObserver observer) { 2431 Paint saved = getPaint(); 2432 setPaint(bgcolor); 2433 fillRect(dx1, dy1, dx2 - dx1, dy2 - dy1); 2434 setPaint(saved); 2435 return drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, observer); 2436 } 2437 2438 /** 2439 * Draws the rendered image. 2440 * 2441 * @param img the image. 2442 * @param xform the transform. 2443 */ 2444 @Override 2445 public void drawRenderedImage(RenderedImage img, AffineTransform xform) { 2446 BufferedImage bi = GraphicsUtils.convertRenderedImage(img); 2447 drawImage(bi, xform, null); 2448 } 2449 2450 /** 2451 * Draws the renderable image. 2452 * 2453 * @param img the renderable image. 2454 * @param xform the transform. 2455 */ 2456 @Override 2457 public void drawRenderableImage(RenderableImage img, 2458 AffineTransform xform) { 2459 RenderedImage ri = img.createDefaultRendering(); 2460 drawRenderedImage(ri, xform); 2461 } 2462 2463 /** 2464 * Draws an image with the specified transform. Note that the 2465 * {@code observer} is ignored. 2466 * 2467 * @param img the image. 2468 * @param xform the transform. 2469 * @param obs the image observer (ignored). 2470 * 2471 * @return {@code true} if the image is drawn. 2472 */ 2473 @Override 2474 public boolean drawImage(Image img, AffineTransform xform, 2475 ImageObserver obs) { 2476 AffineTransform savedTransform = getTransform(); 2477 transform(xform); 2478 boolean result = drawImage(img, 0, 0, obs); 2479 setTransform(savedTransform); 2480 return result; 2481 } 2482 2483 /** 2484 * Draws the image resulting from applying the {@code BufferedImageOp} 2485 * to the specified image at the location {@code (x, y)}. 2486 * 2487 * @param img the image. 2488 * @param op the operation. 2489 * @param x the x-coordinate. 2490 * @param y the y-coordinate. 2491 */ 2492 @Override 2493 public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) { 2494 BufferedImage imageToDraw = op.filter(img, null); 2495 drawImage(imageToDraw, new AffineTransform(1f, 0f, 0f, 1f, x, y), null); 2496 } 2497 2498 /** 2499 * This method does nothing. The operation assumes that the output is in 2500 * bitmap form, which is not the case for SVG, so we silently ignore 2501 * this method call. 2502 * 2503 * @param x the x-coordinate. 2504 * @param y the y-coordinate. 2505 * @param width the width of the area. 2506 * @param height the height of the area. 2507 * @param dx the delta x. 2508 * @param dy the delta y. 2509 */ 2510 @Override 2511 public void copyArea(int x, int y, int width, int height, int dx, int dy) { 2512 // do nothing, this operation is silently ignored. 2513 } 2514 2515 /** 2516 * This method does nothing, there are no resources to dispose. 2517 */ 2518 @Override 2519 public void dispose() { 2520 // nothing to do 2521 } 2522 2523 /** 2524 * Returns the SVG element that has been generated by calls to this 2525 * {@code Graphics2D} implementation. 2526 * 2527 * @return The SVG element. 2528 */ 2529 public String getSVGElement() { 2530 return getSVGElement(null); 2531 } 2532 2533 /** 2534 * Returns the SVG element that has been generated by calls to this 2535 * {@code Graphics2D} implementation, giving it the specified {@code id}. 2536 * If {@code id} is {@code null}, the element will have no {@code id} 2537 * attribute. 2538 * 2539 * @param id the element id ({@code null} permitted). 2540 * 2541 * @return A string containing the SVG element. 2542 * 2543 * @since 1.8 2544 */ 2545 public String getSVGElement(String id) { 2546 StringBuilder svg = new StringBuilder("<svg "); 2547 if (id != null) { 2548 svg.append("id=\"").append(id).append("\" "); 2549 } 2550 svg.append("xmlns=\"http://www.w3.org/2000/svg\" ") 2551 .append("xmlns:xlink=\"http://www.w3.org/1999/xlink\" ") 2552 .append("xmlns:jfreesvg=\"http://www.jfree.org/jfreesvg/svg\" ") 2553 .append("width=\"").append(this.width) 2554 .append("\" height=\"").append(this.height) 2555 .append("\" text-rendering=\"").append(this.textRendering) 2556 .append("\" shape-rendering=\"").append(this.shapeRendering) 2557 .append("\">\n"); 2558 StringBuilder defs = new StringBuilder("<defs>"); 2559 for (GradientPaintKey key : this.gradientPaints.keySet()) { 2560 defs.append(getLinearGradientElement(this.gradientPaints.get(key), 2561 key.getPaint())); 2562 defs.append("\n"); 2563 } 2564 for (LinearGradientPaintKey key : this.linearGradientPaints.keySet()) { 2565 defs.append(getLinearGradientElement( 2566 this.linearGradientPaints.get(key), key.getPaint())); 2567 defs.append("\n"); 2568 } 2569 for (RadialGradientPaintKey key : this.radialGradientPaints.keySet()) { 2570 defs.append(getRadialGradientElement( 2571 this.radialGradientPaints.get(key), key.getPaint())); 2572 defs.append("\n"); 2573 } 2574 for (int i = 0; i < this.clipPaths.size(); i++) { 2575 StringBuilder b = new StringBuilder("<clipPath id=\"") 2576 .append(this.defsKeyPrefix).append(CLIP_KEY_PREFIX).append(i) 2577 .append("\">"); 2578 b.append("<path ").append(this.clipPaths.get(i)).append("/>"); 2579 b.append("</clipPath>").append("\n"); 2580 defs.append(b.toString()); 2581 } 2582 defs.append("</defs>\n"); 2583 svg.append(defs); 2584 svg.append(this.sb); 2585 svg.append("</svg>"); 2586 return svg.toString(); 2587 } 2588 2589 /** 2590 * Returns an SVG document. 2591 * 2592 * @return An SVG document. 2593 */ 2594 public String getSVGDocument() { 2595 StringBuilder b = new StringBuilder(); 2596 b.append("<?xml version=\"1.0\"?>\n"); 2597 b.append("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.0//EN\" "); 2598 b.append("\"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd\">\n"); 2599 b.append(getSVGElement()); 2600 return b.append("\n").toString(); 2601 } 2602 2603 /** 2604 * Returns the list of image elements that have been referenced in the 2605 * SVG output but not embedded. If the image files don't already exist, 2606 * you can use this list as the basis for creating the image files. 2607 * 2608 * @return The list of image elements. 2609 * 2610 * @see SVGHints#KEY_IMAGE_HANDLING 2611 */ 2612 public List<ImageElement> getSVGImages() { 2613 return this.imageElements; 2614 } 2615 2616 /** 2617 * Returns a new set containing the element IDs that have been used in 2618 * output so far. 2619 * 2620 * @return The element IDs. 2621 * 2622 * @since 1.5 2623 */ 2624 public Set<String> getElementIDs() { 2625 return new HashSet<String>(this.elementIDs); 2626 } 2627 2628 /** 2629 * Returns an element to represent a linear gradient. All the linear 2630 * gradients that are used get written to the DEFS element in the SVG. 2631 * 2632 * @param id the reference id. 2633 * @param paint the gradient. 2634 * 2635 * @return The SVG element. 2636 */ 2637 private String getLinearGradientElement(String id, GradientPaint paint) { 2638 StringBuilder b = new StringBuilder("<linearGradient id=\"").append(id) 2639 .append("\" "); 2640 Point2D p1 = paint.getPoint1(); 2641 Point2D p2 = paint.getPoint2(); 2642 b.append("x1=\"").append(geomDP(p1.getX())).append("\" "); 2643 b.append("y1=\"").append(geomDP(p1.getY())).append("\" "); 2644 b.append("x2=\"").append(geomDP(p2.getX())).append("\" "); 2645 b.append("y2=\"").append(geomDP(p2.getY())).append("\" "); 2646 b.append("gradientUnits=\"userSpaceOnUse\">"); 2647 b.append("<stop offset=\"0%\" style=\"stop-color: ").append( 2648 rgbColorStr(paint.getColor1())).append(";\"/>"); 2649 b.append("<stop offset=\"100%\" style=\"stop-color: ").append( 2650 rgbColorStr(paint.getColor2())).append(";\"/>"); 2651 return b.append("</linearGradient>").toString(); 2652 } 2653 2654 /** 2655 * Returns an element to represent a linear gradient. All the linear 2656 * gradients that are used get written to the DEFS element in the SVG. 2657 * 2658 * @param id the reference id. 2659 * @param paint the gradient. 2660 * 2661 * @return The SVG element. 2662 */ 2663 private String getLinearGradientElement(String id, LinearGradientPaint paint) { 2664 StringBuilder b = new StringBuilder("<linearGradient id=\"").append(id) 2665 .append("\" "); 2666 Point2D p1 = paint.getStartPoint(); 2667 Point2D p2 = paint.getEndPoint(); 2668 b.append("x1=\"").append(geomDP(p1.getX())).append("\" "); 2669 b.append("y1=\"").append(geomDP(p1.getY())).append("\" "); 2670 b.append("x2=\"").append(geomDP(p2.getX())).append("\" "); 2671 b.append("y2=\"").append(geomDP(p2.getY())).append("\" "); 2672 if (!paint.getCycleMethod().equals(CycleMethod.NO_CYCLE)) { 2673 String sm = paint.getCycleMethod().equals(CycleMethod.REFLECT) 2674 ? "reflect" : "repeat"; 2675 b.append("spreadMethod=\"").append(sm).append("\" "); 2676 } 2677 b.append("gradientUnits=\"userSpaceOnUse\">"); 2678 for (int i = 0; i < paint.getFractions().length; i++) { 2679 Color c = paint.getColors()[i]; 2680 float fraction = paint.getFractions()[i]; 2681 b.append("<stop offset=\"").append(geomDP(fraction * 100)) 2682 .append("%\" style=\"stop-color: ") 2683 .append(rgbColorStr(c)).append(";\"/>"); 2684 } 2685 return b.append("</linearGradient>").toString(); 2686 } 2687 2688 /** 2689 * Returns an element to represent a radial gradient. All the radial 2690 * gradients that are used get written to the DEFS element in the SVG. 2691 * 2692 * @param id the reference id. 2693 * @param rgp the radial gradient. 2694 * 2695 * @return The SVG element. 2696 */ 2697 private String getRadialGradientElement(String id, RadialGradientPaint rgp) { 2698 StringBuilder b = new StringBuilder("<radialGradient id=\"").append(id) 2699 .append("\" gradientUnits=\"userSpaceOnUse\" "); 2700 Point2D center = rgp.getCenterPoint(); 2701 Point2D focus = rgp.getFocusPoint(); 2702 float radius = rgp.getRadius(); 2703 b.append("cx=\"").append(geomDP(center.getX())).append("\" "); 2704 b.append("cy=\"").append(geomDP(center.getY())).append("\" "); 2705 b.append("r=\"").append(geomDP(radius)).append("\" "); 2706 b.append("fx=\"").append(geomDP(focus.getX())).append("\" "); 2707 b.append("fy=\"").append(geomDP(focus.getY())).append("\">"); 2708 2709 Color[] colors = rgp.getColors(); 2710 float[] fractions = rgp.getFractions(); 2711 for (int i = 0; i < colors.length; i++) { 2712 Color c = colors[i]; 2713 float f = fractions[i]; 2714 b.append("<stop offset=\"").append(geomDP(f * 100)).append("%\" "); 2715 b.append("stop-color=\"").append(rgbColorStr(c)).append("\"/>"); 2716 } 2717 return b.append("</radialGradient>").toString(); 2718 } 2719 2720 /** 2721 * Returns a clip path reference for the current user clip. This is 2722 * written out on all SVG elements that draw or fill shapes or text. 2723 * 2724 * @return A clip path reference. 2725 */ 2726 private String getClipPathRef() { 2727 if (this.clip == null) { 2728 return ""; 2729 } 2730 if (this.clipRef == null) { 2731 this.clipRef = registerClip(getClip()); 2732 } 2733 StringBuilder b = new StringBuilder(); 2734 b.append("clip-path=\"url(#").append(this.clipRef).append(")\""); 2735 return b.toString(); 2736 } 2737 2738 /** 2739 * Sets the attributes of the reusable {@link Rectangle2D} object that is 2740 * used by the {@link SVGGraphics2D#drawRect(int, int, int, int)} and 2741 * {@link SVGGraphics2D#fillRect(int, int, int, int)} methods. 2742 * 2743 * @param x the x-coordinate. 2744 * @param y the y-coordinate. 2745 * @param width the width. 2746 * @param height the height. 2747 */ 2748 private void setRect(int x, int y, int width, int height) { 2749 if (this.rect == null) { 2750 this.rect = new Rectangle2D.Double(x, y, width, height); 2751 } else { 2752 this.rect.setRect(x, y, width, height); 2753 } 2754 } 2755 2756 /** 2757 * Sets the attributes of the reusable {@link RoundRectangle2D} object that 2758 * is used by the {@link #drawRoundRect(int, int, int, int, int, int)} and 2759 * {@link #fillRoundRect(int, int, int, int, int, int)} methods. 2760 * 2761 * @param x the x-coordinate. 2762 * @param y the y-coordinate. 2763 * @param width the width. 2764 * @param height the height. 2765 * @param arcWidth the arc width. 2766 * @param arcHeight the arc height. 2767 */ 2768 private void setRoundRect(int x, int y, int width, int height, int arcWidth, 2769 int arcHeight) { 2770 if (this.roundRect == null) { 2771 this.roundRect = new RoundRectangle2D.Double(x, y, width, height, 2772 arcWidth, arcHeight); 2773 } else { 2774 this.roundRect.setRoundRect(x, y, width, height, 2775 arcWidth, arcHeight); 2776 } 2777 } 2778 2779 /** 2780 * Sets the attributes of the reusable {@link Arc2D} object that is used by 2781 * {@link #drawArc(int, int, int, int, int, int)} and 2782 * {@link #fillArc(int, int, int, int, int, int)} methods. 2783 * 2784 * @param x the x-coordinate. 2785 * @param y the y-coordinate. 2786 * @param width the width. 2787 * @param height the height. 2788 * @param startAngle the start angle in degrees, 0 = 3 o'clock. 2789 * @param arcAngle the angle (anticlockwise) in degrees. 2790 */ 2791 private void setArc(int x, int y, int width, int height, int startAngle, 2792 int arcAngle) { 2793 if (this.arc == null) { 2794 this.arc = new Arc2D.Double(x, y, width, height, startAngle, 2795 arcAngle, Arc2D.OPEN); 2796 } else { 2797 this.arc.setArc(x, y, width, height, startAngle, arcAngle, 2798 Arc2D.OPEN); 2799 } 2800 } 2801 2802 /** 2803 * Sets the attributes of the reusable {@link Ellipse2D} object that is 2804 * used by the {@link #drawOval(int, int, int, int)} and 2805 * {@link #fillOval(int, int, int, int)} methods. 2806 * 2807 * @param x the x-coordinate. 2808 * @param y the y-coordinate. 2809 * @param width the width. 2810 * @param height the height. 2811 */ 2812 private void setOval(int x, int y, int width, int height) { 2813 if (this.oval == null) { 2814 this.oval = new Ellipse2D.Double(x, y, width, height); 2815 } else { 2816 this.oval.setFrame(x, y, width, height); 2817 } 2818 } 2819 2820}