All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hfg.svg.SVG Maven / Gradle / Ivy

There is a newer version: 20240423
Show newest version
package com.hfg.svg;


import java.awt.*;
import java.io.Writer;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.hfg.css.CSS;
import com.hfg.css.CssUtil;
import com.hfg.graphics.units.GfxSize;
import com.hfg.graphics.units.GfxUnits;
import com.hfg.svg.filtereffect.*;
import com.hfg.util.collection.CollectionUtil;
import com.hfg.util.mime.MimeType;
import com.hfg.xml.XMLAttribute;
import com.hfg.xml.XMLNamespace;
import com.hfg.util.StringUtil;
import com.hfg.xml.XMLNamespaceSet;
import com.hfg.xml.XMLTag;
import com.hfg.xml.XMLizable;

//------------------------------------------------------------------------------
/**
 * Object representation of an SVG (Scalable Vector Graphics) tag.
 *
 * @author J. Alex Taylor, hairyfatguy.com
 */
//------------------------------------------------------------------------------
// com.hfg XML/HTML Coding Library
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//
// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
// [email protected]
//------------------------------------------------------------------------------

public class SVG extends AbstractSvgNode
{
   private SvgMetadata mMetadata;
   private XMLTag mStyleTag;

   private static Map sTagToClassMap = new HashMap<>();

   private static final Pattern sMeasurementPattern = Pattern.compile("(\\-?[\\d\\.]+)(\\w+)?");
   private static final Pattern sTrailingZeroPattern = Pattern.compile("(\\.?0+)$");

   private static int sNumSigFigsForCoordinates = 7;

   // SVG tag name constants
   public static final String anchor   = "a";
   public static final String circle   = "circle";
   public static final String defs     = "defs";
   public static final String desc     = "desc";
   public static final String ellipse  = "ellipse";
   public static final String feBlend  = "feBlend";
   public static final String feColorMatrix  = "feColorMatrix";
   public static final String feComponentTransfer = "feComponentTransfer";
   public static final String feComposite = "feComposite";
   public static final String feConvolveMatrix = "feConvolveMatrix";
   public static final String feDiffuseLighting = "feDiffuseLighting";
   public static final String feDisplacementMap = "feDisplacementMap";
   public static final String feDistantLight = "feDistantLight";
   public static final String feFlood = "feFlood";
   public static final String feFuncA = "feFuncA";
   public static final String feFuncB = "feFuncB";
   public static final String feFuncG = "feFuncG";
   public static final String feFuncR = "feFuncR";
   public static final String feGaussianBlur = "feGaussianBlur";
   public static final String feImage = "feImage";
   public static final String feMerge = "feMerge";
   public static final String feMergeNode = "feMergeNode";
   public static final String feMorphology = "feMorphology";
   public static final String feOffset = "feOffset";
   public static final String fePointLight = "fePointLight";
   public static final String feSpecularLighting = "feSpecularLighting";
   public static final String feSpotLight = "feSpotLight";
   public static final String feTile = "feTile";
   public static final String feTurbulence = "feTurbulence";
   public static final String foreignObject = "foreignObject";
   public static final String filter   = "filter";
   public static final String group    = "g";
   public static final String line     = "line";
   public static final String marker   = "marker";
   public static final String metadata = "metadata";
   public static final String path     = "path";
   public static final String polygon  = "polygon";
   public static final String rect     = "rect";
   public static final String script   = "script";
   public static final String style    = "style";
   public static final String svg      = "svg";
   public static final String switch_  = "switch";
   public static final String symbol   = "symbol";
   public static final String text     = "text";
   public static final String title    = "title";
   public static final String tspan    = "tspan";
   public static final String use      = "use";

   static
   {
      sTagToClassMap.put(anchor, SvgLink.class);
      sTagToClassMap.put(circle, SvgCircle.class);
      sTagToClassMap.put(defs, SvgDefs.class);
      sTagToClassMap.put(desc, SvgDesc.class);
      sTagToClassMap.put(ellipse, SvgEllipse.class);
      sTagToClassMap.put(feBlend, SvgFeBlend.class);
      sTagToClassMap.put(feColorMatrix, SvgFeColorMatrix.class);
      sTagToClassMap.put(feComponentTransfer, SvgFeComponentTransfer.class);
      sTagToClassMap.put(feComposite, SvgFeComposite.class);
      sTagToClassMap.put(feConvolveMatrix, SvgFeConvolveMatrix.class);
      sTagToClassMap.put(feDiffuseLighting, SvgFeDiffuseLighting.class);
      sTagToClassMap.put(feDisplacementMap, SvgFeDisplacementMap.class);
      sTagToClassMap.put(feDistantLight, SvgFeDistantLight.class);
      sTagToClassMap.put(feFlood, SvgFeFlood.class);
      sTagToClassMap.put(feFuncA, SvgFeFuncA.class);
      sTagToClassMap.put(feFuncB, SvgFeFuncB.class);
      sTagToClassMap.put(feFuncG, SvgFeFuncG.class);
      sTagToClassMap.put(feFuncR, SvgFeFuncR.class);
      sTagToClassMap.put(feGaussianBlur, SvgFeGaussianBlur.class);
      sTagToClassMap.put(feImage, SvgFeImage.class);
      sTagToClassMap.put(feMerge, SvgFeMerge.class);
      sTagToClassMap.put(feMergeNode, SvgFeMergeNode.class);
      sTagToClassMap.put(feMorphology, SvgFeMorphology.class);
      sTagToClassMap.put(feOffset, SvgFeOffset.class);
      sTagToClassMap.put(fePointLight, SvgFePointLight.class);
      sTagToClassMap.put(feSpecularLighting, SvgFeSpecularLighting.class);
      sTagToClassMap.put(feSpotLight, SvgFeSpotLight.class);
      sTagToClassMap.put(feTile, SvgFeTile.class);
      sTagToClassMap.put(feTurbulence, SvgFeTurbulence.class);
      sTagToClassMap.put(filter, SvgFilter.class);
      sTagToClassMap.put(group, SvgGroup.class);
      sTagToClassMap.put(line, SvgLine.class);
      sTagToClassMap.put(marker, SvgMarker.class);
      sTagToClassMap.put(metadata, SvgMetadata.class);
      sTagToClassMap.put(path, SvgPath.class);
      sTagToClassMap.put(polygon, SvgPolygon.class);
      sTagToClassMap.put(rect, SvgRect.class);
      sTagToClassMap.put(script, SvgScript.class);
      sTagToClassMap.put(symbol, SvgSymbol.class);
      sTagToClassMap.put(text, SvgText.class);
      sTagToClassMap.put(title, SvgTitle.class);
      sTagToClassMap.put(tspan, SvgTSpan.class);
   }

   //##########################################################################
   // CONSTRUCTORS
   //##########################################################################

   //---------------------------------------------------------------------------
   public SVG()
   {
      super(svg);
      setDefaultXMLNamespaceDeclaration(XMLNamespace.SVG);
//      setAttribute("xmlns:" + XMLNamespace.SVG.getPrefix(),   XMLNamespace.SVG.getURI());
      setAttribute("xmlns:" + XMLNamespace.XLINK.getPrefix(), XMLNamespace.XLINK.getURI());
   }


   //---------------------------------------------------------------------------
   public SVG(XMLTag inXMLTag)
   {
      this();

      inXMLTag.verifyTagName(getTagName());

      if (CollectionUtil.hasValues(inXMLTag.getAttributes()))
      {
         for (XMLAttribute attr : inXMLTag.getAttributes())
         {
            setAttribute(attr.clone());
         }
      }

      java.util.List subtags = inXMLTag.getSubtags();
      if (CollectionUtil.hasValues(subtags))
      {
         for (XMLTag subtag : subtags)
         {
            addSubtag(SVG.constructFromXMLTag(subtag));
         }
      }
   }

   //##########################################################################
   // PUBLIC METHODS
   //##########################################################################

   //---------------------------------------------------------------------------
   public static int getNumSigFigsForCoordinates()
   {
      return sNumSigFigsForCoordinates;
   }

   //---------------------------------------------------------------------------
   public static void setNumSigFigsForCoordinates(int inValue)
   {
      sNumSigFigsForCoordinates = inValue;
   }

   //---------------------------------------------------------------------------
   /**
    * Method used by SVG classes to impose the globally specified number of
    * significant figures on coordinate values.
    * @param inValue the number to be sig. fig. adjusted
    * @return the sig. fig. adjusted number
    */
   public static String formatCoordinate(float inValue)
   {
      return (0 == inValue ? "0" : trimTrailingZeros(String.format("%." + sNumSigFigsForCoordinates + "G", inValue)));
   }

   //---------------------------------------------------------------------------
   /**
    * Method used by SVG classes to impose the globally specified number of
    * significant figures on coordinate values.
    * @param inValue the number to be sig. fig. adjusted
    * @return the sig. fig. adjusted number
    */
   public static String formatCoordinate(double inValue)
   {
      return (0 == inValue ? "0" : trimTrailingZeros(String.format("%." + sNumSigFigsForCoordinates + "G", inValue)));
   }

   //---------------------------------------------------------------------------
   public static SvgNode constructFromXMLTag(XMLTag inXMLTag)
   {
      Class clazz = sTagToClassMap.get(inXMLTag.getTagName());
      if (null == clazz)
      {
         throw new SvgException("The tag " + StringUtil.singleQuote(inXMLTag.getTagName()) + " could not be mapped to an SVG class!");
      }

      SvgNode svgNode;
      try
      {
         Constructor constructor = clazz.getConstructor(XMLTag.class);

         svgNode = (SvgNode) constructor.newInstance(inXMLTag);
      }
      catch (NoSuchMethodException e)
      {
         throw new SvgException("The class " + StringUtil.singleQuote(clazz) + " needs a constructor that takes an XMLTag.", e);
      }
      catch (Exception e)
      {
         throw new SvgException("Problem during invocation of class " + StringUtil.singleQuote(clazz) + "'s constructor!", e);
      }

      return svgNode;
   }

   //---------------------------------------------------------------------------
   public SVG setFont(Font inFont)
   {
      addStyle("font-family: " + inFont.getName());
      addStyle("font-size:" + inFont.getSize() + "pt;");
      return this;
   }

   //---------------------------------------------------------------------------
   public SVG setOpacity(Float inValue)
   {
      if (inValue != null)
      {
         setAttribute(SvgAttr.opacity, String.format("%.2f", inValue));
      }
      else
      {
         removeAttribute(SvgAttr.opacity);
      }

      return this;
   }

   //--------------------------------------------------------------------------
   /**
    Adds the specified text to a 'style' block.
    @param inStyle CSS style text for inclusion via a 'style' tag
    */
   public SVG addStyleTag(CSS inStyle)
   {
      return addStyleTag(inStyle.toString());
   }

   //--------------------------------------------------------------------------
   /**
    Adds the specified text to a 'style' block.
    @param inStyle CSS style text for inclusion via a 'style' tag
    */
   public SVG addStyleTag(String inStyle)
   {
      if (null == mStyleTag)
      {
         mStyleTag = new XMLTag(style);
         mStyleTag.setAttribute(SvgAttr.type, MimeType.TEXT_CSS);
         addSubtag(mStyleTag);
      }

      mStyleTag.addContentWithoutEscaping(inStyle);

      return this;
   }

   //--------------------------------------------------------------------------
   @Override
   public SVG addStyle(String inValue)
   {
      CssUtil.addStyle(this, inValue);
      return this;
   }

   //---------------------------------------------------------------------------
   @Override
   public SVG setStyle(String inValue)
   {
      setAttribute(SvgAttr.style, inValue);
      return this;
   }

   //---------------------------------------------------------------------------
   public SVG setWidth(int inValue)
   {
      setAttribute(SvgAttr.width, inValue);
      return this;
   }

   //---------------------------------------------------------------------------
   public int getWidth()
   {
      String widthString = getAttributeValue(SvgAttr.width);
      if (! StringUtil.isSet(widthString))
      {
         // Is a viewBox specified?
         Rectangle viewBox = getViewBox();
         if (viewBox != null)
         {
            widthString = viewBox.getWidth() + "px";
         }
         else
         {
            setDimensionsToContentBoundsBox();
            widthString = getAttributeValue(SvgAttr.width);
         }
      }

      Matcher m = sMeasurementPattern.matcher(widthString);
      if (m.matches())
      {
         widthString = m.group(1);
      }

      return (int) Float.parseFloat(widthString);
   }

   //---------------------------------------------------------------------------
   public SVG setHeight(int inValue)
   {
      setAttribute(SvgAttr.height, inValue);
      return this;
   }

   //---------------------------------------------------------------------------
   public int getHeight()
   {
      String heightString = getAttributeValue(SvgAttr.height);
      if (! StringUtil.isSet(heightString))
      {
         // Is a viewBox specified?
         Rectangle viewBox = getViewBox();
         if (viewBox != null)
         {
            heightString = viewBox.getHeight() + "px";
         }
         else
         {
            setDimensionsToContentBoundsBox();
            heightString = getAttributeValue(SvgAttr.height);
         }
      }

      Matcher m = sMeasurementPattern.matcher(heightString);
      if (m.matches())
      {
         heightString = m.group(1);
      }

      return (int) Float.parseFloat(heightString);
   }

   //---------------------------------------------------------------------------
   public SVG setViewBox(Rectangle inValue)
   {
      setAttribute(SvgAttr.viewBox,
                   String.format("%d %d %d %d", (int)inValue.getMinX(), (int)inValue.getMinY(),
                                 (int)inValue.getMaxX(), (int)inValue.getMaxY()));
      return this;
   }

   //---------------------------------------------------------------------------
   public Rectangle getViewBox()
   {
      Rectangle rect = null;
      String stringValue = getAttributeValue(SvgAttr.viewBox);
      if (StringUtil.isSet(stringValue))
      {
         String[] pieces = stringValue.split("\\s+");
         int x = Integer.parseInt(pieces[0]);
         int y = Integer.parseInt(pieces[1]);
         rect = new Rectangle(x, y, (int) Float.parseFloat(pieces[2]) - x, (int) Float.parseFloat(pieces[3]) - y);
      }

      return rect;
   }

   //---------------------------------------------------------------------------
   @Override
   public synchronized void toXML(Writer inWriter)
   {
      if (! hasAttribute(SvgAttr.height)) setDimensionsToContentBoundsBox();
      super.toXML(inWriter);
   }

   //---------------------------------------------------------------------------
   @Override
   protected void toXML(Writer inWriter, XMLNamespaceSet inDeclaredNamespaces)
   {
      if (! hasAttribute(SvgAttr.height)) setDimensionsToContentBoundsBox();
      super.toXML(inWriter, inDeclaredNamespaces);
   }

   //---------------------------------------------------------------------------
   @Override
   public synchronized void toIndentedXML(Writer inWriter, int inInitialIndentLevel, int inIndentSize,
                                          XMLNamespaceSet inDeclaredNamespaces)
   {
      if (! hasAttribute(SvgAttr.height)) setDimensionsToContentBoundsBox();
      super.toIndentedXML(inWriter, inInitialIndentLevel, inIndentSize, inDeclaredNamespaces);
   }

   //---------------------------------------------------------------------------
   public SVG setDimensionsToContentBoundsBox()
   {
      Rectangle contentRect = getContentBoundsBox();

      setWidth((int) contentRect.getWidth());
      setHeight((int) contentRect.getHeight());

      // Don't think we need to do this be default
//      setViewBox(contentRect);

      return this;
   }

   //---------------------------------------------------------------------------
   @Override
   public Rectangle getBoundsBox()
   {
      int minX = 0;
      int minY = 0;
      int maxX = minX + (getAttributeValue(SvgAttr.width) != null ? GfxSize.allocate(getAttributeValue(SvgAttr.width), GfxUnits.pixels).toInt(GfxUnits.pixels) : 0);
      int maxY = minY + (getAttributeValue(SvgAttr.height) != null ? GfxSize.allocate(getAttributeValue(SvgAttr.height), GfxUnits.pixels).toInt(GfxUnits.pixels) : 0);

      Rectangle rect = getContentBoundsBox();
      if (rect != null)
      {
         if (rect.getX() < minX) minX = (int) rect.getX();
         if (rect.getY() < minY) minY = (int) rect.getY();
         if (rect.getMaxX() > maxX) maxX = (int) rect.getMaxX();
         if (rect.getMaxY() > maxY) maxY = (int) rect.getMaxY();
      }

      Rectangle boundsBox = new Rectangle(minX, minY, maxX - minX, maxY - minY);
      adjustBoundsForTransform(boundsBox);

      return boundsBox;
   }

   //---------------------------------------------------------------------------
   public Rectangle getContentBoundsBox()
   {
      int minX = 0;
      int minY = 0;
      int maxX = 0;
      int maxY = 0;

      for (XMLizable node : getSubtags())
      {
         if (node instanceof SvgNode)
         {
            Rectangle rect = ((SvgNode)node).getBoundsBox();
            if (rect != null)
            {
               if (rect.getX() < minX) minX = (int) rect.getX();
               if (rect.getY() < minY) minY = (int) rect.getY();
               if (rect.getMaxX() > maxX) maxX = (int) rect.getMaxX();
               if (rect.getMaxY() > maxY) maxY = (int) rect.getMaxY();
            }
         }
      }

      Rectangle boundsBox = new Rectangle(minX, minY, maxX - minX, maxY - minY);
      adjustBoundsForTransform(boundsBox);

      return boundsBox;
   }

   //---------------------------------------------------------------------------
   public SvgGroup addGroup()
   {
      SvgGroup group = new SvgGroup();
      addSubtag(group);
      return group;
   }

   //---------------------------------------------------------------------------
   public SvgMetadata getMetadata()
   {
      if (null == mMetadata)
      {
         // Check if it has been added via addSubtag()...
         mMetadata = getOptionalSubtagByName(SVG.metadata);
         if (null == mMetadata)
         {
            mMetadata = new SvgMetadata();
            addSubtag(0, mMetadata);
         }
      }

      return mMetadata;
   }

   //---------------------------------------------------------------------------
   public SvgDefs addDefs()
   {
      SvgDefs defs = new SvgDefs();
      addSubtag(defs);
      return defs;
   }

   //---------------------------------------------------------------------------
   public SvgLine addLine(Point inStart, Point inEnd)
   {
      SvgLine line = new SvgLine(inStart, inEnd);
      addSubtag(line);
      return line;
   }

   //---------------------------------------------------------------------------
   public SvgPath addPath()
   {
      SvgPath path = new SvgPath();
      addSubtag(path);
      return path;
   }

   //---------------------------------------------------------------------------
   public SvgEllipse addEllipse()
   {
      SvgEllipse ellipse = new SvgEllipse();
      addSubtag(ellipse);
      return ellipse;
   }

   //---------------------------------------------------------------------------
   public SvgPolygon addPolygon()
   {
      SvgPolygon polygon = new SvgPolygon();
      addSubtag(polygon);
      return polygon;
   }

   //---------------------------------------------------------------------------
   public SvgPath addPath(String inPathData)
   {
      SvgPath path = new SvgPath(inPathData);
      addSubtag(path);
      return path;
   }

   //---------------------------------------------------------------------------
   public SvgRect addRect(Rectangle inRect)
   {
      SvgRect rect = new SvgRect(inRect);
      addSubtag(rect);
      return rect;
   }

   //---------------------------------------------------------------------------
   public SvgText addText(String inText, Font inFont, Point inLocation)
   {
      SvgText text = new SvgText(inText, inFont, inLocation);
      addSubtag(text);
      return text;
   }

   //---------------------------------------------------------------------------
   @Override
   public void draw(Graphics2D g2)
   {
      // Save settings
      Paint  origPaint      = g2.getPaint();
      Stroke origStroke     = g2.getStroke();
      Color  origBackground = g2.getBackground();

      // Paint the background
      Rectangle canvas = new Rectangle(0, 0, getWidth(), getHeight());
      g2.setPaint(Color.WHITE);
      g2.fill(canvas);

      g2.setPaint(Color.BLACK);

      drawSubnodes(g2);

      // Restore settings
      g2.setPaint(origPaint);
      g2.setStroke(origStroke);
      g2.setBackground(origBackground);
   }

   //---------------------------------------------------------------------------
   public void scale(float inProportionalScalingFactor)
   {
      setHeight((int) (getHeight() * inProportionalScalingFactor));
      setWidth((int) (getWidth() * inProportionalScalingFactor));

      // Wrap all contents in a scaling transform
      SvgGroup scalingGroup = new SvgGroup().setTransform("scale(" + formatCoordinate(inProportionalScalingFactor) + ")");

      scalingGroup.setSubtags(getSubtags());

      List subtags = new ArrayList<>();
      subtags.add(scalingGroup);

      setSubtags(subtags);
   }

   //---------------------------------------------------------------------------
   private static String trimTrailingZeros(String inValue)
   {
      Matcher m = sTrailingZeroPattern.matcher(inValue);

      return (m.find() ? inValue.substring(0, inValue.length() - m.group(1).length()) : inValue);
   }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy