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