/* =========================================================== * Orson Charts : a 3D chart library for the Java(tm) platform * =========================================================== * * (C)opyright 2013-2016, by Object Refinery Limited. All rights reserved. * * http://www.object-refinery.com/orsoncharts/index.html * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. * Other names may be trademarks of their respective owners.] * * If you do not wish to be bound by the terms of the GPL, an alternative * commercial license can be purchased. For details, please see visit the * Orson Charts home page: * * http://www.object-refinery.com/orsoncharts/index.html * */ package com.orsoncharts.util; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.Shape; import java.awt.font.FontRenderContext; import java.awt.font.LineMetrics; import java.awt.font.TextLayout; import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.text.AttributedString; /** * Utility methods for working with text. */ public class TextUtils { private TextUtils() { // no need to instantiate this } /** * Draws a string such that the specified anchor point is aligned to the * given {@code (x, y)} location, and returns a bounding rectangle * for the text. * * @param text the text. * @param g2 the graphics device ({@code null} not permitted). * @param x the x coordinate (Java 2D). * @param y the y coordinate (Java 2D). * @param anchor the anchor location ({@code null} not permitted). * * @return The text bounds (adjusted for the text position). */ public static Rectangle2D drawAlignedString(String text, Graphics2D g2, float x, float y, TextAnchor anchor) { Rectangle2D textBounds = new Rectangle2D.Double(); float[] adjust = deriveTextBoundsAnchorOffsets(g2, text, anchor, textBounds); // adjust text bounds to match string position textBounds.setRect(x + adjust[0], y + adjust[1] + adjust[2], textBounds.getWidth(), textBounds.getHeight()); g2.drawString(text, x + adjust[0], y + adjust[1]); return textBounds; } /** * Returns the bounds of an aligned string. * * @param text the string ({@code null} not permitted). * @param g2 the graphics target ({@code null} not permitted). * @param x the x-coordinate. * @param y the y-coordinate. * @param anchor the anchor point on the text that will be aligned to * {@code (x, y)} ({@code null} not permitted). * * @return The text bounds (never {@code null}). * * @since 1.3 */ public static Rectangle2D calcAlignedStringBounds(String text, Graphics2D g2, float x, float y, TextAnchor anchor) { Rectangle2D textBounds = new Rectangle2D.Double(); float[] adjust = deriveTextBoundsAnchorOffsets(g2, text, anchor, textBounds); // adjust text bounds to match string position textBounds.setRect(x + adjust[0], y + adjust[1] + adjust[2], textBounds.getWidth(), textBounds.getHeight()); return textBounds; } /** * A utility method that calculates the anchor offsets for a string. * Normally, the {@code (x, y)} coordinate for drawing text is a point on * the baseline at the left of the text string. If you add these offsets * to {@code (x, y)} and draw the string, then the anchor point should * coincide with the {@code (x, y)} point. * * @param g2 the graphics device (not {@code null}). * @param text the text. * @param anchor the anchor point ({@code null} not permitted). * * @return The offsets. */ private static float[] deriveTextBoundsAnchorOffsets(Graphics2D g2, String text, TextAnchor anchor) { float[] result = new float[2]; FontRenderContext frc = g2.getFontRenderContext(); Font f = g2.getFont(); FontMetrics fm = g2.getFontMetrics(f); Rectangle2D bounds = getTextBounds(text, fm); LineMetrics metrics = f.getLineMetrics(text, frc); float ascent = metrics.getAscent(); float halfAscent = ascent / 2.0f; float descent = metrics.getDescent(); float leading = metrics.getLeading(); float xAdj = 0.0f; float yAdj = 0.0f; if (anchor.isHorizontalCenter()) { xAdj = (float) -bounds.getWidth() / 2.0f; } else if (anchor.isRight()) { xAdj = (float) -bounds.getWidth(); } if (anchor.isTop()) { yAdj = -descent - leading + (float) bounds.getHeight(); } else if (anchor.isHalfAscent()) { yAdj = halfAscent; } else if (anchor.isHalfHeight()) { yAdj = -descent - leading + (float) (bounds.getHeight() / 2.0); } else if (anchor.isBaseline()) { yAdj = 0.0f; } else if (anchor.isBottom()) { yAdj = -metrics.getDescent() - metrics.getLeading(); } result[0] = xAdj; result[1] = yAdj; return result; } /** * A utility method that calculates the anchor offsets for a string. * Normally, the {@code (x, y)} coordinate for drawing text is a point on * the baseline at the left of the text string. If you add these offsets * to {@code (x, y)} and draw the string, then the anchor point should * coincide with the {@code (x, y)} point. * * @param g2 the graphics device (not {@code null}). * @param text the text. * @param anchor the anchor point ({@code null} not permitted). * @param textBounds the text bounds (if not {@code null}, this * object will be updated by this method to match the * string bounds). * * @return The offsets. */ private static float[] deriveTextBoundsAnchorOffsets(Graphics2D g2, String text, TextAnchor anchor, Rectangle2D textBounds) { float[] result = new float[3]; FontRenderContext frc = g2.getFontRenderContext(); Font f = g2.getFont(); FontMetrics fm = g2.getFontMetrics(f); Rectangle2D bounds = getTextBounds(text, fm); LineMetrics metrics = f.getLineMetrics(text, frc); float ascent = metrics.getAscent(); result[2] = -ascent; float halfAscent = ascent / 2.0f; float descent = metrics.getDescent(); float leading = metrics.getLeading(); float xAdj = 0.0f; float yAdj = 0.0f; if (anchor.isHorizontalCenter()) { xAdj = (float) -bounds.getWidth() / 2.0f; } else if (anchor.isRight()) { xAdj = (float) -bounds.getWidth(); } if (anchor.isTop()) { yAdj = -descent - leading + (float) bounds.getHeight(); } else if (anchor.isHalfAscent()) { yAdj = halfAscent; } else if (anchor.isHorizontalCenter()) { yAdj = -descent - leading + (float) (bounds.getHeight() / 2.0); } else if (anchor.isBaseline()) { yAdj = 0.0f; } else if (anchor.isBottom()) { yAdj = -metrics.getDescent() - metrics.getLeading(); } if (textBounds != null) { textBounds.setRect(bounds); } result[0] = xAdj; result[1] = yAdj; return result; } /** * Returns the bounds for the specified text. The supplied text is * assumed to be on a single line (no carriage return or newline * characters). * * @param text the text ({@code null} not permitted). * @param fm the font metrics ({@code null} not permitted). * * @return The text bounds. */ public static Rectangle2D getTextBounds(String text, FontMetrics fm) { return getTextBounds(text, 0.0, 0.0, fm); } /** * Returns the bounds for the specified text when it is drawn with the * left-baseline aligned to the point {@code (x, y)}. * * @param text the text ({@code null} not permitted). * @param x the x-coordinate. * @param y the y-coordinate. * @param fm the font metrics ({@code null} not permitted). * * @return The bounding rectangle (never {@code null}). */ public static Rectangle2D getTextBounds(String text, double x, double y, FontMetrics fm) { ArgChecks.nullNotPermitted(text, "text"); ArgChecks.nullNotPermitted(fm, "fm"); double width = fm.stringWidth(text); double height = fm.getHeight(); return new Rectangle2D.Double(x, y - fm.getAscent(), width, height); } /** * Draws a string that is aligned by one anchor point and rotated about * another anchor point. * * @param text the text ({@code null} not permitted). * @param g2 the graphics target ({@code null} not permitted). * @param x the x-coordinate for positioning the text. * @param y the y-coordinate for positioning the text. * @param textAnchor the text anchor ({@code null} not permitted). * @param angle the rotation angle. * @param rotationX the x-coordinate for the rotation anchor point. * @param rotationY the y-coordinate for the rotation anchor point. * * @return The text bounds (never {@code null}). */ public static Shape drawRotatedString(String text, Graphics2D g2, float x, float y, TextAnchor textAnchor, double angle, float rotationX, float rotationY) { ArgChecks.nullNotPermitted(text, "text"); float[] textAdj = deriveTextBoundsAnchorOffsets(g2, text, textAnchor); return drawRotatedString(text, g2, x + textAdj[0], y + textAdj[1], angle, rotationX, rotationY); } /** * Draws a string that is aligned by one anchor point and rotated about * another anchor point, and returns a bounding shape for the text. * * @param text the text ({@code null} not permitted). * @param g2 the graphics device ({@code null} not permitted). * @param x the x-coordinate for positioning the text. * @param y the y-coordinate for positioning the text. * @param textAnchor the text anchor ({@code null} not permitted). * @param angle the rotation angle (in radians). * @param rotationAnchor the rotation anchor ({@code null} not permitted). * * @return A bounding shape for the text. */ public static Shape drawRotatedString(String text, Graphics2D g2, float x, float y, TextAnchor textAnchor, double angle, TextAnchor rotationAnchor) { ArgChecks.nullNotPermitted(text, "text"); float[] textAdj = deriveTextBoundsAnchorOffsets(g2, text, textAnchor); float[] rotateAdj = deriveRotationAnchorOffsets(g2, text, rotationAnchor); return drawRotatedString(text, g2, x + textAdj[0], y + textAdj[1], angle, x + textAdj[0] + rotateAdj[0], y + textAdj[1] + rotateAdj[1]); } /** * A utility method that calculates the rotation anchor offsets for a * string. These offsets are relative to the text starting coordinate * ({@code BASELINE_LEFT}). * * @param g2 the graphics device ({@code null} not permitted). * @param text the text ({@code null} not permitted). * @param anchor the anchor point ({@code null} not permitted). * * @return The offsets. */ private static float[] deriveRotationAnchorOffsets(Graphics2D g2, String text, TextAnchor anchor) { float[] result = new float[2]; FontRenderContext frc = g2.getFontRenderContext(); LineMetrics metrics = g2.getFont().getLineMetrics(text, frc); FontMetrics fm = g2.getFontMetrics(); Rectangle2D bounds = TextUtils.getTextBounds(text, fm); float ascent = metrics.getAscent(); float halfAscent = ascent / 2.0f; float descent = metrics.getDescent(); float leading = metrics.getLeading(); float xAdj = 0.0f; float yAdj = 0.0f; if (anchor.isLeft()) { xAdj = 0.0f; } else if (anchor.isHorizontalCenter()) { xAdj = (float) bounds.getWidth() / 2.0f; } else if (anchor.isRight()) { xAdj = (float) bounds.getWidth(); } if (anchor.isTop()) { yAdj = descent + leading - (float) bounds.getHeight(); } else if (anchor.isHalfHeight()) { yAdj = descent + leading - (float) (bounds.getHeight() / 2.0); } else if (anchor.isHalfAscent()) { yAdj = -halfAscent; } else if (anchor.isBaseline()) { yAdj = 0.0f; } else if (anchor.isBottom()) { yAdj = metrics.getDescent() + metrics.getLeading(); } result[0] = xAdj; result[1] = yAdj; return result; } /** * A utility method for drawing rotated text. * <P> * A common rotation is {@code -Math.PI/2} which draws text 'vertically' * (with the top of the characters on the left). * * @param text the text ({@code null} not permitted) * @param g2 the graphics target ({@code null} not permitted). * @param angle the angle of the (clockwise) rotation (in radians). * @param x the x-coordinate. * @param y the y-coordinate. * * @return The text bounds. */ public static Shape drawRotatedString(String text, Graphics2D g2, double angle, float x, float y) { return drawRotatedString(text, g2, x, y, angle, x, y); } /** * A utility method for drawing rotated text. * <P> * A common rotation is {@code -Math.PI/2} which draws text 'vertically' * (with the top of the characters on the left). * * @param text the text ({@code null} not permitted). * @param g2 the graphics device ({@code null} not permitted). * @param textX the x-coordinate for the text (before rotation). * @param textY the y-coordinate for the text (before rotation). * @param angle the angle of the (clockwise) rotation (in radians). * @param rotateX the point about which the text is rotated. * @param rotateY the point about which the text is rotated. * * @return The bounds for the rotated text (never {@code null}). */ public static Shape drawRotatedString(String text, Graphics2D g2, float textX, float textY, double angle, float rotateX, float rotateY) { ArgChecks.nullNotPermitted(text, "text"); AffineTransform saved = g2.getTransform(); Rectangle2D rect = TextUtils.getTextBounds(text, textX, textY, g2.getFontMetrics()); AffineTransform rotate = AffineTransform.getRotateInstance( angle, rotateX, rotateY); Shape bounds = rotate.createTransformedShape(rect); g2.transform(rotate); g2.drawString(text, textX, textY); g2.setTransform(saved); return bounds; } /** * Draws the attributed string at {@code (x, y)}, rotated by the * specified angle about {@code (x, y)}. * * @param text the attributed string ({@code null} not permitted). * @param g2 the graphics output target ({@code null} not permitted). * @param angle the angle. * @param x the x-coordinate. * @param y the y-coordinate. * * @return The text bounds (never {@code null}). * * @since 1.2 */ public static Shape drawRotatedString(AttributedString text, Graphics2D g2, double angle, float x, float y) { return drawRotatedString(text, g2, x, y, angle, x, y); } /** * Draws the attributed string at {@code (textX, textY)}, rotated by * the specified angle about {@code (rotateX, rotateY)}. * * @param text the attributed string ({@code null} not permitted). * @param g2 the graphics output target ({@code null} not permitted). * @param textX the x-coordinate for the text alignment point. * @param textY the y-coordinate for the text alignment point. * @param angle the rotation angle (in radians). * @param rotateX the x-coordinate for the rotation point. * @param rotateY the y-coordinate for the rotation point. * * @return The text bounds (never {@code null}). * * @since 1.2 */ public static Shape drawRotatedString(AttributedString text, Graphics2D g2, float textX, float textY, double angle, float rotateX, float rotateY) { ArgChecks.nullNotPermitted(text, "text"); AffineTransform saved = g2.getTransform(); AffineTransform rotate = AffineTransform.getRotateInstance(angle, rotateX, rotateY); g2.transform(rotate); TextLayout tl = new TextLayout(text.getIterator(), g2.getFontRenderContext()); Rectangle2D rect = tl.getBounds(); tl.draw(g2, textX, textY); g2.setTransform(saved); return rotate.createTransformedShape(rect); } /** * Draws the attributed string aligned to the point {@code (x, y)}, * rotated by the specified angle about {@code rotationAnchor}. * * @param text the attributed string ({@code null} not permitted). * @param g2 the graphics target ({@code null} not permitted). * @param x the x-coordinate. * @param y the y-coordinate. * @param textAnchor the text anchor ({@code null} not permitted). * @param angle the rotation angle (in radians). * @param rotationAnchor the rotation anchor ({@code null} not * permitted). * @param nonRotatedBounds if not {@code null} this rectangle will * be updated with the non-rotated bounds of the text for the caller * to use. * * @return The text bounds (never {@code null}). * * @since 1.2 */ public static Shape drawRotatedString(AttributedString text, Graphics2D g2, float x, float y, TextAnchor textAnchor, double angle, TextAnchor rotationAnchor, Rectangle2D nonRotatedBounds) { ArgChecks.nullNotPermitted(text, "text"); float[] textAdj = deriveTextBoundsAnchorOffsets(g2, text, textAnchor, nonRotatedBounds); float[] rotateAdj = deriveRotationAnchorOffsets(g2, text, rotationAnchor); return drawRotatedString(text, g2, x + textAdj[0], y + textAdj[1], angle, x + textAdj[0] + rotateAdj[0], y + textAdj[1] + rotateAdj[1]); } /** * Calculates the x and y offsets required to align the text with the * specified {@code anchor}. * * @param g2 the graphics target ({@code null} not permitted). * @param text the text ({@code null} not permitted). * @param anchor the anchor ({@code null} not permitted). * @param textBounds if not {@code null}, this rectangle will be * updated with the bounds of the text (for the caller to use). * * @return An array of two floats dx and dy. */ private static float[] deriveTextBoundsAnchorOffsets(Graphics2D g2, AttributedString text, TextAnchor anchor, Rectangle2D textBounds) { TextLayout layout = new TextLayout(text.getIterator(), g2.getFontRenderContext()); Rectangle2D bounds = layout.getBounds(); float[] result = new float[3]; float ascent = layout.getAscent(); result[2] = -ascent; float halfAscent = ascent / 2.0f; float descent = layout.getDescent(); float leading = layout.getLeading(); float xAdj = 0.0f; float yAdj = 0.0f; if (anchor.isHorizontalCenter()) { xAdj = (float) -bounds.getWidth() / 2.0f; } else if (anchor.isRight()) { xAdj = (float) -bounds.getWidth(); } if (anchor.isTop()) { yAdj = -descent - leading + (float) bounds.getHeight(); } else if (anchor.isHalfAscent()) { yAdj = halfAscent; } else if (anchor.isHalfHeight()) { yAdj = -descent - leading + (float) (bounds.getHeight() / 2.0); } else if (anchor.isBaseline()) { yAdj = 0.0f; } else if (anchor.isBottom()) { yAdj = -descent - leading; } if (textBounds != null) { textBounds.setRect(bounds); } result[0] = xAdj; result[1] = yAdj; return result; } /** * A utility method that calculates the rotation anchor offsets for a * string. These offsets are relative to the text starting coordinate * ({@code BASELINE_LEFT}). * * @param g2 the graphics device ({@code null} not permitted). * @param text the text ({@code null} not permitted). * @param anchor the anchor point ({@code null} not permitted). * * @return The offsets. */ private static float[] deriveRotationAnchorOffsets(Graphics2D g2, AttributedString text, TextAnchor anchor) { float[] result = new float[2]; TextLayout layout = new TextLayout(text.getIterator(), g2.getFontRenderContext()); Rectangle2D bounds = layout.getBounds(); float ascent = layout.getAscent(); float halfAscent = ascent / 2.0f; float descent = layout.getDescent(); float leading = layout.getLeading(); float xAdj = 0.0f; float yAdj = 0.0f; if (anchor.isLeft()) { xAdj = 0.0f; } else if (anchor.isHorizontalCenter()) { xAdj = (float) bounds.getWidth() / 2.0f; } else if (anchor.isRight()) { xAdj = (float) bounds.getWidth(); } if (anchor.isTop()) { yAdj = descent + leading - (float) bounds.getHeight(); } else if (anchor.isHalfHeight()) { yAdj = descent + leading - (float) (bounds.getHeight() / 2.0); } else if (anchor.isHalfAscent()) { yAdj = -halfAscent; } else if (anchor.isBaseline()) { yAdj = 0.0f; } else if (anchor.isBottom()) { yAdj = descent + leading; } result[0] = xAdj; result[1] = yAdj; return result; } }