Skip to content

Commit 290cfae

Browse files
committed
8282958: Rendering Issues with Borders on Windows High-DPI systems
Backport-of: 9911405e543dbe07767808bad88534abbcc03c5a
1 parent cfdca89 commit 290cfae

File tree

3 files changed

+663
-6
lines changed

3 files changed

+663
-6
lines changed

src/java.desktop/share/classes/javax/swing/border/LineBorder.java

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 1997, 2015, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 1997, 2022, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -34,6 +34,9 @@
3434
import java.awt.geom.Rectangle2D;
3535
import java.awt.geom.RoundRectangle2D;
3636
import java.beans.ConstructorProperties;
37+
import java.awt.geom.AffineTransform;
38+
39+
import static sun.java2d.pipe.Region.clipRound;
3740

3841
/**
3942
* A class which implements a line border of arbitrary thickness
@@ -144,28 +147,70 @@ public void paintBorder(Component c, Graphics g, int x, int y, int width, int he
144147
if ((this.thickness > 0) && (g instanceof Graphics2D)) {
145148
Graphics2D g2d = (Graphics2D) g;
146149

150+
AffineTransform at = g2d.getTransform();
151+
152+
// if m01 or m10 is non-zero, then there is a rotation or shear
153+
// or if no Scaling enabled,
154+
// skip resetting the transform
155+
boolean resetTransform = ((at.getShearX() == 0) && (at.getShearY() == 0)) &&
156+
((at.getScaleX() > 1) || (at.getScaleY() > 1));
157+
158+
int xtranslation;
159+
int ytranslation;
160+
int w;
161+
int h;
162+
int offs;
163+
164+
if (resetTransform) {
165+
/* Deactivate the HiDPI scaling transform,
166+
* so we can do paint operations in the device
167+
* pixel coordinate system instead of the logical coordinate system.
168+
*/
169+
g2d.setTransform(new AffineTransform());
170+
double xx = at.getScaleX() * x + at.getTranslateX();
171+
double yy = at.getScaleY() * y + at.getTranslateY();
172+
xtranslation = clipRound(xx);
173+
ytranslation = clipRound(yy);
174+
w = clipRound(at.getScaleX() * width + xx) - xtranslation;
175+
h = clipRound(at.getScaleY() * height + yy) - ytranslation;
176+
offs = this.thickness * (int) at.getScaleX();
177+
} else {
178+
w = width;
179+
h = height;
180+
xtranslation = x;
181+
ytranslation = y;
182+
offs = this.thickness;
183+
}
184+
185+
g2d.translate(xtranslation, ytranslation);
186+
147187
Color oldColor = g2d.getColor();
148188
g2d.setColor(this.lineColor);
149189

150190
Shape outer;
151191
Shape inner;
152192

153-
int offs = this.thickness;
154193
int size = offs + offs;
155194
if (this.roundedCorners) {
156195
float arc = .2f * offs;
157-
outer = new RoundRectangle2D.Float(x, y, width, height, offs, offs);
158-
inner = new RoundRectangle2D.Float(x + offs, y + offs, width - size, height - size, arc, arc);
196+
outer = new RoundRectangle2D.Float(0, 0, w, h, offs, offs);
197+
inner = new RoundRectangle2D.Float(offs, offs, w - size, h - size, arc, arc);
159198
}
160199
else {
161-
outer = new Rectangle2D.Float(x, y, width, height);
162-
inner = new Rectangle2D.Float(x + offs, y + offs, width - size, height - size);
200+
outer = new Rectangle2D.Float(0, 0, w, h);
201+
inner = new Rectangle2D.Float(offs, offs, w - size, h - size);
163202
}
164203
Path2D path = new Path2D.Float(Path2D.WIND_EVEN_ODD);
165204
path.append(outer, false);
166205
path.append(inner, false);
167206
g2d.fill(path);
168207
g2d.setColor(oldColor);
208+
209+
g2d.translate(-xtranslation, -ytranslation);
210+
211+
if (resetTransform) {
212+
g2d.setTransform(at);
213+
}
169214
}
170215
}
171216

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
/*
2+
* Copyright (c) 2022, 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+
import java.awt.BorderLayout;
25+
import java.awt.Color;
26+
import java.awt.Component;
27+
import java.awt.Dimension;
28+
import java.awt.Graphics2D;
29+
import java.awt.Point;
30+
import java.awt.image.BufferedImage;
31+
import java.io.File;
32+
import java.io.IOException;
33+
import java.util.ArrayList;
34+
import java.util.Arrays;
35+
import java.util.Collection;
36+
import java.util.List;
37+
38+
import javax.imageio.ImageIO;
39+
import javax.swing.BorderFactory;
40+
import javax.swing.Box;
41+
import javax.swing.JComponent;
42+
import javax.swing.JFrame;
43+
import javax.swing.JPanel;
44+
import javax.swing.SwingUtilities;
45+
46+
/*
47+
* @test
48+
* @bug 8282958
49+
* @summary Verify LineBorder edges have the same width
50+
* @requires (os.family == "windows")
51+
* @run main ScaledLineBorderTest
52+
*/
53+
public class ScaledLineBorderTest {
54+
private static final Dimension SIZE = new Dimension(120, 25);
55+
56+
private static final Color OUTER_COLOR = Color.BLACK;
57+
private static final Color BORDER_COLOR = Color.RED;
58+
private static final Color INSIDE_COLOR = Color.WHITE;
59+
private static final Color TRANSPARENT_COLOR = new Color(0x00000000, true);
60+
61+
private static final double[] scales =
62+
{1.00, 1.25, 1.50, 1.75, 2.00, 2.50, 3.00};
63+
64+
private static final List<BufferedImage> images =
65+
new ArrayList<>(scales.length);
66+
67+
private static final List<Point> panelLocations =
68+
new ArrayList<>(4);
69+
70+
public static void main(String[] args) throws Exception {
71+
Collection<String> params = Arrays.asList(args);
72+
final boolean showFrame = params.contains("-show");
73+
final boolean saveImages = params.contains("-save");
74+
SwingUtilities.invokeAndWait(() -> testScaling(showFrame, saveImages));
75+
}
76+
77+
private static void testScaling(boolean showFrame, boolean saveImages) {
78+
JComponent content = createUI();
79+
if (showFrame) {
80+
showFrame(content);
81+
}
82+
83+
paintToImages(content, saveImages);
84+
verifyBorderRendering(saveImages);
85+
}
86+
87+
private static void verifyBorderRendering(final boolean saveImages) {
88+
String errorMessage = null;
89+
int errorCount = 0;
90+
for (int i = 0; i < images.size(); i++) {
91+
BufferedImage img = images.get(i);
92+
double scaling = scales[i];
93+
try {
94+
int thickness = (int) Math.floor(scaling);
95+
96+
checkVerticalBorders(SIZE.width / 2, thickness, img);
97+
98+
for (Point p : panelLocations) {
99+
int y = (int) (p.y * scaling) + SIZE.height / 2;
100+
checkHorizontalBorder(y, thickness, img);
101+
}
102+
} catch (Error e) {
103+
if (errorMessage == null) {
104+
errorMessage = e.getMessage();
105+
}
106+
errorCount++;
107+
108+
System.err.printf("Scaling: %.2f\n", scaling);
109+
e.printStackTrace();
110+
111+
// Save the image if it wasn't already saved
112+
if (!saveImages) {
113+
saveImage(img, getImageFileName(scaling));
114+
}
115+
}
116+
}
117+
118+
if (errorCount > 0) {
119+
throw new Error("Test failed: "
120+
+ errorCount + " error(s) detected - "
121+
+ errorMessage);
122+
}
123+
}
124+
125+
private static void checkVerticalBorders(final int x,
126+
final int thickness,
127+
final BufferedImage img) {
128+
checkBorder(x, 0,
129+
0, 1,
130+
thickness, img);
131+
}
132+
133+
private static void checkHorizontalBorder(final int y,
134+
final int thickness,
135+
final BufferedImage img) {
136+
checkBorder(0, y,
137+
1, 0,
138+
thickness, img);
139+
}
140+
141+
private static void checkBorder(final int xStart, final int yStart,
142+
final int xStep, final int yStep,
143+
final int thickness,
144+
final BufferedImage img) {
145+
final int width = img.getWidth();
146+
final int height = img.getHeight();
147+
148+
State state = State.BACKGROUND;
149+
int borderThickness = 0;
150+
151+
int x = xStart;
152+
int y = yStart;
153+
do {
154+
do {
155+
final int color = img.getRGB(x, y);
156+
switch (state) {
157+
case BACKGROUND:
158+
if (color == BORDER_COLOR.getRGB()) {
159+
state = State.LEFT;
160+
borderThickness = 1;
161+
} else if (color != OUTER_COLOR.getRGB()
162+
&& color != TRANSPARENT_COLOR.getRGB()) {
163+
throwUnexpectedColor(x, y, color);
164+
}
165+
break;
166+
167+
case LEFT:
168+
if (color == BORDER_COLOR.getRGB()) {
169+
borderThickness++;
170+
} else if (color == INSIDE_COLOR.getRGB()) {
171+
if (borderThickness != thickness) {
172+
throwWrongThickness(thickness, borderThickness, x, y);
173+
}
174+
state = State.INSIDE;
175+
borderThickness = 0;
176+
} else {
177+
throwUnexpectedColor(x, y, color);
178+
}
179+
break;
180+
181+
case INSIDE:
182+
if (color == BORDER_COLOR.getRGB()) {
183+
state = State.RIGHT;
184+
borderThickness = 1;
185+
} else if (color != INSIDE_COLOR.getRGB()) {
186+
throwUnexpectedColor(x, y, color);
187+
}
188+
break;
189+
190+
case RIGHT:
191+
if (color == BORDER_COLOR.getRGB()) {
192+
borderThickness++;
193+
} else if (color == OUTER_COLOR.getRGB()) {
194+
if (borderThickness != thickness) {
195+
throwWrongThickness(thickness, borderThickness, x, y);
196+
}
197+
state = State.BACKGROUND;
198+
borderThickness = 0;
199+
} else {
200+
throwUnexpectedColor(x, y, color);
201+
}
202+
}
203+
} while (yStep > 0 && ((y += yStep) < height));
204+
} while (xStep > 0 && ((x += xStep) < width));
205+
}
206+
207+
private enum State {
208+
BACKGROUND, LEFT, INSIDE, RIGHT
209+
}
210+
211+
private static void throwWrongThickness(int thickness, int borderThickness,
212+
int x, int y) {
213+
throw new Error(
214+
String.format("Wrong border thickness at %d, %d: %d vs %d",
215+
x, y, borderThickness, thickness));
216+
}
217+
218+
private static void throwUnexpectedColor(int x, int y, int color) {
219+
throw new Error(
220+
String.format("Unexpected color at %d, %d: %08x",
221+
x, y, color));
222+
}
223+
224+
private static JComponent createUI() {
225+
Box contentPanel = Box.createVerticalBox();
226+
contentPanel.setBackground(OUTER_COLOR);
227+
228+
Dimension childSize = null;
229+
for (int i = 0; i < 4; i++) {
230+
JComponent filler = new JPanel(null);
231+
filler.setBackground(INSIDE_COLOR);
232+
filler.setPreferredSize(SIZE);
233+
filler.setBounds(i, 0, SIZE.width, SIZE.height);
234+
filler.setBorder(BorderFactory.createLineBorder(BORDER_COLOR));
235+
236+
JPanel childPanel = new JPanel(new BorderLayout());
237+
childPanel.setBorder(BorderFactory.createEmptyBorder(0, i, 4, 4));
238+
childPanel.add(filler, BorderLayout.CENTER);
239+
childPanel.setBackground(OUTER_COLOR);
240+
241+
contentPanel.add(childPanel);
242+
if (childSize == null) {
243+
childSize = childPanel.getPreferredSize();
244+
}
245+
childPanel.setBounds(0, childSize.height * i, childSize.width, childSize.height);
246+
}
247+
248+
contentPanel.setSize(childSize.width, childSize.height * 4);
249+
250+
// Save coordinates of the panels
251+
for (Component comp : contentPanel.getComponents()) {
252+
panelLocations.add(comp.getLocation());
253+
}
254+
255+
return contentPanel;
256+
}
257+
258+
private static void showFrame(JComponent content) {
259+
JFrame frame = new JFrame("Scaled Line Border Test");
260+
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
261+
frame.getContentPane().add(content, BorderLayout.CENTER);
262+
frame.pack();
263+
frame.setLocationRelativeTo(null);
264+
frame.setVisible(true);
265+
}
266+
267+
private static void paintToImages(final JComponent content,
268+
final boolean saveImages) {
269+
for (double scaling : scales) {
270+
BufferedImage image =
271+
new BufferedImage((int) Math.ceil(content.getWidth() * scaling),
272+
(int) Math.ceil(content.getHeight() * scaling),
273+
BufferedImage.TYPE_INT_ARGB);
274+
275+
Graphics2D g2d = image.createGraphics();
276+
g2d.scale(scaling, scaling);
277+
content.paint(g2d);
278+
g2d.dispose();
279+
280+
if (saveImages) {
281+
saveImage(image, getImageFileName(scaling));
282+
}
283+
images.add(image);
284+
}
285+
}
286+
287+
private static String getImageFileName(final double scaling) {
288+
return String.format("test%.2f.png", scaling);
289+
}
290+
291+
private static void saveImage(BufferedImage image, String filename) {
292+
try {
293+
ImageIO.write(image, "png", new File(filename));
294+
} catch (IOException e) {
295+
// Don't propagate the exception
296+
e.printStackTrace();
297+
}
298+
}
299+
}

0 commit comments

Comments
 (0)