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