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