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