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