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