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