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 }