Hi,
I've run into what seems to be a pretty significant bug with Java2D on
OS X. I couldn't find any mention of it in the archives of this list,
so I'm curious about whether anyone else has ever run across it and
what workarounds you might have used.
This is not the only oddity I've experienced with Java2D on OS X, but
it is the first one I've needed/been able to pin down with a test case.
The problem is this: whenever the Graphics2D object used to paint a
JPanel (or, presumably, any JComponent) has both a non-identity scale
factor and a non-default BasicStroke applied to it, any shapes drawn to
it willbe offset by some amount. The amount and direction of the
offset appears to be proportional to the scale factor(s). It doesn't
take a very large scale factor to displace the shape by a lot. On the
other hand, the attributes of the stroke do not seem to affect the
degree of displacement.
(By "non-default BasicStroke", I mean any BasicStroke that's not 1.0f
wide with the JDK-default CAP and JOIN styles. It doesn't have to be
the original Stroke object that "came with" the Graphics2D object, just
an equivalently-constructed one.)
If you remove either the scale or the BasicStroke, the other is applied
to the shape with no problem. Some other variables I've tried:
* If the source of the Graphics2D is a BufferedImage (instead of a
JPanel), the problem doesn't occur.
* The value of the rendering hint KEY_STROKE_CONTROL has no effect.
I've attached a small test case that demonstrates the problem with
different scale factors. I've tested it on OS X 10.2.2 using the
included JDK (1.3.1) where the problem occurs, and on Windows 2000 with
JDK 1.3.1_04 where the problem does not occur. (Not that it matters,
but the test app is compiled under OS X.)
I have worked around this problem by doing something like this:
public void transformAndDrawTo(Graphics2D g2) {
AffineTransform origTx = g2.getTransform();
g2.setTransform(new AffineTransform());
g2.draw(origTx.createTransformedShape(shape));
g2.setTransform(origTx);
}
Where "shape" is whatever Shape I want to draw. Of course, this has
performance penalties (though not as much as I expected) and it ends up
drawing something which does not exactly match what the "natural"
method would have [this could be alleviated (but not fixed) by scaling
the BasicStroke itself]. It works okay, but I wish I didn't have to do
it.
Anyone have any insights? Is there something I'm overlooking?
I understand that this is almost certainly Apple's problem, since they
maintain their own JDK for OS X. So if the answers to those two
questions is "no," the next question is: what is the best way to report
JDK bugs to Apple?
Thanks for reading that whole thing,
Rhett Sutphin
=====================================================
| Rhett Sutphin
| Research Assistant (Software)
| Coordinated Laboratory for Computational Genomics
| and the Center for Macular Degeneration
| University of Iowa - Iowa City, IA 52242 - USA
| 4111 MEBRF - email: [EMAIL PROTECTED]
=====================================================
package test.j2d;
import java.awt.geom.*;
import java.awt.*;
import javax.swing.*;
/** Test for weird BasicStroke-Graphics2D#scale interaction on OS X. It displays
* a bunch of JPanels, each with a different pair of x,y scale factors. Each
* contains two identical ellipses, one red and one blue. If the bug is not
* present, the blue ellipses will be painted on top of the red ones. Otherwise,
* the red ones will be visible and the blue ones will be offset or missing.
*
* @author rsutphin
*/
public class StrokeOffsetTest extends JPanel {
DrawableThing thing;
float xScale, yScale;
boolean drawTransformed;
public StrokeOffsetTest(float xScale, float yScale, boolean drawTransformed) {
thing = new DrawableThing();
this.xScale = xScale;
this.yScale = yScale;
this.drawTransformed = drawTransformed;
setBackground(Color.white);
}
public Dimension getPreferredSize() { return new Dimension(160, 160); }
public Dimension getMaximumSize() { return getPreferredSize(); }
public Dimension getMinimumSize() { return getPreferredSize(); }
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
AffineTransform origTx = g2.getTransform();
Shape origClip = g2.getClip();
g2.translate(60, 60);
g2.scale(xScale, yScale);
g2.setColor(Color.red);
g2.setStroke(new BasicStroke(1.0f));
if (drawTransformed)
thing.transformAndDrawTo(g2);
else
thing.drawTo(g2);
g2.setColor(Color.blue);
g2.setStroke(new BasicStroke(2.5f, BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND));
if (drawTransformed)
thing.transformAndDrawTo(g2);
else
thing.drawTo(g2);
g2.setTransform(origTx);
g2.setColor(Color.black);
g2.drawString("x: " + xScale + " y: " + yScale, 10, 140);
g2.setClip(origClip);
}
public static void main(String[] args) throws Exception {
JFrame fr = new JFrame("StrokeOffsetTest");
fr.setBounds(500, 10, 300, 300);
float[] xScales = new float[] { 0.8f, 0.95f, 1.0f, 1.05f, 1.2f };
//float[] xScales = new float[] { 1.0f };
float[] yScales = new float[] { 0.8f, 0.95f, 1.0f, 1.05f, 1.2f };
fr.getContentPane().setLayout(new GridLayout(xScales.length,yScales.length));
StrokeOffsetTest tester;
for (int x = 0 ; x < xScales.length ; x++) {
for (int y = 0 ; y < yScales.length ; y++) {
tester = new StrokeOffsetTest(xScales[x], yScales[y], false);
tester.setBorder(BorderFactory.createLineBorder(Color.black, 2));
fr.getContentPane().add(tester);
}
}
fr.pack();
fr.show();
while (fr.isVisible())
Thread.sleep(250);
System.exit(0);
}
}
class DrawableThing {
Shape shape;
public DrawableThing() {
shape = new Ellipse2D.Float(0, 0, 40, 40);
//shape = new Line2D.Float(0, 0, 40, 40);
}
public void drawTo(Graphics2D g2) {
g2.draw(shape);
}
public void transformAndDrawTo(Graphics2D g2) {
AffineTransform origTx = g2.getTransform();
g2.setTransform(new AffineTransform());
g2.draw(origTx.createTransformedShape(shape));
g2.setTransform(origTx);
}
}