1 /* 2 * Copyright (c) 2012, 2014, 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.HitInfo; 48 import com.sun.javafx.scene.text.TextLayout; 49 import com.sun.javafx.scene.text.TextSpan; 50 import java.text.Bidi; 51 import java.text.BreakIterator; 52 import java.util.ArrayList; 53 import java.util.Arrays; 54 import java.util.Hashtable; 55 56 public class PrismTextLayout implements TextLayout { 57 private static final BaseTransform IDENTITY = BaseTransform.IDENTITY_TRANSFORM; 58 private static final int X_MIN_INDEX = 0; 59 private static final int Y_MIN_INDEX = 1; 60 private static final int X_MAX_INDEX = 2; 61 private static final int Y_MAX_INDEX = 3; 62 63 private static final Hashtable<Integer, LayoutCache> stringCache = new Hashtable<>(); 64 private static final Object CACHE_SIZE_LOCK = new Object(); 65 private static int cacheSize = 0; 66 private static final int MAX_STRING_SIZE = 256; 67 private static final int MAX_CACHE_SIZE = PrismFontFactory.cacheLayoutSize; 68 69 private char[] text; 70 private TextSpan[] spans; /* Rich text (null for single font text) */ 71 private PGFont font; /* Single font text (null for rich text) */ 72 private FontStrike strike; /* cached strike of font (identity) */ 73 private Integer cacheKey; 74 private TextLine[] lines; 75 private TextRun[] runs; 76 private int runCount; 77 private BaseBounds logicalBounds; 78 private RectBounds visualBounds; 79 private float layoutWidth, layoutHeight; 80 private float wrapWidth, spacing; 81 private LayoutCache layoutCache; 82 private Shape shape; 83 private int flags; 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 HitInfo getHitInfo(float x, float y) { 411 ensureLayout(); 412 HitInfo info = new HitInfo(); 413 int lineIndex = getLineIndex(y); 414 if (lineIndex >= getLineCount()) { 415 info.setCharIndex(getCharCount()); 416 } else { 417 if (isMirrored()) { 418 x = getMirroringWidth() - x; 419 } 420 TextLine line = lines[lineIndex]; 421 TextRun[] runs = line.getRuns(); 422 RectBounds bounds = line.getBounds(); 423 TextRun run = null; 424 x -= bounds.getMinX(); 425 //TODO binary search 426 for (int i = 0; i < runs.length; i++) { 427 run = runs[i]; 428 if (x < run.getWidth()) break; 429 if (i + 1 < runs.length) { 430 if (runs[i + 1].isLinebreak()) break; 431 x -= run.getWidth(); 432 } 433 } 434 if (run != null) { 435 int[] trailing = new int[1]; 436 info.setCharIndex(run.getStart() + run.getOffsetAtX(x, trailing)); 437 info.setLeading(trailing[0] == 0); 438 } else { 439 //empty line, set to line break leading 440 info.setCharIndex(line.getStart()); 441 info.setLeading(true); 442 } 443 } 444 return info; 445 } 446 447 public PathElement[] getRange(int start, int end, int type, 448 float x, float y) { 449 ensureLayout(); 450 int lineCount = getLineCount(); 451 ArrayList<PathElement> result = new ArrayList<PathElement>(); 452 float lineY = 0; 453 454 for (int lineIndex = 0; lineIndex < lineCount; lineIndex++) { 455 TextLine line = lines[lineIndex]; 456 RectBounds lineBounds = line.getBounds(); 457 int lineStart = line.getStart(); 458 if (lineStart >= end) break; 459 int lineEnd = lineStart + line.getLength(); 460 if (start > lineEnd) { 461 lineY += lineBounds.getHeight() + spacing; 462 continue; 463 } 464 465 /* The list of runs in the line is visually ordered. 466 * Thus, finding the run that includes the selection end offset 467 * does not mean that all selected runs have being visited. 468 * Instead, this implementation first computes the number of selected 469 * characters in the current line, then iterates over the runs consuming 470 * selected characters till all of them are found. 471 */ 472 TextRun[] runs = line.getRuns(); 473 int count = Math.min(lineEnd, end) - Math.max(lineStart, start); 474 int runIndex = 0; 475 float left = -1; 476 float right = -1; 477 float lineX = lineBounds.getMinX(); 478 while (count > 0 && runIndex < runs.length) { 479 TextRun run = runs[runIndex]; 480 int runStart = run.getStart(); 481 int runEnd = run.getEnd(); 482 float runWidth = run.getWidth(); 483 int clmapStart = Math.max(runStart, Math.min(start, runEnd)); 484 int clampEnd = Math.max(runStart, Math.min(end, runEnd)); 485 int runCount = clampEnd - clmapStart; 486 if (runCount != 0) { 487 boolean ltr = run.isLeftToRight(); 488 float runLeft; 489 if (runStart > start) { 490 runLeft = ltr ? lineX : lineX + runWidth; 491 } else { 492 runLeft = lineX + run.getXAtOffset(start - runStart, true); 493 } 494 float runRight; 495 if (runEnd < end) { 496 runRight = ltr ? lineX + runWidth : lineX; 497 } else { 498 runRight = lineX + run.getXAtOffset(end - runStart, true); 499 } 500 if (runLeft > runRight) { 501 float tmp = runLeft; 502 runLeft = runRight; 503 runRight = tmp; 504 } 505 count -= runCount; 506 float top = 0, bottom = 0; 507 switch (type) { 508 case TYPE_TEXT: 509 top = lineY; 510 bottom = lineY + lineBounds.getHeight(); 511 break; 512 case TYPE_UNDERLINE: 513 case TYPE_STRIKETHROUGH: 514 FontStrike fontStrike = null; 515 if (spans != null) { 516 TextSpan span = run.getTextSpan(); 517 PGFont font = (PGFont)span.getFont(); 518 if (font == null) break; 519 fontStrike = font.getStrike(IDENTITY); 520 } else { 521 fontStrike = strike; 522 } 523 top = lineY - run.getAscent(); 524 Metrics metrics = fontStrike.getMetrics(); 525 if (type == TYPE_UNDERLINE) { 526 top += metrics.getUnderLineOffset(); 527 bottom = top + metrics.getUnderLineThickness(); 528 } else { 529 top += metrics.getStrikethroughOffset(); 530 bottom = top + metrics.getStrikethroughThickness(); 531 } 532 break; 533 } 534 535 /* Merge continuous rectangles */ 536 if (runLeft != right) { 537 if (left != -1 && right != -1) { 538 float l = left, r = right; 539 if (isMirrored()) { 540 float width = getMirroringWidth(); 541 l = width - l; 542 r = width - r; 543 } 544 result.add(new MoveTo(x + l, y + top)); 545 result.add(new LineTo(x + r, y + top)); 546 result.add(new LineTo(x + r, y + bottom)); 547 result.add(new LineTo(x + l, y + bottom)); 548 result.add(new LineTo(x + l, y + top)); 549 } 550 left = runLeft; 551 right = runRight; 552 } 553 right = runRight; 554 if (count == 0) { 555 float l = left, r = right; 556 if (isMirrored()) { 557 float width = getMirroringWidth(); 558 l = width - l; 559 r = width - r; 560 } 561 result.add(new MoveTo(x + l, y + top)); 562 result.add(new LineTo(x + r, y + top)); 563 result.add(new LineTo(x + r, y + bottom)); 564 result.add(new LineTo(x + l, y + bottom)); 565 result.add(new LineTo(x + l, y + top)); 566 } 567 } 568 lineX += runWidth; 569 runIndex++; 570 } 571 lineY += lineBounds.getHeight() + spacing; 572 } 573 return result.toArray(new PathElement[result.size()]); 574 } 575 576 public Shape getShape(int type, TextSpan filter) { 577 ensureLayout(); 578 boolean text = (type & TYPE_TEXT) != 0; 579 boolean underline = (type & TYPE_UNDERLINE) != 0; 580 boolean strikethrough = (type & TYPE_STRIKETHROUGH) != 0; 581 boolean baselineType = (type & TYPE_BASELINE) != 0; 582 if (shape != null && text && !underline && !strikethrough && baselineType) { 583 return shape; 584 } 585 586 Path2D outline = new Path2D(); 587 BaseTransform tx = new Translate2D(0, 0); 588 /* Return a shape relative to the baseline of the first line so 589 * it can be used for layout */ 590 float firstBaseline = 0; 591 if (baselineType) { 592 firstBaseline = -lines[0].getBounds().getMinY(); 593 } 594 for (int i = 0; i < lines.length; i++) { 595 TextLine line = lines[i]; 596 TextRun[] runs = line.getRuns(); 597 RectBounds bounds = line.getBounds(); 598 float baseline = -bounds.getMinY(); 599 for (int j = 0; j < runs.length; j++) { 600 TextRun run = runs[j]; 601 FontStrike fontStrike = null; 602 if (spans != null) { 603 TextSpan span = run.getTextSpan(); 604 if (filter != null && span != filter) continue; 605 PGFont font = (PGFont)span.getFont(); 606 607 /* skip embedded runs */ 608 if (font == null) continue; 609 fontStrike = font.getStrike(IDENTITY); 610 } else { 611 fontStrike = strike; 612 } 613 Point2D location = run.getLocation(); 614 float runX = location.x; 615 float runY = location.y + baseline - firstBaseline; 616 Metrics metrics = null; 617 if (underline || strikethrough) { 618 metrics = fontStrike.getMetrics(); 619 } 620 if (underline) { 621 RoundRectangle2D rect = new RoundRectangle2D(); 622 rect.x = runX; 623 rect.y = runY + metrics.getUnderLineOffset(); 624 rect.width = run.getWidth(); 625 rect.height = metrics.getUnderLineThickness(); 626 outline.append(rect, false); 627 } 628 if (strikethrough) { 629 RoundRectangle2D rect = new RoundRectangle2D(); 630 rect.x = runX; 631 rect.y = runY + metrics.getStrikethroughOffset(); 632 rect.width = run.getWidth(); 633 rect.height = metrics.getStrikethroughThickness(); 634 outline.append(rect, false); 635 } 636 if (text && run.getGlyphCount() > 0) { 637 tx.restoreTransform(1, 0, 0, 1, runX, runY); 638 Path2D path = (Path2D)fontStrike.getOutline(run, tx); 639 outline.append(path, false); 640 } 641 } 642 } 643 644 if (text && !underline && !strikethrough) { 645 shape = outline; 646 } 647 return outline; 648 } 649 650 /*************************************************************************** 651 * * 652 * Text Layout Implementation * 653 * * 654 **************************************************************************/ 655 656 private int getLineIndex(float y) { 657 int index = 0; 658 float bottom = 0; 659 int lineCount = getLineCount(); 660 while (index < lineCount) { 661 bottom += lines[index].getBounds().getHeight() + spacing; 662 if (index + 1 == lineCount) bottom -= lines[index].getLeading(); 663 if (bottom > y) break; 664 index++; 665 } 666 return index; 667 } 668 669 private boolean copyCache() { 670 int align = flags & ALIGN_MASK; 671 int boundsType = flags & BOUNDS_MASK; 672 /* Caching for boundsType == Center, bias towards Modena */ 673 return wrapWidth != 0 || align != ALIGN_LEFT || boundsType == 0 || isMirrored(); 674 } 675 676 private void initCache() { 677 if (cacheKey != null) { 678 if (layoutCache == null) { 679 LayoutCache cache = stringCache.get(cacheKey); 680 if (cache != null && cache.font.equals(font) && Arrays.equals(cache.text, text)) { 681 layoutCache = cache; 682 runs = cache.runs; 683 runCount = cache.runCount; 684 flags |= cache.analysis; 685 } 686 } 687 if (layoutCache != null) { 688 if (copyCache()) { 689 /* This instance has some property that requires it to 690 * build its own lines (i.e. wrapping width). Thus, only use 691 * the runs from the cache (and it needs to make a copy 692 * before using it as they will be modified). 693 * Note: the copy of the elements in the array happens in 694 * reuseRuns(). 695 */ 696 if (layoutCache.runs == runs) { 697 runs = new TextRun[runCount]; 698 System.arraycopy(layoutCache.runs, 0, runs, 0, runCount); 699 } 700 } else { 701 if (layoutCache.lines != null) { 702 runs = layoutCache.runs; 703 runCount = layoutCache.runCount; 704 flags |= layoutCache.analysis; 705 lines = layoutCache.lines; 706 layoutWidth = layoutCache.layoutWidth; 707 layoutHeight = layoutCache.layoutHeight; 708 float ascent = lines[0].getBounds().getMinY(); 709 logicalBounds = logicalBounds.deriveWithNewBounds(0, ascent, 0, 710 layoutWidth, layoutHeight + ascent, 0); 711 } 712 } 713 } 714 } 715 } 716 717 private int getLineCount() { 718 return lines.length; 719 } 720 721 private int getCharCount() { 722 if (text != null) return text.length; 723 int count = 0; 724 for (int i = 0; i < lines.length; i++) { 725 count += lines[i].getLength(); 726 } 727 return count; 728 } 729 730 public TextSpan[] getTextSpans() { 731 return spans; 732 } 733 734 public PGFont getFont() { 735 return font; 736 } 737 738 public int getDirection() { 739 if ((flags & DIRECTION_LTR) != 0) { 740 return Bidi.DIRECTION_LEFT_TO_RIGHT; 741 } 742 if ((flags & DIRECTION_RTL) != 0) { 743 return Bidi.DIRECTION_RIGHT_TO_LEFT; 744 } 745 if ((flags & DIRECTION_DEFAULT_LTR) != 0) { 746 return Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT; 747 } 748 if ((flags & DIRECTION_DEFAULT_RTL) != 0) { 749 return Bidi.DIRECTION_DEFAULT_RIGHT_TO_LEFT; 750 } 751 return Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT; 752 } 753 754 public void addTextRun(TextRun run) { 755 if (runCount + 1 > runs.length) { 756 TextRun[] newRuns = new TextRun[runs.length + 64]; 757 System.arraycopy(runs, 0, newRuns, 0, runs.length); 758 runs = newRuns; 759 } 760 runs[runCount++] = run; 761 } 762 763 private void buildRuns(char[] chars) { 764 runCount = 0; 765 if (runs == null) { 766 int count = Math.max(4, Math.min(chars.length / 16, 16)); 767 runs = new TextRun[count]; 768 } 769 GlyphLayout layout = GlyphLayout.getInstance(); 770 flags = layout.breakRuns(this, chars, flags); 771 layout.dispose(); 772 for (int j = runCount; j < runs.length; j++) { 773 runs[j] = null; 774 } 775 } 776 777 private void shape(TextRun run, char[] chars, GlyphLayout layout) { 778 FontStrike strike; 779 PGFont font; 780 if (spans != null) { 781 if (spans.length == 0) return; 782 TextSpan span = run.getTextSpan(); 783 font = (PGFont)span.getFont(); 784 if (font == null) { 785 RectBounds bounds = span.getBounds(); 786 run.setEmbedded(bounds, span.getText().length()); 787 return; 788 } 789 strike = font.getStrike(IDENTITY); 790 } else { 791 font = this.font; 792 strike = this.strike; 793 } 794 795 /* init metrics for line breaks for empty lines */ 796 if (run.getAscent() == 0) { 797 Metrics m = strike.getMetrics(); 798 799 /* The implementation of the center layoutBounds mode is to assure the 800 * layout has the same number of pixels above and bellow the cap 801 * height. 802 */ 803 if ((flags & BOUNDS_MASK) == BOUNDS_CENTER) { 804 float ascent = m.getAscent(); 805 /* Segoe UI has a very large internal leading area, applying the 806 * center layoutBounds heuristics on it would result in several pixels 807 * being added to the descent. The final results would be 808 * overly large and visually unappealing. The fix is to reduce 809 * the ascent before applying the algorithm. */ 810 if (font.getFamilyName().equals("Segoe UI")) { 811 ascent *= 0.80; 812 } 813 ascent = (int)(ascent-0.75); 814 float descent = (int)(m.getDescent()+0.75); 815 float leading = (int)(m.getLineGap()+0.75); 816 float capHeight = (int)(m.getCapHeight()+0.75); 817 float topPadding = -ascent - capHeight; 818 if (topPadding > descent) { 819 descent = topPadding; 820 } else { 821 ascent += (topPadding - descent); 822 } 823 run.setMetrics(ascent, descent, leading); 824 } else { 825 run.setMetrics(m.getAscent(), m.getDescent(), m.getLineGap()); 826 } 827 } 828 829 if (run.isTab()) return; 830 if (run.isLinebreak()) return; 831 if (run.getGlyphCount() > 0) return; 832 if (run.isComplex()) { 833 /* Use GlyphLayout to shape complex text */ 834 layout.layout(run, font, strike, chars); 835 } else { 836 FontResource fr = strike.getFontResource(); 837 int start = run.getStart(); 838 int length = run.getLength(); 839 840 /* No glyph layout required */ 841 if (layoutCache == null) { 842 float fontSize = strike.getSize(); 843 CharToGlyphMapper mapper = fr.getGlyphMapper(); 844 845 /* The text contains complex and non-complex runs */ 846 int[] glyphs = new int[length]; 847 mapper.charsToGlyphs(start, length, chars, glyphs); 848 float[] positions = new float[(length + 1) << 1]; 849 float xadvance = 0; 850 for (int i = 0; i < length; i++) { 851 float width = fr.getAdvance(glyphs[i], fontSize); 852 positions[i<<1] = xadvance; 853 //yadvance always zero 854 xadvance += width; 855 } 856 positions[length<<1] = xadvance; 857 run.shape(length, glyphs, positions, null); 858 } else { 859 860 /* The text only contains non-complex runs, all the glyphs and 861 * advances are stored in the shapeCache */ 862 if (!layoutCache.valid) { 863 float fontSize = strike.getSize(); 864 CharToGlyphMapper mapper = fr.getGlyphMapper(); 865 mapper.charsToGlyphs(start, length, chars, layoutCache.glyphs, start); 866 int end = start + length; 867 float width = 0; 868 for (int i = start; i < end; i++) { 869 float adv = fr.getAdvance(layoutCache.glyphs[i], fontSize); 870 layoutCache.advances[i] = adv; 871 width += adv; 872 } 873 run.setWidth(width); 874 } 875 run.shape(length, layoutCache.glyphs, layoutCache.advances); 876 } 877 } 878 } 879 880 private TextLine createLine(int start, int end, int startOffset) { 881 int count = end - start + 1; 882 TextRun[] lineRuns = new TextRun[count]; 883 if (start < runCount) { 884 System.arraycopy(runs, start, lineRuns, 0, count); 885 } 886 887 /* Recompute line width, height, and length (wrapping) */ 888 float width = 0, ascent = 0, descent = 0, leading = 0; 889 int length = 0; 890 for (int i = 0; i < lineRuns.length; i++) { 891 TextRun run = lineRuns[i]; 892 width += run.getWidth(); 893 ascent = Math.min(ascent, run.getAscent()); 894 descent = Math.max(descent, run.getDescent()); 895 leading = Math.max(leading, run.getLeading()); 896 length += run.getLength(); 897 } 898 if (width > layoutWidth) layoutWidth = width; 899 return new TextLine(startOffset, length, lineRuns, 900 width, ascent, descent, leading); 901 } 902 903 private void reorderLine(TextLine line) { 904 TextRun[] runs = line.getRuns(); 905 int length = runs.length; 906 if (length > 0 && runs[length - 1].isLinebreak()) { 907 length--; 908 } 909 if (length < 2) return; 910 byte[] levels = new byte[length]; 911 for (int i = 0; i < length; i++) { 912 levels[i] = runs[i].getLevel(); 913 } 914 Bidi.reorderVisually(levels, 0, runs, 0, length); 915 } 916 917 private char[] getText() { 918 if (text == null) { 919 int count = 0; 920 for (int i = 0; i < spans.length; i++) { 921 count += spans[i].getText().length(); 922 } 923 text = new char[count]; 924 int offset = 0; 925 for (int i = 0; i < spans.length; i++) { 926 String string = spans[i].getText(); 927 int length = string.length(); 928 string.getChars(0, length, text, offset); 929 offset += length; 930 } 931 } 932 return text; 933 } 934 935 private boolean isSimpleLayout() { 936 int textAlignment = flags & ALIGN_MASK; 937 boolean justify = wrapWidth > 0 && textAlignment == ALIGN_JUSTIFY; 938 int mask = FLAGS_HAS_BIDI | FLAGS_HAS_COMPLEX; 939 return (flags & mask) == 0 && !justify; 940 } 941 942 private boolean isMirrored() { 943 boolean mirrored = false; 944 switch (flags & DIRECTION_MASK) { 945 case DIRECTION_RTL: mirrored = true; break; 946 case DIRECTION_LTR: mirrored = false; break; 947 case DIRECTION_DEFAULT_LTR: 948 case DIRECTION_DEFAULT_RTL: 949 mirrored = (flags & FLAGS_RTL_BASE) != 0; 950 } 951 return mirrored; 952 } 953 954 private float getMirroringWidth() { 955 /* The text node in the scene layer is mirrored based on 956 * result of impl_computeLayoutBounds. The coordinate translation 957 * in text layout has to be based on the same width. 958 */ 959 return wrapWidth != 0 ? wrapWidth : layoutWidth; 960 } 961 962 private void reuseRuns() { 963 /* The runs list is always accessed by the same thread (as TextLayout 964 * is not thread safe) thus it can be modified at any time, but the 965 * elements inside of the list are shared among threads and cannot be 966 * modified. Each reused element has to be cloned.*/ 967 runCount = 0; 968 int index = 0;; 969 while (index < runs.length) { 970 TextRun run = runs[index]; 971 if (run == null) break; 972 runs[index] = null; 973 index++; 974 runs[runCount++] = run = run.unwrap(); 975 976 if (run.isSplit()) { 977 run.merge(null); /* unmark split */ 978 while (index < runs.length) { 979 TextRun nextRun = runs[index]; 980 if (nextRun == null) break; 981 run.merge(nextRun); 982 runs[index] = null; 983 index++; 984 if (nextRun.isSplitLast()) break; 985 } 986 } 987 } 988 } 989 990 private float getTabAdvance() { 991 float spaceAdvance = 0; 992 if (spans != null) { 993 /* Rich text case - use the first font (for now) */ 994 for (int i = 0; i < spans.length; i++) { 995 TextSpan span = spans[i]; 996 PGFont font = (PGFont)span.getFont(); 997 if (font != null) { 998 FontStrike strike = font.getStrike(IDENTITY); 999 spaceAdvance = strike.getCharAdvance(' '); 1000 break; 1001 } 1002 } 1003 } else { 1004 spaceAdvance = strike.getCharAdvance(' '); 1005 } 1006 return 8 * spaceAdvance; 1007 } 1008 1009 private void layout() { 1010 /* Try the cache */ 1011 initCache(); 1012 1013 /* Whole layout retrieved from the cache */ 1014 if (lines != null) return; 1015 char[] chars = getText(); 1016 1017 /* runs and runCount are set in reuseRuns or buildRuns */ 1018 if ((flags & FLAGS_ANALYSIS_VALID) != 0 && isSimpleLayout()) { 1019 reuseRuns(); 1020 } else { 1021 buildRuns(chars); 1022 } 1023 1024 GlyphLayout layout = null; 1025 if ((flags & (FLAGS_HAS_COMPLEX)) != 0) { 1026 layout = GlyphLayout.getInstance(); 1027 } 1028 1029 float tabAdvance = 0; 1030 if ((flags & FLAGS_HAS_TABS) != 0) { 1031 tabAdvance = getTabAdvance(); 1032 } 1033 1034 BreakIterator boundary = null; 1035 if (wrapWidth > 0) { 1036 if ((flags & (FLAGS_HAS_COMPLEX | FLAGS_HAS_CJK)) != 0) { 1037 boundary = BreakIterator.getLineInstance(); 1038 boundary.setText(new CharArrayIterator(chars)); 1039 } 1040 } 1041 int textAlignment = flags & ALIGN_MASK; 1042 1043 /* Optimize simple case: reuse the glyphs and advances as long as the 1044 * text and font are the same. 1045 * The simple case is no bidi, no complex, no justify, no features. 1046 */ 1047 1048 if (isSimpleLayout()) { 1049 if (layoutCache == null) { 1050 layoutCache = new LayoutCache(); 1051 layoutCache.glyphs = new int[chars.length]; 1052 layoutCache.advances = new float[chars.length]; 1053 } 1054 } else { 1055 layoutCache = null; 1056 } 1057 1058 float lineWidth = 0; 1059 int startIndex = 0; 1060 int startOffset = 0; 1061 ArrayList<TextLine> linesList = new ArrayList<TextLine>(); 1062 for (int i = 0; i < runCount; i++) { 1063 TextRun run = runs[i]; 1064 shape(run, chars, layout); 1065 if (run.isTab()) { 1066 float tabStop = ((int)(lineWidth / tabAdvance) +1) * tabAdvance; 1067 run.setWidth(tabStop - lineWidth); 1068 } 1069 1070 float runWidth = run.getWidth(); 1071 if (wrapWidth > 0 && lineWidth + runWidth > wrapWidth && !run.isLinebreak()) { 1072 1073 /* Find offset of the first character that does not fit on the line */ 1074 int hitOffset = run.getStart() + run.getWrapIndex(wrapWidth - lineWidth); 1075 1076 /* Only keep whitespaces (not tabs) in the current run to avoid 1077 * dealing with unshaped runs. 1078 */ 1079 int offset = hitOffset; 1080 int runEnd = run.getEnd(); 1081 while (offset + 1 < runEnd && chars[offset] == ' ') { 1082 offset++; 1083 /* Preserve behaviour: only keep one white space in the line 1084 * before wrapping. Needed API to allow change. 1085 */ 1086 break; 1087 } 1088 1089 /* Find the break opportunity */ 1090 int breakOffset = offset; 1091 if (boundary != null) { 1092 /* Use Java BreakIterator when complex script are present */ 1093 breakOffset = boundary.isBoundary(offset) || chars[offset] == '\t' ? offset : boundary.preceding(offset); 1094 } else { 1095 /* Simple break strategy for latin text (Performance) */ 1096 boolean currentChar = Character.isWhitespace(chars[breakOffset]); 1097 while (breakOffset > startOffset) { 1098 boolean previousChar = Character.isWhitespace(chars[breakOffset - 1]); 1099 if (!currentChar && previousChar) break; 1100 currentChar = previousChar; 1101 breakOffset--; 1102 } 1103 } 1104 1105 /* Never break before the line start offset */ 1106 if (breakOffset < startOffset) breakOffset = startOffset; 1107 1108 /* Find the run that contains the break offset */ 1109 int breakRunIndex = startIndex; 1110 TextRun breakRun = null; 1111 while (breakRunIndex < runCount) { 1112 breakRun = runs[breakRunIndex]; 1113 if (breakRun.getEnd() > breakOffset) break; 1114 breakRunIndex++; 1115 } 1116 1117 /* No line breaks between hit offset and line start offset. 1118 * Try character wrapping mode at the hit offset. 1119 */ 1120 if (breakOffset == startOffset) { 1121 breakRun = run; 1122 breakRunIndex = i; 1123 breakOffset = hitOffset; 1124 } 1125 1126 int breakOffsetInRun = breakOffset - breakRun.getStart(); 1127 /* Wrap the entire run to the next (only if it is not the first 1128 * run of the line). 1129 */ 1130 if (breakOffsetInRun == 0 && breakRunIndex != startIndex) { 1131 i = breakRunIndex - 1; 1132 } else { 1133 i = breakRunIndex; 1134 1135 /* The break offset is at the first offset of the first run of the line. 1136 * This happens when the wrap width is smaller than the width require 1137 * to show the first character for the line. 1138 */ 1139 if (breakOffsetInRun == 0) { 1140 breakOffsetInRun++; 1141 } 1142 if (breakOffsetInRun < breakRun.getLength()) { 1143 if (runCount >= runs.length) { 1144 TextRun[] newRuns = new TextRun[runs.length + 64]; 1145 System.arraycopy(runs, 0, newRuns, 0, i + 1); 1146 System.arraycopy(runs, i + 1, newRuns, i + 2, runs.length - i - 1); 1147 runs = newRuns; 1148 } else { 1149 System.arraycopy(runs, i + 1, runs, i + 2, runCount - i - 1); 1150 } 1151 runs[i + 1] = breakRun.split(breakOffsetInRun); 1152 if (breakRun.isComplex()) { 1153 shape(breakRun, chars, layout); 1154 } 1155 runCount++; 1156 } 1157 } 1158 1159 /* No point marking the last run of a line a softbreak */ 1160 if (i + 1 < runCount && !runs[i + 1].isLinebreak()) { 1161 run = runs[i]; 1162 run.setSoftbreak(); 1163 flags |= FLAGS_WRAPPED; 1164 1165 // Tabs should preserve width 1166 1167 /* 1168 * Due to contextual forms (arabic) it is possible this line 1169 * is still too big since the splitting of the arabic run 1170 * changes the shape of boundary glyphs. For now the 1171 * implementation has opted to have the appropriate 1172 * initial/final shapes and allow those glyphs to 1173 * potentially overlap the wrapping width, rather than use 1174 * the medial form within the wrappingWidth. A better place 1175 * to solve this would be TextRun#getWrapIndex - but its TBD 1176 * there too. 1177 */ 1178 } 1179 } 1180 1181 lineWidth += runWidth; 1182 if (run.isBreak()) { 1183 TextLine line = createLine(startIndex, i, startOffset); 1184 linesList.add(line); 1185 startIndex = i + 1; 1186 startOffset += line.getLength(); 1187 lineWidth = 0; 1188 } 1189 } 1190 if (layout != null) layout.dispose(); 1191 1192 linesList.add(createLine(startIndex, runCount - 1, startOffset)); 1193 lines = new TextLine[linesList.size()]; 1194 linesList.toArray(lines); 1195 1196 float fullWidth = Math.max(wrapWidth, layoutWidth); 1197 float lineY = 0; 1198 float align; 1199 if (isMirrored()) { 1200 align = 1; /* Left and Justify */ 1201 if (textAlignment == ALIGN_RIGHT) align = 0; 1202 } else { 1203 align = 0; /* Left and Justify */ 1204 if (textAlignment == ALIGN_RIGHT) align = 1; 1205 } 1206 if (textAlignment == ALIGN_CENTER) align = 0.5f; 1207 for (int i = 0; i < lines.length; i++) { 1208 TextLine line = lines[i]; 1209 int lineStart = line.getStart(); 1210 RectBounds bounds = line.getBounds(); 1211 1212 /* Center and right alignment */ 1213 float lineX = (fullWidth - bounds.getWidth()) * align; 1214 line.setAlignment(lineX); 1215 1216 /* Justify */ 1217 boolean justify = wrapWidth > 0 && textAlignment == ALIGN_JUSTIFY; 1218 if (justify) { 1219 TextRun[] lineRuns = line.getRuns(); 1220 int lineRunCount = lineRuns.length; 1221 if (lineRunCount > 0 && lineRuns[lineRunCount - 1].isSoftbreak()) { 1222 /* count white spaces but skipping trailings whitespaces */ 1223 int lineEnd = lineStart + line.getLength(); 1224 int wsCount = 0; 1225 boolean hitChar = false; 1226 for (int j = lineEnd - 1; j >= lineStart; j--) { 1227 if (!hitChar && chars[j] != ' ') hitChar = true; 1228 if (hitChar && chars[j] == ' ') wsCount++; 1229 } 1230 if (wsCount != 0) { 1231 float inc = (fullWidth - bounds.getWidth()) / wsCount; 1232 done: 1233 for (int j = 0; j < lineRunCount; j++) { 1234 TextRun textRun = lineRuns[j]; 1235 int runStart = textRun.getStart(); 1236 int runEnd = textRun.getEnd(); 1237 for (int k = runStart; k < runEnd; k++) { 1238 // TODO kashidas 1239 if (chars[k] == ' ') { 1240 textRun.justify(k - runStart, inc); 1241 if (--wsCount == 0) break done; 1242 } 1243 } 1244 } 1245 lineX = 0; 1246 line.setAlignment(lineX); 1247 line.setWidth(fullWidth); 1248 } 1249 } 1250 } 1251 1252 if ((flags & FLAGS_HAS_BIDI) != 0) { 1253 reorderLine(line); 1254 } 1255 1256 computeSideBearings(line); 1257 1258 /* Set run location */ 1259 float runX = lineX; 1260 TextRun[] lineRuns = line.getRuns(); 1261 for (int j = 0; j < lineRuns.length; j++) { 1262 TextRun run = lineRuns[j]; 1263 run.setLocation(runX, lineY); 1264 run.setLine(line); 1265 runX += run.getWidth(); 1266 } 1267 if (i + 1 < lines.length) { 1268 lineY = Math.max(lineY, lineY + bounds.getHeight() + spacing); 1269 } else { 1270 lineY += (bounds.getHeight() - line.getLeading()); 1271 } 1272 } 1273 float ascent = lines[0].getBounds().getMinY(); 1274 layoutHeight = lineY; 1275 logicalBounds = logicalBounds.deriveWithNewBounds(0, ascent, 0, layoutWidth, 1276 layoutHeight + ascent, 0); 1277 1278 1279 if (layoutCache != null) { 1280 if (cacheKey != null && !layoutCache.valid && !copyCache()) { 1281 /* After layoutCache is added to the stringCache it can be 1282 * accessed by multiple threads. All the data in it must 1283 * be immutable. See copyCache() for the cases where the entire 1284 * layout is immutable. 1285 */ 1286 layoutCache.font = font; 1287 layoutCache.text = text; 1288 layoutCache.runs = runs; 1289 layoutCache.runCount = runCount; 1290 layoutCache.lines = lines; 1291 layoutCache.layoutWidth = layoutWidth; 1292 layoutCache.layoutHeight = layoutHeight; 1293 layoutCache.analysis = flags & ANALYSIS_MASK; 1294 synchronized (CACHE_SIZE_LOCK) { 1295 int charCount = chars.length; 1296 if (cacheSize + charCount > MAX_CACHE_SIZE) { 1297 stringCache.clear(); 1298 cacheSize = 0; 1299 } 1300 stringCache.put(cacheKey, layoutCache); 1301 cacheSize += charCount; 1302 } 1303 } 1304 layoutCache.valid = true; 1305 } 1306 } 1307 1308 @Override 1309 public BaseBounds getVisualBounds(int type) { 1310 ensureLayout(); 1311 1312 /* Not defined for rich text */ 1313 if (strike == null) { 1314 return null; 1315 } 1316 1317 boolean underline = (type & TYPE_UNDERLINE) != 0; 1318 boolean hasUnderline = (flags & FLAGS_CACHED_UNDERLINE) != 0; 1319 boolean strikethrough = (type & TYPE_STRIKETHROUGH) != 0; 1320 boolean hasStrikethrough = (flags & FLAGS_CACHED_STRIKETHROUGH) != 0; 1321 if (visualBounds != null && underline == hasUnderline 1322 && strikethrough == hasStrikethrough) { 1323 /* Return last cached value */ 1324 return visualBounds; 1325 } 1326 1327 flags &= ~(FLAGS_CACHED_STRIKETHROUGH | FLAGS_CACHED_UNDERLINE); 1328 if (underline) flags |= FLAGS_CACHED_UNDERLINE; 1329 if (strikethrough) flags |= FLAGS_CACHED_STRIKETHROUGH; 1330 visualBounds = new RectBounds(); 1331 1332 float xMin = Float.POSITIVE_INFINITY; 1333 float yMin = Float.POSITIVE_INFINITY; 1334 float xMax = Float.NEGATIVE_INFINITY; 1335 float yMax = Float.NEGATIVE_INFINITY; 1336 float bounds[] = new float[4]; 1337 FontResource fr = strike.getFontResource(); 1338 Metrics metrics = strike.getMetrics(); 1339 float size = strike.getSize(); 1340 for (int i = 0; i < lines.length; i++) { 1341 TextLine line = lines[i]; 1342 TextRun[] runs = line.getRuns(); 1343 for (int j = 0; j < runs.length; j++) { 1344 TextRun run = runs[j]; 1345 Point2D pt = run.getLocation(); 1346 if (run.isLinebreak()) continue; 1347 int glyphCount = run.getGlyphCount(); 1348 for (int gi = 0; gi < glyphCount; gi++) { 1349 int gc = run.getGlyphCode(gi); 1350 if (gc != CharToGlyphMapper.INVISIBLE_GLYPH_ID) { 1351 fr.getGlyphBoundingBox(run.getGlyphCode(gi), size, bounds); 1352 if (bounds[X_MIN_INDEX] != bounds[X_MAX_INDEX]) { 1353 float glyphX = pt.x + run.getPosX(gi); 1354 float glyphY = pt.y + run.getPosY(gi); 1355 float glyphMinX = glyphX + bounds[X_MIN_INDEX]; 1356 float glyphMinY = glyphY - bounds[Y_MAX_INDEX]; 1357 float glyphMaxX = glyphX + bounds[X_MAX_INDEX]; 1358 float glyphMaxY = glyphY - bounds[Y_MIN_INDEX]; 1359 if (glyphMinX < xMin) xMin = glyphMinX; 1360 if (glyphMinY < yMin) yMin = glyphMinY; 1361 if (glyphMaxX > xMax) xMax = glyphMaxX; 1362 if (glyphMaxY > yMax) yMax = glyphMaxY; 1363 } 1364 } 1365 } 1366 if (underline) { 1367 float underlineMinX = pt.x; 1368 float underlineMinY = pt.y + metrics.getUnderLineOffset(); 1369 float underlineMaxX = underlineMinX + run.getWidth(); 1370 float underlineMaxY = underlineMinY + metrics.getUnderLineThickness(); 1371 if (underlineMinX < xMin) xMin = underlineMinX; 1372 if (underlineMinY < yMin) yMin = underlineMinY; 1373 if (underlineMaxX > xMax) xMax = underlineMaxX; 1374 if (underlineMaxY > yMax) yMax = underlineMaxY; 1375 } 1376 if (strikethrough) { 1377 float strikethroughMinX = pt.x; 1378 float strikethroughMinY = pt.y + metrics.getStrikethroughOffset(); 1379 float strikethroughMaxX = strikethroughMinX + run.getWidth(); 1380 float strikethroughMaxY = strikethroughMinY + metrics.getStrikethroughThickness(); 1381 if (strikethroughMinX < xMin) xMin = strikethroughMinX; 1382 if (strikethroughMinY < yMin) yMin = strikethroughMinY; 1383 if (strikethroughMaxX > xMax) xMax = strikethroughMaxX; 1384 if (strikethroughMaxY > yMax) yMax = strikethroughMaxY; 1385 } 1386 } 1387 } 1388 1389 if (xMin < xMax && yMin < yMax) { 1390 visualBounds.setBounds(xMin, yMin, xMax, yMax); 1391 } 1392 return visualBounds; 1393 } 1394 1395 private void computeSideBearings(TextLine line) { 1396 TextRun[] runs = line.getRuns(); 1397 if (runs.length == 0) return; 1398 float bounds[] = new float[4]; 1399 FontResource defaultFontResource = null; 1400 float size = 0; 1401 if (strike != null) { 1402 defaultFontResource = strike.getFontResource(); 1403 size = strike.getSize(); 1404 } 1405 1406 /* The line lsb is the lsb of the first visual character in the line */ 1407 float lsb = 0; 1408 float width = 0; 1409 lsbdone: 1410 for (int i = 0; i < runs.length; i++) { 1411 TextRun run = runs[i]; 1412 int glyphCount = run.getGlyphCount(); 1413 for (int gi = 0; gi < glyphCount; gi++) { 1414 float advance = run.getAdvance(gi); 1415 /* Skip any leading zero-width glyphs in the line */ 1416 if (advance != 0) { 1417 int gc = run.getGlyphCode(gi); 1418 /* Skip any leading invisible glyphs in the line */ 1419 if (gc != CharToGlyphMapper.INVISIBLE_GLYPH_ID) { 1420 FontResource fr = defaultFontResource; 1421 if (fr == null) { 1422 TextSpan span = run.getTextSpan(); 1423 PGFont font = (PGFont)span.getFont(); 1424 /* No need to check font != null (run.glyphCount > 0) */ 1425 size = font.getSize(); 1426 fr = font.getFontResource(); 1427 } 1428 fr.getGlyphBoundingBox(gc, size, bounds); 1429 float glyphLsb = bounds[X_MIN_INDEX]; 1430 lsb = Math.min(0, glyphLsb + width); 1431 run.setLeftBearing(); 1432 break lsbdone; 1433 } 1434 } 1435 width += advance; 1436 } 1437 // tabs 1438 if (glyphCount == 0) { 1439 width += run.getWidth(); 1440 } 1441 } 1442 1443 /* The line rsb is the rsb of the last visual character in the line */ 1444 float rsb = 0; 1445 width = 0; 1446 rsbdone: 1447 for (int i = runs.length - 1; i >= 0 ; i--) { 1448 TextRun run = runs[i]; 1449 int glyphCount = run.getGlyphCount(); 1450 for (int gi = glyphCount - 1; gi >= 0; gi--) { 1451 float advance = run.getAdvance(gi); 1452 /* Skip any trailing zero-width glyphs in the line */ 1453 if (advance != 0) { 1454 int gc = run.getGlyphCode(gi); 1455 /* Skip any trailing invisible glyphs in the line */ 1456 if (gc != CharToGlyphMapper.INVISIBLE_GLYPH_ID) { 1457 FontResource fr = defaultFontResource; 1458 if (fr == null) { 1459 TextSpan span = run.getTextSpan(); 1460 PGFont font = (PGFont)span.getFont(); 1461 /* No need to check font != null (run.glyphCount > 0) */ 1462 size = font.getSize(); 1463 fr = font.getFontResource(); 1464 } 1465 fr.getGlyphBoundingBox(gc, size, bounds); 1466 float glyphRsb = bounds[X_MAX_INDEX] - advance; 1467 rsb = Math.max(0, glyphRsb - width); 1468 run.setRightBearing(); 1469 break rsbdone; 1470 } 1471 } 1472 width += advance; 1473 } 1474 // tabs 1475 if (glyphCount == 0) { 1476 width += run.getWidth(); 1477 } 1478 } 1479 line.setSideBearings(lsb, rsb); 1480 } 1481 }