JDK-4463424 : LineBreakMeasurer inconsistent with TextLayout
  • Type: Bug
  • Component: client-libs
  • Sub-Component: 2d
  • Affected Version: 1.3.1
  • Priority: P4
  • Status: Resolved
  • Resolution: Fixed
  • OS: linux
  • CPU: x86
  • Submitted: 2001-05-25
  • Updated: 2001-06-18
  • Resolved: 2001-06-18
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
Other
1.4.0 beta2Fixed
Description

Name: bsC130419			Date: 05/25/2001


java version "1.3.1-beta"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.3.1-beta-b15)
Java HotSpot(TM) Client VM (build 1.3.1beta-b15, mixed mode)


LineBreakMeasurer.nextLayout() measures length in a way that is
inconsistent with TextLayout.getBounds() and TextLayout.getAdvance().  We
use TextLayout.getBounds() to measure the width of a string during the
layout of the X axis of a custom component.  After our component decides
on an X-axis layout, it uses the LineBreakMeasurer to divide up the
pargraph string into lines for Y-axis layout and then drawing.  If a
particular string is allocated exactly as much horizontal space as
TextLayout.getBounds() indicates it should need, this algorithm fails.

The problem is that using the width from TextLayout.getBounds() as the
maxAdvance argument to LineBreakMeasurer.nextAdvance() doesn't yield a
single TextLayout object with the whole string.  If the string has no
whitespace, the last character or few gets moved to a second line.  If the
string has whitespace in it, the last word is moved to a second line.
                           
The following code demonstrates the problem.

import java.awt.*;
import java.awt.font.*;
import java.text.*;

import javax.swing.*;

public class TestLayout {
  static String longString = "VeryVeryVeryVeryVeryVeryVeryLongTestString";
  static String brokenString = "Line wraps where it should not.";
  JFrame jf = new JFrame();
  AttributedString asLong;
  AttributedString asBroken;
  LineBreakMeasurer lbmLong;
  LineBreakMeasurer lbmBroken;
  FontRenderContext frc;

  public TestLayout() {
    jf = new JFrame();
    jf.setVisible( true );
    Graphics g = jf.getGraphics();
    frc = ((Graphics2D) g).getFontRenderContext();

    asLong = new AttributedString( longString );
    lbmLong = new LineBreakMeasurer( asLong.getIterator(), frc );
    asBroken = new AttributedString( brokenString );
    lbmBroken = new LineBreakMeasurer( asBroken.getIterator(), frc );
  }

  public void runTest() {
    lbmLong.setPosition( 0 );
    TextLayout tl1 = lbmLong.nextLayout( Short.MAX_VALUE );
    int neededSpace = (int) Math.ceil( tl1.getBounds().getWidth() );
    System.err.println( "End position after advancing the whole string: " +
			lbmLong.getPosition() );

    lbmLong.setPosition( 0 );
    TextLayout tl2 = lbmLong.nextLayout( neededSpace );
    // On our system, the previous println shows a position of 42
    // while this println shows a position of 41.  The last character
    // is left to the next TextLayout.
    System.err.println( "End position after minimal advance: " +
			lbmLong.getPosition() );
    TextLayout tl3 = lbmLong.nextLayout( Short.MAX_VALUE );


    lbmBroken.setPosition( 0 );
    TextLayout tl4 = lbmBroken.nextLayout( Short.MAX_VALUE );
    neededSpace = (int) Math.ceil( tl4.getBounds().getWidth() );
    System.err.println( "End position after advancing the whole string: " +
			lbmBroken.getPosition() );

    lbmBroken.setPosition( 0 );
    TextLayout tl5 = lbmBroken.nextLayout( neededSpace );
    // On our system, the previous println shows a position of 31, while this
    // println shows a position of 27.  The last word is left to the next
    // text layout.
    System.err.println( "End position after minimal advance: " +
			lbmBroken.getPosition() );
    TextLayout tl6 = lbmBroken.nextLayout( Short.MAX_VALUE );

    jf.setSize( 1000, 500 );
    Graphics g = jf.getGraphics();
    // tl1 shows the whole string on one line, this is the string being
    // measured.
    tl1.draw( (Graphics2D) g, 100, 100 );
    // tl2 shows all but the last character because the LineBreakMeasurer
    // doesn't return the whole string using the width measured with by
    // tl1.
    tl2.draw( (Graphics2D) g, 100, 200 );
    // tl3 shows the character that did not make it into tl2
    tl3.draw( (Graphics2D) g, 100, 220 );

    // tl4 shows the whole line the was used to measure the string
    tl4.draw( (Graphics2D) g, 100, 300 );

    // tl5 shows the line that nextLayout returned using the measurement
    // provided by tl4.getBounds().getWidth()
    tl5.draw( (Graphics2D) g, 100, 400 );

    // tl6 shows the part of the line that was left over.
    tl6.draw( (Graphics2D) g, 100, 420 );
  }

  public static void main(String args[]) {
    TestLayout t = new TestLayout();
    t.runTest();
  }
}

We've tried using both TextLayout.getBounds() and TextLayout.getAdvance()
to measure the string, and the problem is the same both ways.  Simply
adding some padding to the maxAdvance argument for the nextLayout call hasn't
worked around the problem for us.  The amount of padding necessary seems to
depend on the formatting attributes of the string (though we're less
certain of this -- we might just not have played with the padding constant
enough).
(Review ID: 124280) 
======================================================================

Comments
CONVERTED DATA BugTraq+ Release Management Values COMMIT TO FIX: merlin-beta2 FIXED IN: merlin-beta2 INTEGRATED IN: merlin-beta2
14-06-2004

WORK AROUND Name: bsC130419 Date: 05/25/2001 We have found one workaround. When we create a TextLayout from the string to mesaure it, we cache that TextLayout. Later, if the cached TextLayout's width is less than or equal to the X axis space it has been allocated, that TextLayout is used, skipping the LineBreakMeasurer altogether. We only use the LineBreakMeasuerer if the amount of space allocated is less than the cached single TextLayout would need to draw properly. By not using the LineBreakMeasurer to try to re-derive the TextLayout, we avoid the problem. ======================================================================
11-06-2004

PUBLIC COMMENTS Need to use logical advance to compute line breaks
10-06-2004

EVALUATION Line breaking uses the logical advance of the glyphs, not the visual advance, when computing line breaks. For example, the glyph 'i' will have a small visual advance, since the glyph is narrow, but a larger logical advance, since the next glyph needs to be spaced apart from the 'i'. It could be argued that the visual advance is more appropriate to narrowly 'fit' the text to a line, but this generally involves a bit more computation as a precise visual advance requires rasterization of the glyph at the requested resolution, and the benefits seem minimal. At any rate, we use the logical advance. Using the logical advance I was not able to reproduce this bug on Solaris using the latest build of 1.3.1. The build the bug was reported on (b15) has been archived so I can't test on it, but I believe the problem is fixed. I did, however, notice a bug in 1.4, in computing the visual advance of a glyphvector that includes spaces. The positions of the spaces were being ignored and subsequent glyphs were being mispositioned. When this is fixed, the provided test (using the visual advance of the glyphvector) doesn't show the severe problem it did previously in the case of the string with spaces. So I will submit that fix. doug.felt@eng 2001-06-01
01-06-2001