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

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

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

import com.hfg.css.CSS;
import com.hfg.css.CSSDeclaration;
import com.hfg.css.CSSProperty;
import com.hfg.graphics.Graphics2DState;
import com.hfg.graphics.units.GfxUnits;
import com.hfg.graphics.units.Points;
import com.hfg.graphics.ColorUtil;
import com.hfg.graphics.TextUtil;
import com.hfg.svg.attribute.SvgTextAnchor;
import com.hfg.svg.attribute.SvgVerticalAlign;
import com.hfg.util.StringBuilderPlus;
import com.hfg.util.StringUtil;
import com.hfg.util.collection.CollectionUtil;
import com.hfg.xml.XMLContainer;
import com.hfg.xml.XMLTag;
import com.hfg.xml.XMLizable;

import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphMetrics;
import java.awt.font.GlyphVector;
import java.awt.geom.*;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

//------------------------------------------------------------------------------
/**
 * Object representation of an SVG (Scalable Vector Graphics) text 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 SvgText extends AbstractSvgNode implements SvgNode
{
   private Font mFont;
   
   //**************************************************************************
   // CONSTRUCTORS
   //**************************************************************************

   //---------------------------------------------------------------------------
   public SvgText()
   {
      super(SVG.text);
//      setFont(sDefaultFont);
   }

   //---------------------------------------------------------------------------
   public SvgText(String inText)
   {
      this();
      setContent(inText);
   }

   //---------------------------------------------------------------------------
   public SvgText(String inText, Point2D inLocation)
   {
      this(inText, null, inLocation);
   }

   //---------------------------------------------------------------------------
   public SvgText(String inText, Font inFont, Point2D inLocation)
   {
      this(inText);
      setFont(inFont);

      double x = inLocation.getX();
      double y = inLocation.getY();

      setAttribute(SvgAttr.x, (Math.floor(x) == x ? (int) x : String.format("%.3f", inLocation.getX())));
      setAttribute(SvgAttr.y, (Math.floor(y) == y ? (int) y : String.format("%.3f", inLocation.getY())));
   }

   //---------------------------------------------------------------------------
   public SvgText(XMLTag inXMLTag)
   {
      this();
      initFromXMLTag(inXMLTag);
   }

   //**************************************************************************
   // PUBLIC METHODS
   //**************************************************************************

   //---------------------------------------------------------------------------
   @Override
   public String toString()
   {
      StringBuilderPlus content = new StringBuilderPlus(super.toString() + ":").setDelimiter(" ");
      List subtags = getSubtags();
      if (CollectionUtil.hasValues(subtags))
      {
         for (XMLizable subtag : subtags)
         {
            if (subtag instanceof XMLContainer)
            {
               content.delimitedAppend(((XMLContainer) subtag).getContent());
            }
         }
      }
      else
      {
         content.delimitedAppend(getContent());
      }

      return content.toString();
   }

   //---------------------------------------------------------------------------
   @Override
   public SvgText setContent(CharSequence inValue)
   {
      return (SvgText) super.setContent(inValue);
   }

   //---------------------------------------------------------------------------
   public SvgText setX(int inValue)
   {
      setAttribute(SvgAttr.x, inValue);
      return this;
   }

   //---------------------------------------------------------------------------
   public SvgText setX(float inValue)
   {
      setAttribute(SvgAttr.x, inValue);
      return this;
   }

   //---------------------------------------------------------------------------
   public Float getX()
   {
      String xAttr = getAttributeValue(SvgAttr.x);
      return (StringUtil.isSet(xAttr) ? Float.parseFloat(xAttr) : null);
   }


   //---------------------------------------------------------------------------
   public SvgText setY(int inValue)
   {
      setAttribute(SvgAttr.y, inValue);
      return this;
   }

   //---------------------------------------------------------------------------
   public SvgText setY(float inValue)
   {
      setAttribute(SvgAttr.y, inValue);
      return this;
   }

   //---------------------------------------------------------------------------
   public Float getY()
   {
      String yAttr = getAttributeValue(SvgAttr.y);
      return (StringUtil.isSet(yAttr) ? Float.parseFloat(yAttr) : null);
   }


   //---------------------------------------------------------------------------
   /**
    * Sets a relative (delta) X coordinate adjustment to the text position.
    * Can be absolute (in, cm, mm, pt, pc) or relative (em, ex, px) lengths.
    * @param inValue a relative X coordinate adjustment to the text position
    * @return this text object to allow for method chaining
    */
   public SvgText setDx(String inValue)
   {
      setAttribute(SvgAttr.dx, inValue);
      return this;
   }

   //---------------------------------------------------------------------------
   public String getDx()
   {
      return getAttributeValue(SvgAttr.dx);
   }


   //---------------------------------------------------------------------------
   /**
    * Sets a relative (delta) Y coordinate adjustment to the text position.
    * Can be absolute (in, cm, mm, pt, pc) or relative (em, ex, px) lengths.
    * @param inValue a relative Y coordinate adjustment to the text position
    * @return this text object to allow for method chaining
    */
   public SvgText setDy(String inValue)
   {
      setAttribute(SvgAttr.dy, inValue);
      return this;
   }

   //---------------------------------------------------------------------------
   public String getDy()
   {
      return getAttributeValue(SvgAttr.dy);
   }

   
   //---------------------------------------------------------------------------
   public int getWidth()
   {
      return (int) Float.parseFloat(getAttributeValue(SvgAttr.width));
   }

   //---------------------------------------------------------------------------
   public int getHeight()
   {
      return (int) Float.parseFloat(getAttributeValue(SvgAttr.height));
   }

   //---------------------------------------------------------------------------
   public SvgText setFill(Color inColor)
   {
      setAttribute(SvgAttr.fill, "#" + ColorUtil.colorToHex(inColor));
      return this;
   }

   //---------------------------------------------------------------------------
   public SvgText setFont(Font inValue)
   {
      if (inValue != null)
      {
         // TODO: Remove existing style fields relating to the font.

         mFont = inValue;
         addStyle(CSSProperty.font_family.name() + ":" + mFont.getName());
         addStyle(CSSProperty.font_size.name() + ":" + new Points(mFont.getSize2D()).to(GfxUnits.pixels) + "px");
         if (mFont.isBold())
         {
            addStyle(CSS.BOLD);
         }

         if (mFont.isItalic())
         {
            addStyle(CSS.ITALIC);
         }
      }

      return this;
   }

   //---------------------------------------------------------------------------
   public Font getFont()
   {
      return mFont;
   }


   //---------------------------------------------------------------------------
   @Override
   public SvgText setFilter(String inValue)
   {
      return (SvgText) super.setFilter(inValue);
   }

   //---------------------------------------------------------------------------
   public SvgText setTitle(String inValue)
   {
      setAttribute(SvgAttr.title, inValue);
      return this;
   }

   //---------------------------------------------------------------------------
   @Override
   public SvgText setClass(String inValue)
   {
      return (SvgText) super.setClass(inValue);
   }

   //---------------------------------------------------------------------------
   @Override
   public SvgText addClass(String inValue)
   {
      return (SvgText) super.addClass(inValue);
   }

   //---------------------------------------------------------------------------
   @Override
   public SvgText setId(String inValue)
   {
      return (SvgText) super.setId(inValue);
   }

   //--------------------------------------------------------------------------
   @Override
   public SvgText addStyle(CharSequence inValue)
   {
      return (SvgText) super.addStyle(inValue);
   }

   //---------------------------------------------------------------------------
   @Override
   public SvgText setStyle(CharSequence inValue)
   {
      return (SvgText) super.setStyle(inValue);
   }

   //---------------------------------------------------------------------------
   @Override
   public SvgText setTransform(String inValue)
   {
      return (SvgText) super.setTransform(inValue);
   }


   //---------------------------------------------------------------------------
   /**
    * Specify how to align the text.
    * @param inValue the text-anchor value
    * @return this text object to enable method chaining
    */
   public SvgText setTextAnchor(SvgTextAnchor inValue)
   {
      if (inValue != null)
      {
         setAttribute(SvgAttr.textAnchor, inValue.name());
      }
      else
      {
         removeAttribute(SvgAttr.textAnchor);
      }

      return this;
   }

   //---------------------------------------------------------------------------
   /**
    * Shorthand setting to specify how an inline-level box is aligned within the line.
    * @param inValue the vertical-align value
    * @return this text object to enable method chaining
    */
   public SvgText setVerticalAlign(SvgVerticalAlign inValue)
   {
      if (inValue != null)
      {
         setAttribute(SvgAttr.verticalAlign, inValue.name());
      }
      else
      {
         removeAttribute(SvgAttr.verticalAlign);
      }

      return this;
   }

   //---------------------------------------------------------------------------
   @Override
   public SvgText setAttribute(String inName, Object inValue)
   {
      return (SvgText) super.setAttribute(inName, inValue);
   }

   //---------------------------------------------------------------------------
   public SvgTSpan addTSpan()
   {
      SvgTSpan tSpan = new SvgTSpan();
      addSubtag(tSpan);
      
      return tSpan;
   }

   //---------------------------------------------------------------------------
   public SvgTSpan addTSpan(CharSequence inTitle)
   {
      SvgTSpan tSpan = new SvgTSpan(inTitle);
      addSubtag(tSpan);
      
      return tSpan;
   }
   
   //---------------------------------------------------------------------------
   // Determining the bounds box for a text node requires special handling.
   @Override
   public Rectangle2D getBoundsBox()
   {
      Rectangle2D boundsBox;

      List tSpans = getSubtagsByClass(SvgTSpan.class);
      if (CollectionUtil.hasValues(tSpans))
      {
         boundsBox = super.getBoundsBox();
      }
      else
      {
         float x = (getX() != null ? getX() : 0);
         float y = (getAttributeValue(SvgAttr.y) != null ? Float.parseFloat(getAttributeValue(SvgAttr.y)) : 0);

         Rectangle textBounds = (mFont != null ? TextUtil.getStringRect(getContent(), mFont) : new Rectangle(0, 0, 0, 0));

         boundsBox = new Rectangle2D.Float(x, y - textBounds.height, textBounds.width, textBounds.height).getBounds();
         adjustBoundsForTransform(boundsBox);
      }

      return boundsBox;
   }

   //--------------------------------------------------------------------------
   @Override
   public void draw(Graphics2D g2, CSS inCSS)
   {
      // Save settings
      Graphics2DState origState = new Graphics2DState(g2);

      List cssDeclarations = getCSSDeclarations(inCSS);

      Paint paint   = getG2Paint(cssDeclarations);
      if (paint != null) g2.setPaint(paint);

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

      applyTransforms(g2);

      float x = (getX() != null ? getX() : 0);
      float y = (getY() != null ? getY() : 0);

      boolean centerText = doCenterText(cssDeclarations);

      if (StringUtil.isSet(getContent()))
      {
         if (centerText)
         {
            FontMetrics metrics = g2.getFontMetrics(g2.getFont());
            x = x - (metrics.stringWidth(getContent()) / 2f);
         }

         g2.drawString(getContent(), x, y);
      }
      else
      {
         // Tspans?
         List textSpans = getSubtagsByName(SVG.tspan);
         if (CollectionUtil.hasValues(textSpans))
         {
            float tSpanX = x;
            float tSpanY = y;
            for (SvgTSpan tspan : textSpans)
            {
               tSpanX = (tspan.getX() != null ? tspan.getX() : x);
               if (tspan.getDy() != null)
               {
                  tSpanY += tspan.getDy();
               }
               else
               {
                  tSpanY = (tspan.getY() != null ? tspan.getY() : y);
               }

               if (centerText)
               {
                  FontMetrics metrics = g2.getFontMetrics(g2.getFont());
                  tSpanX = tSpanX - (metrics.stringWidth(tspan.getContent()) / 2f);
               }

               g2.drawString(tspan.getContent(), tSpanX, tSpanY);
            }
         }
         else
         {
            // textPath?
            List textPaths = getSubtagsByName(SVG.textPath);
            if (CollectionUtil.hasValues(textPaths))
            {
               for (SvgTextPath textPath : textPaths)
               {
                  drawTextPath(g2, textPath, centerText);
               }
            }
         }
      }

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


   //--------------------------------------------------------------------------
   private void drawTextPath(Graphics2D g2, SvgTextPath inTextPath, boolean inCenterText)
   {
      // Find the parent svg
      SVG parentSVG = findParentSVG();

      String href = inTextPath.getAttributeValue(SvgAttr.href);
      if (StringUtil.isSet(href))
      {
         // Find the path by the specified id
         SvgPath path = (SvgPath) parentSVG.getElementById(href.substring(1));
         if (path != null)
         {
            // Break the path down to straight line steps (using interpolation if necessary)
            GeneralPath generalPath = path.generateGeneralPath();
            PathIterator pi = generalPath.getPathIterator(new AffineTransform());
            int segmentType;
            Point2D currentPoint = new Point2D.Double();
            Point2D lastMovePoint = new Point2D.Double();
            float[] coords = new float[6];
            PathTracer pathTracer = new PathTracer();
            while (! pi.isDone())
            {
               segmentType = pi.currentSegment(coords);
               switch (segmentType)
               {
                  case PathIterator.SEG_MOVETO:
                     currentPoint = new Point2D.Double(coords[0], coords[1]);
                     lastMovePoint = new Point2D.Double(coords[0], coords[1]);
                     pathTracer.addSegment(new LinearPathSegment(segmentType, currentPoint, 0));
                     pi.next();
                     break;
                  case PathIterator.SEG_LINETO:
                     double length = currentPoint.distance(coords[0], coords[1]);
                     currentPoint = new Point2D.Double(coords[0], coords[1]);
                     pathTracer.addSegment(new LinearPathSegment(segmentType, currentPoint, length));
                     pi.next();
                     break;
                  case PathIterator.SEG_CLOSE:
                     length = currentPoint.distance(lastMovePoint.getX(), lastMovePoint.getY());
                     currentPoint = new Point2D.Double(lastMovePoint.getX(), lastMovePoint.getY());
                     pathTracer.addSegment(new LinearPathSegment(PathIterator.SEG_LINETO, lastMovePoint, length));

                     pi.next();
                     break;
                  default:
                     // Interpolate curves
                     SingleSegmentPathIterator sspi = new SingleSegmentPathIterator(pi, currentPoint);
                     FlatteningPathIterator fpi = new FlatteningPathIterator(sspi, 0.01f);
                     while (! fpi.isDone())
                     {
                        segmentType = fpi.currentSegment(coords);
                        // All the segments should be lines
                        if (segmentType == PathIterator.SEG_LINETO)
                        {
                           length = currentPoint.distance(coords[0], coords[1]);
                           currentPoint = new Point2D.Double(coords[0], coords[1]);
                           pathTracer.addSegment(new LinearPathSegment(segmentType, currentPoint, length));
                        }

                        fpi.next();
                     }
               }
            }

            String text = inTextPath.getContent();

            FontRenderContext frc = g2.getFontRenderContext();
            GlyphVector gv = g2.getFont().createGlyphVector(frc, text);
            int numGlyphs = gv.getNumGlyphs();
            double totalGlyphLength = 0;

            double startOffset = 0;
            String startOffsetString = inTextPath.getAttributeValue(SvgAttr.startOffset);
            if (StringUtil.isSet(startOffsetString))
            {
               if (startOffsetString.endsWith("%"))
               {
                  float pct = Float.parseFloat(startOffsetString.substring(0, startOffsetString.length() - 1));
                  startOffset = pathTracer.getTotalLength() * pct/100f;
               }
               else
               {
                  startOffset = Double.parseDouble(startOffsetString);
               }
            }

            if (inCenterText)
            {
               FontMetrics metrics = g2.getFontMetrics(g2.getFont());
               startOffset -= (metrics.stringWidth(text) / 2f);
            }

//          pathTracer.show(g2);// For debugging

            for (int i = 0; i < numGlyphs; i++)
            {
               Point2D glyphPosition = pathTracer.pointAtLength(startOffset + totalGlyphLength);
               if (glyphPosition != null)
               {
                  //      AffineTransform affineTransform = AffineTransform.getTranslateInstance(glyphPosition.getX(), glyphPosition.getY());
                  //      affineTransform.rotate(pathTracer.slopeAtLength(startOffset + totalGlyphLength));

                  //      Shape shape1 = gv.getGlyphOutline(i);
                  //      Shape shape2 = affineTransform.createTransformedShape(shape1);
                  //      g2.fill(shape2);

                  char theChar = text.charAt(i);
                  if (theChar != ' ')
                  {
                     AffineTransform transform = new AffineTransform();
                     transform.rotate(pathTracer.slopeAtLength(startOffset + totalGlyphLength));
                     Font rotatedFont = g2.getFont().deriveFont(transform);
                     Font origFont = g2.getFont();
                     g2.setFont(rotatedFont);
                     g2.drawString(theChar + "", (float) glyphPosition.getX(), (float) glyphPosition.getY());
                     g2.setFont(origFont);
                  }
                  else
                  {
                     // A space doesn't have any glyph metric width
                     totalGlyphLength += 3;
                  }

                  GlyphMetrics gm = gv.getGlyphMetrics(i);
                  totalGlyphLength += gm.getBounds2D().getWidth() + 0.7;
               }
            }
         }
      }
   }

   //--------------------------------------------------------------------------
   // Returns whether or not the text should be centered.
   private boolean doCenterText(List inCSSDeclarations)
   {
      boolean center = false;
      if (SvgTextAnchor.middle.name().equalsIgnoreCase(getAttributeValue(SvgAttr.textAnchor)))
      {
         center = true;
      }
      else if (CollectionUtil.hasValues(inCSSDeclarations))
      {
         for (CSSDeclaration cssDeclaration : inCSSDeclarations)
         {
            if (cssDeclaration.getProperty().name().equals(SvgAttr.textAnchor)
                && cssDeclaration.getValue().equalsIgnoreCase("middle"))
            {
               center = true;
               break;
            }
         }
      }

      return center;
   }

   //**************************************************************************
   // INNER CLASS
   //**************************************************************************

   private class SingleSegmentPathIterator implements PathIterator
   {
      private PathIterator mInnerPathIterator;
      private Point2D mPoint;
      private boolean mDone;
      private boolean mMoveDone;

      //-----------------------------------------------------------------------
      public SingleSegmentPathIterator(PathIterator inPathIterator, Point2D inPoint)
      {
         mInnerPathIterator = inPathIterator;
         mPoint = inPoint;
      }

      //-----------------------------------------------------------------------
      @Override
      public int getWindingRule()
      {
         return mInnerPathIterator.getWindingRule();
      }

      //-----------------------------------------------------------------------
      @Override
      public boolean isDone()
      {
         return mDone || mInnerPathIterator.isDone();
      }

      //-----------------------------------------------------------------------
      @Override
      public void next()
      {
         if (! mDone)
         {
            if (! mMoveDone)
            {
               mMoveDone = true;
            }
            else
            {
               mInnerPathIterator.next();
               mDone = true;
            }
         }
      }

      //-----------------------------------------------------------------------
      @Override
      public int currentSegment(float[] inCoords)
      {
         int type = mInnerPathIterator.currentSegment(inCoords);
         if (! mMoveDone)
         {
            // We start with a move
            inCoords[0] = (float) mPoint.getX();
            inCoords[1] = (float) mPoint.getY();
            return PathIterator.SEG_MOVETO;
         }

         return type;
      }

      //-----------------------------------------------------------------------
      @Override
      public int currentSegment(double[] inCoords)
      {
         int type = mInnerPathIterator.currentSegment(inCoords);
         if (! mMoveDone)
         {
            // We start with a move
            inCoords[0] = mPoint.getX();
            inCoords[1] = mPoint.getY();
            return PathIterator.SEG_MOVETO;
         }

         return type;
      }
   }

   //**************************************************************************
   // INNER CLASS
   //**************************************************************************

   private class PathTracer
   {
      private List mSegments;
      private double                  mTotalLength;

      //-----------------------------------------------------------------------
      public PathTracer addSegment(LinearPathSegment inValue)
      {
         if (null == mSegments)
         {
            mSegments = new ArrayList<>(5);
         }

         mSegments.add(inValue);
         mTotalLength += inValue.getLength();
         return this;
      }

      //-----------------------------------------------------------------------
      // Solely for debugging purposes.
      public void show(Graphics2D inGraphics2D)
      {
         Paint paint = inGraphics2D.getPaint();
         inGraphics2D.setPaint(Color.RED);

         Point2D prevPoint = null;
         for (LinearPathSegment segment : mSegments)
         {
            if (prevPoint != null)
            {
               inGraphics2D.drawLine((int)prevPoint.getX(), (int)prevPoint.getY(), (int)segment.getX(), (int)segment.getY());
            }
            prevPoint = segment.getEndPoint2D();
         }

         inGraphics2D.setPaint(paint);

      }

      //-----------------------------------------------------------------------
      public double getTotalLength()
      {
         return mTotalLength;
      }

      //-----------------------------------------------------------------------
      public Point2D pointAtLength(double inLength)
      {
         // Find the segment
         Iterator segIter = mSegments.iterator();

         Point2D point = null;

         if (inLength <= mTotalLength)
         {
            LinearPathSegment segment = segIter.next();
            LinearPathSegment prevSegment = segment;
            int length  = 0;
            while (length + segment.getLength() < inLength)
            {
               length += segment.getLength();
               prevSegment = segment;
               if (! segIter.hasNext())
               {
                  segment = null;
                  break;
               }
               segment = segIter.next();
            }

            if (segment != null)
            {
               double offset = inLength - length;

               double theta = Math.atan2(segment.getY() - prevSegment.getY(),
                                         segment.getX() - prevSegment.getX());

               point = new Point2D.Double(prevSegment.getX() + offset * Math.cos(theta),
                                          prevSegment.getY() + offset * Math.sin(theta));
            }
         }

         return point;
      }

      //-----------------------------------------------------------------------
      public double slopeAtLength(double inLength)
      {
         // Find the segment
         Iterator segIter = mSegments.iterator();

         double slope = 0;

         if (inLength <= mTotalLength)
         {
            LinearPathSegment segment = segIter.next();
            LinearPathSegment prevSegment = segment;
            int               length  = 0;
            while (length + segment.getLength() < inLength)
            {
               length += segment.getLength();
               prevSegment = segment;
               segment = segIter.next();
            }

            slope = Math.atan2(segment.getY() - prevSegment.getY(),
                               segment.getX() - prevSegment.getX());
         }

         return slope;
      }
   }

   //**************************************************************************
   // INNER CLASS
   //**************************************************************************

   private class LinearPathSegment
   {
      private int mSegType;
      private double mLength;
      private Point2D mCoord;

      //-----------------------------------------------------------------------
      public LinearPathSegment(int inSegType, Point2D inCoord, double inLength)
      {
         mSegType = inSegType;
         mCoord = inCoord;
         mLength = inLength;

      }

      //-----------------------------------------------------------------------
      public double getX()
      {
         return mCoord.getX();
      }

      //-----------------------------------------------------------------------
      public double getY()
      {
         return mCoord.getY();
      }

      //-----------------------------------------------------------------------
      public Point2D getEndPoint2D()
      {
         return new Point2D.Double(getX(), getY());
      }

      //-----------------------------------------------------------------------
      public double getLength()
      {
         return mLength;
      }
   }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy