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