org.jfree.text.TextUtilities Maven / Gradle / Ivy
Show all versions of jcommon Show documentation
/* ======================================================================== * JCommon : a free general purpose class library for the Java(tm) platform * ======================================================================== * * (C) Copyright 2000-2013, by Object Refinery Limited and Contributors. * * Project Info: http://www.jfree.org/jcommon/index.html * * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, * USA. * * [Java is a trademark or registered trademark of Sun Microsystems, Inc. * in the United States and other countries.] * * ------------------ * TextUtilities.java * ------------------ * (C) Copyright 2004-2013, by Object Refinery Limited and Contributors. * * Original Author: David Gilbert (for Object Refinery Limited); * Contributor(s): Brian Fischer; * * $Id: TextUtilities.java,v 1.27 2011/12/14 20:25:40 mungady Exp $ * * Changes * ------- * 07-Jan-2004 : Version 1 (DG); * 24-Mar-2004 : Added 'paint' argument to createTextBlock() method (DG); * 07-Apr-2004 : Added getTextBounds() method and useFontMetricsGetStringBounds * flag (DG); * 08-Apr-2004 : Changed word break iterator to line break iterator in the * createTextBlock() method - see bug report 926074 (DG); * 03-Sep-2004 : Updated createTextBlock() method to add ellipses when limit * is reached (DG); * 30-Sep-2004 : Modified bounds returned by drawAlignedString() method (DG); * 10-Nov-2004 : Added new createTextBlock() method that works with * newlines (DG); * 19-Apr-2005 : Changed default value of useFontMetricsGetStringBounds (DG); * 17-May-2005 : createTextBlock() now recognises '\n' (DG); * 27-Jun-2005 : Added code to getTextBounds() method to work around Sun's bug * parade item 6183356 (DG); * 06-Jan-2006 : Reformatted (DG); * 27-Apr-2009 : Fix text wrapping with new lines (DG); * 27-Jul-2009 : Use AttributedString in drawRotatedString() (DG); * 14-Dec-2011 : Fix for nextLineBreak() method - thanks to Brian Fischer (DG); * 24-Oct-2013 : Update drawRotatedString() to use drawAlignedString() when * the rotation angle is 0.0 (DG); * 25-Oct-2013 : Added drawStringsWithFontAttributes flag (DG); * */ package org.jfree.text; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.Paint; 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; import java.text.BreakIterator; import org.jfree.base.BaseBoot; import org.jfree.ui.TextAnchor; import org.jfree.util.Log; import org.jfree.util.LogContext; import org.jfree.util.ObjectUtilities; /** * Some utility methods for working with text in Java2D. */ public class TextUtilities { /** Access to logging facilities. */ protected static final LogContext logger = Log.createContext( TextUtilities.class); /** * When this flag is set to
null or has zero length). */ public static Shape calculateRotatedStringBounds(String text, Graphics2D g2, float textX, float textY, double angle, float rotateX, float rotateY) { if ((text == null) || (text.equals(""))) { return null; } FontMetrics fm = g2.getFontMetrics(); Rectangle2D bounds = TextUtilities.getTextBounds(text, g2, fm); AffineTransform translate = AffineTransform.getTranslateInstance( textX, textY); Shape translatedBounds = translate.createTransformedShape(bounds); AffineTransform rotate = AffineTransform.getRotateInstance( angle, rotateX, rotateY); Shape result = rotate.createTransformedShape(translatedBounds); return result; } /** * Returns the flag that controls whether the FontMetrics.getStringBounds() * method is used or not. If you are having trouble with label alignment * or positioning, try changing the value of this flag. * * @return A boolean. */ public static boolean getUseFontMetricsGetStringBounds() { return useFontMetricsGetStringBounds; } /** * Sets the flag that controls whether the FontMetrics.getStringBounds() * method is used or not. If you are having trouble with label alignment * or positioning, try changing the value of this flag. * * @param use the flag. */ public static void setUseFontMetricsGetStringBounds(boolean use) { useFontMetricsGetStringBounds = use; } /** * Returns the flag that controls whether or not a workaround is used for * drawing rotated strings. * * @return A boolean. */ public static boolean isUseDrawRotatedStringWorkaround() { return useDrawRotatedStringWorkaround; } /** * Sets the flag that controls whether or not a workaround is used for * drawing rotated strings. The related bug is on Sun's bug parade * (id 4312117) and the workaround involves using atrue
, strings will be drawn * as attributed strings with the attributes taken from the current font. * This allows for underlining, strike-out etc, but it means that * TextLayout will be used to render the text: * * http://www.jfree.org/phpBB2/viewtopic.php?p=45459&highlight=#45459 */ private static boolean drawStringsWithFontAttributes = false; /** * A flag that controls whether or not the rotated string workaround is * used. */ private static boolean useDrawRotatedStringWorkaround; /** * A flag that controls whether the FontMetrics.getStringBounds() method * is used or a workaround is applied. */ private static boolean useFontMetricsGetStringBounds; static { try { boolean isJava14 = ObjectUtilities.isJDK14(); String configRotatedStringWorkaround = BaseBoot.getInstance() .getGlobalConfig().getConfigProperty( "org.jfree.text.UseDrawRotatedStringWorkaround", "auto"); if (configRotatedStringWorkaround.equals("auto")) { useDrawRotatedStringWorkaround = !isJava14; } else { useDrawRotatedStringWorkaround = configRotatedStringWorkaround.equals("true"); } String configFontMetricsStringBounds = BaseBoot.getInstance() .getGlobalConfig().getConfigProperty( "org.jfree.text.UseFontMetricsGetStringBounds", "auto"); if (configFontMetricsStringBounds.equals("auto")) { useFontMetricsGetStringBounds = isJava14; } else { useFontMetricsGetStringBounds = configFontMetricsStringBounds.equals("true"); } } catch (Exception e) { // ignore everything. useDrawRotatedStringWorkaround = true; useFontMetricsGetStringBounds = true; } } /** * Private constructor prevents object creation. */ private TextUtilities() { // prevent instantiation } /** * Creates a {@link TextBlock} from aString
. Line breaks * are added where theString
contains '\n' characters. * * @param text the text. * @param font the font. * @param paint the paint. * * @return A text block. */ public static TextBlock createTextBlock(String text, Font font, Paint paint) { if (text == null) { throw new IllegalArgumentException("Null 'text' argument."); } TextBlock result = new TextBlock(); String input = text; boolean moreInputToProcess = (text.length() > 0); int start = 0; while (moreInputToProcess) { int index = input.indexOf("\n"); if (index > start) { String line = input.substring(start, index); if (index < input.length() - 1) { result.addLine(line, font, paint); input = input.substring(index + 1); } else { moreInputToProcess = false; } } else if (index == start) { if (index < input.length() - 1) { input = input.substring(index + 1); } else { moreInputToProcess = false; } } else { result.addLine(input, font, paint); moreInputToProcess = false; } } return result; } /** * Creates a new text block from the given string, breaking the * text into lines so that themaxWidth
value is * respected. * * @param text the text. * @param font the font. * @param paint the paint. * @param maxWidth the maximum width for each line. * @param measurer the text measurer. * * @return A text block. */ public static TextBlock createTextBlock(String text, Font font, Paint paint, float maxWidth, TextMeasurer measurer) { return createTextBlock(text, font, paint, maxWidth, Integer.MAX_VALUE, measurer); } /** * Creates a new text block from the given string, breaking the * text into lines so that themaxWidth
value is * respected. * * @param text the text. * @param font the font. * @param paint the paint. * @param maxWidth the maximum width for each line. * @param maxLines the maximum number of lines. * @param measurer the text measurer. * * @return A text block. */ public static TextBlock createTextBlock(String text, Font font, Paint paint, float maxWidth, int maxLines, TextMeasurer measurer) { TextBlock result = new TextBlock(); BreakIterator iterator = BreakIterator.getLineInstance(); iterator.setText(text); int current = 0; int lines = 0; int length = text.length(); while (current < length && lines < maxLines) { int next = nextLineBreak(text, current, maxWidth, iterator, measurer); if (next == BreakIterator.DONE) { result.addLine(text.substring(current), font, paint); return result; } result.addLine(text.substring(current, next), font, paint); lines++; current = next; while (current < text.length()&& text.charAt(current) == '\n') { current++; } } if (current < length) { TextLine lastLine = result.getLastLine(); TextFragment lastFragment = lastLine.getLastTextFragment(); String oldStr = lastFragment.getText(); String newStr = "..."; if (oldStr.length() > 3) { newStr = oldStr.substring(0, oldStr.length() - 3) + "..."; } lastLine.removeFragment(lastFragment); TextFragment newFragment = new TextFragment(newStr, lastFragment.getFont(), lastFragment.getPaint()); lastLine.addFragment(newFragment); } return result; } /** * Returns the character index of the next line break. * * @param text the text (null
not permitted). * @param start the start index. * @param width the target display width. * @param iterator the word break iterator. * @param measurer the text measurer. * * @return The index of the next line break. */ private static int nextLineBreak(String text, int start, float width, BreakIterator iterator, TextMeasurer measurer) { // this method is (loosely) based on code in JFreeReport's // TextParagraph class int current = start; int end; float x = 0.0f; boolean firstWord = true; int newline = text.indexOf('\n', start); if (newline < 0) { newline = Integer.MAX_VALUE; } while (((end = iterator.following(current)) != BreakIterator.DONE)) { x += measurer.getStringWidth(text, current, end); if (x > width) { if (firstWord) { while (measurer.getStringWidth(text, start, end) > width) { end--; if (end <= start) { return end; } } return end; } else { end = iterator.previous(); return end; } } else { if (end > newline) { return newline; } } // we found at least one word that fits ... firstWord = false; current = end; } return BreakIterator.DONE; } /** * Returns the bounds for the specified text. * * @param text the text (null
permitted). * @param g2 the graphics context (notnull
). * @param fm the font metrics (notnull
). * * @return The text bounds (null
if thetext
* argument isnull
). */ public static Rectangle2D getTextBounds(String text, Graphics2D g2, FontMetrics fm) { Rectangle2D bounds; if (TextUtilities.useFontMetricsGetStringBounds) { bounds = fm.getStringBounds(text, g2); // getStringBounds() can return incorrect height for some Unicode // characters...see bug parade 6183356, let's replace it with // something correct LineMetrics lm = fm.getFont().getLineMetrics(text, g2.getFontRenderContext()); bounds.setRect(bounds.getX(), bounds.getY(), bounds.getWidth(), lm.getHeight()); } else { double width = fm.stringWidth(text); double height = fm.getHeight(); if (logger.isDebugEnabled()) { logger.debug("Height = " + height); } bounds = new Rectangle2D.Double(0.0, -fm.getAscent(), width, height); } return bounds; } /** * Draws a string such that the specified anchor point is aligned to the * given (x, y) location. * * @param text the text. * @param g2 the graphics device. * @param x the x coordinate (Java 2D). * @param y the y coordinate (Java 2D). * @param anchor the anchor location. * * @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()); if (!drawStringsWithFontAttributes) { g2.drawString(text, x + adjust[0], y + adjust[1]); } else { AttributedString as = new AttributedString(text, g2.getFont().getAttributes()); g2.drawString(as.getIterator(), x + adjust[0], y + adjust[1]); } return textBounds; } /** * A utility method that calculates the anchor offsets for a string. * Normally, the (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 * (x, y) and draw the string, then the anchor point should coincide with * the (x, y) point. * * @param g2 the graphics device (notnull
). * @param text the text. * @param anchor the anchor point. * @param textBounds the text bounds (if notnull
, 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 = TextUtilities.getTextBounds(text, g2, 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.isVerticalCenter()) { 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; } /** * A utility method for drawing rotated text. ** A common rotation is -Math.PI/2 which draws text 'vertically' (with the * top of the characters on the left). * * @param text the text. * @param g2 the graphics device. * @param angle the angle of the (clockwise) rotation (in radians). * @param x the x-coordinate. * @param y the y-coordinate. */ public static void drawRotatedString(String text, Graphics2D g2, double angle, float x, float y) { drawRotatedString(text, g2, x, y, angle, x, y); } /** * A utility method for drawing rotated text. *
* A common rotation is -Math.PI/2 which draws text 'vertically' (with the * top of the characters on the left). * * @param text the text. * @param g2 the graphics device. * @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. */ public static void drawRotatedString(String text, Graphics2D g2, float textX, float textY, double angle, float rotateX, float rotateY) { if ((text == null) || (text.equals(""))) { return; } if (angle == 0.0) { drawAlignedString(text, g2, textY, textY, TextAnchor.BASELINE_LEFT); return; } AffineTransform saved = g2.getTransform(); AffineTransform rotate = AffineTransform.getRotateInstance( angle, rotateX, rotateY); g2.transform(rotate); if (useDrawRotatedStringWorkaround) { // workaround for JDC bug ID 4312117 and others... TextLayout tl = new TextLayout(text, g2.getFont(), g2.getFontRenderContext()); tl.draw(g2, textX, textY); } else { if (!drawStringsWithFontAttributes) { g2.drawString(text, textX, textY); } else { AttributedString as = new AttributedString(text, g2.getFont().getAttributes()); g2.drawString(as.getIterator(), textX, textY); } } g2.setTransform(saved); } /** * Draws a string that is aligned by one anchor point and rotated about * another anchor point. * * @param text the text. * @param g2 the graphics device. * @param x the x-coordinate for positioning the text. * @param y the y-coordinate for positioning the text. * @param textAnchor the text anchor. * @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. */ public static void drawRotatedString(String text, Graphics2D g2, float x, float y, TextAnchor textAnchor, double angle, float rotationX, float rotationY) { if (text == null || text.equals("")) { return; } if (angle == 0.0) { drawAlignedString(text, g2, x, y, textAnchor); } else { float[] textAdj = deriveTextBoundsAnchorOffsets(g2, text, textAnchor); 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. * * @param text the text. * @param g2 the graphics device. * @param x the x-coordinate for positioning the text. * @param y the y-coordinate for positioning the text. * @param textAnchor the text anchor. * @param angle the rotation angle (in radians). * @param rotationAnchor the rotation anchor. */ public static void drawRotatedString(String text, Graphics2D g2, float x, float y, TextAnchor textAnchor, double angle, TextAnchor rotationAnchor) { if (text == null || text.equals("")) { return; } if (angle == 0.0) { drawAlignedString(text, g2, x, y, textAnchor); } else { float[] textAdj = deriveTextBoundsAnchorOffsets(g2, text, textAnchor); float[] rotateAdj = deriveRotationAnchorOffsets(g2, text, rotationAnchor); drawRotatedString(text, g2, x + textAdj[0], y + textAdj[1], angle, x + textAdj[0] + rotateAdj[0], y + textAdj[1] + rotateAdj[1]); } } /** * Returns a shape that represents the bounds of the string after the * specified rotation has been applied. * * @param text the text (
null
permitted). * @param g2 the graphics device. * @param x the x coordinate for the anchor point. * @param y the y coordinate for the anchor point. * @param textAnchor the text anchor. * @param angle the angle. * @param rotationAnchor the rotation anchor. * * @return The bounds (possiblynull
). */ public static Shape calculateRotatedStringBounds(String text, Graphics2D g2, float x, float y, TextAnchor textAnchor, double angle, TextAnchor rotationAnchor) { if (text == null || text.equals("")) { return null; } float[] textAdj = deriveTextBoundsAnchorOffsets(g2, text, textAnchor); if (logger.isDebugEnabled()) { logger.debug("TextBoundsAnchorOffsets = " + textAdj[0] + ", " + textAdj[1]); } float[] rotateAdj = deriveRotationAnchorOffsets(g2, text, rotationAnchor); if (logger.isDebugEnabled()) { logger.debug("RotationAnchorOffsets = " + rotateAdj[0] + ", " + rotateAdj[1]); } Shape result = calculateRotatedStringBounds(text, g2, x + textAdj[0], y + textAdj[1], angle, x + textAdj[0] + rotateAdj[0], y + textAdj[1] + rotateAdj[1]); return result; } /** * A utility method that calculates the anchor offsets for a string. * Normally, the (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 * (x, y) and draw the string, then the anchor point should coincide with * the (x, y) point. * * @param g2 the graphics device (notnull
). * @param text the text. * @param anchor the anchor point. * * @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 = TextUtilities.getTextBounds(text, g2, 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.isVerticalCenter()) { 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 rotation anchor offsets for a * string. These offsets are relative to the text starting coordinate * (BASELINE_LEFT
). * * @param g2 the graphics device. * @param text the text. * @param anchor the anchor point. * * @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 = TextUtilities.getTextBounds(text, g2, 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.isVerticalCenter()) { 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; } /** * Returns a shape that represents the bounds of the string after the * specified rotation has been applied. * * @param text the text (null
permitted). * @param g2 the graphics device. * @param textX the x coordinate for the text. * @param textY the y coordinate for the text. * @param angle the angle. * @param rotateX the x coordinate for the rotation point. * @param rotateY the y coordinate for the rotation point. * * @return The bounds (null
iftext
is *TextLayout
* instance to draw the text instead of calling the *drawString()
method in theGraphics2D
class. * * @param use the new flag value. */ public static void setUseDrawRotatedStringWorkaround(boolean use) { TextUtilities.useDrawRotatedStringWorkaround = use; } /** * Returns the flag that controls whether or not strings are drawn using * the current font attributes (such as underlining, strikethrough etc). * The default value isfalse
. * * @return A boolean. * * @since 1.0.21 */ public static boolean getDrawStringsWithFontAttributes() { return TextUtilities.drawStringsWithFontAttributes; } /** * Sets the flag that controls whether or not strings are drawn using the * current font attributes. This is a hack to allow underlining of titles * without big changes to the API. See: * http://www.jfree.org/phpBB2/viewtopic.php?p=45459&highlight=#45459 * * @param b the new flag value. * * @since 1.0.21 */ public static void setDrawStringsWithFontAttributes(boolean b) { TextUtilities.drawStringsWithFontAttributes = b; } }