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