Skip to content

Commit 85db463

Browse files
Daniel Gredleraivanov-jdk
authored andcommitted
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
1 parent 38bb8ad commit 85db463

File tree

2 files changed

+195
-2
lines changed

2 files changed

+195
-2
lines changed

src/java.desktop/macosx/classes/sun/font/CCharToGlyphMapper.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public synchronized int charToGlyph(char unicode) {
9292
int glyph = cache.get(unicode);
9393
if (glyph != 0) return glyph;
9494

95-
if (FontUtilities.isDefaultIgnorable(unicode)) {
95+
if (FontUtilities.isDefaultIgnorable(unicode) || isIgnorableWhitespace(unicode)) {
9696
glyph = INVISIBLE_GLYPH_ID;
9797
} else {
9898
final char[] unicodeArray = new char[] { unicode };
@@ -130,6 +130,12 @@ public synchronized void charsToGlyphs(int count, int[] unicodes, int[] glyphs)
130130
}
131131
}
132132

133+
// Matches behavior in e.g. CMap.getControlCodeGlyph(int, boolean)
134+
// and RasterPrinterJob.removeControlChars(String)
135+
private static boolean isIgnorableWhitespace(int code) {
136+
return code == 0x0009 || code == 0x000a || code == 0x000d;
137+
}
138+
133139
// This mapper returns either the glyph code, or if the character can be
134140
// replaced on-the-fly using CoreText substitution; the negative unicode
135141
// value. If this "glyph code int" is treated as an opaque code, it will
@@ -253,7 +259,7 @@ public synchronized void get(int count, char[] indices, int[] values)
253259
values[i+1] = INVISIBLE_GLYPH_ID;
254260
i++;
255261
}
256-
} else if (FontUtilities.isDefaultIgnorable(code)) {
262+
} else if (FontUtilities.isDefaultIgnorable(code) || isIgnorableWhitespace(code)) {
257263
values[i] = INVISIBLE_GLYPH_ID;
258264
put(code, INVISIBLE_GLYPH_ID);
259265
} else {
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/*
2+
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
/*
25+
* @test
26+
* @bug 8350203
27+
* @summary Confirm that a few special whitespace characters are ignored.
28+
*/
29+
30+
import java.awt.Color;
31+
import java.awt.Font;
32+
import java.awt.Graphics2D;
33+
import java.awt.GraphicsEnvironment;
34+
import java.awt.Rectangle;
35+
import java.awt.RenderingHints;
36+
import java.awt.font.FontRenderContext;
37+
import java.awt.font.TextAttribute;
38+
import java.awt.image.BufferedImage;
39+
import java.text.AttributedString;
40+
import java.util.Map;
41+
42+
public class IgnoredWhitespaceTest {
43+
44+
public static void main(String[] args) throws Exception {
45+
BufferedImage image = new BufferedImage(600, 600, BufferedImage.TYPE_BYTE_BINARY);
46+
Graphics2D g2d = image.createGraphics();
47+
48+
Font font = new Font(Font.DIALOG, Font.PLAIN, 40);
49+
test(image, g2d, font);
50+
51+
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
52+
test(image, g2d, font);
53+
54+
g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
55+
test(image, g2d, font);
56+
57+
Font kerningFont = font.deriveFont(Map.of(TextAttribute.KERNING, TextAttribute.KERNING_ON));
58+
test(image, g2d, kerningFont);
59+
60+
Font physicalFont = getPhysicalFont(40);
61+
if (physicalFont != null) {
62+
test(image, g2d, physicalFont);
63+
}
64+
65+
g2d.dispose();
66+
}
67+
68+
private static void test(BufferedImage image, Graphics2D g2d, Font font) {
69+
test(image, g2d, font, "XXXXX", "\t\t\t\t\tXXXXX");
70+
test(image, g2d, font, "XXXXX", "\tX\tX\tX\tX\tX\t");
71+
test(image, g2d, font, "XXXXX", "\r\r\r\r\rXXXXX");
72+
test(image, g2d, font, "XXXXX", "\rX\rX\rX\rX\rX\r");
73+
test(image, g2d, font, "XXXXX", "\n\n\n\n\nXXXXX");
74+
test(image, g2d, font, "XXXXX", "\nX\nX\nX\nX\nX\n");
75+
}
76+
77+
private static void test(BufferedImage image, Graphics2D g2d, Font font, String reference, String text) {
78+
g2d.setFont(font);
79+
FontRenderContext frc = g2d.getFontRenderContext();
80+
int w = image.getWidth();
81+
int h = image.getHeight();
82+
int x = w / 2;
83+
int y = h / 2;
84+
85+
g2d.setColor(Color.WHITE);
86+
g2d.fillRect(0, 0, w, h);
87+
g2d.setColor(Color.BLACK);
88+
g2d.drawString(reference, x, y);
89+
Rectangle expected = findTextBoundingBox(image);
90+
91+
g2d.setColor(Color.WHITE);
92+
g2d.fillRect(0, 0, w, h);
93+
g2d.setColor(Color.BLACK);
94+
g2d.drawString(text, x, y);
95+
Rectangle actual = findTextBoundingBox(image);
96+
assertEqual(expected, actual, text);
97+
98+
g2d.setColor(Color.WHITE);
99+
g2d.fillRect(0, 0, w, h);
100+
g2d.setColor(Color.BLACK);
101+
g2d.drawString(new AttributedString(text, Map.of(TextAttribute.FONT, font)).getIterator(), x, y);
102+
actual = findTextBoundingBox(image);
103+
assertEqual(expected, actual, text);
104+
105+
g2d.setColor(Color.WHITE);
106+
g2d.fillRect(0, 0, w, h);
107+
g2d.setColor(Color.BLACK);
108+
g2d.drawChars(text.toCharArray(), 0, text.length(), x, y);
109+
actual = findTextBoundingBox(image);
110+
assertEqual(expected, actual, text);
111+
112+
g2d.setColor(Color.WHITE);
113+
g2d.fillRect(0, 0, w, h);
114+
g2d.setColor(Color.BLACK);
115+
g2d.drawGlyphVector(font.createGlyphVector(frc, text), x, y);
116+
actual = findTextBoundingBox(image);
117+
assertEqual(expected, actual, text);
118+
}
119+
120+
private static void assertEqual(Rectangle r1, Rectangle r2, String text) {
121+
if (!r1.equals(r2)) {
122+
String escaped = text.replace("\r", "\\r")
123+
.replace("\n", "\\n")
124+
.replace("\t", "\\t");
125+
String msg = String.format("for text '%s': %s != %s", escaped, r1.toString(), r2.toString());
126+
throw new RuntimeException(msg);
127+
}
128+
}
129+
130+
private static Font getPhysicalFont(int size) {
131+
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
132+
String[] names = ge.getAvailableFontFamilyNames();
133+
for (String n : names) {
134+
switch (n) {
135+
case Font.DIALOG:
136+
case Font.DIALOG_INPUT:
137+
case Font.SERIF:
138+
case Font.SANS_SERIF:
139+
case Font.MONOSPACED:
140+
continue;
141+
default:
142+
Font f = new Font(n, Font.PLAIN, size);
143+
if (f.canDisplayUpTo("AZaz09") == -1) {
144+
return f;
145+
}
146+
}
147+
}
148+
return null;
149+
}
150+
151+
private static Rectangle findTextBoundingBox(BufferedImage image) {
152+
int minX = Integer.MAX_VALUE;
153+
int minY = Integer.MAX_VALUE;
154+
int maxX = Integer.MIN_VALUE;
155+
int maxY = Integer.MIN_VALUE;
156+
int width = image.getWidth();
157+
int height = image.getHeight();
158+
159+
int[] rowPixels = new int[width];
160+
for (int y = 0; y < height; y++) {
161+
image.getRGB(0, y, width, 1, rowPixels, 0, width);
162+
for (int x = 0; x < width; x++) {
163+
boolean white = (rowPixels[x] == -1);
164+
if (!white) {
165+
if (x < minX) {
166+
minX = x;
167+
}
168+
if (y < minY) {
169+
minY = y;
170+
}
171+
if (x > maxX) {
172+
maxX = x;
173+
}
174+
if (y > maxY) {
175+
maxY = y;
176+
}
177+
}
178+
}
179+
}
180+
181+
if (minX != Integer.MAX_VALUE) {
182+
return new Rectangle(minX, minY, maxX - minX, maxY - minY);
183+
} else {
184+
return null;
185+
}
186+
}
187+
}

0 commit comments

Comments
 (0)