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