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

[macos] Unicode characters can be drawn with G2D.drawString but not TextLayout

XMLWordPrintable

    • 2d
    • generic
    • os_x

      ADDITIONAL SYSTEM INFORMATION :
      MacOS 14.6.1 (23G93) on MacBook Air M3 2024.
      Bug confirmed present on both OpenJDK 21.0.4 and OpenJDK 24.0.1.

      A DESCRIPTION OF THE PROBLEM :
      On MacOS, certain Unicode characters can be drawn with Graphics2D.drawString, but not with Graphics2D.drawGlyphVector or via TextLayout.draw. This is despite the fact that Font.canDisplay returns true for the codepoints in question.

      Examples of such characters include subscript-k (\u2096) and subscript-n (\u2099). In the basic multilingual plane, there are 4756 characters that render to a tofu symbol despite Font.canDisplay returning true for them. (Set CALCULATE_NUMBER_OF_AFFECTED_CHARACTERS=true in the test code to calculate this number.)

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      Run the attached exhibit code, MacTofuSubscriptExhibit, on MacOS.

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      The string "subscript_k\u2096 and subscript_n\u2099" should be shown three times, with the two subscript symbols shown correctly.
      ACTUAL -
      Only the first line, which is drawn using Graphics2D.drawString, prints correctly. The other two, which are drawn using Graphics2D.drawGlyphVector and TextLayout.draw, respectively, show the subscript characters either missing (on Java 24) or with missing-character tofu symbols (on Java 21).

      ---------- BEGIN SOURCE ----------
      import java.awt.*;
      import java.awt.font.*;
      import java.awt.image.BufferedImage;
      import java.text.AttributedString;
      import java.util.Arrays;
      import java.util.HashMap;
      import java.util.Map;
      import javax.swing.JFrame;
      import javax.swing.JPanel;
      import javax.swing.SwingUtilities;

      public class MacTofuSubscriptExhibit extends JFrame {
        private static final boolean CALCULATE_NUMBER_OF_AFFECTED_CHARACTERS = false;
        private final Font font = new Font("Dialog", Font.PLAIN, 20);

        public MacTofuSubscriptExhibit() {
          setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
          pack();
          add(new PaintPanel(), BorderLayout.CENTER);
          setSize(800, 300);
          setLocationRelativeTo(null);

          if (CALCULATE_NUMBER_OF_AFFECTED_CHARACTERS) {
            /* Assume this codepoint renders to a tofu symbol with Graphics2D.drawGlyphVector, as shown in
            the JFrame. Then find the number of characters who render to a tofu symbol despite
            Font.canDisplay() returning true. (We don't check here if drawString would have painted the
            character correctly.) */
            BufferedImage subscriptKImage = renderGlyph(font, '\u2099');
            int affectedCharCount = 0;
            // Includes Basic Multilingual Plane only.
            for (int cp = 0; cp < 65536; cp++) {
              if (!font.canDisplay(cp) || Character.isISOControl(cp))
                continue;
              BufferedImage currentGlyphImage = renderGlyph(font, cp);
              if (imagesAreEqual(subscriptKImage, currentGlyphImage)) {
                // System.out.println("Affected character: " + Character.toString(cp));
                affectedCharCount++;
              }
            }
            // On both OpenJDK 21.0.4 and OpenJDK 24.0.1 for Mac OS X, prints affectedCharCount=4756.
            System.out.println("affectedCharCount=" + affectedCharCount);
          }
        }

        public static void main(String args[]) {
          System.out.println(
              "Java " + System.getProperty("java.version") + " on " + System.getProperty("os.name"));

          SwingUtilities.invokeLater(() -> {
            new MacTofuSubscriptExhibit().setVisible(true);
          });
        }

        private static class PaintPanel extends JPanel {
          @Override
          protected void paintComponent(Graphics g0) {
            Graphics2D g = (Graphics2D) g0;
            Font font = new Font("Dialog", Font.PLAIN, 20);
            g.setFont(font);
            g.setColor(Color.BLACK);

            // Indicated outputs were confirmed on both OpenJDK 21.0.4 and OpenJDK 24.0.1 for MacOS.

            String s = "subscript_k\u2096 and subscript_n\u2099";
            System.out.println("String to be rendered: " + s);
            // Prints true.
            System.out.println("canDisplay('\\u2096')=" + font.canDisplay('\u2096'));
            // Prints true.
            System.out.println("canDisplay('\\u2099')=" + font.canDisplay('\u2099'));
            // Prints width=8.0.
            System.out.println("getStringBounds(\"\\u2096\").width=" +
                font.getStringBounds("\u2096", g.getFontRenderContext()).getWidth());
            /* Prints width=9.0. (The width difference between the two characters suggests that
            getStringBounds is accessing a fallback font correctly, like drawString but unlike GlyphVector
            or TextLayout, see below.) */
            System.out.println("getStringBounds(\"\\u2099\").width=" +
                font.getStringBounds("\u2099", g.getFontRenderContext()).getWidth());

            // Method 1 (drawString): The subscript characters appear correctly.
            g.drawString(s, 20, 50);

            /* Method 2 (GlyphVector): The subscript characters are shown as missing-character rectangles
                   (tofu) on OpenJDK 21.0.4 for MacOS, or as empty spaces on OpenJDK 24.0.1 for MacOS. */
            GlyphVector gv = font.layoutGlyphVector(g.getFontRenderContext(),
                s.toCharArray(), 0, s.length(), 0);
            g.drawGlyphVector(gv, 20, 100);

            // Method 3 (TextLayout): Same result as with Method 2 (GlyphVector).
            final Map<TextAttribute,Object> attributes = new HashMap<>();
            attributes.put(TextAttribute.FONT, font);
            AttributedString as = new AttributedString(s, attributes);
            TextMeasurer tm = new TextMeasurer(as.getIterator(), g.getFontRenderContext());
            TextLayout tl = tm.getLayout(0, s.length());
            tl.draw(g, 20, 150);
          }
        }

        private static BufferedImage renderGlyph(Font font, int codePoint) {
          int imageSize = Math.max(font.getSize() * 4, 100);
          float fontSize = 20;
          BufferedImage ret = new BufferedImage(imageSize, imageSize, BufferedImage.TYPE_INT_ARGB);
          Graphics2D g = ret.createGraphics();
          try {
            g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
            g.setColor(Color.WHITE);
            g.fillRect(0, 0, imageSize, imageSize);
            g.setColor(Color.BLACK);
            String s = Character.toString(codePoint);
            GlyphVector gv = font.createGlyphVector(g.getFontRenderContext(), s);
            g.drawGlyphVector(gv, 10, fontSize * 2);
          } finally {
            g.dispose();
          }
          return ret;
        }

        public static boolean imagesAreEqual(BufferedImage img1, BufferedImage img2) {
          if (img1.getWidth() != img2.getWidth() || img1.getHeight() != img2.getHeight())
            return false;
          int width = img1.getWidth();
          int height = img1.getHeight();
          int[] pixels1 = img1.getRGB(0, 0, width, height, null, 0, width);
          int[] pixels2 = img2.getRGB(0, 0, width, height, null, 0, width);
          return Arrays.equals(pixels1, pixels2);
        }
      }

      ---------- END SOURCE ----------

            prr Philip Race
            webbuggrp Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            3 Start watching this issue

              Created:
              Updated: