FileDocCategorySizeDatePackage
TextMeasurer.javaAPI DocJava SE 5 API27000Fri Aug 26 14:56:52 BST 2005java.awt.font

TextMeasurer.java

/*
 * @(#)TextMeasurer.java	1.39 03/12/19
 *
 * Copyright 2004 Sun Microsystems, Inc. All rights reserved.
 * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */

/*
 * (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.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.BidiUtils;
import sun.font.TextLineComponent;
import sun.font.TextLabelFactory;
import sun.font.FontResolver;

/**
 * The <code>TextMeasurer</code> 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</code> for a range of characters. It also provides 
 * methods for incremental editing of paragraphs.
 * <p>
 * A <code>TextMeasurer</code> 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</code>
 * defines the absolute index of the first character.  The value
 * returned by the 
 * {@link AttributedCharacterIterator#getEndIndex() getEndIndex}
 * method of <code>AttributedCharacterIterator</code> defines the index
 * past the last character.  These values define the range of indexes to 
 * use in calls to the <code>TextMeasurer</code>.  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</code> to use the beginning index and end
 * index of the <code>AttributedCharacterIterator</code> passed in those calls.
 * <p>
 * Most clients will use the more convenient <code>LineBreakMeasurer</code>, 
 * which implements the standard line break policy (placing as many words
 * as will fit on each line). 
 *
 * @author John Raley
 * @version 1.31, 04/20/01
 * @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 = new Float(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</code> 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 = (TextLineComponent[]) 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 != text.DONE; c = text.next()) {
            fChars[n++] = c;
        }
        
        text.first();
                
        fBidi = new Bidi(text);
	if (fBidi.isLeftToRight()) {
	    fBidi = null;
	}

        text.first();
        Map paragraphAttrs = text.getAttributes();
        if (paragraphAttrs != null) {
            try {
                NumericShaper shaper = (NumericShaper)paragraphAttrs.get(TextAttribute.NUMERIC_SHAPING);
                if (shaper != null) {
                    shaper.shape(fChars, 0, fChars.length);
                }
            }
            catch (ClassCastException e) {
            }
        }

        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 = TextLine.getJustifyRatio(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);
                Font dummyFont = new Font(new Hashtable(5, (float)0.9));
                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(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</code> and possible
     * measuring up to <code>maxAdvance</code> in graphical width.
     *
     * @param start the character index at which to start measuring.
     *  <code>start</code> 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</code>, which is not longer
     *  than <code>maxAdvance</code> in graphical width
     * @throws IllegalArgumentException if <code>start</code> 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</code>
     * and including characters up to <code>limit</code>.
     * <code>start</code> and <code>limit</code> 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</code>
     *   and including characters up to <code>limit</code>
     * @throws IndexOutOfBoundsException if <code>limit</code> is less
     *         than <code>start</code>
     * @throws IllegalArgumentException if <code>start</code> or 
     *		<code>limit</code> 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</code> 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</code>
     * @return a <code>TextLayout</code> for the characters beginning at
     *  <code>start</code> up to (but not including) <code>limit</code>
     * @throws IndexOutOfBoundsException if <code>limit</code> is less
     *         than <code>start</code>
     * @throws IllegalArgumentException if <code>start</code> or 
     *		<code>limit</code> 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</code> after a single character has 
     * been inserted
     * into the paragraph currently represented by this
     * <code>TextMeasurer</code>.  After this call, this
     * <code>TextMeasurer</code> is equivalent to a new 
     * <code>TextMeasurer</code> created from the text;  however, it will
     * usually be more efficient to update an existing
     * <code>TextMeasurer</code> 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</code>, and must be less than the end of
     * <code>newParagraph</code>.
     * @throws IndexOutOfBoundsException if <code>insertPos</code> is less
     *         than the start of <code>newParagraph</code> or greater than
     *         or equal to the end of <code>newParagraph</code>
     * @throws NullPointerException if <code>newParagraph</code> is   
     *         <code>null</code>
     */
    public void insertChar(AttributedCharacterIterator newParagraph, int insertPos) {

        if (collectStats) {
            printStats();
        }
        if (wantStats) {
            collectStats = true;
        }
        
        fStart = newParagraph.getBeginIndex();
        int end = newParagraph.getEndIndex();
        if (end - fStart != fChars.length+1) {
            initAll(newParagraph);
        }
        
        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</code> after a single character has 
     * been deleted
     * from the paragraph currently represented by this
     * <code>TextMeasurer</code>.  After this call, this
     * <code>TextMeasurer</code> is equivalent to a new <code>TextMeasurer</code>
     * created from the text;  however, it will usually be more efficient
     * to update an existing <code>TextMeasurer</code> 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</code>, and must not be greater than the
     * end of <code>newParagraph</code>.
     * @throws IndexOutOfBoundsException if <code>deletePos</code> is
     *         less than the start of <code>newParagraph</code> or greater
     *         than the end of <code>newParagraph</code>
     * @throws NullPointerException if <code>newParagraph</code> is
     *         <code>null</code>
     */
    public void deleteChar(AttributedCharacterIterator newParagraph, int deletePos) {

        fStart = newParagraph.getBeginIndex();
        int end = newParagraph.getEndIndex();
        if (end - fStart != fChars.length-1) {
            initAll(newParagraph);
        }
        
        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;
    }
}