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}