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