-
Bug
-
Resolution: Fixed
-
P4
-
11, 16, 17
-
b15
-
x86_64
-
windows_7
ADDITIONAL SYSTEM INFORMATION :
Microsoft Windows [Version 6.1.7601]
openjdk version "17-ea" 2021-09-14
OpenJDK Runtime Environment (build 17-ea+28-2534)
OpenJDK 64-Bit Server VM (build 17-ea+28-2534, mixed mode, sharing)
A DESCRIPTION OF THE PROBLEM :
When using the LineBreakMeasurer to split text across lines of text, the presence of zero-width characters like zero-width space (ZWSP, U+200B), zero-width non-joiner (ZWNJ, U+200C) or zero-width joiner (ZWJ, U+200D) often cause mismeasurement of the correct line break positions.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Run the code below, which generates 3 PNG files, one for each zero-width character being tested (ZWJ, ZWNJ, ZWSP). Each file tests 6 fonts: Tahoma, Arial, Noto, and the built-in Serif, Sans Serif and Monospaced fonts. The following characters and fonts seem to be break the LineBreakMeasurer measurements:
ZWJ: Tahoma, Arial, Noto
ZWNJ: Tahoma, Arial, Noto
ZWSP: Tahoma, Arial, Noto, Serif, Sans Serif, Monospaced
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Each pair of lines match, i.e. have the same output (line 1 and line 2, line 3 and line 4, line 5 and line 6, etc).
ACTUAL -
Many of the pairs of lines do not match, because the LineBreakMeasurer breaks lines differently depending on whether or not the text contains zero-width characters.
---------- BEGIN SOURCE ----------
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
import java.awt.image.BufferedImage;
import java.io.File;
import java.text.AttributedString;
import java.util.Map;
import javax.imageio.ImageIO;
public class ZwMeasureTest {
private static final char ZWSP = '\u200B'; // https://en.wikipedia.org/wiki/Zero-width_space
private static final char ZWNJ = '\u200C'; // https://en.wikipedia.org/wiki/Zero-width_non-joiner
private static final char ZWJ = '\u200D'; // https://en.wikipedia.org/wiki/Zero-width_joiner
public static void main(String... args) throws Exception {
test(ZWSP, "measure-test-zwsp.png");
test(ZWNJ, "measure-test-zwnj.png");
test(ZWJ, "measure-test-zwj.png");
}
private static void test(char c, String filename) throws Exception {
Font tahoma = Font.createFont(Font.TRUETYPE_FONT, new File("C:/Windows/Fonts/tahoma.ttf")).deriveFont(50f);
Font arial = Font.createFont(Font.TRUETYPE_FONT, new File("C:/Windows/Fonts/ARIALUNI.TTF")).deriveFont(50f);
Font noto = Font.createFont(Font.TRUETYPE_FONT, new File("noto-sans-regular.ttf")).deriveFont(50f);
Font serif = new Font("Serif", Font.PLAIN, 50);
Font sans = new Font("SansSerif", Font.PLAIN, 50);
Font mono = new Font("Monospaced", Font.PLAIN, 50);
BufferedImage img = new BufferedImage(1000, 600, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = img.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setColor(Color.WHITE);
g2d.fillRect(0, 0, img.getWidth(), img.getHeight());
g2d.setColor(Color.BLACK);
drawWithBounds(g2d, 30, 100, tahoma, "TahomaTest");
drawWithBounds(g2d, 30, 150, tahoma, insert("TahomaTest", c)); // should match line above
drawWithLineBM(g2d, 30, 200, tahoma, "TahomaTest", 200);
drawWithLineBM(g2d, 30, 250, tahoma, insert("TahomaTest", c), 200); // should match line above
drawWithBounds(g2d, 400, 100, arial, "ArialTest");
drawWithBounds(g2d, 400, 150, arial, insert("ArialTest", c)); // should match line above
drawWithLineBM(g2d, 400, 200, arial, "ArialTest", 200);
drawWithLineBM(g2d, 400, 250, arial, insert("ArialTest", c), 200); // should match line above
drawWithBounds(g2d, 700, 100, noto, "NotoTest");
drawWithBounds(g2d, 700, 150, noto, insert("NotoTest", c)); // should match line above
drawWithLineBM(g2d, 700, 200, noto, "NotoTest", 200);
drawWithLineBM(g2d, 700, 250, noto, insert("NotoTest", c), 200); // should match line above
drawWithBounds(g2d, 30, 350, serif, "SerifTest");
drawWithBounds(g2d, 30, 400, serif, insert("SerifTest", c)); // should match line above
drawWithLineBM(g2d, 30, 450, serif, "SerifTest", 150);
drawWithLineBM(g2d, 30, 500, serif, insert("SerifTest", c), 150); // should match line above
drawWithBounds(g2d, 400, 350, sans, "SansTest");
drawWithBounds(g2d, 400, 400, sans, insert("SansTest", c)); // should match line above
drawWithLineBM(g2d, 400, 450, sans, "SansTest", 150);
drawWithLineBM(g2d, 400, 500, sans, insert("SansTest", c), 150); // should match line above
drawWithBounds(g2d, 700, 350, mono, "MonoTest");
drawWithBounds(g2d, 700, 400, mono, insert("MonoTest", c)); // should match line above
drawWithLineBM(g2d, 700, 450, mono, "MonoTest", 150);
drawWithLineBM(g2d, 700, 500, mono, insert("MonoTest", c), 150); // should match line above
g2d.dispose();
ImageIO.write(img, "png", new File(filename));
}
private static final void drawWithBounds(Graphics2D g2d, int x, int y, Font font, String s) {
g2d.setFont(font);
int width = (int) g2d.getFontMetrics().getStringBounds(s, g2d).getWidth();
g2d.drawString(s, x, y);
g2d.drawLine(x, y, x + width, y);
}
private static final void drawWithLineBM(Graphics2D g2d, int x, int y, Font font, String s, int maxWidth) {
g2d.setFont(font);
AttributedString as = new AttributedString(s, Map.of(TextAttribute.FONT, font));
LineBreakMeasurer lbm = new LineBreakMeasurer(as.getIterator(), g2d.getFontRenderContext());
TextLayout layout = lbm.nextLayout(maxWidth);
layout.draw(g2d, x, y);
int advance = (int) layout.getAdvance();
g2d.drawLine(x, y, x + advance, y);
}
private static final String insert(String s, char c) {
return s.replaceAll(".", "$0" + c);
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
Remove the zero-width characters, if they are not needed to e.g. prevent character shaping. Otherwise, there is no workaround.
FREQUENCY : always
Microsoft Windows [Version 6.1.7601]
openjdk version "17-ea" 2021-09-14
OpenJDK Runtime Environment (build 17-ea+28-2534)
OpenJDK 64-Bit Server VM (build 17-ea+28-2534, mixed mode, sharing)
A DESCRIPTION OF THE PROBLEM :
When using the LineBreakMeasurer to split text across lines of text, the presence of zero-width characters like zero-width space (ZWSP, U+200B), zero-width non-joiner (ZWNJ, U+200C) or zero-width joiner (ZWJ, U+200D) often cause mismeasurement of the correct line break positions.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Run the code below, which generates 3 PNG files, one for each zero-width character being tested (ZWJ, ZWNJ, ZWSP). Each file tests 6 fonts: Tahoma, Arial, Noto, and the built-in Serif, Sans Serif and Monospaced fonts. The following characters and fonts seem to be break the LineBreakMeasurer measurements:
ZWJ: Tahoma, Arial, Noto
ZWNJ: Tahoma, Arial, Noto
ZWSP: Tahoma, Arial, Noto, Serif, Sans Serif, Monospaced
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Each pair of lines match, i.e. have the same output (line 1 and line 2, line 3 and line 4, line 5 and line 6, etc).
ACTUAL -
Many of the pairs of lines do not match, because the LineBreakMeasurer breaks lines differently depending on whether or not the text contains zero-width characters.
---------- BEGIN SOURCE ----------
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
import java.awt.image.BufferedImage;
import java.io.File;
import java.text.AttributedString;
import java.util.Map;
import javax.imageio.ImageIO;
public class ZwMeasureTest {
private static final char ZWSP = '\u200B'; // https://en.wikipedia.org/wiki/Zero-width_space
private static final char ZWNJ = '\u200C'; // https://en.wikipedia.org/wiki/Zero-width_non-joiner
private static final char ZWJ = '\u200D'; // https://en.wikipedia.org/wiki/Zero-width_joiner
public static void main(String... args) throws Exception {
test(ZWSP, "measure-test-zwsp.png");
test(ZWNJ, "measure-test-zwnj.png");
test(ZWJ, "measure-test-zwj.png");
}
private static void test(char c, String filename) throws Exception {
Font tahoma = Font.createFont(Font.TRUETYPE_FONT, new File("C:/Windows/Fonts/tahoma.ttf")).deriveFont(50f);
Font arial = Font.createFont(Font.TRUETYPE_FONT, new File("C:/Windows/Fonts/ARIALUNI.TTF")).deriveFont(50f);
Font noto = Font.createFont(Font.TRUETYPE_FONT, new File("noto-sans-regular.ttf")).deriveFont(50f);
Font serif = new Font("Serif", Font.PLAIN, 50);
Font sans = new Font("SansSerif", Font.PLAIN, 50);
Font mono = new Font("Monospaced", Font.PLAIN, 50);
BufferedImage img = new BufferedImage(1000, 600, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = img.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setColor(Color.WHITE);
g2d.fillRect(0, 0, img.getWidth(), img.getHeight());
g2d.setColor(Color.BLACK);
drawWithBounds(g2d, 30, 100, tahoma, "TahomaTest");
drawWithBounds(g2d, 30, 150, tahoma, insert("TahomaTest", c)); // should match line above
drawWithLineBM(g2d, 30, 200, tahoma, "TahomaTest", 200);
drawWithLineBM(g2d, 30, 250, tahoma, insert("TahomaTest", c), 200); // should match line above
drawWithBounds(g2d, 400, 100, arial, "ArialTest");
drawWithBounds(g2d, 400, 150, arial, insert("ArialTest", c)); // should match line above
drawWithLineBM(g2d, 400, 200, arial, "ArialTest", 200);
drawWithLineBM(g2d, 400, 250, arial, insert("ArialTest", c), 200); // should match line above
drawWithBounds(g2d, 700, 100, noto, "NotoTest");
drawWithBounds(g2d, 700, 150, noto, insert("NotoTest", c)); // should match line above
drawWithLineBM(g2d, 700, 200, noto, "NotoTest", 200);
drawWithLineBM(g2d, 700, 250, noto, insert("NotoTest", c), 200); // should match line above
drawWithBounds(g2d, 30, 350, serif, "SerifTest");
drawWithBounds(g2d, 30, 400, serif, insert("SerifTest", c)); // should match line above
drawWithLineBM(g2d, 30, 450, serif, "SerifTest", 150);
drawWithLineBM(g2d, 30, 500, serif, insert("SerifTest", c), 150); // should match line above
drawWithBounds(g2d, 400, 350, sans, "SansTest");
drawWithBounds(g2d, 400, 400, sans, insert("SansTest", c)); // should match line above
drawWithLineBM(g2d, 400, 450, sans, "SansTest", 150);
drawWithLineBM(g2d, 400, 500, sans, insert("SansTest", c), 150); // should match line above
drawWithBounds(g2d, 700, 350, mono, "MonoTest");
drawWithBounds(g2d, 700, 400, mono, insert("MonoTest", c)); // should match line above
drawWithLineBM(g2d, 700, 450, mono, "MonoTest", 150);
drawWithLineBM(g2d, 700, 500, mono, insert("MonoTest", c), 150); // should match line above
g2d.dispose();
ImageIO.write(img, "png", new File(filename));
}
private static final void drawWithBounds(Graphics2D g2d, int x, int y, Font font, String s) {
g2d.setFont(font);
int width = (int) g2d.getFontMetrics().getStringBounds(s, g2d).getWidth();
g2d.drawString(s, x, y);
g2d.drawLine(x, y, x + width, y);
}
private static final void drawWithLineBM(Graphics2D g2d, int x, int y, Font font, String s, int maxWidth) {
g2d.setFont(font);
AttributedString as = new AttributedString(s, Map.of(TextAttribute.FONT, font));
LineBreakMeasurer lbm = new LineBreakMeasurer(as.getIterator(), g2d.getFontRenderContext());
TextLayout layout = lbm.nextLayout(maxWidth);
layout.draw(g2d, x, y);
int advance = (int) layout.getAdvance();
g2d.drawLine(x, y, x + advance, y);
}
private static final String insert(String s, char c) {
return s.replaceAll(".", "$0" + c);
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
Remove the zero-width characters, if they are not needed to e.g. prevent character shaping. Otherwise, there is no workaround.
FREQUENCY : always
- links to
-
Commit(master) openjdk/jdk/7fc776e2
-
Review(master) openjdk/jdk/23603