    001/* ===========================================================
    002 * Orson Charts : a 3D chart library for the Java(tm) platform
    003 * ===========================================================
    004 *
    005 * (C)opyright 2013-2016, by Object Refinery Limited.  All rights reserved.
    006 *
    007 * http://www.object-refinery.com/orsoncharts/index.html
    008 *
    009 * This program is free software: you can redistribute it and/or modify
    010 * it under the terms of the GNU General Public License as published by
    011 * the Free Software Foundation, either version 3 of the License, or
    012 * (at your option) any later version.
    013 *
    014 * This program is distributed in the hope that it will be useful,
    015 * but WITHOUT ANY WARRANTY; without even the implied warranty of
    016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    017 * GNU General Public License for more details.
    018 *
    019 * You should have received a copy of the GNU General Public License
    020 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    021 *
    022 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
    023 * Other names may be trademarks of their respective owners.]
    024 *
    025 * If you do not wish to be bound by the terms of the GPL, an alternative
    026 * commercial license can be purchased.  For details, please see visit the
    027 * Orson Charts home page:
    028 *
    029 * http://www.object-refinery.com/orsoncharts/index.html
    030 *
    031 */
    032
    033package com.orsoncharts.util;
    034
    035import java.awt.Font;
    036import java.awt.FontMetrics;
    037import java.awt.Graphics2D;
    038import java.awt.Shape;
    039import java.awt.font.FontRenderContext;
    040import java.awt.font.LineMetrics;
    041import java.awt.font.TextLayout;
    042import java.awt.geom.AffineTransform;
    043import java.awt.geom.Rectangle2D;
    044import java.text.AttributedString;
    045
    046/**
    047 * Utility methods for working with text.
    048 */
    049public class TextUtils {
    050
    051    private TextUtils() {
    052        // no need to instantiate this
    053    }
    054
    055    /**
    056     * Draws a string such that the specified anchor point is aligned to the
    057     * given {@code (x, y)} location, and returns a bounding rectangle
    058     * for the text.
    059     *
    060     * @param text  the text.
    061     * @param g2  the graphics device ({@code null} not permitted).
    062     * @param x  the x coordinate (Java 2D).
    063     * @param y  the y coordinate (Java 2D).
    064     * @param anchor  the anchor location ({@code null} not permitted).
    065     *
    066     * @return The text bounds (adjusted for the text position).
    067     */
    068    public static Rectangle2D drawAlignedString(String text,
    069            Graphics2D g2, float x, float y, TextAnchor anchor) {
    070
    071        Rectangle2D textBounds = new Rectangle2D.Double();
    072        float[] adjust = deriveTextBoundsAnchorOffsets(g2, text, anchor,
    073                textBounds);
    074        // adjust text bounds to match string position
    075        textBounds.setRect(x + adjust[0], y + adjust[1] + adjust[2],
    076            textBounds.getWidth(), textBounds.getHeight());
    077        g2.drawString(text, x + adjust[0], y + adjust[1]);
    078        return textBounds;
    079    }
    080
    081    /**
    082     * Returns the bounds of an aligned string.
    083     *
    084     * @param text  the string ({@code null} not permitted).
    085     * @param g2  the graphics target ({@code null} not permitted).
    086     * @param x  the x-coordinate.
    087     * @param y  the y-coordinate.
    088     * @param anchor  the anchor point on the text that will be aligned to
    089     *     {@code (x, y)} ({@code null} not permitted).
    090     *
    091     * @return The text bounds (never {@code null}).
    092     *
    093     * @since 1.3
    094     */
    095    public static Rectangle2D calcAlignedStringBounds(String text,
    096            Graphics2D g2, float x, float y, TextAnchor anchor) {
    097
    098        Rectangle2D textBounds = new Rectangle2D.Double();
    099        float[] adjust = deriveTextBoundsAnchorOffsets(g2, text, anchor,
    100                textBounds);
    101        // adjust text bounds to match string position
    102        textBounds.setRect(x + adjust[0], y + adjust[1] + adjust[2],
    103            textBounds.getWidth(), textBounds.getHeight());
    104        return textBounds;
    105    }
    106
    107    /**
    108     * A utility method that calculates the anchor offsets for a string.
    109     * Normally, the {@code (x, y)} coordinate for drawing text is a point on
    110     * the baseline at the left of the text string.  If you add these offsets
    111     * to {@code (x, y)} and draw the string, then the anchor point should
    112     * coincide with the {@code (x, y)} point.
    113     *
    114     * @param g2  the graphics device (not {@code null}).
    115     * @param text  the text.
    116     * @param anchor  the anchor point ({@code null} not permitted).
    117     *
    118     * @return  The offsets.
    119     */
    120    private static float[] deriveTextBoundsAnchorOffsets(Graphics2D g2,
    121            String text, TextAnchor anchor) {
    122
    123        float[] result = new float[2];
    124        FontRenderContext frc = g2.getFontRenderContext();
    125        Font f = g2.getFont();
    126        FontMetrics fm = g2.getFontMetrics(f);
    127        Rectangle2D bounds = getTextBounds(text, fm);
    128        LineMetrics metrics = f.getLineMetrics(text, frc);
    129        float ascent = metrics.getAscent();
    130        float halfAscent = ascent / 2.0f;
    131        float descent = metrics.getDescent();
    132        float leading = metrics.getLeading();
    133        float xAdj = 0.0f;
    134        float yAdj = 0.0f;
    135
    136        if (anchor.isHorizontalCenter()) {
    137            xAdj = (float) -bounds.getWidth() / 2.0f;
    138        } else if (anchor.isRight()) {
    139            xAdj = (float) -bounds.getWidth();
    140        }
    141
    142        if (anchor.isTop()) {
    143            yAdj = -descent - leading + (float) bounds.getHeight();
    144        } else if (anchor.isHalfAscent()) {
    145            yAdj = halfAscent;
    146        } else if (anchor.isHalfHeight()) {
    147            yAdj = -descent - leading + (float) (bounds.getHeight() / 2.0);
    148        } else if (anchor.isBaseline()) {
    149            yAdj = 0.0f;
    150        } else if (anchor.isBottom()) {
    151            yAdj = -metrics.getDescent() - metrics.getLeading();
    152        }
    153        result[0] = xAdj;
    154        result[1] = yAdj;
    155        return result;
    156    }
    157
    158    /**
    159     * A utility method that calculates the anchor offsets for a string.
    160     * Normally, the {@code (x, y)} coordinate for drawing text is a point on
    161     * the baseline at the left of the text string.  If you add these offsets
    162     * to {@code (x, y)} and draw the string, then the anchor point should
    163     * coincide with the {@code (x, y)} point.
    164     *
    165     * @param g2  the graphics device (not {@code null}).
    166     * @param text  the text.
    167     * @param anchor  the anchor point ({@code null} not permitted).
    168     * @param textBounds  the text bounds (if not {@code null}, this
    169     *                    object will be updated by this method to match the
    170     *                    string bounds).
    171     *
    172     * @return  The offsets.
    173     */
    174    private static float[] deriveTextBoundsAnchorOffsets(Graphics2D g2,
    175            String text, TextAnchor anchor, Rectangle2D textBounds) {
    176
    177        float[] result = new float[3];
    178        FontRenderContext frc = g2.getFontRenderContext();
    179        Font f = g2.getFont();
    180        FontMetrics fm = g2.getFontMetrics(f);
    181        Rectangle2D bounds = getTextBounds(text, fm);
    182        LineMetrics metrics = f.getLineMetrics(text, frc);
    183        float ascent = metrics.getAscent();
    184        result[2] = -ascent;
    185        float halfAscent = ascent / 2.0f;
    186        float descent = metrics.getDescent();
    187        float leading = metrics.getLeading();
    188        float xAdj = 0.0f;
    189        float yAdj = 0.0f;
    190
    191        if (anchor.isHorizontalCenter()) {
    192            xAdj = (float) -bounds.getWidth() / 2.0f;
    193        } else if (anchor.isRight()) {
    194            xAdj = (float) -bounds.getWidth();
    195        }
    196
    197        if (anchor.isTop()) {
    198            yAdj = -descent - leading + (float) bounds.getHeight();
    199        } else if (anchor.isHalfAscent()) {
    200            yAdj = halfAscent;
    201        } else if (anchor.isHorizontalCenter()) {
    202            yAdj = -descent - leading + (float) (bounds.getHeight() / 2.0);
    203        } else if (anchor.isBaseline()) {
    204            yAdj = 0.0f;
    205        } else if (anchor.isBottom()) {
    206            yAdj = -metrics.getDescent() - metrics.getLeading();
    207        }
    208        if (textBounds != null) {
    209            textBounds.setRect(bounds);
    210        }
    211        result[0] = xAdj;
    212        result[1] = yAdj;
    213        return result;
    214    }
    215
    216    /**
    217     * Returns the bounds for the specified text.  The supplied text is
    218     * assumed to be on a single line (no carriage return or newline
    219     * characters).
    220     *
    221     * @param text  the text ({@code null} not permitted).
    222     * @param fm  the font metrics ({@code null} not permitted).
    223     *
    224     * @return The text bounds.
    225     */
    226    public static Rectangle2D getTextBounds(String text, FontMetrics fm) {
    227        return getTextBounds(text, 0.0, 0.0, fm);
    228    }
    229
    230    /**
    231     * Returns the bounds for the specified text when it is drawn with the
    232     * left-baseline aligned to the point {@code (x, y)}.
    233     *
    234     * @param text  the text ({@code null} not permitted).
    235     * @param x  the x-coordinate.
    236     * @param y  the y-coordinate.
    237     * @param fm  the font metrics ({@code null} not permitted).
    238     *
    239     * @return The bounding rectangle (never {@code null}).
    240     */
    241    public static Rectangle2D getTextBounds(String text, double x, double y,
    242            FontMetrics fm) {
    243        ArgChecks.nullNotPermitted(text, "text");
    244        ArgChecks.nullNotPermitted(fm, "fm");
    245        double width = fm.stringWidth(text);
    246        double height = fm.getHeight();
    247        return new Rectangle2D.Double(x, y - fm.getAscent(), width, height);
    248    }
    249
    250    /**
    251     * Draws a string that is aligned by one anchor point and rotated about
    252     * another anchor point.
    253     *
    254     * @param text  the text ({@code null} not permitted).
    255     * @param g2  the graphics target ({@code null} not permitted).
    256     * @param x  the x-coordinate for positioning the text.
    257     * @param y  the y-coordinate for positioning the text.
    258     * @param textAnchor  the text anchor ({@code null} not permitted).
    259     * @param angle  the rotation angle.
    260     * @param rotationX  the x-coordinate for the rotation anchor point.
    261     * @param rotationY  the y-coordinate for the rotation anchor point.
    262     *
    263     * @return The text bounds (never {@code null}).
    264     */
    265    public static Shape drawRotatedString(String text, Graphics2D g2, float x,
    266            float y, TextAnchor textAnchor, double angle,
    267            float rotationX, float rotationY) {
    268        ArgChecks.nullNotPermitted(text, "text");
    269        float[] textAdj = deriveTextBoundsAnchorOffsets(g2, text, textAnchor);
    270        return drawRotatedString(text, g2, x + textAdj[0], y + textAdj[1],
    271                angle, rotationX, rotationY);
    272    }
    273
    274    /**
    275     * Draws a string that is aligned by one anchor point and rotated about
    276     * another anchor point, and returns a bounding shape for the text.
    277     *
    278     * @param text  the text ({@code null} not permitted).
    279     * @param g2  the graphics device ({@code null} not permitted).
    280     * @param x  the x-coordinate for positioning the text.
    281     * @param y  the y-coordinate for positioning the text.
    282     * @param textAnchor  the text anchor ({@code null} not permitted).
    283     * @param angle  the rotation angle (in radians).
    284     * @param rotationAnchor  the rotation anchor ({@code null} not permitted).
    285     *
    286     * @return A bounding shape for the text.
    287     */
    288    public static Shape drawRotatedString(String text, Graphics2D g2,
    289            float x, float y, TextAnchor textAnchor,
    290            double angle, TextAnchor rotationAnchor) {
    291
    292        ArgChecks.nullNotPermitted(text, "text");
    293        float[] textAdj = deriveTextBoundsAnchorOffsets(g2, text, textAnchor);
    294        float[] rotateAdj = deriveRotationAnchorOffsets(g2, text,
    295                rotationAnchor);
    296        return drawRotatedString(text, g2, x + textAdj[0], y + textAdj[1],
    297                angle, x + textAdj[0] + rotateAdj[0],
    298                y + textAdj[1] + rotateAdj[1]);
    299    }
    300
    301    /**
    302     * A utility method that calculates the rotation anchor offsets for a
    303     * string.  These offsets are relative to the text starting coordinate
    304     * ({@code BASELINE_LEFT}).
    305     *
    306     * @param g2  the graphics device ({@code null} not permitted).
    307     * @param text  the text ({@code null} not permitted).
    308     * @param anchor  the anchor point ({@code null} not permitted).
    309     *
    310     * @return  The offsets.
    311     */
    312    private static float[] deriveRotationAnchorOffsets(Graphics2D g2,
    313            String text, TextAnchor anchor) {
    314
    315        float[] result = new float[2];
    316        FontRenderContext frc = g2.getFontRenderContext();
    317        LineMetrics metrics = g2.getFont().getLineMetrics(text, frc);
    318        FontMetrics fm = g2.getFontMetrics();
    319        Rectangle2D bounds = TextUtils.getTextBounds(text, fm);
    320        float ascent = metrics.getAscent();
    321        float halfAscent = ascent / 2.0f;
    322        float descent = metrics.getDescent();
    323        float leading = metrics.getLeading();
    324        float xAdj = 0.0f;
    325        float yAdj = 0.0f;
    326
    327        if (anchor.isLeft()) {
    328            xAdj = 0.0f;
    329        } else if (anchor.isHorizontalCenter()) {
    330            xAdj = (float) bounds.getWidth() / 2.0f;
    331        } else if (anchor.isRight()) {
    332            xAdj = (float) bounds.getWidth();
    333        }
    334
    335        if (anchor.isTop()) {
    336            yAdj = descent + leading - (float) bounds.getHeight();
    337        } else if (anchor.isHalfHeight()) {
    338            yAdj = descent + leading - (float) (bounds.getHeight() / 2.0);
    339        } else if (anchor.isHalfAscent()) {
    340            yAdj = -halfAscent;
    341        } else if (anchor.isBaseline()) {
    342            yAdj = 0.0f;
    343        } else if (anchor.isBottom()) {
    344            yAdj = metrics.getDescent() + metrics.getLeading();
    345        }
    346        result[0] = xAdj;
    347        result[1] = yAdj;
    348        return result;
    349    }
    350
    351    /**
    352     * A utility method for drawing rotated text.
    353     * <P>
    354     * A common rotation is {@code -Math.PI/2} which draws text 'vertically'
    355     * (with the top of the characters on the left).
    356     *
    357     * @param text  the text ({@code null} not permitted)
    358     * @param g2  the graphics target ({@code null} not permitted).
    359     * @param angle  the angle of the (clockwise) rotation (in radians).
    360     * @param x  the x-coordinate.
    361     * @param y  the y-coordinate.
    362     *
    363     * @return The text bounds.
    364     */
    365    public static Shape drawRotatedString(String text, Graphics2D g2,
    366            double angle, float x, float y) {
    367        return drawRotatedString(text, g2, x, y, angle, x, y);
    368    }
    369
    370    /**
    371     * A utility method for drawing rotated text.
    372     * <P>
    373     * A common rotation is {@code -Math.PI/2} which draws text 'vertically'
    374     * (with the top of the characters on the left).
    375     *
    376     * @param text  the text ({@code null} not permitted).
    377     * @param g2  the graphics device ({@code null} not permitted).
    378     * @param textX  the x-coordinate for the text (before rotation).
    379     * @param textY  the y-coordinate for the text (before rotation).
    380     * @param angle  the angle of the (clockwise) rotation (in radians).
    381     * @param rotateX  the point about which the text is rotated.
    382     * @param rotateY  the point about which the text is rotated.
    383     *
    384     * @return The bounds for the rotated text (never {@code null}).
    385     */
    386    public static Shape drawRotatedString(String text, Graphics2D g2,
    387            float textX, float textY, double angle,
    388            float rotateX, float rotateY) {
    389        ArgChecks.nullNotPermitted(text, "text");
    390        AffineTransform saved = g2.getTransform();
    391        Rectangle2D rect = TextUtils.getTextBounds(text, textX, textY,
    392                g2.getFontMetrics());
    393        AffineTransform rotate = AffineTransform.getRotateInstance(
    394                angle, rotateX, rotateY);
    395        Shape bounds = rotate.createTransformedShape(rect);
    396        g2.transform(rotate);
    397        g2.drawString(text, textX, textY);
    398        g2.setTransform(saved);
    399        return bounds;
    400    }
    401
    402    /**
    403     * Draws the attributed string at {@code (x, y)}, rotated by the
    404     * specified angle about {@code (x, y)}.
    405     *
    406     * @param text  the attributed string ({@code null} not permitted).
    407     * @param g2  the graphics output target ({@code null} not permitted).
    408     * @param angle  the angle.
    409     * @param x  the x-coordinate.
    410     * @param y  the y-coordinate.
    411     *
    412     * @return The text bounds (never {@code null}).
    413     *
    414     * @since 1.2
    415     */
    416    public static Shape drawRotatedString(AttributedString text, Graphics2D g2,
    417            double angle, float x, float y) {
    418        return drawRotatedString(text, g2, x, y, angle, x, y);
    419    }
    420
    421    /**
    422     * Draws the attributed string at {@code (textX, textY)}, rotated by
    423     * the specified angle about {@code (rotateX, rotateY)}.
    424     *
    425     * @param text  the attributed string ({@code null} not permitted).
    426     * @param g2  the graphics output target ({@code null} not permitted).
    427     * @param textX  the x-coordinate for the text alignment point.
    428     * @param textY  the y-coordinate for the text alignment point.
    429     * @param angle  the rotation angle (in radians).
    430     * @param rotateX  the x-coordinate for the rotation point.
    431     * @param rotateY  the y-coordinate for the rotation point.
    432     *
    433     * @return The text bounds (never {@code null}).
    434     *
    435     * @since 1.2
    436     */
    437    public static Shape drawRotatedString(AttributedString text, Graphics2D g2,
    438            float textX, float textY, double angle, float rotateX,
    439            float rotateY) {
    440
    441        ArgChecks.nullNotPermitted(text, "text");
    442        AffineTransform saved = g2.getTransform();
    443        AffineTransform rotate = AffineTransform.getRotateInstance(angle,
    444                rotateX, rotateY);
    445        g2.transform(rotate);
    446        TextLayout tl = new TextLayout(text.getIterator(),
    447                    g2.getFontRenderContext());
    448        Rectangle2D rect = tl.getBounds();
    449        tl.draw(g2, textX, textY);
    450        g2.setTransform(saved);
    451        return rotate.createTransformedShape(rect);
    452    }
    453
    454    /**
    455     * Draws the attributed string aligned to the point {@code (x, y)},
    456     * rotated by the specified angle about {@code rotationAnchor}.
    457     *
    458     * @param text  the attributed string ({@code null} not permitted).
    459     * @param g2  the graphics target ({@code null} not permitted).
    460     * @param x  the x-coordinate.
    461     * @param y  the y-coordinate.
    462     * @param textAnchor  the text anchor ({@code null} not permitted).
    463     * @param angle  the rotation angle (in radians).
    464     * @param rotationAnchor  the rotation anchor ({@code null} not
    465     *     permitted).
    466     * @param nonRotatedBounds  if not {@code null} this rectangle will
    467     *     be updated with the non-rotated bounds of the text for the caller
    468     *     to use.
    469     *
    470     * @return The text bounds (never {@code null}).
    471     *
    472     * @since 1.2
    473     */
    474    public static Shape drawRotatedString(AttributedString text, Graphics2D g2,
    475            float x, float y, TextAnchor textAnchor,
    476            double angle, TextAnchor rotationAnchor,
    477            Rectangle2D nonRotatedBounds) {
    478        ArgChecks.nullNotPermitted(text, "text");
    479        float[] textAdj = deriveTextBoundsAnchorOffsets(g2, text, textAnchor,
    480                nonRotatedBounds);
    481        float[] rotateAdj = deriveRotationAnchorOffsets(g2, text,
    482                rotationAnchor);
    483        return drawRotatedString(text, g2, x + textAdj[0], y + textAdj[1],
    484                angle, x + textAdj[0] + rotateAdj[0],
    485                y + textAdj[1] + rotateAdj[1]);
    486    }
    487
    488    /**
    489     * Calculates the x and y offsets required to align the text with the
    490     * specified {@code anchor}.
    491     *
    492     * @param g2  the graphics target ({@code null} not permitted).
    493     * @param text  the text ({@code null} not permitted).
    494     * @param anchor  the anchor ({@code null} not permitted).
    495     * @param textBounds  if not {@code null}, this rectangle will be
    496     *     updated with the bounds of the text (for the caller to use).
    497     *
    498     * @return An array of two floats dx and dy.
    499     */
    500    private static float[] deriveTextBoundsAnchorOffsets(Graphics2D g2,
    501            AttributedString text, TextAnchor anchor, Rectangle2D textBounds) {
    502
    503        TextLayout layout = new TextLayout(text.getIterator(),
    504                g2.getFontRenderContext());
    505        Rectangle2D bounds = layout.getBounds();
    506
    507        float[] result = new float[3];
    508        float ascent = layout.getAscent();
    509        result[2] = -ascent;
    510        float halfAscent = ascent / 2.0f;
    511        float descent = layout.getDescent();
    512        float leading = layout.getLeading();
    513        float xAdj = 0.0f;
    514        float yAdj = 0.0f;
    515
    516        if (anchor.isHorizontalCenter()) {
    517            xAdj = (float) -bounds.getWidth() / 2.0f;
    518        } else if (anchor.isRight()) {
    519            xAdj = (float) -bounds.getWidth();
    520        }
    521
    522        if (anchor.isTop()) {
    523            yAdj = -descent - leading + (float) bounds.getHeight();
    524        } else if (anchor.isHalfAscent()) {
    525            yAdj = halfAscent;
    526        } else if (anchor.isHalfHeight()) {
    527            yAdj = -descent - leading + (float) (bounds.getHeight() / 2.0);
    528        } else if (anchor.isBaseline()) {
    529            yAdj = 0.0f;
    530        } else if (anchor.isBottom()) {
    531            yAdj = -descent - leading;
    532        }
    533        if (textBounds != null) {
    534            textBounds.setRect(bounds);
    535        }
    536        result[0] = xAdj;
    537        result[1] = yAdj;
    538        return result;
    539    }
    540
    541    /**
    542     * A utility method that calculates the rotation anchor offsets for a
    543     * string.  These offsets are relative to the text starting coordinate
    544     * ({@code BASELINE_LEFT}).
    545     *
    546     * @param g2  the graphics device ({@code null} not permitted).
    547     * @param text  the text ({@code null} not permitted).
    548     * @param anchor  the anchor point ({@code null} not permitted).
    549     *
    550     * @return  The offsets.
    551     */
    552    private static float[] deriveRotationAnchorOffsets(Graphics2D g2,
    553            AttributedString text, TextAnchor anchor) {
    554
    555        float[] result = new float[2];
    556
    557        TextLayout layout = new TextLayout(text.getIterator(),
    558                g2.getFontRenderContext());
    559        Rectangle2D bounds = layout.getBounds();
    560        float ascent = layout.getAscent();
    561        float halfAscent = ascent / 2.0f;
    562        float descent = layout.getDescent();
    563        float leading = layout.getLeading();
    564        float xAdj = 0.0f;
    565        float yAdj = 0.0f;
    566
    567        if (anchor.isLeft()) {
    568            xAdj = 0.0f;
    569        } else if (anchor.isHorizontalCenter()) {
    570            xAdj = (float) bounds.getWidth() / 2.0f;
    571        } else if (anchor.isRight()) {
    572            xAdj = (float) bounds.getWidth();
    573        }
    574
    575        if (anchor.isTop()) {
    576            yAdj = descent + leading - (float) bounds.getHeight();
    577        } else if (anchor.isHalfHeight()) {
    578            yAdj = descent + leading - (float) (bounds.getHeight() / 2.0);
    579        } else if (anchor.isHalfAscent()) {
    580            yAdj = -halfAscent;
    581        } else if (anchor.isBaseline()) {
    582            yAdj = 0.0f;
    583        } else if (anchor.isBottom()) {
    584            yAdj = descent + leading;
    585        }
    586        result[0] = xAdj;
    587        result[1] = yAdj;
    588        return result;
    589    }
    590
    591}
    592
    593