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