8165943: LineBreakMeasurer does not measure correctly if TextAttribute.TRACKING is set.

Co-authored-by: Jason Fordham <jclf@azul.com>
Reviewed-by: prr
This commit is contained in:
Olga Mikhaltsova 2022-12-07 18:02:20 +00:00 committed by Andrew Brygin
parent 39344840c7
commit 8edb98df3d
5 changed files with 322 additions and 10 deletions

@ -846,6 +846,23 @@ public final class AttributeValues implements Cloneable {
return null;
}
@SuppressWarnings("unchecked")
public static float getTracking(Map<?, ?> map) {
if (map != null) {
AttributeValues av = null;
if (map instanceof AttributeMap &&
((AttributeMap) map).getValues() != null) {
av = ((AttributeMap)map).getValues();
} else if (map.get(TextAttribute.TRACKING) != null) {
av = AttributeValues.fromMap((Map<Attribute, ?>)map);
}
if (av != null) {
return av.tracking;
}
}
return 0;
}
public void updateDerivedTransforms() {
// this also updates the mask for the baseline transform
if (transform == null) {

@ -71,6 +71,8 @@ class ExtendedTextSourceLabel extends ExtendedTextLabel implements Decoration.La
StandardGlyphVector gv;
float[] charinfo;
float advTracking;
/**
* Create from a TextSource.
*/
@ -110,6 +112,8 @@ class ExtendedTextSourceLabel extends ExtendedTextLabel implements Decoration.La
source.getStart() + source.getLength(), source.getFRC());
cm = CoreMetrics.get(lm);
}
advTracking = font.getSize() * AttributeValues.getTracking(atts);
}
@ -378,10 +382,10 @@ class ExtendedTextSourceLabel extends ExtendedTextLabel implements Decoration.La
validate(index);
float[] charinfo = getCharinfo();
int idx = l2v(index) * numvals + advx;
if (charinfo == null || idx >= charinfo.length) {
if (charinfo == null || idx >= charinfo.length || charinfo[idx] == 0) {
return 0f;
} else {
return charinfo[idx];
return charinfo[idx] + advTracking;
}
}
@ -477,16 +481,25 @@ class ExtendedTextSourceLabel extends ExtendedTextLabel implements Decoration.La
}
public int getLineBreakIndex(int start, float width) {
final float epsilon = 0.005f;
float[] charinfo = getCharinfo();
int length = source.getLength();
if (advTracking > 0) {
width += advTracking;
}
--start;
while (width >= 0 && ++start < length) {
while (width >= -epsilon && ++start < length) {
int cidx = l2v(start) * numvals + advx;
if (cidx >= charinfo.length) {
break; // layout bailed for some reason
}
float adv = charinfo[cidx];
width -= adv;
if (adv != 0) {
width -= adv + advTracking;
}
}
return start;
@ -502,7 +515,10 @@ class ExtendedTextSourceLabel extends ExtendedTextLabel implements Decoration.La
if (cidx >= charinfo.length) {
break; // layout bailed for some reason
}
a += charinfo[cidx];
float adv = charinfo[cidx];
if (adv != 0) {
a += adv + advTracking;
}
}
return a;

@ -197,21 +197,22 @@ public class StandardGlyphVector extends GlyphVector {
// how do we know its a base glyph
// for now, it is if the natural advance of the glyph is non-zero
Font2D f2d = FontUtilities.getFont2D(font);
FontStrike strike = f2d.getStrike(font, frc);
float[] deltas = { trackPt.x, trackPt.y };
for (int j = 0; j < deltas.length; ++j) {
float inc = deltas[j];
float prevPos = 0;
if (inc != 0) {
float delta = 0;
for (int i = j, n = 0; n < glyphs.length; i += 2) {
if (strike.getGlyphAdvance(glyphs[n++]) != 0) { // might be an inadequate test
for (int i = j; i < positions.length; i += 2) {
if (i == j || prevPos != positions[i]) {
prevPos = positions[i];
positions[i] += delta;
delta += inc;
} else if (prevPos == positions[i]) {
positions[i] = positions[i - 2];
}
}
positions[positions.length-2+j] += delta;
}
}
}

@ -0,0 +1,165 @@
/*
* Copyright (c) 2022, 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.
*
* 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.
*/
/*
* @test
* @bug 8165943
* @summary LineBreakMeasurer does not measure correctly if TextAttribute.TRACKING is set
* @library ../../regtesthelpers
* @build PassFailJFrame
* @run main/manual LineBreakWithTracking
*/
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.font.FontRenderContext;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
import java.text.AttributedString;
import java.util.Hashtable;
import java.lang.reflect.InvocationTargetException;
class LineBreakPanel extends JPanel implements ActionListener {
private float textTracking = 0.0f;
private static String fontName = "Dialog";
private static String text = "This is a long line of text that should be broken across multiple lines. "
+ "Please set the different tracking values to test via menu! This test should pass if "
+ "these lines are broken to fit the width, and fail otherwise. It should "
+ "also format the hebrew (\u05d0\u05d1\u05d2 \u05d3\u05d4\u05d5) and arabic "
+ "(\u0627\u0628\u062a\u062c \u062e\u0644\u0627\u062e) and CJK "
+ "(\u4e00\u4e01\u4e02\uac00\uac01\uc4fa\u67b1\u67b2\u67b3\u67b4\u67b5\u67b6\u67b7"
+ "\u67b8\u67b9) text correctly.";
private LineBreakMeasurer lineMeasurer;
public void actionPerformed(ActionEvent e) {
textTracking = (float)((JRadioButtonMenuItem)e.getSource()).getClientProperty( "tracking" );
lineMeasurer = null;
invalidate();
repaint();
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
setBackground(Color.white);
Graphics2D g2d = (Graphics2D)g;
if (lineMeasurer == null) {
Float regular = Float.valueOf(16.0f);
Float big = Float.valueOf(24.0f);
Hashtable map = new Hashtable();
map.put(TextAttribute.SIZE, (float)18.0);
map.put(TextAttribute.TRACKING, (float)textTracking);
AttributedString astr = new AttributedString(text, map);
astr.addAttribute(TextAttribute.SIZE, regular, 0, text.length());
astr.addAttribute(TextAttribute.FAMILY, fontName, 0, text.length());
int ix = text.indexOf("broken");
astr.addAttribute(TextAttribute.SIZE, big, ix, ix + 6);
ix = text.indexOf("hebrew");
astr.addAttribute(TextAttribute.SIZE, big, ix, ix + 6);
ix = text.indexOf("arabic");
astr.addAttribute(TextAttribute.SIZE, big, ix, ix + 6);
ix = text.indexOf("CJK");
astr.addAttribute(TextAttribute.SIZE, big, ix, ix + 3);
FontRenderContext frc = g2d.getFontRenderContext();
lineMeasurer = new LineBreakMeasurer(astr.getIterator(), frc);
}
lineMeasurer.setPosition(0);
float w = (float)getSize().width;
float x = 0, y = 0;
TextLayout layout;
while ((layout = lineMeasurer.nextLayout(w)) != null) {
x = layout.isLeftToRight() ? 0 : w - layout.getAdvance();
y += layout.getAscent();
layout.draw(g2d, x, y);
y += layout.getDescent() + layout.getLeading();
}
}
}
public class LineBreakWithTracking {
private static final String INSTRUCTIONS = """
This manual test verifies that LineBreakMeasurer measures the lines'
breaks correctly taking into account the TextAttribute.TRACKING value.
The test string includes Latin, Arabic, CJK and Hebrew.
You should choose a tracking value from the menu and resize the window.
If the text lines break exactly to the wrapping width:
no room for one more word exists and
the text lines are not too long for given wrapping width, -
then press PASS, otherwise - FAIL.
""";
public void createGUI(JFrame frame) {
LineBreakPanel panel = new LineBreakPanel();
frame.getContentPane().add(panel, BorderLayout.CENTER);
JMenuBar menuBar = new JMenuBar();
JMenu menu = new JMenu("Tracking");
ButtonGroup btnGroup = new ButtonGroup();
String btnLabels[] = {"-0.1", "0", "0.1", "0.2", "0.3"};
float val = -0.1f;
for (String label : btnLabels) {
JRadioButtonMenuItem btn = new JRadioButtonMenuItem(label);
btn.putClientProperty( "tracking", val );
btn.addActionListener(panel);
btnGroup.add(btn);
menu.add(btn);
val += 0.1f;
}
menuBar.add(menu);
frame.setJMenuBar(menuBar);
}
public static void main(String[] args) throws InterruptedException, InvocationTargetException {
JFrame frame = new JFrame("LineBreakMeasurer with Tracking");
frame.setSize(new Dimension(640, 480));
LineBreakWithTracking controller = new LineBreakWithTracking();
controller.createGUI(frame);
PassFailJFrame passFailJFrame = new PassFailJFrame(INSTRUCTIONS);
PassFailJFrame.addTestWindow(frame);
PassFailJFrame.positionTestWindow(frame, PassFailJFrame.Position.HORIZONTAL);
frame.setVisible(true);
passFailJFrame.awaitAndCheck();
}
}

@ -0,0 +1,113 @@
/*
* Copyright (c) 2022, 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.
*
* 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.
*/
/*
@test
@bug 8165943
@summary LineBreakMeasurer does not measure correctly if TextAttribute.TRACKING is set
@run main/othervm LineBreakWithTrackingAuto
*/
import java.awt.font.FontRenderContext;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
import java.text.AttributedString;
public class LineBreakWithTrackingAuto {
private static final String WORD = "word";
private static final String SPACE = " ";
private static final int NUM_WORDS = 12;
private static final float FONT_SIZE = 24.0f;
private static final float TEXT_TRACKING[] = { -0.1f, 0f, 0.1f, 0.2f, 0.3f };
private static final float EPSILON = 0.005f;
public static void main(String[] args) {
new LineBreakWithTrackingAuto().test();
}
public void test() {
final FontRenderContext frc = new FontRenderContext(null, false, false);
// construct a paragraph as follows: [SPACE + WORD] + ...
StringBuffer text = new StringBuffer();
for (int i = 0; i < NUM_WORDS; i++) {
text.append(SPACE);
text.append(WORD);
}
AttributedString attrString = new AttributedString(text.toString());
attrString.addAttribute(TextAttribute.SIZE, Float.valueOf(FONT_SIZE));
// test different tracking values: -0.1f, 0f, 0.1f, 0.2f, 0.3f
for (float textTracking : TEXT_TRACKING) {
final float trackingAdvance = FONT_SIZE * textTracking;
attrString.addAttribute(TextAttribute.TRACKING, textTracking);
LineBreakMeasurer measurer = new LineBreakMeasurer(attrString.getIterator(), frc);
final int sequenceLength = WORD.length() + SPACE.length();
final float sequenceAdvance = getSequenceAdvance(measurer, text.length(), sequenceLength);
final float textAdvance = NUM_WORDS * sequenceAdvance;
// test different wrapping width starting from the WORD+SPACE to TEXT width
for (float wrappingWidth = sequenceAdvance; wrappingWidth < textAdvance; wrappingWidth += sequenceAdvance / sequenceLength) {
measurer.setPosition(0);
// break a paragraph into lines that fit the given wrapping width
do {
TextLayout layout = measurer.nextLayout(wrappingWidth);
float visAdvance = layout.getVisibleAdvance();
int currPos = measurer.getPosition();
if ((trackingAdvance <= 0 && visAdvance - wrappingWidth > EPSILON)
|| (trackingAdvance > 0 && visAdvance - wrappingWidth > trackingAdvance + EPSILON)) {
throw new Error("text line is too long for given wrapping width");
}
if (currPos < text.length() && visAdvance <= wrappingWidth - sequenceAdvance) {
throw new Error("text line is too short for given wrapping width");
}
} while (measurer.getPosition() != text.length());
}
}
}
private float getSequenceAdvance(LineBreakMeasurer measurer, int textLength, int sequenceLength) {
measurer.setPosition(textLength - sequenceLength);
TextLayout layout = measurer.nextLayout(10000.0f);
if (layout.getCharacterCount() != sequenceLength) {
throw new Error("layout length is incorrect");
}
return layout.getVisibleAdvance();
}
}