com.hfg.svg.SvgText Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of com_hfg Show documentation
Show all versions of com_hfg Show documentation
com.hfg xml, html, svg, and bioinformatics utility library
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;
}
}
}