From 85db4631ae5208fba74fa8b8638c32d4e42d3268 Mon Sep 17 00:00:00 2001 From: Daniel Gredler Date: Tue, 13 May 2025 10:37:57 +0000 Subject: [PATCH] 8350203: [macos] Newlines and tabs are not ignored when drawing text to a Graphics2D object 8353187: Test TextLayout/TestControls fails on macOS: width of 0x9, 0xa, 0xd isn't zero Reviewed-by: honkar, aivanov, prr --- .../classes/sun/font/CCharToGlyphMapper.java | 10 +- .../DrawString/IgnoredWhitespaceTest.java | 187 ++++++++++++++++++ 2 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 test/jdk/java/awt/Graphics2D/DrawString/IgnoredWhitespaceTest.java diff --git a/src/java.desktop/macosx/classes/sun/font/CCharToGlyphMapper.java b/src/java.desktop/macosx/classes/sun/font/CCharToGlyphMapper.java index a919fcbbd88..5eb05a9f93e 100644 --- a/src/java.desktop/macosx/classes/sun/font/CCharToGlyphMapper.java +++ b/src/java.desktop/macosx/classes/sun/font/CCharToGlyphMapper.java @@ -92,7 +92,7 @@ public class CCharToGlyphMapper extends CharToGlyphMapper { int glyph = cache.get(unicode); if (glyph != 0) return glyph; - if (FontUtilities.isDefaultIgnorable(unicode)) { + if (FontUtilities.isDefaultIgnorable(unicode) || isIgnorableWhitespace(unicode)) { glyph = INVISIBLE_GLYPH_ID; } else { final char[] unicodeArray = new char[] { unicode }; @@ -130,6 +130,12 @@ public class CCharToGlyphMapper extends CharToGlyphMapper { } } + // Matches behavior in e.g. CMap.getControlCodeGlyph(int, boolean) + // and RasterPrinterJob.removeControlChars(String) + private static boolean isIgnorableWhitespace(int code) { + return code == 0x0009 || code == 0x000a || code == 0x000d; + } + // This mapper returns either the glyph code, or if the character can be // replaced on-the-fly using CoreText substitution; the negative unicode // value. If this "glyph code int" is treated as an opaque code, it will @@ -253,7 +259,7 @@ public class CCharToGlyphMapper extends CharToGlyphMapper { values[i+1] = INVISIBLE_GLYPH_ID; i++; } - } else if (FontUtilities.isDefaultIgnorable(code)) { + } else if (FontUtilities.isDefaultIgnorable(code) || isIgnorableWhitespace(code)) { values[i] = INVISIBLE_GLYPH_ID; put(code, INVISIBLE_GLYPH_ID); } else { diff --git a/test/jdk/java/awt/Graphics2D/DrawString/IgnoredWhitespaceTest.java b/test/jdk/java/awt/Graphics2D/DrawString/IgnoredWhitespaceTest.java new file mode 100644 index 00000000000..e7c38f1c16e --- /dev/null +++ b/test/jdk/java/awt/Graphics2D/DrawString/IgnoredWhitespaceTest.java @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2025, 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 8350203 + * @summary Confirm that a few special whitespace characters are ignored. + */ + +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.GraphicsEnvironment; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.font.FontRenderContext; +import java.awt.font.TextAttribute; +import java.awt.image.BufferedImage; +import java.text.AttributedString; +import java.util.Map; + +public class IgnoredWhitespaceTest { + + public static void main(String[] args) throws Exception { + BufferedImage image = new BufferedImage(600, 600, BufferedImage.TYPE_BYTE_BINARY); + Graphics2D g2d = image.createGraphics(); + + Font font = new Font(Font.DIALOG, Font.PLAIN, 40); + test(image, g2d, font); + + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + test(image, g2d, font); + + g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); + test(image, g2d, font); + + Font kerningFont = font.deriveFont(Map.of(TextAttribute.KERNING, TextAttribute.KERNING_ON)); + test(image, g2d, kerningFont); + + Font physicalFont = getPhysicalFont(40); + if (physicalFont != null) { + test(image, g2d, physicalFont); + } + + g2d.dispose(); + } + + private static void test(BufferedImage image, Graphics2D g2d, Font font) { + test(image, g2d, font, "XXXXX", "\t\t\t\t\tXXXXX"); + test(image, g2d, font, "XXXXX", "\tX\tX\tX\tX\tX\t"); + test(image, g2d, font, "XXXXX", "\r\r\r\r\rXXXXX"); + test(image, g2d, font, "XXXXX", "\rX\rX\rX\rX\rX\r"); + test(image, g2d, font, "XXXXX", "\n\n\n\n\nXXXXX"); + test(image, g2d, font, "XXXXX", "\nX\nX\nX\nX\nX\n"); + } + + private static void test(BufferedImage image, Graphics2D g2d, Font font, String reference, String text) { + g2d.setFont(font); + FontRenderContext frc = g2d.getFontRenderContext(); + int w = image.getWidth(); + int h = image.getHeight(); + int x = w / 2; + int y = h / 2; + + g2d.setColor(Color.WHITE); + g2d.fillRect(0, 0, w, h); + g2d.setColor(Color.BLACK); + g2d.drawString(reference, x, y); + Rectangle expected = findTextBoundingBox(image); + + g2d.setColor(Color.WHITE); + g2d.fillRect(0, 0, w, h); + g2d.setColor(Color.BLACK); + g2d.drawString(text, x, y); + Rectangle actual = findTextBoundingBox(image); + assertEqual(expected, actual, text); + + g2d.setColor(Color.WHITE); + g2d.fillRect(0, 0, w, h); + g2d.setColor(Color.BLACK); + g2d.drawString(new AttributedString(text, Map.of(TextAttribute.FONT, font)).getIterator(), x, y); + actual = findTextBoundingBox(image); + assertEqual(expected, actual, text); + + g2d.setColor(Color.WHITE); + g2d.fillRect(0, 0, w, h); + g2d.setColor(Color.BLACK); + g2d.drawChars(text.toCharArray(), 0, text.length(), x, y); + actual = findTextBoundingBox(image); + assertEqual(expected, actual, text); + + g2d.setColor(Color.WHITE); + g2d.fillRect(0, 0, w, h); + g2d.setColor(Color.BLACK); + g2d.drawGlyphVector(font.createGlyphVector(frc, text), x, y); + actual = findTextBoundingBox(image); + assertEqual(expected, actual, text); + } + + private static void assertEqual(Rectangle r1, Rectangle r2, String text) { + if (!r1.equals(r2)) { + String escaped = text.replace("\r", "\\r") + .replace("\n", "\\n") + .replace("\t", "\\t"); + String msg = String.format("for text '%s': %s != %s", escaped, r1.toString(), r2.toString()); + throw new RuntimeException(msg); + } + } + + private static Font getPhysicalFont(int size) { + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + String[] names = ge.getAvailableFontFamilyNames(); + for (String n : names) { + switch (n) { + case Font.DIALOG: + case Font.DIALOG_INPUT: + case Font.SERIF: + case Font.SANS_SERIF: + case Font.MONOSPACED: + continue; + default: + Font f = new Font(n, Font.PLAIN, size); + if (f.canDisplayUpTo("AZaz09") == -1) { + return f; + } + } + } + return null; + } + + private static Rectangle findTextBoundingBox(BufferedImage image) { + int minX = Integer.MAX_VALUE; + int minY = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxY = Integer.MIN_VALUE; + int width = image.getWidth(); + int height = image.getHeight(); + + int[] rowPixels = new int[width]; + for (int y = 0; y < height; y++) { + image.getRGB(0, y, width, 1, rowPixels, 0, width); + for (int x = 0; x < width; x++) { + boolean white = (rowPixels[x] == -1); + if (!white) { + if (x < minX) { + minX = x; + } + if (y < minY) { + minY = y; + } + if (x > maxX) { + maxX = x; + } + if (y > maxY) { + maxY = y; + } + } + } + } + + if (minX != Integer.MAX_VALUE) { + return new Rectangle(minX, minY, maxX - minX, maxY - minY); + } else { + return null; + } + } +}