Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8270265

LineBreakMeasurer calculates incorrect line breaks with zero-width characters

XMLWordPrintable

    • 2d
    • 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


            dgredler Daniel Gredler
            webbuggrp Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            6 Start watching this issue

              Created:
              Updated:
              Resolved: