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

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

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

import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.hfg.css.*;
import com.hfg.graphics.Graphics2DState;
import com.hfg.graphics.GraphicsUtil;
import com.hfg.graphics.units.GfxSize;
import com.hfg.graphics.units.GfxUnits;
import com.hfg.html.attribute.HTMLColor;
import com.hfg.util.StringBuilderPlus;
import com.hfg.util.StringUtil;
import com.hfg.util.collection.CollectionUtil;
import com.hfg.util.io.GZIP;
import com.hfg.xml.*;

//------------------------------------------------------------------------------
/**
 * SVG (Scalable Vector Graphics) tag base class.
 *
 * @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 abstract class AbstractSvgNode extends XMLTag implements SvgNode
{
   private static final Pattern FILL_PATTERN = Pattern.compile("\\bfill\\s*:\\s*([^;\\s]+)", Pattern.CASE_INSENSITIVE);
   private static final Pattern OPACITY_PATTERN = Pattern.compile("\\bopacity\\s*:\\s*([^;\\s]+)", Pattern.CASE_INSENSITIVE);
   private static final Pattern FILL_OPACITY_PATTERN = Pattern.compile("\\bfill-opacity\\s*:\\s*([^;\\s]+)", Pattern.CASE_INSENSITIVE);
   private static final Pattern STROKE_PATTERN = Pattern.compile("\\bstroke\\s*:\\s*([^;$]+)", Pattern.CASE_INSENSITIVE);
   private static final Pattern STROKE_WIDTH_PATTERN = Pattern.compile("\\bstroke-width\\s*:\\s*([^;\\s]+?)(:?px)?", Pattern.CASE_INSENSITIVE);
   private static final Pattern STROKE_OPACITY_PATTERN = Pattern.compile("\\bstroke-opacity\\s*:\\s*([^;\\s]+)", Pattern.CASE_INSENSITIVE);
   private static final Pattern TRANSFORM_PATTERN = Pattern.compile("\\btransform\\s*:\\s*([^;$]+)", Pattern.CASE_INSENSITIVE);

   private XMLTag mTitleTag;

   //---------------------------------------------------------------------------
   public AbstractSvgNode(String inTagName)
   {
      super(inTagName);
      setNamespace(XMLNamespace.SVG);
   }

   //---------------------------------------------------------------------------
   protected void initFromXMLTag(XMLTag inXMLTag)
   {
      inXMLTag.verifyTagName(getTagName());

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

      List subtagsAndContent = inXMLTag.getContentPlusSubtagList();
      if (CollectionUtil.hasValues(subtagsAndContent))
      {
         for (Object object : subtagsAndContent)
         {
            if (object instanceof XMLTag)
            {
               addSubtag(SVG.constructFromXMLTag((XMLTag) object));
            }
            else if (object instanceof XMLizable)
            {
               addSubtag((XMLizable) object);
            }
            else
            {
               String content;
               if (object instanceof byte[])
               {
                  // Large content gets gzip-compressed
                  content = GZIP.uncompressToString((byte[]) object);
               }
               else
               {
                  content = (String) object;
               }

               int index = content.indexOf("= 0)
               {
                  boolean hideWithCommentsFromLegacyBrowsers = false;
                  content = content.substring(index + 9);
                  if (content.startsWith("//> cssDeclarations = getCSSDeclarations(inCSS);

      applyTransforms(g2);

      Composite composite = getG2Composite();
      if (composite != null)
      {
         g2.setComposite(composite);
      }

      Font locallyAdjustedFont = getAdjustedFont(origState.getFont(), cssDeclarations);
      if (locallyAdjustedFont != null)
      {
         g2.setFont(locallyAdjustedFont);
      }

      drawSubnodes(g2, inCSS);

      // Restore settings
      origState.applyTo(g2);
   }

   //---------------------------------------------------------------------------
   public String getCssTransform()
   {
      String transform = null;

      String style = getAttributeValue(SvgAttr.style);
      if (StringUtil.isSet(style))
      {
         List cssDeclarations = CSSDeclaration.parse(style);
         for (CSSDeclaration cssDeclaration : cssDeclarations)
         {
            if (cssDeclaration.getProperty().equals(CSSProperty.transform))
            {
               transform = cssDeclaration.getValue();
               break;
            }
         }
      }

      return transform;
   }

   //---------------------------------------------------------------------------
   /**
    Specifies the upper left corner of the bounding rectangle.
    */
   public SvgNode setPosition(Point2D inValue)
   {
      Rectangle2D bbox = getBoundsBox();

      StringBuilderPlus transform = new StringBuilderPlus(getCssTransform()).setDelimiter(" ");
      transform.delimitedAppend("translate(" + (inValue.getX() - bbox.getX()) + ", " + (inValue.getY() - bbox.getY()) + ")");

      setTransform(transform.toString());

      return this;
   }

   //---------------------------------------------------------------------------
   public Rectangle2D getBoundsBox()
   {
      Double minX = null;
      String xString = getAttributeValue(SvgAttr.x);
      if (StringUtil.isSet(xString))
      {
         if (xString.endsWith("%"))
         {
            // TODO: How to handle percents?
         }
         else
         {
            minX = Double.parseDouble(xString);
         }
      }

      Double minY = null;
      String yString = getAttributeValue(SvgAttr.y);
      if (StringUtil.isSet(yString))
      {
         if (yString.endsWith("%"))
         {
            // TODO: How to handle percents?
         }
         else
         {
            minY = Double.parseDouble(yString);
         }
      }

      Double maxX = null;
      Double maxY = null;

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

      Rectangle2D boundsBox = null;
      if (minX != null
            && minY != null
            && maxX != null
            && maxY != null)
      {
         boundsBox = new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY);
         adjustBoundsForTransform(boundsBox);
      }

      return boundsBox;
   }

   //---------------------------------------------------------------------------
   public Point2D getCenterPoint()
   {
      Rectangle2D bbox = getBoundsBox();

      return new Point2D.Double(bbox.getX() + (bbox.getWidth() / 2f),
                                bbox.getY() + (bbox.getHeight()) / 2f);
   }



   //---------------------------------------------------------------------------
   protected void rangeCheckOpacityValue(float inValue)
   {
      if (inValue > 1 || inValue < 0)
      {
         throw new RuntimeException("Illegal opacity value: " + inValue + "! Must be between 0 and 1 (inclusive).");
      }
   }

   //---------------------------------------------------------------------------
   // TODO: Font style and weight
   protected Font getAdjustedFont(Font inOrigFont, List inCSSDeclarations)
   {
      Font font = null;

      if (CollectionUtil.hasValues(inCSSDeclarations))
      {
         for (CSSDeclaration cssDeclaration : inCSSDeclarations)
         {
            if (cssDeclaration.getProperty().equals(CSSProperty.font_size))
            {
               if (null == font)
               {
                  font = new Font(inOrigFont.getFontName(), inOrigFont.getStyle(), inOrigFont.getSize());
               }

 //              GfxSize size = GfxSize.allocate(cssDeclaration.getValue(), GfxUnits.pixels);
 //              font = new Font(font.getFontName(), font.getStyle(), size.toInt(GfxUnits.points));
               int fontSize;
               try
               {
                  fontSize = Integer.parseInt(cssDeclaration.getValue());
               }
               catch (NumberFormatException e)
               {
                  GfxSize size = GfxSize.allocate(cssDeclaration.getValue());
                  fontSize = size.toInt(GfxUnits.points);
               }

               font = new Font(font.getFontName(), font.getStyle(), fontSize);
            }
            else if (cssDeclaration.getProperty().equals(CSSProperty.font_family))
            {
               if (null == font)
               {
                  font = new Font(inOrigFont.getFontName(), inOrigFont.getStyle(), inOrigFont.getSize());
               }

               font = new Font(cssDeclaration.getValue(), font.getStyle(), font.getSize());
            }
         }
      }

      if (hasAttribute(SvgAttr.fontSize))
      {
         if (null == font)
         {
            font = new Font(inOrigFont.getFontName(), inOrigFont.getStyle(), inOrigFont.getSize());
         }

         GfxSize size = GfxSize.allocate(getAttributeValue(SvgAttr.fontSize), GfxUnits.pixels);
         font = new Font(font.getFontName(), font.getStyle(), size.toInt(GfxUnits.points));
      }

      if (hasAttribute(SvgAttr.fontFamily))
      {
         if (null == font)
         {
            font = new Font(inOrigFont.getFontName(), inOrigFont.getStyle(), inOrigFont.getSize());
         }

         font = new Font(getAttributeValue(SvgAttr.fontFamily), font.getStyle(), font.getSize());
      }

      return font;
   }

   //--------------------------------------------------------------------------
   protected void applyTransforms(Graphics2D g2)
   {
      if (getAttributeValue(SvgAttr.transform) != null)
      {
         applyTransform(g2, getAttributeValue(SvgAttr.transform));
      }


      if (StringUtil.isSet(getAttributeValue(SvgAttr.style)))
      {
         Matcher m = TRANSFORM_PATTERN.matcher(getAttributeValue(SvgAttr.style));
         if (m.find())
         {
            applyTransform(g2, m.group(1));
         }
      }
   }

   //---------------------------------------------------------------------------
   protected void applyTransform(Graphics2D g2, String inTransformAttributeValue)
   {
      // Ex: "rotate(-90.0 118 9) translate(-33.0 3.0 )"
      if (StringUtil.isSet(inTransformAttributeValue))
      {
         String transformValue = inTransformAttributeValue.trim();

         int index;
         while ((index = transformValue.indexOf("(")) > 0)
         {
            String functionName = transformValue.substring(0, index).trim();
            int endArgIndex = transformValue.indexOf(")");
            String[] functionArgs = transformValue.substring(index + 1, endArgIndex).split("\\s*[,\\s]\\s*");
            for (int i = 0; i < functionArgs.length; i++)
            {
               if (functionArgs[i].endsWith("px"))
               {
                  functionArgs[i] = functionArgs[i].substring(0, functionArgs[i].length() - 2);
               }
               else if (functionArgs[i].endsWith("deg"))
               {
                  functionArgs[i] = functionArgs[i].substring(0, functionArgs[i].length() - 3);
               }
            }

            switch (functionName)
            {
               case "rotate":
                  // The first arg is the rotation angle in degrees. The optional second and third args are the anchor point for rotation.
                  // If no anchor point is specified, used the center point of the bounds box.
                  Rectangle2D contentRectangle = getBoundsBox();

                  double anchorPointX = (3 == functionArgs.length ? Double.parseDouble(functionArgs[1]) : contentRectangle.getX() + (contentRectangle.getWidth() / 2));
                  double anchorPointY = (3 == functionArgs.length ? Double.parseDouble(functionArgs[2]) : contentRectangle.getY() + (contentRectangle.getHeight() / 2));

                  AffineTransform rotation = new AffineTransform();

                  rotation.rotate(Math.toRadians(Double.parseDouble(functionArgs[0])), anchorPointX, anchorPointY);

                  g2.transform(rotation);
                  break;

               case "translate":
                  AffineTransform translation = new AffineTransform();

                  // The y value is optional. Use zero if it was not specified
                  double yTranslation = (functionArgs.length > 1 ? Double.parseDouble(functionArgs[1]) : 0);

                  translation.translate(Double.parseDouble(functionArgs[0]), yTranslation);
                  g2.transform(translation);
                  break;

               case "translateX":
                  translation = new AffineTransform();

                  translation.translate(Double.parseDouble(functionArgs[0]), 0);
                  g2.transform(translation);
                  break;

               case "translateY":
                  translation = new AffineTransform();

                  translation.translate(0, Double.parseDouble(functionArgs[0]));
                  g2.transform(translation);
                  break;

               default:
                  System.err.println(StringUtil.singleQuote(functionName) + " is not a currently supported transform function for conversion to Graphics2D!");
            }

            if (endArgIndex < transformValue.length() - 1)
            {
               transformValue = transformValue.substring(endArgIndex + 1);
            }
            else
            {
               break;
            }
         }
      }
   }


   //--------------------------------------------------------------------------
   protected void adjustBoundsForTransform(Rectangle2D inBoundsBox)
   {
      if (hasAttribute(SvgAttr.transform))
      {
         String value = getAttributeValue(SvgAttr.transform);
         Matcher m = SvgAttr.TRANSLATE_PATTERN.matcher(value);
         if (m.find())
         {
            float translateX = Float.parseFloat(m.group(1));
            float translateY = Float.parseFloat(m.group(2));

//            inBoundsBox.translate(translateX, translateY);
            inBoundsBox.setRect(inBoundsBox.getX() + translateX, inBoundsBox.getY() + translateY, inBoundsBox.getWidth(), inBoundsBox.getHeight());
         }
         else
         {
            m = SvgAttr.ROTATE_PATTERN.matcher(value);
            if (m.find())
            {
               Point2D centerOfRotation;
               float angle = Float.parseFloat(m.group(1));
               if (m.group(2) != null)
               {
                  // A center of rotation was specified
                  centerOfRotation = new Point2D.Float(Float.parseFloat(m.group(2)),
                                                       Float.parseFloat(m.group(3)));
               }
               else
               {
                  // Default the center of rotation to the center of the bounds box
                  centerOfRotation = new Point2D.Double(inBoundsBox.getX() + (inBoundsBox.getWidth() / 2),
                                                       inBoundsBox.getY() + (inBoundsBox.getHeight() / 2));

               }

               Rectangle2D adjustedBounds = GraphicsUtil.rotate(inBoundsBox, angle, centerOfRotation).getBounds2D();
               inBoundsBox.setRect(adjustedBounds.getX(), adjustedBounds.getY(), adjustedBounds.getWidth(), adjustedBounds.getHeight());
            }
         }

      }
   }

   //--------------------------------------------------------------------------
   protected void drawSubnodes(Graphics2D g2, CSS inCSS)
   {
      if (CollectionUtil.hasValues(getSubtags()))
      {
         for (XMLizable node : getSubtags())
         {
            if (node instanceof AbstractSvgNode)
            {
               ((AbstractSvgNode)node).draw(g2, inCSS);
            }
         }
      }
   }

   //--------------------------------------------------------------------------
   protected Composite getG2Composite()
   {
      String opacityString = null;
      if (StringUtil.isSet(getAttributeValue(SvgAttr.opacity)))
      {
         if (! getAttributeValue(SvgAttr.opacity).equals(SvgAttr.Value.none))
         {
            opacityString = getAttributeValue(SvgAttr.opacity);
         }
      }
      else if (StringUtil.isSet(getAttributeValue(SvgAttr.style)))
      {
         Matcher m = OPACITY_PATTERN.matcher(getAttributeValue(SvgAttr.style));
         if (m.find())
         {
            opacityString = m.group(1);
         }
      }

      Composite composite = null;
      if (StringUtil.isSet(opacityString))
      {
         composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, Float.parseFloat(opacityString.trim()));
      }

      return composite;
   }

   //--------------------------------------------------------------------------
   protected Paint getG2Paint(List inCSSDeclarations)
   {
      String colorString = null;
      if (StringUtil.isSet(getAttributeValue(SvgAttr.fill)))
      {
         if (! getAttributeValue(SvgAttr.fill).equals(SvgAttr.Value.none))
         {
            colorString = getAttributeValue(SvgAttr.fill);
         }
      }
      else if (CollectionUtil.hasValues(inCSSDeclarations))
      {
         for (CSSDeclaration cssDeclaration : inCSSDeclarations)
         {
            if (cssDeclaration.getProperty().name().equals(SvgAttr.fill))
            {
               colorString = cssDeclaration.getValue();
            }
         }
      }

      Color color = null;
      if (StringUtil.isSet(colorString))
      {
         if (colorString.startsWith("#"))
         {
            color = Color.decode(colorString);
         }
         else
         {
            color = Color.getColor(colorString);
         }
      }

      String opacityString = null;
      if (StringUtil.isSet(getAttributeValue(SvgAttr.fillOpacity)))
      {
         if (! getAttributeValue(SvgAttr.fillOpacity).equals(SvgAttr.Value.none))
         {
            opacityString = getAttributeValue(SvgAttr.fillOpacity);
         }
      }
      else if (CollectionUtil.hasValues(inCSSDeclarations))
      {
         for (CSSDeclaration cssDeclaration : inCSSDeclarations)
         {
            if (cssDeclaration.getProperty().name().equals(SvgAttr.fillOpacity))
            {
               opacityString = cssDeclaration.getValue();
            }
         }
      }

      if (color != null
          && StringUtil.isSet(opacityString))
      {
         color = new Color(color.getRed()/255f, color.getGreen()/255f, color.getBlue()/255f, Float.parseFloat(opacityString));
      }

      return color;
   }

   //--------------------------------------------------------------------------
   protected BasicStroke getG2Stroke(List inCSSDeclarations)
   {
      BasicStroke stroke = null;
      if ("none".equalsIgnoreCase(getAttributeValue(SvgAttr.stroke)))
      {
         // We were requested not to display the stroke so give it zero width
         stroke = new BasicStroke(0f);
      }
      else if (StringUtil.isSet(getAttributeValue(SvgAttr.strokeWidth)))
      {
         stroke = new BasicStroke(Float.parseFloat(getAttributeValue(SvgAttr.strokeWidth)));
      }
      else if (CollectionUtil.hasValues(inCSSDeclarations))
      {
         for (CSSDeclaration cssDeclaration : inCSSDeclarations)
         {
            if (cssDeclaration.getProperty().name().equals(SvgAttr.strokeWidth))
            {
               try
               {
                  stroke = new BasicStroke(Float.parseFloat(cssDeclaration.getValue()));
               }
               catch (NumberFormatException e)
               {
                  GfxSize size = GfxSize.allocate(cssDeclaration.getValue());
                  stroke = new BasicStroke(size.to(GfxUnits.pixels));
               }
            }
         }
      }

      return stroke;
   }

   //--------------------------------------------------------------------------
   protected Color getG2StrokeColor(List inCSSDeclarations)
   {
      String colorString = null;
      if (StringUtil.isSet(getAttributeValue(SvgAttr.stroke)))
      {
         colorString = getAttributeValue(SvgAttr.stroke);
      }
      else if (CollectionUtil.hasValues(inCSSDeclarations))
      {
         for (CSSDeclaration cssDeclaration : inCSSDeclarations)
         {
            if (cssDeclaration.getProperty().name().equals(SvgAttr.stroke))
            {
               colorString = cssDeclaration.getValue();
            }
         }
      }

      Color color = null;
      if (StringUtil.isSet(colorString))
      {
         if (colorString.startsWith("#"))
         {
            try
            {
               color = Color.decode(colorString);
            }
            catch (Exception e)
            {
               throw new RuntimeException("Problem decoding color string " + StringUtil.singleQuote(colorString) + "!");
            }
         }
         else
         {
            color = HTMLColor.valueOf(colorString);
         }
      }

      String opacityString = null;
      if (StringUtil.isSet(getAttributeValue(SvgAttr.strokeOpacity)))
      {
         if (! getAttributeValue(SvgAttr.strokeOpacity).equals(SvgAttr.Value.none))
         {
            opacityString = getAttributeValue(SvgAttr.strokeOpacity);
         }
      }
      else if (StringUtil.isSet(getAttributeValue(SvgAttr.style)))
      {
         Matcher m = STROKE_OPACITY_PATTERN.matcher(getAttributeValue(SvgAttr.style));
         if (m.find())
         {
            opacityString = m.group(1);
         }
      }

      if (StringUtil.isSet(opacityString))
      {
         if (null == color)
         {
            color = Color.BLACK;
         }

         float opacityValue;
         if (opacityString.endsWith("%"))
         {
            opacityValue = Float.parseFloat(opacityString.substring(0, opacityString.length() - 1)) / 100f;
         }
         else
         {
            opacityValue = Float.parseFloat(opacityString);
         }

         color = new Color(color.getRed()/255f, color.getGreen()/255f, color.getBlue()/255f, opacityValue);
      }

      return color;
   }

   //---------------------------------------------------------------------------
   // Returns the in-line and CSS-defined styling for the specified tag.
   protected List getCSSDeclarations(CSS inCSS)
   {
      List cssDeclarations = new ArrayList<>(20);
      if (inCSS != null)
      {
         List declarations = inCSS.getCSSDeclarationsForSvgNode(this, CSSMediaType.print);
         if (CollectionUtil.hasValues(declarations))
         {
            cssDeclarations.addAll(declarations);
         }
      }

      String styleString = getAttributeValue(SvgAttr.style);
      if (StringUtil.isSet(styleString))
      {
         List styleCSSDeclarations = CSSDeclaration.parse(styleString);
         if (CollectionUtil.hasValues(styleCSSDeclarations))
         {
            cssDeclarations.addAll(styleCSSDeclarations);
         }
      }

      return cssDeclarations;
   }

   //---------------------------------------------------------------------------
   // Walks up the parent chain until it finds the SVG object.
   protected SVG findParentSVG()
   {
      SVG parentSVG = null;

      SvgNode svgNode = this;
      while (svgNode != null)
      {
         if (svgNode.getTagName().equals(SVG.svg))
         {
            parentSVG = (SVG) svgNode;
            break;
         }
         svgNode = (SvgNode) svgNode.getParentNode();
      }

      return parentSVG;
   }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy