001package com.hfg.svg; 002 003 004import java.awt.*; 005import java.awt.geom.Rectangle2D; 006import java.io.Writer; 007import java.lang.reflect.Constructor; 008import java.util.ArrayList; 009import java.util.HashMap; 010import java.util.List; 011import java.util.Map; 012import java.util.regex.Matcher; 013import java.util.regex.Pattern; 014 015import com.hfg.css.CSS; 016import com.hfg.css.CssUtil; 017import com.hfg.graphics.units.GfxSize; 018import com.hfg.graphics.units.GfxUnits; 019import com.hfg.svg.filtereffect.*; 020import com.hfg.util.collection.CollectionUtil; 021import com.hfg.util.mime.MimeType; 022import com.hfg.xml.XMLAttribute; 023import com.hfg.xml.XMLComment; 024import com.hfg.xml.XMLNamespace; 025import com.hfg.util.StringUtil; 026import com.hfg.xml.XMLNamespaceSet; 027import com.hfg.xml.XMLNode; 028import com.hfg.xml.XMLTag; 029import com.hfg.xml.XMLizable; 030 031//------------------------------------------------------------------------------ 032/** 033 * Object representation of an SVG (Scalable Vector Graphics) tag. 034 * 035 * @author J. Alex Taylor, hairyfatguy.com 036 */ 037//------------------------------------------------------------------------------ 038// com.hfg XML/HTML Coding Library 039// 040// This library is free software; you can redistribute it and/or 041// modify it under the terms of the GNU Lesser General Public 042// License as published by the Free Software Foundation; either 043// version 2.1 of the License, or (at your option) any later version. 044// 045// This library is distributed in the hope that it will be useful, 046// but WITHOUT ANY WARRANTY; without even the implied warranty of 047// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 048// Lesser General Public License for more details. 049// 050// You should have received a copy of the GNU Lesser General Public 051// License along with this library; if not, write to the Free Software 052// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 053// 054// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com 055// jataylor@hairyfatguy.com 056//------------------------------------------------------------------------------ 057 058public class SVG extends AbstractSvgNode 059{ 060 private SvgMetadata mMetadata; 061 private XMLTag mStyleTag; 062 063 private static Map<String, Class> sTagToClassMap = new HashMap<>(); 064 065 private static final Pattern sMeasurementPattern = Pattern.compile("(\\-?[\\d\\.]+)(\\w+)?"); 066 private static final Pattern sTrailingZeroPattern = Pattern.compile("(\\.?0+)$"); 067 068 private static int sNumSigFigsForCoordinates = 7; 069 070 // SVG tag name constants 071 public static final String anchor = "a"; 072 public static final String circle = "circle"; 073 public static final String defs = "defs"; 074 public static final String desc = "desc"; 075 public static final String ellipse = "ellipse"; 076 public static final String feBlend = "feBlend"; 077 public static final String feColorMatrix = "feColorMatrix"; 078 public static final String feComponentTransfer = "feComponentTransfer"; 079 public static final String feComposite = "feComposite"; 080 public static final String feConvolveMatrix = "feConvolveMatrix"; 081 public static final String feDiffuseLighting = "feDiffuseLighting"; 082 public static final String feDisplacementMap = "feDisplacementMap"; 083 public static final String feDistantLight = "feDistantLight"; 084 public static final String feFlood = "feFlood"; 085 public static final String feFuncA = "feFuncA"; 086 public static final String feFuncB = "feFuncB"; 087 public static final String feFuncG = "feFuncG"; 088 public static final String feFuncR = "feFuncR"; 089 public static final String feGaussianBlur = "feGaussianBlur"; 090 public static final String feImage = "feImage"; 091 public static final String feMerge = "feMerge"; 092 public static final String feMergeNode = "feMergeNode"; 093 public static final String feMorphology = "feMorphology"; 094 public static final String feOffset = "feOffset"; 095 public static final String fePointLight = "fePointLight"; 096 public static final String feSpecularLighting = "feSpecularLighting"; 097 public static final String feSpotLight = "feSpotLight"; 098 public static final String feTile = "feTile"; 099 public static final String feTurbulence = "feTurbulence"; 100 public static final String foreignObject = "foreignObject"; 101 public static final String filter = "filter"; 102 public static final String group = "g"; 103 public static final String line = "line"; 104 public static final String marker = "marker"; 105 public static final String metadata = "metadata"; 106 public static final String path = "path"; 107 public static final String polygon = "polygon"; 108 public static final String polyline = "polyline"; 109 public static final String rect = "rect"; 110 public static final String script = "script"; 111 public static final String style = "style"; 112 public static final String svg = "svg"; 113 public static final String switch_ = "switch"; 114 public static final String symbol = "symbol"; 115 public static final String text = "text"; 116 public static final String title = "title"; 117 public static final String tspan = "tspan"; 118 public static final String use = "use"; 119 120 static 121 { 122 sTagToClassMap.put(anchor, SvgLink.class); 123 sTagToClassMap.put(circle, SvgCircle.class); 124 sTagToClassMap.put(defs, SvgDefs.class); 125 sTagToClassMap.put(desc, SvgDesc.class); 126 sTagToClassMap.put(ellipse, SvgEllipse.class); 127 sTagToClassMap.put(feBlend, SvgFeBlend.class); 128 sTagToClassMap.put(feColorMatrix, SvgFeColorMatrix.class); 129 sTagToClassMap.put(feComponentTransfer, SvgFeComponentTransfer.class); 130 sTagToClassMap.put(feComposite, SvgFeComposite.class); 131 sTagToClassMap.put(feConvolveMatrix, SvgFeConvolveMatrix.class); 132 sTagToClassMap.put(feDiffuseLighting, SvgFeDiffuseLighting.class); 133 sTagToClassMap.put(feDisplacementMap, SvgFeDisplacementMap.class); 134 sTagToClassMap.put(feDistantLight, SvgFeDistantLight.class); 135 sTagToClassMap.put(feFlood, SvgFeFlood.class); 136 sTagToClassMap.put(feFuncA, SvgFeFuncA.class); 137 sTagToClassMap.put(feFuncB, SvgFeFuncB.class); 138 sTagToClassMap.put(feFuncG, SvgFeFuncG.class); 139 sTagToClassMap.put(feFuncR, SvgFeFuncR.class); 140 sTagToClassMap.put(feGaussianBlur, SvgFeGaussianBlur.class); 141 sTagToClassMap.put(feImage, SvgFeImage.class); 142 sTagToClassMap.put(feMerge, SvgFeMerge.class); 143 sTagToClassMap.put(feMergeNode, SvgFeMergeNode.class); 144 sTagToClassMap.put(feMorphology, SvgFeMorphology.class); 145 sTagToClassMap.put(feOffset, SvgFeOffset.class); 146 sTagToClassMap.put(fePointLight, SvgFePointLight.class); 147 sTagToClassMap.put(feSpecularLighting, SvgFeSpecularLighting.class); 148 sTagToClassMap.put(feSpotLight, SvgFeSpotLight.class); 149 sTagToClassMap.put(feTile, SvgFeTile.class); 150 sTagToClassMap.put(feTurbulence, SvgFeTurbulence.class); 151 sTagToClassMap.put(filter, SvgFilter.class); 152 sTagToClassMap.put(group, SvgGroup.class); 153 sTagToClassMap.put(line, SvgLine.class); 154 sTagToClassMap.put(marker, SvgMarker.class); 155 sTagToClassMap.put(metadata, SvgMetadata.class); 156 sTagToClassMap.put(path, SvgPath.class); 157 sTagToClassMap.put(polygon, SvgPolygon.class); 158 sTagToClassMap.put(polyline, SvgPolyline.class); 159 sTagToClassMap.put(rect, SvgRect.class); 160 sTagToClassMap.put(script, SvgScript.class); 161 sTagToClassMap.put(symbol, SvgSymbol.class); 162 sTagToClassMap.put(text, SvgText.class); 163 sTagToClassMap.put(title, SvgTitle.class); 164 sTagToClassMap.put(tspan, SvgTSpan.class); 165 sTagToClassMap.put(use, SvgUse.class); 166 } 167 168 //########################################################################## 169 // CONSTRUCTORS 170 //########################################################################## 171 172 //--------------------------------------------------------------------------- 173 public SVG() 174 { 175 super(svg); 176 setDefaultXMLNamespaceDeclaration(XMLNamespace.SVG); 177// setAttribute("xmlns:" + XMLNamespace.SVG.getPrefix(), XMLNamespace.SVG.getURI()); 178 setAttribute("xmlns:" + XMLNamespace.XLINK.getPrefix(), XMLNamespace.XLINK.getURI()); 179 } 180 181 182 //--------------------------------------------------------------------------- 183 public SVG(XMLNode inXMLTag) 184 { 185 this(); 186 187 inXMLTag.verifyTagName(getTagName()); 188 189 if (CollectionUtil.hasValues(inXMLTag.getAttributes())) 190 { 191 for (XMLAttribute attr : inXMLTag.getAttributes()) 192 { 193 setAttribute(attr.clone()); 194 } 195 } 196 197 List<XMLTag> subtags = inXMLTag.getSubtags(); 198 if (CollectionUtil.hasValues(subtags)) 199 { 200 for (XMLizable subtag : subtags) 201 { 202 if (subtag instanceof XMLComment 203 || ((XMLTag) subtag).getTagName().equalsIgnoreCase("style") 204 || ((XMLTag) subtag).getTagName().equalsIgnoreCase("use")) 205 { 206 addSubtag(subtag); 207 } 208 else 209 { 210 addSubtag(SVG.constructFromXMLTag((XMLTag) subtag)); 211 } 212 } 213 } 214 } 215 216 //########################################################################## 217 // PUBLIC METHODS 218 //########################################################################## 219 220 //--------------------------------------------------------------------------- 221 public static int getNumSigFigsForCoordinates() 222 { 223 return sNumSigFigsForCoordinates; 224 } 225 226 //--------------------------------------------------------------------------- 227 public static void setNumSigFigsForCoordinates(int inValue) 228 { 229 sNumSigFigsForCoordinates = inValue; 230 } 231 232 //--------------------------------------------------------------------------- 233 /** 234 * Method used by SVG classes to impose the globally specified number of 235 * significant figures on coordinate values. 236 * @param inValue the number to be sig. fig. adjusted 237 * @return the sig. fig. adjusted number 238 */ 239 public static String formatCoordinate(float inValue) 240 { 241 return (0 == inValue ? "0" : trimTrailingZeros(String.format("%." + sNumSigFigsForCoordinates + "G", inValue))); 242 } 243 244 //--------------------------------------------------------------------------- 245 /** 246 * Method used by SVG classes to impose the globally specified number of 247 * significant figures on coordinate values. 248 * @param inValue the number to be sig. fig. adjusted 249 * @return the sig. fig. adjusted number 250 */ 251 public static String formatCoordinate(double inValue) 252 { 253 return (0 == inValue ? "0" : trimTrailingZeros(String.format("%." + sNumSigFigsForCoordinates + "G", inValue))); 254 } 255 256 //--------------------------------------------------------------------------- 257 public static SvgNode constructFromXMLTag(XMLTag inXMLTag) 258 { 259 Class clazz = sTagToClassMap.get(inXMLTag.getTagName()); 260 if (null == clazz) 261 { 262 throw new SvgException("The tag " + StringUtil.singleQuote(inXMLTag.getTagName()) + " could not be mapped to an SVG class!"); 263 } 264 265 SvgNode svgNode; 266 try 267 { 268 Constructor constructor = clazz.getConstructor(XMLTag.class); 269 270 svgNode = (SvgNode) constructor.newInstance(inXMLTag); 271 } 272 catch (NoSuchMethodException e) 273 { 274 throw new SvgException("The class " + StringUtil.singleQuote(clazz) + " needs a constructor that takes an XMLTag.", e); 275 } 276 catch (Exception e) 277 { 278 throw new SvgException("Problem during invocation of class " + StringUtil.singleQuote(clazz) + "'s constructor!", e); 279 } 280 281 return svgNode; 282 } 283 284 //--------------------------------------------------------------------------- 285 public SVG setFont(Font inFont) 286 { 287 addStyle("font-family: " + inFont.getName()); 288 addStyle("font-size:" + inFont.getSize() + "pt;"); 289 return this; 290 } 291 292 //--------------------------------------------------------------------------- 293 public SVG setOpacity(Float inValue) 294 { 295 if (inValue != null) 296 { 297 setAttribute(SvgAttr.opacity, String.format("%.2f", inValue)); 298 } 299 else 300 { 301 removeAttribute(SvgAttr.opacity); 302 } 303 304 return this; 305 } 306 307 //-------------------------------------------------------------------------- 308 /** 309 Adds the specified text to a 'style' block. 310 @param inStyle CSS style text for inclusion via a 'style' tag 311 */ 312 public SVG addStyleTag(CSS inStyle) 313 { 314 return addStyleTag(inStyle.toString()); 315 } 316 317 //-------------------------------------------------------------------------- 318 /** 319 Adds the specified text to a 'style' block. 320 @param inStyle CSS style text for inclusion via a 'style' tag 321 */ 322 public SVG addStyleTag(String inStyle) 323 { 324 if (null == mStyleTag) 325 { 326 mStyleTag = new XMLTag(style); 327 mStyleTag.setAttribute(SvgAttr.type, MimeType.TEXT_CSS); 328 addSubtag(mStyleTag); 329 } 330 331 mStyleTag.addContentWithoutEscaping(inStyle); 332 333 return this; 334 } 335 336 //-------------------------------------------------------------------------- 337 @Override 338 public SVG addStyle(String inValue) 339 { 340 CssUtil.addStyle(this, inValue); 341 return this; 342 } 343 344 //--------------------------------------------------------------------------- 345 @Override 346 public SVG setStyle(String inValue) 347 { 348 setAttribute(SvgAttr.style, inValue); 349 return this; 350 } 351 352 //--------------------------------------------------------------------------- 353 public SVG setWidth(int inValue) 354 { 355 setAttribute(SvgAttr.width, inValue); 356 return this; 357 } 358 359 //--------------------------------------------------------------------------- 360 public int getWidth() 361 { 362 String widthString = getAttributeValue(SvgAttr.width); 363 if (! StringUtil.isSet(widthString)) 364 { 365 // Is a viewBox specified? 366 Rectangle viewBox = getViewBox(); 367 if (viewBox != null) 368 { 369 widthString = viewBox.getWidth() + "px"; 370 } 371 else 372 { 373 setDimensionsToContentBoundsBox(); 374 widthString = getAttributeValue(SvgAttr.width); 375 } 376 } 377 378 Matcher m = sMeasurementPattern.matcher(widthString); 379 if (m.matches()) 380 { 381 widthString = m.group(1); 382 } 383 384 return (int) Float.parseFloat(widthString); 385 } 386 387 //--------------------------------------------------------------------------- 388 public SVG setHeight(int inValue) 389 { 390 setAttribute(SvgAttr.height, inValue); 391 return this; 392 } 393 394 //--------------------------------------------------------------------------- 395 public int getHeight() 396 { 397 String heightString = getAttributeValue(SvgAttr.height); 398 if (! StringUtil.isSet(heightString)) 399 { 400 // Is a viewBox specified? 401 Rectangle viewBox = getViewBox(); 402 if (viewBox != null) 403 { 404 heightString = viewBox.getHeight() + "px"; 405 } 406 else 407 { 408 setDimensionsToContentBoundsBox(); 409 heightString = getAttributeValue(SvgAttr.height); 410 } 411 } 412 413 Matcher m = sMeasurementPattern.matcher(heightString); 414 if (m.matches()) 415 { 416 heightString = m.group(1); 417 } 418 419 return (int) Float.parseFloat(heightString); 420 } 421 422 //--------------------------------------------------------------------------- 423 public SVG setViewBox(Rectangle inValue) 424 { 425 setAttribute(SvgAttr.viewBox, 426 String.format("%d %d %d %d", (int)inValue.getMinX(), (int)inValue.getMinY(), 427 (int)inValue.getMaxX(), (int)inValue.getMaxY())); 428 return this; 429 } 430 431 //--------------------------------------------------------------------------- 432 public SVG setViewBox(Rectangle2D inValue) 433 { 434 return setViewBox(inValue.getBounds()); 435 } 436 437 //--------------------------------------------------------------------------- 438 public Rectangle getViewBox() 439 { 440 Rectangle rect = null; 441 String stringValue = getAttributeValue(SvgAttr.viewBox); 442 if (StringUtil.isSet(stringValue)) 443 { 444 String[] pieces = stringValue.split("\\s+"); 445 int x = Integer.parseInt(pieces[0]); 446 int y = Integer.parseInt(pieces[1]); 447 rect = new Rectangle(x, y, (int) Float.parseFloat(pieces[2]) - x, (int) Float.parseFloat(pieces[3]) - y); 448 } 449 450 return rect; 451 } 452 453 //--------------------------------------------------------------------------- 454 @Override 455 public synchronized void toXML(Writer inWriter) 456 { 457 if (! hasAttribute(SvgAttr.height)) setDimensionsToContentBoundsBox(); 458 super.toXML(inWriter); 459 } 460 461 //--------------------------------------------------------------------------- 462 @Override 463 protected void toXML(Writer inWriter, XMLNamespaceSet inDeclaredNamespaces) 464 { 465 if (! hasAttribute(SvgAttr.height)) setDimensionsToContentBoundsBox(); 466 super.toXML(inWriter, inDeclaredNamespaces); 467 } 468 469 //--------------------------------------------------------------------------- 470 @Override 471 public synchronized void toIndentedXML(Writer inWriter, int inInitialIndentLevel, int inIndentSize, 472 XMLNamespaceSet inDeclaredNamespaces) 473 { 474 if (! hasAttribute(SvgAttr.height)) setDimensionsToContentBoundsBox(); 475 super.toIndentedXML(inWriter, inInitialIndentLevel, inIndentSize, inDeclaredNamespaces); 476 } 477 478 //--------------------------------------------------------------------------- 479 public SVG setDimensionsToContentBoundsBox() 480 { 481 Rectangle2D contentRect = getContentBoundsBox(); 482 483 setWidth((int) contentRect.getWidth()); 484 setHeight((int) contentRect.getHeight()); 485 486 // Don't think we need to do this be default 487// setViewBox(contentRect); 488 489 return this; 490 } 491 492 //--------------------------------------------------------------------------- 493 @Override 494 public Rectangle2D getBoundsBox() 495 { 496 double minX = 0; 497 double minY = 0; 498 double maxX = minX + (getAttributeValue(SvgAttr.width) != null ? GfxSize.allocate(getAttributeValue(SvgAttr.width), GfxUnits.pixels).to(GfxUnits.pixels) : 0); 499 double maxY = minY + (getAttributeValue(SvgAttr.height) != null ? GfxSize.allocate(getAttributeValue(SvgAttr.height), GfxUnits.pixels).to(GfxUnits.pixels) : 0); 500 501 Rectangle2D rect = getContentBoundsBox(); 502 if (rect != null) 503 { 504 if (rect.getX() < minX) minX = rect.getX(); 505 if (rect.getY() < minY) minY = rect.getY(); 506 if (rect.getMaxX() > maxX) maxX = rect.getMaxX(); 507 if (rect.getMaxY() > maxY) maxY = rect.getMaxY(); 508 } 509 510 Rectangle2D boundsBox = new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY); 511 adjustBoundsForTransform(boundsBox); 512 513 return boundsBox; 514 } 515 516 //--------------------------------------------------------------------------- 517 public Rectangle2D getContentBoundsBox() 518 { 519 double minX = 0; 520 double minY = 0; 521 double maxX = 0; 522 double maxY = 0; 523 524 for (XMLizable node : getSubtags()) 525 { 526 if (node instanceof SvgNode) 527 { 528 Rectangle2D rect = ((SvgNode)node).getBoundsBox(); 529 if (rect != null) 530 { 531 if (rect.getX() < minX) minX = rect.getX(); 532 if (rect.getY() < minY) minY = rect.getY(); 533 if (rect.getMaxX() > maxX) maxX = rect.getMaxX(); 534 if (rect.getMaxY() > maxY) maxY = rect.getMaxY(); 535 } 536 } 537 } 538 539 Rectangle2D boundsBox = new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY); 540 adjustBoundsForTransform(boundsBox); 541 542 return boundsBox; 543 } 544 545 //--------------------------------------------------------------------------- 546 public SvgGroup addGroup() 547 { 548 SvgGroup group = new SvgGroup(); 549 addSubtag(group); 550 return group; 551 } 552 553 //--------------------------------------------------------------------------- 554 public SvgMetadata getMetadata() 555 { 556 if (null == mMetadata) 557 { 558 // Check if it has been added via addSubtag()... 559 mMetadata = getOptionalSubtagByName(SVG.metadata); 560 if (null == mMetadata) 561 { 562 mMetadata = new SvgMetadata(); 563 addSubtag(0, mMetadata); 564 } 565 } 566 567 return mMetadata; 568 } 569 570 //--------------------------------------------------------------------------- 571 public SvgDefs addDefs() 572 { 573 SvgDefs defs = new SvgDefs(); 574 addSubtag(defs); 575 return defs; 576 } 577 578 //--------------------------------------------------------------------------- 579 public SvgLine addLine(Point inStart, Point inEnd) 580 { 581 SvgLine line = new SvgLine(inStart, inEnd); 582 addSubtag(line); 583 return line; 584 } 585 586 //--------------------------------------------------------------------------- 587 public SvgPath addPath() 588 { 589 SvgPath path = new SvgPath(); 590 addSubtag(path); 591 return path; 592 } 593 594 //--------------------------------------------------------------------------- 595 public SvgEllipse addEllipse() 596 { 597 SvgEllipse ellipse = new SvgEllipse(); 598 addSubtag(ellipse); 599 return ellipse; 600 } 601 602 //--------------------------------------------------------------------------- 603 public SvgPolygon addPolygon() 604 { 605 SvgPolygon polygon = new SvgPolygon(); 606 addSubtag(polygon); 607 return polygon; 608 } 609 610 //--------------------------------------------------------------------------- 611 public SvgPath addPath(String inPathData) 612 { 613 SvgPath path = new SvgPath(inPathData); 614 addSubtag(path); 615 return path; 616 } 617 618 //--------------------------------------------------------------------------- 619 public SvgRect addRect(Rectangle inRect) 620 { 621 SvgRect rect = new SvgRect(inRect); 622 addSubtag(rect); 623 return rect; 624 } 625 626 //--------------------------------------------------------------------------- 627 public SvgText addText(String inText, Font inFont, Point inLocation) 628 { 629 SvgText text = new SvgText(inText, inFont, inLocation); 630 addSubtag(text); 631 return text; 632 } 633 634 //--------------------------------------------------------------------------- 635 @Override 636 public void draw(Graphics2D g2) 637 { 638 // Save settings 639 Paint origPaint = g2.getPaint(); 640 Stroke origStroke = g2.getStroke(); 641 Color origBackground = g2.getBackground(); 642 643 // Paint the background 644 Rectangle canvas = new Rectangle(0, 0, getWidth(), getHeight()); 645 g2.setPaint(Color.WHITE); 646 g2.fill(canvas); 647 648 g2.setPaint(Color.BLACK); 649 650 drawSubnodes(g2); 651 652 // Restore settings 653 g2.setPaint(origPaint); 654 g2.setStroke(origStroke); 655 g2.setBackground(origBackground); 656 } 657 658 //--------------------------------------------------------------------------- 659 public void scale(float inProportionalScalingFactor) 660 { 661 setHeight((int) (getHeight() * inProportionalScalingFactor)); 662 setWidth((int) (getWidth() * inProportionalScalingFactor)); 663 664 // Wrap all contents in a scaling transform 665 SvgGroup scalingGroup = new SvgGroup().setTransform("scale(" + formatCoordinate(inProportionalScalingFactor) + ")"); 666 667 scalingGroup.setSubtags(getSubtags()); 668 669 List<AbstractSvgNode> subtags = new ArrayList<>(); 670 subtags.add(scalingGroup); 671 672 setSubtags(subtags); 673 } 674 675 //--------------------------------------------------------------------------- 676 private static String trimTrailingZeros(String inValue) 677 { 678 Matcher m = sTrailingZeroPattern.matcher(inValue); 679 680 return (m.find() ? inValue.substring(0, inValue.length() - m.group(1).length()) : inValue); 681 } 682}