1 /* 2 * Copyright (c) 2012, 2016, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package com.sun.javafx.text; 27 28 29 import javafx.scene.shape.LineTo; 30 import javafx.scene.shape.MoveTo; 31 import javafx.scene.shape.PathElement; 32 import com.sun.javafx.font.CharToGlyphMapper; 33 import com.sun.javafx.font.FontResource; 34 import com.sun.javafx.font.FontStrike; 35 import com.sun.javafx.font.Metrics; 36 import com.sun.javafx.font.PGFont; 37 import com.sun.javafx.font.PrismFontFactory; 38 import com.sun.javafx.geom.BaseBounds; 39 import com.sun.javafx.geom.Path2D; 40 import com.sun.javafx.geom.Point2D; 41 import com.sun.javafx.geom.RectBounds; 42 import com.sun.javafx.geom.RoundRectangle2D; 43 import com.sun.javafx.geom.Shape; 44 import com.sun.javafx.geom.transform.BaseTransform; 45 import com.sun.javafx.geom.transform.Translate2D; 46 import com.sun.javafx.scene.text.GlyphList; 47 import com.sun.javafx.scene.text.TextLayout; 48 import com.sun.javafx.scene.text.TextSpan; 49 import java.text.Bidi; 50 import java.text.BreakIterator; 51 import java.util.ArrayList; 52 import java.util.Arrays; 53 import java.util.Hashtable; 54 55 public class PrismTextLayout implements TextLayout { 56 private static final BaseTransform IDENTITY = BaseTransform.IDENTITY_TRANSFORM; 57 private static final int X_MIN_INDEX = 0; 58 private static final int Y_MIN_INDEX = 1; 59 private static final int X_MAX_INDEX = 2; 60 private static final int Y_MAX_INDEX = 3; 61 62 private static final Hashtable<Integer, LayoutCache> stringCache = new Hashtable<>(); 63 private static final Object CACHE_SIZE_LOCK = new Object(); 64 private static int cacheSize = 0; 65 private static final int MAX_STRING_SIZE = 256; 66 private static final int MAX_CACHE_SIZE = PrismFontFactory.cacheLayoutSize; 67 68 private char[] text; 69 private TextSpan[] spans; /* Rich text (null for single font text) */ 70 private PGFont font; /* Single font text (null for rich text) */ 71 private FontStrike strike; /* cached strike of font (identity) */ 72 private Integer cacheKey; 73 private TextLine[] lines; 74 private TextRun[] runs; 75 private int runCount; 76 private BaseBounds logicalBounds; 77 private RectBounds visualBounds; 78 private float layoutWidth, layoutHeight; 79 private float wrapWidth, spacing; 80 private LayoutCache layoutCache; 81 private Shape shape; 82 private int flags; 83 84 public PrismTextLayout() { 85 logicalBounds = new RectBounds(); 86 flags = ALIGN_LEFT; 87 } 88 89 private void reset() { 90 layoutCache = null; 91 runs = null; 92 flags &= ~ANALYSIS_MASK; 93 relayout(); 94 } 95 96 private void relayout() { 97 logicalBounds.makeEmpty(); 98 visualBounds = null; 99 layoutWidth = layoutHeight = 0; 100 flags &= ~(FLAGS_WRAPPED | FLAGS_CACHED_UNDERLINE | FLAGS_CACHED_STRIKETHROUGH); 101 lines = null; 102 shape = null; 103 } 104 105 /*************************************************************************** 106 * * 107 * TextLayout API * 108 * * 109 **************************************************************************/ 110 111 public boolean setContent(TextSpan[] spans) { 112 if (spans == null && this.spans == null) return false; 113 if (spans != null && this.spans != null) { 114 if (spans.length == this.spans.length) { 115 int i = 0; 116 while (i < spans.length) { 117 if (spans[i] != this.spans[i]) break; 118 i++; 119 } 120 if (i == spans.length) return false; 121 } 122 } 123 124 reset(); 125 this.spans = spans; 126 this.font = null; 127 this.strike = null; 128 this.text = null; /* Initialized in getText() */ 129 this.cacheKey = null; 130 return true; 131 } 132 133 public boolean setContent(String text, Object font) { 134 reset(); 135 this.spans = null; 136 this.font = (PGFont)font; 137 this.strike = ((PGFont)font).getStrike(IDENTITY); 138 this.text = text.toCharArray(); 139 if (MAX_CACHE_SIZE > 0) { 140 int length = text.length(); 141 if (0 < length && length <= MAX_STRING_SIZE) { 142 cacheKey = text.hashCode() * strike.hashCode(); 143 } 144 } 145 return true; 146 } 147 148 public boolean setDirection(int direction) { 149 if ((flags & DIRECTION_MASK) == direction) return false; 150 flags &= ~DIRECTION_MASK; 151 flags |= (direction & DIRECTION_MASK); 152 reset(); 153 return true; 154 } 155 156 public boolean setBoundsType(int type) { 157 if ((flags & BOUNDS_MASK) == type) return false; 158 flags &= ~BOUNDS_MASK; 159 flags |= (type & BOUNDS_MASK); 160 reset(); 161 return true; 162 } 163 164 public boolean setAlignment(int alignment) { 165 int align = ALIGN_LEFT; 166 switch (alignment) { 167 case 0: align = ALIGN_LEFT; break; 168 case 1: align = ALIGN_CENTER; break; 169 case 2: align = ALIGN_RIGHT; break; 170 case 3: align = ALIGN_JUSTIFY; break; 171 } 172 if ((flags & ALIGN_MASK) == align) return false; 173 if (align == ALIGN_JUSTIFY || (flags & ALIGN_JUSTIFY) != 0) { 174 reset(); 175 } 176 flags &= ~ALIGN_MASK; 177 flags |= align; 178 relayout(); 179 return true; 180 } 181 182 public boolean setWrapWidth(float newWidth) { 183 if (Float.isInfinite(newWidth)) newWidth = 0; 184 if (Float.isNaN(newWidth)) newWidth = 0; 185 float oldWidth = this.wrapWidth; 186 this.wrapWidth = Math.max(0, newWidth); 187 188 boolean needsLayout = true; 189 if (lines != null && oldWidth != 0 && newWidth != 0) { 190 if ((flags & ALIGN_LEFT) != 0) { 191 if (newWidth > oldWidth) { 192 /* If wrapping width is increasing and there is no 193 * wrapped lines then the text remains valid. 194 */ 195 if ((flags & FLAGS_WRAPPED) == 0) { 196 needsLayout = false; 197 } 198 } else { 199 /* If wrapping width is decreasing but it is still 200 * greater than the max line width then the text 201 * remains valid. 202 */ 203 if (newWidth >= layoutWidth) { 204 needsLayout = false; 205 } 206 } 207 } 208 } 209 if (needsLayout) relayout(); 210 return needsLayout; 211 } 212 213 public boolean setLineSpacing(float spacing) { 214 if (this.spacing == spacing) return false; 215 this.spacing = spacing; 216 relayout(); 217 return true; 218 } 219 220 private void ensureLayout() { 221 if (lines == null) { 222 layout(); 223 } 224 } 225 226 public com.sun.javafx.scene.text.TextLine[] getLines() { 227 ensureLayout(); 228 return lines; 229 } 230 231 public GlyphList[] getRuns() { 232 ensureLayout(); 233 GlyphList[] result = new GlyphList[runCount]; 234 int count = 0; 235 for (int i = 0; i < lines.length; i++) { 236 GlyphList[] lineRuns = lines[i].getRuns(); 237 int length = lineRuns.length; 238 System.arraycopy(lineRuns, 0, result, count, length); 239 count += length; 240 } 241 return result; 242 } 243 244 public BaseBounds getBounds() { 245 ensureLayout(); 246 return logicalBounds; 247 } 248 249 public BaseBounds getBounds(TextSpan filter, BaseBounds bounds) { 250 ensureLayout(); 251 float left = Float.POSITIVE_INFINITY; 252 float top = Float.POSITIVE_INFINITY; 253 float right = Float.NEGATIVE_INFINITY; 254 float bottom = Float.NEGATIVE_INFINITY; 255 if (filter != null) { 256 for (int i = 0; i < lines.length; i++) { 257 TextLine line = lines[i]; 258 TextRun[] lineRuns = line.getRuns(); 259 for (int j = 0; j < lineRuns.length; j++) { 260 TextRun run = lineRuns[j]; 261 TextSpan span = run.getTextSpan(); 262 if (span != filter) continue; 263 Point2D location = run.getLocation(); 264 float runLeft = location.x; 265 if (run.isLeftBearing()) { 266 runLeft += line.getLeftSideBearing(); 267 } 268 float runRight = location.x + run.getWidth(); 269 if (run.isRightBearing()) { 270 runRight += line.getRightSideBearing(); 271 } 272 float runTop = location.y; 273 float runBottom = location.y + line.getBounds().getHeight() + spacing; 274 if (runLeft < left) left = runLeft; 275 if (runTop < top) top = runTop; 276 if (runRight > right) right = runRight; 277 if (runBottom > bottom) bottom = runBottom; 278 } 279 } 280 } else { 281 top = bottom = 0; 282 for (int i = 0; i < lines.length; i++) { 283 TextLine line = lines[i]; 284 RectBounds lineBounds = line.getBounds(); 285 float lineLeft = lineBounds.getMinX() + line.getLeftSideBearing(); 286 if (lineLeft < left) left = lineLeft; 287 float lineRight = lineBounds.getMaxX() + line.getRightSideBearing(); 288 if (lineRight > right) right = lineRight; 289 bottom += lineBounds.getHeight(); 290 } 291 if (isMirrored()) { 292 float width = getMirroringWidth(); 293 float bearing = left; 294 left = width - right; 295 right = width - bearing; 296 } 297 } 298 return bounds.deriveWithNewBounds(left, top, 0, right, bottom, 0); 299 } 300 301 public PathElement[] getCaretShape(int offset, boolean isLeading, 302 float x, float y) { 303 ensureLayout(); 304 int lineIndex = 0; 305 int lineCount = getLineCount(); 306 while (lineIndex < lineCount - 1) { 307 TextLine line = lines[lineIndex]; 308 int lineEnd = line.getStart() + line.getLength(); 309 if (lineEnd > offset) break; 310 lineIndex++; 311 } 312 int sliptCaretOffset = -1; 313 int level = 0; 314 float lineX = 0, lineY = 0, lineHeight = 0; 315 TextLine line = lines[lineIndex]; 316 TextRun[] runs = line.getRuns(); 317 int runCount = runs.length; 318 int runIndex = -1; 319 for (int i = 0; i < runCount; i++) { 320 TextRun run = runs[i]; 321 int runStart = run.getStart(); 322 int runEnd = run.getEnd(); 323 if (runStart <= offset && offset < runEnd) { 324 if (!run.isLinebreak()) { 325 runIndex = i; 326 } 327 break; 328 } 329 } 330 if (runIndex != -1) { 331 TextRun run = runs[runIndex]; 332 int runStart = run.getStart(); 333 Point2D location = run.getLocation(); 334 lineX = location.x + run.getXAtOffset(offset - runStart, isLeading); 335 lineY = location.y; 336 lineHeight = line.getBounds().getHeight(); 337 338 if (isLeading) { 339 if (runIndex > 0 && offset == runStart) { 340 level = run.getLevel(); 341 sliptCaretOffset = offset - 1; 342 } 343 } else { 344 int runEnd = run.getEnd(); 345 if (runIndex + 1 < runs.length && offset + 1 == runEnd) { 346 level = run.getLevel(); 347 sliptCaretOffset = offset + 1; 348 } 349 } 350 } else { 351 /* end of line (line break or offset>=charCount) */ 352 int maxOffset = 0; 353 354 /* set run index to zero to handle empty line case (only break line) */ 355 runIndex = 0; 356 for (int i = 0; i < runCount; i++) { 357 TextRun run = runs[i]; 358 /*use the trailing edge of the last logical run*/ 359 if (run.getStart() >= maxOffset && !run.isLinebreak()) { 360 maxOffset = run.getStart(); 361 runIndex = i; 362 } 363 } 364 TextRun run = runs[runIndex]; 365 Point2D location = run.getLocation(); 366 lineX = location.x + (run.isLeftToRight() ? run.getWidth() : 0); 367 lineY = location.y; 368 lineHeight = line.getBounds().getHeight(); 369 } 370 if (isMirrored()) { 371 lineX = getMirroringWidth() - lineX; 372 } 373 lineX += x; 374 lineY += y; 375 if (sliptCaretOffset != -1) { 376 for (int i = 0; i < runs.length; i++) { 377 TextRun run = runs[i]; 378 int runStart = run.getStart(); 379 int runEnd = run.getEnd(); 380 if (runStart <= sliptCaretOffset && sliptCaretOffset < runEnd) { 381 if ((run.getLevel() & 1) != (level & 1)) { 382 Point2D location = run.getLocation(); 383 float lineX2 = location.x; 384 if (isLeading) { 385 if ((level & 1) != 0) lineX2 += run.getWidth(); 386 } else { 387 if ((level & 1) == 0) lineX2 += run.getWidth(); 388 } 389 if (isMirrored()) { 390 lineX2 = getMirroringWidth() - lineX2; 391 } 392 lineX2 += x; 393 PathElement[] result = new PathElement[4]; 394 result[0] = new MoveTo(lineX, lineY); 395 result[1] = new LineTo(lineX, lineY + lineHeight / 2); 396 result[2] = new MoveTo(lineX2, lineY + lineHeight / 2); 397 result[3] = new LineTo(lineX2, lineY + lineHeight); 398 return result; 399 } 400 } 401 } 402 } 403 PathElement[] result = new PathElement[2]; 404 result[0] = new MoveTo(lineX, lineY); 405 result[1] = new LineTo(lineX, lineY + lineHeight); 406 return result; 407 } 408 409 public Hit getHitInfo(float x, float y) { 410 int charIndex = -1; 411 boolean leading = false; 412 413 ensureLayout(); 414 int lineIndex = getLineIndex(y); 415 if (lineIndex >= getLineCount()) { 416 charIndex = getCharCount(); 417 } else { 418 if (isMirrored()) { 419 x = getMirroringWidth() - x; 420 } 421 TextLine line = lines[lineIndex]; 422 TextRun[] runs = line.getRuns(); 423 RectBounds bounds = line.getBounds(); 424 TextRun run = null; 425 x -= bounds.getMinX(); 426 //TODO binary search 427 for (int i = 0; i < runs.length; i++) { 428 run = runs[i]; 429 if (x < run.getWidth()) break; 430 if (i + 1 < runs.length) { 431 if (runs[i + 1].isLinebreak()) break; 432 x -= run.getWidth(); 433 } 434 } 435 if (run != null) { 436 int[] trailing = new int[1]; 437 charIndex = run.getStart() + run.getOffsetAtX(x, trailing); 438 leading = (trailing[0] == 0); 439 } else { 440 //empty line, set to line break leading 441 charIndex = line.getStart(); 442 leading = true; 443 } 444 } 445 return new Hit(charIndex, -1, leading); 446 } 447 448 public PathElement[] getRange(int start, int end, int type, 449 float x, float y) { 450 ensureLayout(); 451 int lineCount = getLineCount(); 452 ArrayList<PathElement> result = new ArrayList<PathElement>(); 453 float lineY = 0; 454 455 for (int lineIndex = 0; lineIndex < lineCount; lineIndex++) { 456 TextLine line = lines[lineIndex]; 457 RectBounds lineBounds = line.getBounds(); 458 int lineStart = line.getStart(); 459 if (lineStart >= end) break; 460 int lineEnd = lineStart + line.getLength(); 461 if (start > lineEnd) { 462 lineY += lineBounds.getHeight() + spacing; 463 continue; 464 } 465 466 /* The list of runs in the line is visually ordered. 467 * Thus, finding the run that includes the selection end offset 468 * does not mean that all selected runs have being visited. 469 * Instead, this implementation first computes the number of selected 470 * characters in the current line, then iterates over the runs consuming 471 * selected characters till all of them are found. 472 */ 473 TextRun[] runs = line.getRuns(); 474 int count = Math.min(lineEnd, end) - Math.max(lineStart, start); 475 int runIndex = 0; 476 float left = -1; 477 float right = -1; 478 float lineX = lineBounds.getMinX(); 479 while (count > 0 && runIndex < runs.length) { 480 TextRun run = runs[runIndex]; 481 int runStart = run.getStart(); 482 int runEnd = run.getEnd(); 483 float runWidth = run.getWidth(); 484 int clmapStart = Math.max(runStart, Math.min(start, runEnd)); 485 int clampEnd = Math.max(runStart, Math.min(end, runEnd)); 486 int runCount = clampEnd - clmapStart; 487 if (runCount != 0) { 488 boolean ltr = run.isLeftToRight(); 489 float runLeft; 490 if (runStart > start) { 491 runLeft = ltr ? lineX : lineX + runWidth; 492 } else { 493 runLeft = lineX + run.getXAtOffset(start - runStart, true); 494 } 495 float runRight; 496 if (runEnd < end) { 497 runRight = ltr ? lineX + runWidth : lineX; 498 } else { 499 runRight = lineX + run.getXAtOffset(end - runStart, true); 500 } 501 if (runLeft > runRight) { 502 float tmp = runLeft; 503 runLeft = runRight; 504 runRight = tmp; 505 } 506 count -= runCount; 507 float top = 0, bottom = 0; 508 switch (type) { 509 case TYPE_TEXT: 510 top = lineY; 511 bottom = lineY + lineBounds.getHeight(); 512 break; 513 case TYPE_UNDERLINE: 514 case TYPE_STRIKETHROUGH: 515 FontStrike fontStrike = null; 516 if (spans != null) { 517 TextSpan span = run.getTextSpan(); 518 PGFont font = (PGFont)span.getFont(); 519 if (font == null) break; 520 fontStrike = font.getStrike(IDENTITY); 521 } else { 522 fontStrike = strike; 523 } 524 top = lineY - run.getAscent(); 525 Metrics metrics = fontStrike.getMetrics(); 526 if (type == TYPE_UNDERLINE) { 527 top += metrics.getUnderLineOffset(); 528 bottom = top + metrics.getUnderLineThickness(); 529 } else { 530 top += metrics.getStrikethroughOffset(); 531 bottom = top + metrics.getStrikethroughThickness(); 532 } 533 break; 534 } 535 536 /* Merge continuous rectangles */ 537 if (runLeft != right) { 538 if (left != -1 && right != -1) { 539 float l = left, r = right; 540 if (isMirrored()) { 541 float width = getMirroringWidth(); 542 l = width - l; 543 r = width - r; 544 } 545 result.add(new MoveTo(x + l, y + top)); 546 result.add(new LineTo(x + r, y + top)); 547 result.add(new LineTo(x + r, y + bottom)); 548 result.add(new LineTo(x + l, y + bottom)); 549 result.add(new LineTo(x + l, y + top)); 550 } 551 left = runLeft; 552 right = runRight; 553 } 554 right = runRight; 555 if (count == 0) { 556 float l = left, r = right; 557 if (isMirrored()) { 558 float width = getMirroringWidth(); 559 l = width - l; 560 r = width - r; 561 } 562 result.add(new MoveTo(x + l, y + top)); 563 result.add(new LineTo(x + r, y + top)); 564 result.add(new LineTo(x + r, y + bottom)); 565 result.add(new LineTo(x + l, y + bottom)); 566 result.add(new LineTo(x + l, y + top)); 567 } 568 } 569 lineX += runWidth; 570 runIndex++; 571 } 572 lineY += lineBounds.getHeight() + spacing; 573 } 574 return result.toArray(new PathElement[result.size()]); 575 } 576 577 public Shape getShape(int type, TextSpan filter) { 578 ensureLayout(); 579 boolean text = (type & TYPE_TEXT) != 0; 580 boolean underline = (type & TYPE_UNDERLINE) != 0; 581 boolean strikethrough = (type & TYPE_STRIKETHROUGH) != 0; 582 boolean baselineType = (type & TYPE_BASELINE) != 0; 583 if (shape != null && text && !underline && !strikethrough && baselineType) { 584 return shape; 585 } 586 587 Path2D outline = new Path2D(); 588 BaseTransform tx = new Translate2D(0, 0); 589 /* Return a shape relative to the baseline of the first line so 590 * it can be used for layout */ 591 float firstBaseline = 0; 592 if (baselineType) { 593 firstBaseline = -lines[0].getBounds().getMinY(); 594 } 595 for (int i = 0; i < lines.length; i++) { 596 TextLine line = lines[i]; 597 TextRun[] runs = line.getRuns(); 598 RectBounds bounds = line.getBounds(); 599 float baseline = -bounds.getMinY(); 600 for (int j = 0; j < runs.length; j++) { 601 TextRun run = runs[j]; 602 FontStrike fontStrike = null; 603 if (spans != null) { 604 TextSpan span = run.getTextSpan(); 605 if (filter != null && span != filter) continue; 606 PGFont font = (PGFont)span.getFont(); 607 608 /* skip embedded runs */ 609 if (font == null) continue; 610 fontStrike = font.getStrike(IDENTITY); 611 } else { 612 fontStrike = strike; 613 } 614 Point2D location = run.getLocation(); 615 float runX = location.x; 616 float runY = location.y + baseline - firstBaseline; 617 Metrics metrics = null; 618 if (underline || strikethrough) { 619 metrics = fontStrike.getMetrics(); 620 } 621 if (underline) { 622 RoundRectangle2D rect = new RoundRectangle2D(); 623 rect.x = runX; 624 rect.y = runY + metrics.getUnderLineOffset(); 625 rect.width = run.getWidth(); 626 rect.height = metrics.getUnderLineThickness(); 627 outline.append(rect, false); 628 } 629 if (strikethrough) { 630 RoundRectangle2D rect = new RoundRectangle2D(); 631 rect.x = runX; 632 rect.y = runY + metrics.getStrikethroughOffset(); 633 rect.width = run.getWidth(); 634 rect.height = metrics.getStrikethroughThickness(); 635 outline.append(rect, false); 636 } 637 if (text && run.getGlyphCount() > 0) { 638 tx.restoreTransform(1, 0, 0, 1, runX, runY); 639 Path2D path = (Path2D)fontStrike.getOutline(run, tx); 640 outline.append(path, false); 641 } 642 } 643 } 644 645 if (text && !underline && !strikethrough) { 646 shape = outline; 647 } 648 return outline; 649 } 650 651 /*************************************************************************** 652 * * 653 * Text Layout Implementation * 654 * * 655 **************************************************************************/ 656 657 private int getLineIndex(float y) { 658 int index = 0; 659 float bottom = 0; 660 int lineCount = getLineCount(); 661 while (index < lineCount) { 662 bottom += lines[index].getBounds().getHeight() + spacing; 663 if (index + 1 == lineCount) bottom -= lines[index].getLeading(); 664 if (bottom > y) break; 665 index++; 666 } 667 return index; 668 } 669 670 private boolean copyCache() { 671 int align = flags & ALIGN_MASK; 672 int boundsType = flags & BOUNDS_MASK; 673 /* Caching for boundsType == Center, bias towards Modena */ 674 return wrapWidth != 0 || align != ALIGN_LEFT || boundsType == 0 || isMirrored(); 675 } 676 677 private void initCache() { 678 if (cacheKey != null) { 679 if (layoutCache == null) { 680 LayoutCache cache = stringCache.get(cacheKey); 681 if (cache != null && cache.font.equals(font) && Arrays.equals(cache.text, text)) { 682 layoutCache = cache; 683 runs = cache.runs; 684 runCount = cache.runCount; 685 flags |= cache.analysis; 686 } 687 } 688 if (layoutCache != null) { 689 if (copyCache()) { 690 /* This instance has some property that requires it to 691 * build its own lines (i.e. wrapping width). Thus, only use 692 * the runs from the cache (and it needs to make a copy 693 * before using it as they will be modified). 694 * Note: the copy of the elements in the array happens in 695 * reuseRuns(). 696 */ 697 if (layoutCache.runs == runs) { 698 runs = new TextRun[runCount]; 699 System.arraycopy(layoutCache.runs, 0, runs, 0, runCount); 700 } 701 } else { 702 if (layoutCache.lines != null) { 703 runs = layoutCache.runs; 704 runCount = layoutCache.runCount; 705 flags |= layoutCache.analysis; 706 lines = layoutCache.lines; 707 layoutWidth = layoutCache.layoutWidth; 708 layoutHeight = layoutCache.layoutHeight; 709 float ascent = lines[0].getBounds().getMinY(); 710 logicalBounds = logicalBounds.deriveWithNewBounds(0, ascent, 0, 711 layoutWidth, layoutHeight + ascent, 0); 712 } 713 } 714 } 715 } 716 } 717 718 private int getLineCount() { 719 return lines.length; 720 } 721 722 private int getCharCount() { 723 if (text != null) return text.length; 724 int count = 0; 725 for (int i = 0; i < lines.length; i++) { 726 count += lines[i].getLength(); 727 } 728 return count; 729 } 730 731 public TextSpan[] getTextSpans() { 732 return spans; 733 } 734 735 public PGFont getFont() { 736 return font; 737 } 738 739 public int getDirection() { 740 if ((flags & DIRECTION_LTR) != 0) { 741 return Bidi.DIRECTION_LEFT_TO_RIGHT; 742 } 743 if ((flags & DIRECTION_RTL) != 0) { 744 return Bidi.DIRECTION_RIGHT_TO_LEFT; 745 } 746 if ((flags & DIRECTION_DEFAULT_LTR) != 0) { 747 return Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT; 748 } 749 if ((flags & DIRECTION_DEFAULT_RTL) != 0) { 750 return Bidi.DIRECTION_DEFAULT_RIGHT_TO_LEFT; 751 } 752 return Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT; 753 } 754 755 public void addTextRun(TextRun run) { 756 if (runCount + 1 > runs.length) { 757 TextRun[] newRuns = new TextRun[runs.length + 64]; 758 System.arraycopy(runs, 0, newRuns, 0, runs.length); 759 runs = newRuns; 760 } 761 runs[runCount++] = run; 762 } 763 764 private void buildRuns(char[] chars) { 765 runCount = 0; 766 if (runs == null) { 767 int count = Math.max(4, Math.min(chars.length / 16, 16)); 768 runs = new TextRun[count]; 769 } 770 GlyphLayout layout = GlyphLayout.getInstance(); 771 flags = layout.breakRuns(this, chars, flags); 772 layout.dispose(); 773 for (int j = runCount; j < runs.length; j++) { 774 runs[j] = null; 775 } 776 } 777 778 private void shape(TextRun run, char[] chars, GlyphLayout layout) { 779 FontStrike strike; 780 PGFont font; 781 if (spans != null) { 782 if (spans.length == 0) return; 783 TextSpan span = run.getTextSpan(); 784 font = (PGFont)span.getFont(); 785 if (font == null) { 786 RectBounds bounds = span.getBounds(); 787 run.setEmbedded(bounds, span.getText().length()); 788 return; 789 } 790 strike = font.getStrike(IDENTITY); 791 } else { 792 font = this.font; 793 strike = this.strike; 794 } 795 796 /* init metrics for line breaks for empty lines */ 797 if (run.getAscent() == 0) { 798 Metrics m = strike.getMetrics(); 799 800 /* The implementation of the center layoutBounds mode is to assure the 801 * layout has the same number of pixels above and bellow the cap 802 * height. 803 */ 804 if ((flags & BOUNDS_MASK) == BOUNDS_CENTER) { 805 float ascent = m.getAscent(); 806 /* Segoe UI has a very large internal leading area, applying the 807 * center layoutBounds heuristics on it would result in several pixels 808 * being added to the descent. The final results would be 809 * overly large and visually unappealing. The fix is to reduce 810 * the ascent before applying the algorithm. */ 811 if (font.getFamilyName().equals("Segoe UI")) { 812 ascent *= 0.80; 813 } 814 ascent = (int)(ascent-0.75); 815 float descent = (int)(m.getDescent()+0.75); 816 float leading = (int)(m.getLineGap()+0.75); 817 float capHeight = (int)(m.getCapHeight()+0.75); 818 float topPadding = -ascent - capHeight; 819 if (topPadding > descent) { 820 descent = topPadding; 821 } else { 822 ascent += (topPadding - descent); 823 } 824 run.setMetrics(ascent, descent, leading); 825 } else { 826 run.setMetrics(m.getAscent(), m.getDescent(), m.getLineGap()); 827 } 828 } 829 830 if (run.isTab()) return; 831 if (run.isLinebreak()) return; 832 if (run.getGlyphCount() > 0) return; 833 if (run.isComplex()) { 834 /* Use GlyphLayout to shape complex text */ 835 layout.layout(run, font, strike, chars); 836 } else { 837 FontResource fr = strike.getFontResource(); 838 int start = run.getStart(); 839 int length = run.getLength(); 840 841 /* No glyph layout required */ 842 if (layoutCache == null) { 843 float fontSize = strike.getSize(); 844 CharToGlyphMapper mapper = fr.getGlyphMapper(); 845 846 /* The text contains complex and non-complex runs */ 847 int[] glyphs = new int[length]; 848 mapper.charsToGlyphs(start, length, chars, glyphs); 849 float[] positions = new float[(length + 1) << 1]; 850 float xadvance = 0; 851 for (int i = 0; i < length; i++) { 852 float width = fr.getAdvance(glyphs[i], fontSize); 853 positions[i<<1] = xadvance; 854 //yadvance always zero 855 xadvance += width; 856 } 857 positions[length<<1] = xadvance; 858 run.shape(length, glyphs, positions, null); 859 } else { 860 861 /* The text only contains non-complex runs, all the glyphs and 862 * advances are stored in the shapeCache */ 863 if (!layoutCache.valid) { 864 float fontSize = strike.getSize(); 865 CharToGlyphMapper mapper = fr.getGlyphMapper(); 866 mapper.charsToGlyphs(start, length, chars, layoutCache.glyphs, start); 867 int end = start + length; 868 float width = 0; 869 for (int i = start; i < end; i++) { 870 float adv = fr.getAdvance(layoutCache.glyphs[i], fontSize); 871 layoutCache.advances[i] = adv; 872 width += adv; 873 } 874 run.setWidth(width); 875 } 876 run.shape(length, layoutCache.glyphs, layoutCache.advances); 877 } 878 } 879 } 880 881 private TextLine createLine(int start, int end, int startOffset) { 882 int count = end - start + 1; 883 TextRun[] lineRuns = new TextRun[count]; 884 if (start < runCount) { 885 System.arraycopy(runs, start, lineRuns, 0, count); 886 } 887 888 /* Recompute line width, height, and length (wrapping) */ 889 float width = 0, ascent = 0, descent = 0, leading = 0; 890 int length = 0; 891 for (int i = 0; i < lineRuns.length; i++) { 892 TextRun run = lineRuns[i]; 893 width += run.getWidth(); 894 ascent = Math.min(ascent, run.getAscent()); 895 descent = Math.max(descent, run.getDescent()); 896 leading = Math.max(leading, run.getLeading()); 897 length += run.getLength(); 898 } 899 if (width > layoutWidth) layoutWidth = width; 900 return new TextLine(startOffset, length, lineRuns, 901 width, ascent, descent, leading); 902 } 903 904 private void reorderLine(TextLine line) { 905 TextRun[] runs = line.getRuns(); 906 int length = runs.length; 907 if (length > 0 && runs[length - 1].isLinebreak()) { 908 length--; 909 } 910 if (length < 2) return; 911 byte[] levels = new byte[length]; 912 for (int i = 0; i < length; i++) { 913 levels[i] = runs[i].getLevel(); 914 } 915 Bidi.reorderVisually(levels, 0, runs, 0, length); 916 } 917 918 private char[] getText() { 919 if (text == null) { 920 int count = 0; 921 for (int i = 0; i < spans.length; i++) { 922 count += spans[i].getText().length(); 923 } 924 text = new char[count]; 925 int offset = 0; 926 for (int i = 0; i < spans.length; i++) { 927 String string = spans[i].getText(); 928 int length = string.length(); 929 string.getChars(0, length, text, offset); 930 offset += length; 931 } 932 } 933 return text; 934 } 935 936 private boolean isSimpleLayout() { 937 int textAlignment = flags & ALIGN_MASK; 938 boolean justify = wrapWidth > 0 && textAlignment == ALIGN_JUSTIFY; 939 int mask = FLAGS_HAS_BIDI | FLAGS_HAS_COMPLEX; 940 return (flags & mask) == 0 && !justify; 941 } 942 943 private boolean isMirrored() { 944 boolean mirrored = false; 945 switch (flags & DIRECTION_MASK) { 946 case DIRECTION_RTL: mirrored = true; break; 947 case DIRECTION_LTR: mirrored = false; break; 948 case DIRECTION_DEFAULT_LTR: 949 case DIRECTION_DEFAULT_RTL: 950 mirrored = (flags & FLAGS_RTL_BASE) != 0; 951 } 952 return mirrored; 953 } 954 955 private float getMirroringWidth() { 956 /* The text node in the scene layer is mirrored based on 957 * result of computeLayoutBounds. The coordinate translation 958 * in text layout has to be based on the same width. 959 */ 960 return wrapWidth != 0 ? wrapWidth : layoutWidth; 961 } 962 963 private void reuseRuns() { 964 /* The runs list is always accessed by the same thread (as TextLayout 965 * is not thread safe) thus it can be modified at any time, but the 966 * elements inside of the list are shared among threads and cannot be 967 * modified. Each reused element has to be cloned.*/ 968 runCount = 0; 969 int index = 0;; 970 while (index < runs.length) { 971 TextRun run = runs[index]; 972 if (run == null) break; 973 runs[index] = null; 974 index++; 975 runs[runCount++] = run = run.unwrap(); 976 977 if (run.isSplit()) { 978 run.merge(null); /* unmark split */ 979 while (index < runs.length) { 980 TextRun nextRun = runs[index]; 981 if (nextRun == null) break; 982 run.merge(nextRun); 983 runs[index] = null; 984 index++; 985 if (nextRun.isSplitLast()) break; 986 } 987 } 988 } 989 } 990 991 private float getTabAdvance() { 992 float spaceAdvance = 0; 993 if (spans != null) { 994 /* Rich text case - use the first font (for now) */ 995 for (int i = 0; i < spans.length; i++) { 996 TextSpan span = spans[i]; 997 PGFont font = (PGFont)span.getFont(); 998 if (font != null) { 999 FontStrike strike = font.getStrike(IDENTITY); 1000 spaceAdvance = strike.getCharAdvance(' '); 1001 break; 1002 } 1003 } 1004 } else { 1005 spaceAdvance = strike.getCharAdvance(' '); 1006 } 1007 return 8 * spaceAdvance; 1008 } 1009 1010 private void layout() { 1011 /* Try the cache */ 1012 initCache(); 1013 1014 /* Whole layout retrieved from the cache */ 1015 if (lines != null) return; 1016 char[] chars = getText(); 1017 1018 /* runs and runCount are set in reuseRuns or buildRuns */ 1019 if ((flags & FLAGS_ANALYSIS_VALID) != 0 && isSimpleLayout()) { 1020 reuseRuns(); 1021 } else { 1022 buildRuns(chars); 1023 } 1024 1025 GlyphLayout layout = null; 1026 if ((flags & (FLAGS_HAS_COMPLEX)) != 0) { 1027 layout = GlyphLayout.getInstance(); 1028 } 1029 1030 float tabAdvance = 0; 1031 if ((flags & FLAGS_HAS_TABS) != 0) { 1032 tabAdvance = getTabAdvance(); 1033 } 1034 1035 BreakIterator boundary = null; 1036 if (wrapWidth > 0) { 1037 if ((flags & (FLAGS_HAS_COMPLEX | FLAGS_HAS_CJK)) != 0) { 1038 boundary = BreakIterator.getLineInstance(); 1039 boundary.setText(new CharArrayIterator(chars)); 1040 } 1041 } 1042 int textAlignment = flags & ALIGN_MASK; 1043 1044 /* Optimize simple case: reuse the glyphs and advances as long as the 1045 * text and font are the same. 1046 * The simple case is no bidi, no complex, no justify, no features. 1047 */ 1048 1049 if (isSimpleLayout()) { 1050 if (layoutCache == null) { 1051 layoutCache = new LayoutCache(); 1052 layoutCache.glyphs = new int[chars.length]; 1053 layoutCache.advances = new float[chars.length]; 1054 } 1055 } else { 1056 layoutCache = null; 1057 } 1058 1059 float lineWidth = 0; 1060 int startIndex = 0; 1061 int startOffset = 0; 1062 ArrayList<TextLine> linesList = new ArrayList<TextLine>(); 1063 for (int i = 0; i < runCount; i++) { 1064 TextRun run = runs[i]; 1065 shape(run, chars, layout); 1066 if (run.isTab()) { 1067 float tabStop = ((int)(lineWidth / tabAdvance) +1) * tabAdvance; 1068 run.setWidth(tabStop - lineWidth); 1069 } 1070 1071 float runWidth = run.getWidth(); 1072 if (wrapWidth > 0 && lineWidth + runWidth > wrapWidth && !run.isLinebreak()) { 1073 1074 /* Find offset of the first character that does not fit on the line */ 1075 int hitOffset = run.getStart() + run.getWrapIndex(wrapWidth - lineWidth); 1076 1077 /* Only keep whitespaces (not tabs) in the current run to avoid 1078 * dealing with unshaped runs. 1079 */ 1080 int offset = hitOffset; 1081 int runEnd = run.getEnd(); 1082 while (offset + 1 < runEnd && chars[offset] == ' ') { 1083 offset++; 1084 /* Preserve behaviour: only keep one white space in the line 1085 * before wrapping. Needed API to allow change. 1086 */ 1087 break; 1088 } 1089 1090 /* Find the break opportunity */ 1091 int breakOffset = offset; 1092 if (boundary != null) { 1093 /* Use Java BreakIterator when complex script are present */ 1094 breakOffset = boundary.isBoundary(offset) || chars[offset] == '\t' ? offset : boundary.preceding(offset); 1095 } else { 1096 /* Simple break strategy for latin text (Performance) */ 1097 boolean currentChar = Character.isWhitespace(chars[breakOffset]); 1098 while (breakOffset > startOffset) { 1099 boolean previousChar = Character.isWhitespace(chars[breakOffset - 1]); 1100 if (!currentChar && previousChar) break; 1101 currentChar = previousChar; 1102 breakOffset--; 1103 } 1104 } 1105 1106 /* Never break before the line start offset */ 1107 if (breakOffset < startOffset) breakOffset = startOffset; 1108 1109 /* Find the run that contains the break offset */ 1110 int breakRunIndex = startIndex; 1111 TextRun breakRun = null; 1112 while (breakRunIndex < runCount) { 1113 breakRun = runs[breakRunIndex]; 1114 if (breakRun.getEnd() > breakOffset) break; 1115 breakRunIndex++; 1116 } 1117 1118 /* No line breaks between hit offset and line start offset. 1119 * Try character wrapping mode at the hit offset. 1120 */ 1121 if (breakOffset == startOffset) { 1122 breakRun = run; 1123 breakRunIndex = i; 1124 breakOffset = hitOffset; 1125 } 1126 1127 int breakOffsetInRun = breakOffset - breakRun.getStart(); 1128 /* Wrap the entire run to the next (only if it is not the first 1129 * run of the line). 1130 */ 1131 if (breakOffsetInRun == 0 && breakRunIndex != startIndex) { 1132 i = breakRunIndex - 1; 1133 } else { 1134 i = breakRunIndex; 1135 1136 /* The break offset is at the first offset of the first run of the line. 1137 * This happens when the wrap width is smaller than the width require 1138 * to show the first character for the line. 1139 */ 1140 if (breakOffsetInRun == 0) { 1141 breakOffsetInRun++; 1142 } 1143 if (breakOffsetInRun < breakRun.getLength()) { 1144 if (runCount >= runs.length) { 1145 TextRun[] newRuns = new TextRun[runs.length + 64]; 1146 System.arraycopy(runs, 0, newRuns, 0, i + 1); 1147 System.arraycopy(runs, i + 1, newRuns, i + 2, runs.length - i - 1); 1148 runs = newRuns; 1149 } else { 1150 System.arraycopy(runs, i + 1, runs, i + 2, runCount - i - 1); 1151 } 1152 runs[i + 1] = breakRun.split(breakOffsetInRun); 1153 if (breakRun.isComplex()) { 1154 shape(breakRun, chars, layout); 1155 } 1156 runCount++; 1157 } 1158 } 1159 1160 /* No point marking the last run of a line a softbreak */ 1161 if (i + 1 < runCount && !runs[i + 1].isLinebreak()) { 1162 run = runs[i]; 1163 run.setSoftbreak(); 1164 flags |= FLAGS_WRAPPED; 1165 1166 // Tabs should preserve width 1167 1168 /* 1169 * Due to contextual forms (arabic) it is possible this line 1170 * is still too big since the splitting of the arabic run 1171 * changes the shape of boundary glyphs. For now the 1172 * implementation has opted to have the appropriate 1173 * initial/final shapes and allow those glyphs to 1174 * potentially overlap the wrapping width, rather than use 1175 * the medial form within the wrappingWidth. A better place 1176 * to solve this would be TextRun#getWrapIndex - but its TBD 1177 * there too. 1178 */ 1179 } 1180 } 1181 1182 lineWidth += runWidth; 1183 if (run.isBreak()) { 1184 TextLine line = createLine(startIndex, i, startOffset); 1185 linesList.add(line); 1186 startIndex = i + 1; 1187 startOffset += line.getLength(); 1188 lineWidth = 0; 1189 } 1190 } 1191 if (layout != null) layout.dispose(); 1192 1193 linesList.add(createLine(startIndex, runCount - 1, startOffset)); 1194 lines = new TextLine[linesList.size()]; 1195 linesList.toArray(lines); 1196 1197 float fullWidth = Math.max(wrapWidth, layoutWidth); 1198 float lineY = 0; 1199 float align; 1200 if (isMirrored()) { 1201 align = 1; /* Left and Justify */ 1202 if (textAlignment == ALIGN_RIGHT) align = 0; 1203 } else { 1204 align = 0; /* Left and Justify */ 1205 if (textAlignment == ALIGN_RIGHT) align = 1; 1206 } 1207 if (textAlignment == ALIGN_CENTER) align = 0.5f; 1208 for (int i = 0; i < lines.length; i++) { 1209 TextLine line = lines[i]; 1210 int lineStart = line.getStart(); 1211 RectBounds bounds = line.getBounds(); 1212 1213 /* Center and right alignment */ 1214 float lineX = (fullWidth - bounds.getWidth()) * align; 1215 line.setAlignment(lineX); 1216 1217 /* Justify */ 1218 boolean justify = wrapWidth > 0 && textAlignment == ALIGN_JUSTIFY; 1219 if (justify) { 1220 TextRun[] lineRuns = line.getRuns(); 1221 int lineRunCount = lineRuns.length; 1222 if (lineRunCount > 0 && lineRuns[lineRunCount - 1].isSoftbreak()) { 1223 /* count white spaces but skipping trailings whitespaces */ 1224 int lineEnd = lineStart + line.getLength(); 1225 int wsCount = 0; 1226 boolean hitChar = false; 1227 for (int j = lineEnd - 1; j >= lineStart; j--) { 1228 if (!hitChar && chars[j] != ' ') hitChar = true; 1229 if (hitChar && chars[j] == ' ') wsCount++; 1230 } 1231 if (wsCount != 0) { 1232 float inc = (fullWidth - bounds.getWidth()) / wsCount; 1233 done: 1234 for (int j = 0; j < lineRunCount; j++) { 1235 TextRun textRun = lineRuns[j]; 1236 int runStart = textRun.getStart(); 1237 int runEnd = textRun.getEnd(); 1238 for (int k = runStart; k < runEnd; k++) { 1239 // TODO kashidas 1240 if (chars[k] == ' ') { 1241 textRun.justify(k - runStart, inc); 1242 if (--wsCount == 0) break done; 1243 } 1244 } 1245 } 1246 lineX = 0; 1247 line.setAlignment(lineX); 1248 line.setWidth(fullWidth); 1249 } 1250 } 1251 } 1252 1253 if ((flags & FLAGS_HAS_BIDI) != 0) { 1254 reorderLine(line); 1255 } 1256 1257 computeSideBearings(line); 1258 1259 /* Set run location */ 1260 float runX = lineX; 1261 TextRun[] lineRuns = line.getRuns(); 1262 for (int j = 0; j < lineRuns.length; j++) { 1263 TextRun run = lineRuns[j]; 1264 run.setLocation(runX, lineY); 1265 run.setLine(line); 1266 runX += run.getWidth(); 1267 } 1268 if (i + 1 < lines.length) { 1269 lineY = Math.max(lineY, lineY + bounds.getHeight() + spacing); 1270 } else { 1271 lineY += (bounds.getHeight() - line.getLeading()); 1272 } 1273 } 1274 float ascent = lines[0].getBounds().getMinY(); 1275 layoutHeight = lineY; 1276 logicalBounds = logicalBounds.deriveWithNewBounds(0, ascent, 0, layoutWidth, 1277 layoutHeight + ascent, 0); 1278 1279 1280 if (layoutCache != null) { 1281 if (cacheKey != null && !layoutCache.valid && !copyCache()) { 1282 /* After layoutCache is added to the stringCache it can be 1283 * accessed by multiple threads. All the data in it must 1284 * be immutable. See copyCache() for the cases where the entire 1285 * layout is immutable. 1286 */ 1287 layoutCache.font = font; 1288 layoutCache.text = text; 1289 layoutCache.runs = runs; 1290 layoutCache.runCount = runCount; 1291 layoutCache.lines = lines; 1292 layoutCache.layoutWidth = layoutWidth; 1293 layoutCache.layoutHeight = layoutHeight; 1294 layoutCache.analysis = flags & ANALYSIS_MASK; 1295 synchronized (CACHE_SIZE_LOCK) { 1296 int charCount = chars.length; 1297 if (cacheSize + charCount > MAX_CACHE_SIZE) { 1298 stringCache.clear(); 1299 cacheSize = 0; 1300 } 1301 stringCache.put(cacheKey, layoutCache); 1302 cacheSize += charCount; 1303 } 1304 } 1305 layoutCache.valid = true; 1306 } 1307 } 1308 1309 @Override 1310 public BaseBounds getVisualBounds(int type) { 1311 ensureLayout(); 1312 1313 /* Not defined for rich text */ 1314 if (strike == null) { 1315 return null; 1316 } 1317 1318 boolean underline = (type & TYPE_UNDERLINE) != 0; 1319 boolean hasUnderline = (flags & FLAGS_CACHED_UNDERLINE) != 0; 1320 boolean strikethrough = (type & TYPE_STRIKETHROUGH) != 0; 1321 boolean hasStrikethrough = (flags & FLAGS_CACHED_STRIKETHROUGH) != 0; 1322 if (visualBounds != null && underline == hasUnderline 1323 && strikethrough == hasStrikethrough) { 1324 /* Return last cached value */ 1325 return visualBounds; 1326 } 1327 1328 flags &= ~(FLAGS_CACHED_STRIKETHROUGH | FLAGS_CACHED_UNDERLINE); 1329 if (underline) flags |= FLAGS_CACHED_UNDERLINE; 1330 if (strikethrough) flags |= FLAGS_CACHED_STRIKETHROUGH; 1331 visualBounds = new RectBounds(); 1332 1333 float xMin = Float.POSITIVE_INFINITY; 1334 float yMin = Float.POSITIVE_INFINITY; 1335 float xMax = Float.NEGATIVE_INFINITY; 1336 float yMax = Float.NEGATIVE_INFINITY; 1337 float bounds[] = new float[4]; 1338 FontResource fr = strike.getFontResource(); 1339 Metrics metrics = strike.getMetrics(); 1340 float size = strike.getSize(); 1341 for (int i = 0; i < lines.length; i++) { 1342 TextLine line = lines[i]; 1343 TextRun[] runs = line.getRuns(); 1344 for (int j = 0; j < runs.length; j++) { 1345 TextRun run = runs[j]; 1346 Point2D pt = run.getLocation(); 1347 if (run.isLinebreak()) continue; 1348 int glyphCount = run.getGlyphCount(); 1349 for (int gi = 0; gi < glyphCount; gi++) { 1350 int gc = run.getGlyphCode(gi); 1351 if (gc != CharToGlyphMapper.INVISIBLE_GLYPH_ID) { 1352 fr.getGlyphBoundingBox(run.getGlyphCode(gi), size, bounds); 1353 if (bounds[X_MIN_INDEX] != bounds[X_MAX_INDEX]) { 1354 float glyphX = pt.x + run.getPosX(gi); 1355 float glyphY = pt.y + run.getPosY(gi); 1356 float glyphMinX = glyphX + bounds[X_MIN_INDEX]; 1357 float glyphMinY = glyphY - bounds[Y_MAX_INDEX]; 1358 float glyphMaxX = glyphX + bounds[X_MAX_INDEX]; 1359 float glyphMaxY = glyphY - bounds[Y_MIN_INDEX]; 1360 if (glyphMinX < xMin) xMin = glyphMinX; 1361 if (glyphMinY < yMin) yMin = glyphMinY; 1362 if (glyphMaxX > xMax) xMax = glyphMaxX; 1363 if (glyphMaxY > yMax) yMax = glyphMaxY; 1364 } 1365 } 1366 } 1367 if (underline) { 1368 float underlineMinX = pt.x; 1369 float underlineMinY = pt.y + metrics.getUnderLineOffset(); 1370 float underlineMaxX = underlineMinX + run.getWidth(); 1371 float underlineMaxY = underlineMinY + metrics.getUnderLineThickness(); 1372 if (underlineMinX < xMin) xMin = underlineMinX; 1373 if (underlineMinY < yMin) yMin = underlineMinY; 1374 if (underlineMaxX > xMax) xMax = underlineMaxX; 1375 if (underlineMaxY > yMax) yMax = underlineMaxY; 1376 } 1377 if (strikethrough) { 1378 float strikethroughMinX = pt.x; 1379 float strikethroughMinY = pt.y + metrics.getStrikethroughOffset(); 1380 float strikethroughMaxX = strikethroughMinX + run.getWidth(); 1381 float strikethroughMaxY = strikethroughMinY + metrics.getStrikethroughThickness(); 1382 if (strikethroughMinX < xMin) xMin = strikethroughMinX; 1383 if (strikethroughMinY < yMin) yMin = strikethroughMinY; 1384 if (strikethroughMaxX > xMax) xMax = strikethroughMaxX; 1385 if (strikethroughMaxY > yMax) yMax = strikethroughMaxY; 1386 } 1387 } 1388 } 1389 1390 if (xMin < xMax && yMin < yMax) { 1391 visualBounds.setBounds(xMin, yMin, xMax, yMax); 1392 } 1393 return visualBounds; 1394 } 1395 1396 private void computeSideBearings(TextLine line) { 1397 TextRun[] runs = line.getRuns(); 1398 if (runs.length == 0) return; 1399 float bounds[] = new float[4]; 1400 FontResource defaultFontResource = null; 1401 float size = 0; 1402 if (strike != null) { 1403 defaultFontResource = strike.getFontResource(); 1404 size = strike.getSize(); 1405 } 1406 1407 /* The line lsb is the lsb of the first visual character in the line */ 1408 float lsb = 0; 1409 float width = 0; 1410 lsbdone: 1411 for (int i = 0; i < runs.length; i++) { 1412 TextRun run = runs[i]; 1413 int glyphCount = run.getGlyphCount(); 1414 for (int gi = 0; gi < glyphCount; gi++) { 1415 float advance = run.getAdvance(gi); 1416 /* Skip any leading zero-width glyphs in the line */ 1417 if (advance != 0) { 1418 int gc = run.getGlyphCode(gi); 1419 /* Skip any leading invisible glyphs in the line */ 1420 if (gc != CharToGlyphMapper.INVISIBLE_GLYPH_ID) { 1421 FontResource fr = defaultFontResource; 1422 if (fr == null) { 1423 TextSpan span = run.getTextSpan(); 1424 PGFont font = (PGFont)span.getFont(); 1425 /* No need to check font != null (run.glyphCount > 0) */ 1426 size = font.getSize(); 1427 fr = font.getFontResource(); 1428 } 1429 fr.getGlyphBoundingBox(gc, size, bounds); 1430 float glyphLsb = bounds[X_MIN_INDEX]; 1431 lsb = Math.min(0, glyphLsb + width); 1432 run.setLeftBearing(); 1433 break lsbdone; 1434 } 1435 } 1436 width += advance; 1437 } 1438 // tabs 1439 if (glyphCount == 0) { 1440 width += run.getWidth(); 1441 } 1442 } 1443 1444 /* The line rsb is the rsb of the last visual character in the line */ 1445 float rsb = 0; 1446 width = 0; 1447 rsbdone: 1448 for (int i = runs.length - 1; i >= 0 ; i--) { 1449 TextRun run = runs[i]; 1450 int glyphCount = run.getGlyphCount(); 1451 for (int gi = glyphCount - 1; gi >= 0; gi--) { 1452 float advance = run.getAdvance(gi); 1453 /* Skip any trailing zero-width glyphs in the line */ 1454 if (advance != 0) { 1455 int gc = run.getGlyphCode(gi); 1456 /* Skip any trailing invisible glyphs in the line */ 1457 if (gc != CharToGlyphMapper.INVISIBLE_GLYPH_ID) { 1458 FontResource fr = defaultFontResource; 1459 if (fr == null) { 1460 TextSpan span = run.getTextSpan(); 1461 PGFont font = (PGFont)span.getFont(); 1462 /* No need to check font != null (run.glyphCount > 0) */ 1463 size = font.getSize(); 1464 fr = font.getFontResource(); 1465 } 1466 fr.getGlyphBoundingBox(gc, size, bounds); 1467 float glyphRsb = bounds[X_MAX_INDEX] - advance; 1468 rsb = Math.max(0, glyphRsb - width); 1469 run.setRightBearing(); 1470 break rsbdone; 1471 } 1472 } 1473 width += advance; 1474 } 1475 // tabs 1476 if (glyphCount == 0) { 1477 width += run.getWidth(); 1478 } 1479 } 1480 line.setSideBearings(lsb, rsb); 1481 } 1482 }