JDK-8270265 : LineBreakMeasurer calculates incorrect line breaks with zero-width characters
  • Type: Bug
  • Component: client-libs
  • Sub-Component: 2d
  • Affected Version: 11,16,17
  • Priority: P4
  • Status: Resolved
  • Resolution: Fixed
  • OS: windows_7
  • CPU: x86_64
  • Submitted: 2021-07-06
  • Updated: 2025-08-06
  • Resolved: 2025-03-13
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.
JDK 25
25 b15Fixed
Related Reports
Relates :  
Description
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



Comments
Changeset: 7fc776e2 Branch: master Author: Daniel Gredler <dgredler@openjdk.org> Committer: Phil Race <prr@openjdk.org> Date: 2025-03-13 20:27:27 +0000 URL: https://git.openjdk.org/jdk/commit/7fc776e2ace920a3b1b319c021e6d3d440305b5e
13-03-2025

A pull request was submitted for review. Branch: master URL: https://git.openjdk.org/jdk/pull/23603 Date: 2025-02-12 23:53:43 +0000
12-02-2025

This isn't new. Same can be seen with JDK 11. The test is a bit extreme in that it looks as if it inserts a ZW code point after before single normal character. ie it turns "Hello" into "\u200bH\u200be\u200bl\u200bl\u200bo" I don't know if that some how contributes. Also the test is not ideal since it assumes you have two non-standard fonts installed. The test says "Arial" but actually loads "Arial Unicode" - a different beast that does NOT come with Windows. Same for the Noto font. So I was not able to verify those. And why use createFont ???
14-07-2021

Additional information from submitter: =========================== There are 3 output files attached for JDK-8270265: measure-test-zwj.png, measure-test-zwnj.png, and measure-test-zwsp.png. I have manually annotated the images, noting where the line lengths match (as expected -> "OK" in blue) and where the line lengths do not match (unexpected -> "mismatch" in red).
13-07-2021

Mail to submitter: ============= Please can you share screenshot of the issues [1] and [2] From the description, Windows 6 is being used, as we are triaging the issue in Windows 10. [1] https://bugs.openjdk.java.net/browse/JDK-8270265 [2] https://bugs.openjdk.java.net/browse/JDK-8269888
12-07-2021