/* * Copyright (c) 1997, 2018, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ /* * (C) Copyright Taligent, Inc. 1996 - 1997, All Rights Reserved * (C) Copyright IBM Corp. 1996 - 1998, All Rights Reserved * * The original version of this source code and documentation is * copyrighted and owned by Taligent, Inc., a wholly-owned subsidiary * of IBM. These materials are provided under terms of a License * Agreement between Taligent and Sun. This technology is protected * by multiple US and International patents. * * This notice and attribution to Taligent may not be removed. * Taligent is a registered trademark of Taligent, Inc. * */ package java.awt.font; import java.awt.Font; import java.text.AttributedCharacterIterator; import java.text.AttributedCharacterIterator.Attribute; import java.text.AttributedString; import java.text.Bidi; import java.text.BreakIterator; import java.text.CharacterIterator; import java.awt.font.FontRenderContext; import java.util.Hashtable; import java.util.Map; import sun.font.AttributeValues; import sun.font.BidiUtils; import sun.font.TextLineComponent; import sun.font.TextLabelFactory; import sun.font.FontResolver; /** * The {@code TextMeasurer} class provides the primitive operations * needed for line break: measuring up to a given advance, determining the * advance of a range of characters, and generating a * {@code TextLayout} for a range of characters. It also provides * methods for incremental editing of paragraphs. *

* A {@code TextMeasurer} object is constructed with an * {@link java.text.AttributedCharacterIterator AttributedCharacterIterator} * representing a single paragraph of text. The value returned by the * {@link AttributedCharacterIterator#getBeginIndex() getBeginIndex} * method of {@code AttributedCharacterIterator} * defines the absolute index of the first character. The value * returned by the * {@link AttributedCharacterIterator#getEndIndex() getEndIndex} * method of {@code AttributedCharacterIterator} defines the index * past the last character. These values define the range of indexes to * use in calls to the {@code TextMeasurer}. For example, calls to * get the advance of a range of text or the line break of a range of text * must use indexes between the beginning and end index values. Calls to * {@link #insertChar(java.text.AttributedCharacterIterator, int) insertChar} * and * {@link #deleteChar(java.text.AttributedCharacterIterator, int) deleteChar} * reset the {@code TextMeasurer} to use the beginning index and end * index of the {@code AttributedCharacterIterator} passed in those calls. *

* Most clients will use the more convenient {@code LineBreakMeasurer}, * which implements the standard line break policy (placing as many words * as will fit on each line). * * @author John Raley * @see LineBreakMeasurer * @since 1.3 */ public final class TextMeasurer implements Cloneable { // Number of lines to format to. private static float EST_LINES = (float) 2.1; /* static { String s = System.getProperty("estLines"); if (s != null) { try { Float f = Float.valueOf(s); EST_LINES = f.floatValue(); } catch(NumberFormatException e) { } } //System.out.println("EST_LINES="+EST_LINES); } */ private FontRenderContext fFrc; private int fStart; // characters in source text private char[] fChars; // Bidi for this paragraph private Bidi fBidi; // Levels array for chars in this paragraph - needed to reorder // trailing counterdirectional whitespace private byte[] fLevels; // line components in logical order private TextLineComponent[] fComponents; // index where components begin private int fComponentStart; // index where components end private int fComponentLimit; private boolean haveLayoutWindow; // used to find valid starting points for line components private BreakIterator fLineBreak = null; private CharArrayIterator charIter = null; int layoutCount = 0; int layoutCharCount = 0; // paragraph, with resolved fonts and styles private StyledParagraph fParagraph; // paragraph data - same across all layouts private boolean fIsDirectionLTR; private byte fBaseline; private float[] fBaselineOffsets; private float fJustifyRatio = 1; /** * Constructs a {@code TextMeasurer} from the source text. * The source text should be a single entire paragraph. * @param text the source paragraph. Cannot be null. * @param frc the information about a graphics device which is needed * to measure the text correctly. Cannot be null. */ public TextMeasurer(AttributedCharacterIterator text, FontRenderContext frc) { fFrc = frc; initAll(text); } protected Object clone() { TextMeasurer other; try { other = (TextMeasurer) super.clone(); } catch(CloneNotSupportedException e) { throw new Error(); } if (fComponents != null) { other.fComponents = fComponents.clone(); } return other; } private void invalidateComponents() { fComponentStart = fComponentLimit = fChars.length; fComponents = null; haveLayoutWindow = false; } /** * Initialize state, including fChars array, direction, and * fBidi. */ private void initAll(AttributedCharacterIterator text) { fStart = text.getBeginIndex(); // extract chars fChars = new char[text.getEndIndex() - fStart]; int n = 0; for (char c = text.first(); c != CharacterIterator.DONE; c = text.next()) { fChars[n++] = c; } text.first(); fBidi = new Bidi(text); if (fBidi.isLeftToRight()) { fBidi = null; } text.first(); Map paragraphAttrs = text.getAttributes(); NumericShaper shaper = AttributeValues.getNumericShaping(paragraphAttrs); if (shaper != null) { shaper.shape(fChars, 0, fChars.length); } fParagraph = new StyledParagraph(text, fChars); // set paragraph attributes { // If there's an embedded graphic at the start of the // paragraph, look for the first non-graphic character // and use it and its font to initialize the paragraph. // If not, use the first graphic to initialize. fJustifyRatio = AttributeValues.getJustification(paragraphAttrs); boolean haveFont = TextLine.advanceToFirstFont(text); if (haveFont) { Font defaultFont = TextLine.getFontAtCurrentPos(text); int charsStart = text.getIndex() - text.getBeginIndex(); LineMetrics lm = defaultFont.getLineMetrics(fChars, charsStart, charsStart+1, fFrc); fBaseline = (byte) lm.getBaselineIndex(); fBaselineOffsets = lm.getBaselineOffsets(); } else { // hmmm what to do here? Just try to supply reasonable // values I guess. GraphicAttribute graphic = (GraphicAttribute) paragraphAttrs.get(TextAttribute.CHAR_REPLACEMENT); fBaseline = TextLayout.getBaselineFromGraphic(graphic); Hashtable fmap = new Hashtable<>(5, (float)0.9); Font dummyFont = new Font(fmap); LineMetrics lm = dummyFont.getLineMetrics(" ", 0, 1, fFrc); fBaselineOffsets = lm.getBaselineOffsets(); } fBaselineOffsets = TextLine.getNormalizedOffsets(fBaselineOffsets, fBaseline); } invalidateComponents(); } /** * Generate components for the paragraph. fChars, fBidi should have been * initialized already. */ private void generateComponents(int startingAt, int endingAt) { if (collectStats) { formattedChars += (endingAt-startingAt); } int layoutFlags = 0; // no extra info yet, bidi determines run and line direction TextLabelFactory factory = new TextLabelFactory(fFrc, fChars, fBidi, layoutFlags); int[] charsLtoV = null; if (fBidi != null) { fLevels = BidiUtils.getLevels(fBidi); int[] charsVtoL = BidiUtils.createVisualToLogicalMap(fLevels); charsLtoV = BidiUtils.createInverseMap(charsVtoL); fIsDirectionLTR = fBidi.baseIsLeftToRight(); } else { fLevels = null; fIsDirectionLTR = true; } try { fComponents = TextLine.getComponents( fParagraph, fChars, startingAt, endingAt, charsLtoV, fLevels, factory); } catch(IllegalArgumentException e) { System.out.println("startingAt="+startingAt+"; endingAt="+endingAt); System.out.println("fComponentLimit="+fComponentLimit); throw e; } fComponentStart = startingAt; fComponentLimit = endingAt; //debugFormatCount += (endingAt-startingAt); } private int calcLineBreak(final int pos, final float maxAdvance) { // either of these statements removes the bug: //generateComponents(0, fChars.length); //generateComponents(pos, fChars.length); int startPos = pos; float width = maxAdvance; int tlcIndex; int tlcStart = fComponentStart; for (tlcIndex = 0; tlcIndex < fComponents.length; tlcIndex++) { int gaLimit = tlcStart + fComponents[tlcIndex].getNumCharacters(); if (gaLimit > startPos) { break; } else { tlcStart = gaLimit; } } // tlcStart is now the start of the tlc at tlcIndex for (; tlcIndex < fComponents.length; tlcIndex++) { TextLineComponent tlc = fComponents[tlcIndex]; int numCharsInGa = tlc.getNumCharacters(); int lineBreak = tlc.getLineBreakIndex(startPos - tlcStart, width); if (lineBreak == numCharsInGa && tlcIndex < fComponents.length) { width -= tlc.getAdvanceBetween(startPos - tlcStart, lineBreak); tlcStart += numCharsInGa; startPos = tlcStart; } else { return tlcStart + lineBreak; } } if (fComponentLimit < fChars.length) { // format more text and try again //if (haveLayoutWindow) { // outOfWindow++; //} generateComponents(pos, fChars.length); return calcLineBreak(pos, maxAdvance); } return fChars.length; } /** * According to the Unicode Bidirectional Behavior specification * (Unicode Standard 2.0, section 3.11), whitespace at the ends * of lines which would naturally flow against the base direction * must be made to flow with the line direction, and moved to the * end of the line. This method returns the start of the sequence * of trailing whitespace characters to move to the end of a * line taken from the given range. */ private int trailingCdWhitespaceStart(int startPos, int limitPos) { if (fLevels != null) { // Back up over counterdirectional whitespace final byte baseLevel = (byte) (fIsDirectionLTR? 0 : 1); for (int cdWsStart = limitPos; --cdWsStart >= startPos;) { if ((fLevels[cdWsStart] % 2) == baseLevel || Character.getDirectionality(fChars[cdWsStart]) != Character.DIRECTIONALITY_WHITESPACE) { return ++cdWsStart; } } } return startPos; } private TextLineComponent[] makeComponentsOnRange(int startPos, int limitPos) { // sigh I really hate to do this here since it's part of the // bidi algorithm. // cdWsStart is the start of the trailing counterdirectional // whitespace final int cdWsStart = trailingCdWhitespaceStart(startPos, limitPos); int tlcIndex; int tlcStart = fComponentStart; for (tlcIndex = 0; tlcIndex < fComponents.length; tlcIndex++) { int gaLimit = tlcStart + fComponents[tlcIndex].getNumCharacters(); if (gaLimit > startPos) { break; } else { tlcStart = gaLimit; } } // tlcStart is now the start of the tlc at tlcIndex int componentCount; { boolean split = false; int compStart = tlcStart; int lim=tlcIndex; for (boolean cont=true; cont; lim++) { int gaLimit = compStart + fComponents[lim].getNumCharacters(); if (cdWsStart > Math.max(compStart, startPos) && cdWsStart < Math.min(gaLimit, limitPos)) { split = true; } if (gaLimit >= limitPos) { cont=false; } else { compStart = gaLimit; } } componentCount = lim-tlcIndex; if (split) { componentCount++; } } TextLineComponent[] components = new TextLineComponent[componentCount]; int newCompIndex = 0; int linePos = startPos; int breakPt = cdWsStart; int subsetFlag; if (breakPt == startPos) { subsetFlag = fIsDirectionLTR? TextLineComponent.LEFT_TO_RIGHT : TextLineComponent.RIGHT_TO_LEFT; breakPt = limitPos; } else { subsetFlag = TextLineComponent.UNCHANGED; } while (linePos < limitPos) { int compLength = fComponents[tlcIndex].getNumCharacters(); int tlcLimit = tlcStart + compLength; int start = Math.max(linePos, tlcStart); int limit = Math.min(breakPt, tlcLimit); components[newCompIndex++] = fComponents[tlcIndex].getSubset( start-tlcStart, limit-tlcStart, subsetFlag); linePos += (limit-start); if (linePos == breakPt) { breakPt = limitPos; subsetFlag = fIsDirectionLTR? TextLineComponent.LEFT_TO_RIGHT : TextLineComponent.RIGHT_TO_LEFT; } if (linePos == tlcLimit) { tlcIndex++; tlcStart = tlcLimit; } } return components; } private TextLine makeTextLineOnRange(int startPos, int limitPos) { int[] charsLtoV = null; byte[] charLevels = null; if (fBidi != null) { Bidi lineBidi = fBidi.createLineBidi(startPos, limitPos); charLevels = BidiUtils.getLevels(lineBidi); int[] charsVtoL = BidiUtils.createVisualToLogicalMap(charLevels); charsLtoV = BidiUtils.createInverseMap(charsVtoL); } TextLineComponent[] components = makeComponentsOnRange(startPos, limitPos); return new TextLine(fFrc, components, fBaselineOffsets, fChars, startPos, limitPos, charsLtoV, charLevels, fIsDirectionLTR); } private void ensureComponents(int start, int limit) { if (start < fComponentStart || limit > fComponentLimit) { generateComponents(start, limit); } } private void makeLayoutWindow(int localStart) { int compStart = localStart; int compLimit = fChars.length; // If we've already gone past the layout window, format to end of paragraph if (layoutCount > 0 && !haveLayoutWindow) { float avgLineLength = Math.max(layoutCharCount / layoutCount, 1); compLimit = Math.min(localStart + (int)(avgLineLength*EST_LINES), fChars.length); } if (localStart > 0 || compLimit < fChars.length) { if (charIter == null) { charIter = new CharArrayIterator(fChars); } else { charIter.reset(fChars); } if (fLineBreak == null) { fLineBreak = BreakIterator.getLineInstance(); } fLineBreak.setText(charIter); if (localStart > 0) { if (!fLineBreak.isBoundary(localStart)) { compStart = fLineBreak.preceding(localStart); } } if (compLimit < fChars.length) { if (!fLineBreak.isBoundary(compLimit)) { compLimit = fLineBreak.following(compLimit); } } } ensureComponents(compStart, compLimit); haveLayoutWindow = true; } /** * Returns the index of the first character which will not fit on * on a line beginning at {@code start} and possible * measuring up to {@code maxAdvance} in graphical width. * * @param start the character index at which to start measuring. * {@code start} is an absolute index, not relative to the * start of the paragraph * @param maxAdvance the graphical width in which the line must fit * @return the index after the last character that will fit * on a line beginning at {@code start}, which is not longer * than {@code maxAdvance} in graphical width * @throws IllegalArgumentException if {@code start} is * less than the beginning of the paragraph. */ public int getLineBreakIndex(int start, float maxAdvance) { int localStart = start - fStart; if (!haveLayoutWindow || localStart < fComponentStart || localStart >= fComponentLimit) { makeLayoutWindow(localStart); } return calcLineBreak(localStart, maxAdvance) + fStart; } /** * Returns the graphical width of a line beginning at {@code start} * and including characters up to {@code limit}. * {@code start} and {@code limit} are absolute indices, * not relative to the start of the paragraph. * * @param start the character index at which to start measuring * @param limit the character index at which to stop measuring * @return the graphical width of a line beginning at {@code start} * and including characters up to {@code limit} * @throws IndexOutOfBoundsException if {@code limit} is less * than {@code start} * @throws IllegalArgumentException if {@code start} or * {@code limit} is not between the beginning of * the paragraph and the end of the paragraph. */ public float getAdvanceBetween(int start, int limit) { int localStart = start - fStart; int localLimit = limit - fStart; ensureComponents(localStart, localLimit); TextLine line = makeTextLineOnRange(localStart, localLimit); return line.getMetrics().advance; // could cache line in case getLayout is called with same start, limit } /** * Returns a {@code TextLayout} on the given character range. * * @param start the index of the first character * @param limit the index after the last character. Must be greater * than {@code start} * @return a {@code TextLayout} for the characters beginning at * {@code start} up to (but not including) {@code limit} * @throws IndexOutOfBoundsException if {@code limit} is less * than {@code start} * @throws IllegalArgumentException if {@code start} or * {@code limit} is not between the beginning of * the paragraph and the end of the paragraph. */ public TextLayout getLayout(int start, int limit) { int localStart = start - fStart; int localLimit = limit - fStart; ensureComponents(localStart, localLimit); TextLine textLine = makeTextLineOnRange(localStart, localLimit); if (localLimit < fChars.length) { layoutCharCount += limit-start; layoutCount++; } return new TextLayout(textLine, fBaseline, fBaselineOffsets, fJustifyRatio); } private int formattedChars = 0; private static boolean wantStats = false;/*"true".equals(System.getProperty("collectStats"));*/ private boolean collectStats = false; private void printStats() { System.out.println("formattedChars: " + formattedChars); //formattedChars = 0; collectStats = false; } /** * Updates the {@code TextMeasurer} after a single character has * been inserted * into the paragraph currently represented by this * {@code TextMeasurer}. After this call, this * {@code TextMeasurer} is equivalent to a new * {@code TextMeasurer} created from the text; however, it will * usually be more efficient to update an existing * {@code TextMeasurer} than to create a new one from scratch. * * @param newParagraph the text of the paragraph after performing * the insertion. Cannot be null. * @param insertPos the position in the text where the character was * inserted. Must not be less than the start of * {@code newParagraph}, and must be less than the end of * {@code newParagraph}. * @throws IllegalArgumentException if multiple characters are inserted * in the text represented by {@code newParagraph} * @throws IndexOutOfBoundsException if {@code insertPos} is less * than the start of {@code newParagraph} or greater than * or equal to the end of {@code newParagraph} * @throws NullPointerException if {@code newParagraph} is * {@code null} */ public void insertChar(AttributedCharacterIterator newParagraph, int insertPos) { if (collectStats) { printStats(); } if (wantStats) { collectStats = true; } int start = newParagraph.getBeginIndex(); int end = newParagraph.getEndIndex(); if (end - start != fChars.length+1) { // The new paragraph attempts to insert multiple characters throw new IllegalArgumentException("The new paragraph" + " attempts to insert multiple characters into the text."); } fStart = start; char[] newChars = new char[end-fStart]; int newCharIndex = insertPos - fStart; System.arraycopy(fChars, 0, newChars, 0, newCharIndex); char newChar = newParagraph.setIndex(insertPos); newChars[newCharIndex] = newChar; System.arraycopy(fChars, newCharIndex, newChars, newCharIndex+1, end-insertPos-1); fChars = newChars; if (fBidi != null || Bidi.requiresBidi(newChars, newCharIndex, newCharIndex + 1) || newParagraph.getAttribute(TextAttribute.BIDI_EMBEDDING) != null) { fBidi = new Bidi(newParagraph); if (fBidi.isLeftToRight()) { fBidi = null; } } fParagraph = StyledParagraph.insertChar(newParagraph, fChars, insertPos, fParagraph); invalidateComponents(); } /** * Updates the {@code TextMeasurer} after a single character has * been deleted * from the paragraph currently represented by this * {@code TextMeasurer}. After this call, this * {@code TextMeasurer} is equivalent to a new {@code TextMeasurer} * created from the text; however, it will usually be more efficient * to update an existing {@code TextMeasurer} than to create a new one * from scratch. * * @param newParagraph the text of the paragraph after performing * the deletion. Cannot be null. * @param deletePos the position in the text where the character was removed. * Must not be less than * the start of {@code newParagraph}, and must not be greater than the * end of {@code newParagraph}. * @throws IllegalArgumentException if multiple characters are deleted from * the text represented by {@code newParagraph} * @throws IndexOutOfBoundsException if {@code deletePos} is * less than the start of {@code newParagraph} or greater * than the end of {@code newParagraph} * @throws NullPointerException if {@code newParagraph} is * {@code null} */ public void deleteChar(AttributedCharacterIterator newParagraph, int deletePos) { int start = newParagraph.getBeginIndex(); int end = newParagraph.getEndIndex(); if (end - start != fChars.length-1) { // The new paragraph attempts to delete multiple characters throw new IllegalArgumentException("The new paragraph" + " attempts to delete multiple characters from the text."); } fStart = start; char[] newChars = new char[end-fStart]; int changedIndex = deletePos-fStart; System.arraycopy(fChars, 0, newChars, 0, deletePos-fStart); System.arraycopy(fChars, changedIndex+1, newChars, changedIndex, end-deletePos); fChars = newChars; if (fBidi != null) { fBidi = new Bidi(newParagraph); if (fBidi.isLeftToRight()) { fBidi = null; } } fParagraph = StyledParagraph.deleteChar(newParagraph, fChars, deletePos, fParagraph); invalidateComponents(); } /** * NOTE: This method is only for LineBreakMeasurer's use. It is package- * private because it returns internal data. */ char[] getChars() { return fChars; } }