-
Bug
-
Resolution: Unresolved
-
P4
-
7
-
x86
-
linux_ubuntu
FULL PRODUCT VERSION :
java version "1.7.0_04"
Java(TM) SE Runtime Environment (build 1.7.0_04-b20)
Java HotSpot(TM) 64-Bit Server VM (build 23.0-b21, mixed mode)
Also tested on JDK 1.6.0 update 26
ADDITIONAL OS VERSION INFORMATION :
Mint 12 (ubuntu variant)
Linux EP-LL-0387 3.0.0-12-generic #20-Ubuntu SMP Fri Oct 7 14:56:25 UTC 2011 x86_64 x86_64 x86_64 GNU/Linux
Also found same behaviour on latest Mac OS X Loin
A DESCRIPTION OF THE PROBLEM :
Drawing with Line2D.Double at non integer coordinates results in drawing a line as if the coordinates were casted down to integers.
This means the new interface:
g2d.draw(new Line2D.Double(...)) does NOT give you any more power than g2d.drawLine(...)
I've attached a Swing application to demonstrate the problem visually.
My test is to draw two lines. One static with the full length of the line and the other above it to draw fractions of the full line. The intent is to show the rendering of lines of partial lengths of the original.
If rendering was correct, then one would expect the animating line to appear to simply grow and shrink.
I've run this experiment on 4 different cases.
1. Using g2d.draw(new Line2D.Double(...)) expected to pass
2. Using g2d.drawLine(...) expected to fail
3. Using an implementation of Wu's anti aliasing line expected to pass
4. Using g2d.shape(...) where the shape is the ideal line expected to pass
The results of the test is:
1. failed when expected to pass
2. failed as expected
3. passed
4. passed, but has some issues
Run the attached source code to run these 4 test cases for yourself. Ideally case #1 should look like case #3. Right now case #1 jitters up and down like case #2 which is why it leads me to believe case #1 is casting down to an integer somewhere.
Case #4 is the 100% JDK way to work around the issues without writing your own line rendering algorithm. Its not perfect because while the line renders correctly for non integer coordinates the antialiasing shading isn't stable. Ideally case #4 should also look like case #3.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Render an anti aliased line with g2d.draw(new Line2D.Double(...)) at non integer coordinates
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The line end points renders correct as specified
ACTUAL -
The line end points renders to the nearest integer coordinate
REPRODUCIBILITY :
This bug can be reproduced always.
---------- BEGIN SOURCE ----------
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.util.Timer;
import java.util.TimerTask;
import javax.swing.JComponent;
import javax.swing.JFrame;
public class Line {
private static void plot(final BufferedImage image, final int x, final int y, double c) {
c = Math.max(0, Math.min(1 - c, 1));
image.setRGB(x, y, new Color((float) c, (float) c, (float) c).getRGB());
}
private static int round(final double x) {
return (int) (x + 0.5);
}
private static double floatPart(final double x) {
return x - (int) x;
}
private static void drawWuLine(final BufferedImage image, double x1, double y1, double x2, double y2) {
double dx = x2 - x1;
double dy = y2 - y1;
if (Math.abs(dx) < Math.abs(dy)) {
double t = x1;
x1 = y1;
y1 = t;
t = x2;
x2 = y2;
y2 = t;
t = dx;
dx = dy;
dy = t;
}
if (x2 < x1) {
double t = x1;
x1 = x2;
x2 = t;
t = y1;
y1 = y2;
y2 = t;
}
double gradient = dy / dx;
// handle first endpoint
int xend = round(x1);
double yend = y1 + gradient * (xend - x1);
double xgap = 1 - floatPart(x1 + 0.5);
int xpxl1 = xend; // this will be used in the main loop
int ypxl1 = (int) yend;
plot(image, xpxl1, ypxl1, (1 - floatPart(yend)) * xgap);
plot(image, xpxl1, ypxl1 + 1, floatPart(yend) * xgap);
double intery = yend + gradient; // first y-intersection for the main loop
// handle second endpoint
xend = round(x2);
yend = y2 + gradient * (xend - x2);
xgap = floatPart(x2 + 0.5);
int xpxl2 = xend; // this will be used in the main loop
int ypxl2 = (int) yend;
plot(image, xpxl2, ypxl2, (1 - floatPart(yend)) * xgap);
plot(image, xpxl2, ypxl2 + 1, floatPart(yend) * xgap);
// main loop
for (int x = xpxl1 + 1; x <= xpxl2 - 1; x++) {
plot(image, x, (int) intery, 1 - floatPart(intery));
plot(image, x, (int) intery + 1, floatPart(intery));
intery = intery + gradient;
}
}
private static Shape getLineBox(final double x1, final double y1, final double x2, final double y2) {
Point2D.Double p1 = new Point2D.Double(x1, y1);
Point2D.Double p2 = new Point2D.Double(x2, y2);
Point2D.Double p3 = new Point2D.Double(p2.x - p1.x, p2.y - p1.y);
p3 = new Point2D.Double(p3.x / p1.distance(p2), p3.y / p1.distance(p2));
double w = .5;
Point2D.Double p4 = new Point2D.Double(w * -p3.y, w * p3.x);
Path2D.Double box = new Path2D.Double();
box.moveTo(p1.x - p4.x, p1.y - p4.y);
box.lineTo(p2.x - p4.x, p2.y - p4.y);
box.lineTo(p2.x + p4.x, p2.y + p4.y);
box.lineTo(p1.x + p4.x, p1.y + p4.y);
box.closePath();
double[][] p = new double[4][];
PathIterator it = box.getPathIterator(null);
double[] coords = new double[6];
int j = 0;
while (!it.isDone()) {
int segType = it.currentSegment(coords);
if (segType != PathIterator.SEG_CLOSE) {
p[j++] = new double[] {
coords[0],
coords[1]
};
}
it.next();
}
double area = 0;
for (int i = 0; i < 4; i++) {
area += p[i][0] * p[(i + 1) % 4][1] - p[(i + 1) % 4][0] * p[i][1];
}
area /= 2;
area = Math.abs(area);
return box;
}
private static double t = 0.5;
private static double dt = .05;
public static void main(final String[] args) throws Exception {
JFrame frame = new JFrame("AA Lines");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
final JComponent comp = new JComponent() {
@Override
public void paint(final Graphics g) {
final BufferedImage image = new BufferedImage(150, 100, BufferedImage.TYPE_INT_ARGB);
final Graphics2D g2d = image.createGraphics();
g2d.setColor(Color.WHITE);
g2d.fillRect(0, 0, 150, 100);
int x = 7;
int y = 13 - 5;
int x2 = 85;
int y2 = 24 - 5;
g2d.setColor(Color.BLACK);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
Point2D.Double pt;
int dy;
dy = 0;
g2d.drawString("New Java2D", 80, 12 + dy);
pt = getT(t, x, y + dy, x2, y2 + dy);
g2d.draw(new Line2D.Double(x, y + dy, pt.x, pt.y));
g2d.draw(new Line2D.Double(x, y + dy + 5, x2, y2 + dy + 5));
dy = 25;
g2d.drawString("Old Java2D", 80, 12 + dy);
pt = getT(t, x, y + dy, x2, y2 + dy);
g2d.drawLine(x, y + dy, (int) pt.x, (int) pt.y);
g2d.drawLine(x, y + dy + 5, x2, y2 + dy + 5);
dy = 50;
g2d.drawString("Wu Line", 80, 12 + dy);
pt = getT(t, x, y + dy, x2, y2 + dy);
drawWuLine(image, x, y + dy, pt.x, pt.y);
drawWuLine(image, x, y + dy + 5, x2, y2 + dy + 5);
dy = 75;
g2d.drawString("Line Shape", 80, 12 + dy);
pt = getT(t, x, y + dy, x2, y2 + dy);
g2d.fill(getLineBox(x, y + dy, pt.x, pt.y));
g2d.fill(getLineBox(x, y + dy + 5, x2, y2 + dy + 5));
g2d.dispose();
g.drawImage(image.getScaledInstance(1200, 800, BufferedImage.SCALE_REPLICATE), 0, 0, null);
}
private Point2D.Double getT(final double t, final int x1, final int y1, final int x2, final int y2) {
return new Point2D.Double((1 - t) * x1 + t * x2, (1 - t) * y1 + t * y2);
}
};
Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
t += dt;
if (t <= 0.25 || t >= 1) {
dt = -dt;
}
t = Math.max(0, Math.min(t, 1));
comp.repaint();
}
}, 0, 250);
comp.setPreferredSize(new Dimension(1200, 800));
frame.setLayout(new BorderLayout());
frame.add(comp, BorderLayout.CENTER);
frame.pack();
frame.setVisible(true);
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
Use case #3 or #4. See description and attached code for details.
java version "1.7.0_04"
Java(TM) SE Runtime Environment (build 1.7.0_04-b20)
Java HotSpot(TM) 64-Bit Server VM (build 23.0-b21, mixed mode)
Also tested on JDK 1.6.0 update 26
ADDITIONAL OS VERSION INFORMATION :
Mint 12 (ubuntu variant)
Linux EP-LL-0387 3.0.0-12-generic #20-Ubuntu SMP Fri Oct 7 14:56:25 UTC 2011 x86_64 x86_64 x86_64 GNU/Linux
Also found same behaviour on latest Mac OS X Loin
A DESCRIPTION OF THE PROBLEM :
Drawing with Line2D.Double at non integer coordinates results in drawing a line as if the coordinates were casted down to integers.
This means the new interface:
g2d.draw(new Line2D.Double(...)) does NOT give you any more power than g2d.drawLine(...)
I've attached a Swing application to demonstrate the problem visually.
My test is to draw two lines. One static with the full length of the line and the other above it to draw fractions of the full line. The intent is to show the rendering of lines of partial lengths of the original.
If rendering was correct, then one would expect the animating line to appear to simply grow and shrink.
I've run this experiment on 4 different cases.
1. Using g2d.draw(new Line2D.Double(...)) expected to pass
2. Using g2d.drawLine(...) expected to fail
3. Using an implementation of Wu's anti aliasing line expected to pass
4. Using g2d.shape(...) where the shape is the ideal line expected to pass
The results of the test is:
1. failed when expected to pass
2. failed as expected
3. passed
4. passed, but has some issues
Run the attached source code to run these 4 test cases for yourself. Ideally case #1 should look like case #3. Right now case #1 jitters up and down like case #2 which is why it leads me to believe case #1 is casting down to an integer somewhere.
Case #4 is the 100% JDK way to work around the issues without writing your own line rendering algorithm. Its not perfect because while the line renders correctly for non integer coordinates the antialiasing shading isn't stable. Ideally case #4 should also look like case #3.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Render an anti aliased line with g2d.draw(new Line2D.Double(...)) at non integer coordinates
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The line end points renders correct as specified
ACTUAL -
The line end points renders to the nearest integer coordinate
REPRODUCIBILITY :
This bug can be reproduced always.
---------- BEGIN SOURCE ----------
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.util.Timer;
import java.util.TimerTask;
import javax.swing.JComponent;
import javax.swing.JFrame;
public class Line {
private static void plot(final BufferedImage image, final int x, final int y, double c) {
c = Math.max(0, Math.min(1 - c, 1));
image.setRGB(x, y, new Color((float) c, (float) c, (float) c).getRGB());
}
private static int round(final double x) {
return (int) (x + 0.5);
}
private static double floatPart(final double x) {
return x - (int) x;
}
private static void drawWuLine(final BufferedImage image, double x1, double y1, double x2, double y2) {
double dx = x2 - x1;
double dy = y2 - y1;
if (Math.abs(dx) < Math.abs(dy)) {
double t = x1;
x1 = y1;
y1 = t;
t = x2;
x2 = y2;
y2 = t;
t = dx;
dx = dy;
dy = t;
}
if (x2 < x1) {
double t = x1;
x1 = x2;
x2 = t;
t = y1;
y1 = y2;
y2 = t;
}
double gradient = dy / dx;
// handle first endpoint
int xend = round(x1);
double yend = y1 + gradient * (xend - x1);
double xgap = 1 - floatPart(x1 + 0.5);
int xpxl1 = xend; // this will be used in the main loop
int ypxl1 = (int) yend;
plot(image, xpxl1, ypxl1, (1 - floatPart(yend)) * xgap);
plot(image, xpxl1, ypxl1 + 1, floatPart(yend) * xgap);
double intery = yend + gradient; // first y-intersection for the main loop
// handle second endpoint
xend = round(x2);
yend = y2 + gradient * (xend - x2);
xgap = floatPart(x2 + 0.5);
int xpxl2 = xend; // this will be used in the main loop
int ypxl2 = (int) yend;
plot(image, xpxl2, ypxl2, (1 - floatPart(yend)) * xgap);
plot(image, xpxl2, ypxl2 + 1, floatPart(yend) * xgap);
// main loop
for (int x = xpxl1 + 1; x <= xpxl2 - 1; x++) {
plot(image, x, (int) intery, 1 - floatPart(intery));
plot(image, x, (int) intery + 1, floatPart(intery));
intery = intery + gradient;
}
}
private static Shape getLineBox(final double x1, final double y1, final double x2, final double y2) {
Point2D.Double p1 = new Point2D.Double(x1, y1);
Point2D.Double p2 = new Point2D.Double(x2, y2);
Point2D.Double p3 = new Point2D.Double(p2.x - p1.x, p2.y - p1.y);
p3 = new Point2D.Double(p3.x / p1.distance(p2), p3.y / p1.distance(p2));
double w = .5;
Point2D.Double p4 = new Point2D.Double(w * -p3.y, w * p3.x);
Path2D.Double box = new Path2D.Double();
box.moveTo(p1.x - p4.x, p1.y - p4.y);
box.lineTo(p2.x - p4.x, p2.y - p4.y);
box.lineTo(p2.x + p4.x, p2.y + p4.y);
box.lineTo(p1.x + p4.x, p1.y + p4.y);
box.closePath();
double[][] p = new double[4][];
PathIterator it = box.getPathIterator(null);
double[] coords = new double[6];
int j = 0;
while (!it.isDone()) {
int segType = it.currentSegment(coords);
if (segType != PathIterator.SEG_CLOSE) {
p[j++] = new double[] {
coords[0],
coords[1]
};
}
it.next();
}
double area = 0;
for (int i = 0; i < 4; i++) {
area += p[i][0] * p[(i + 1) % 4][1] - p[(i + 1) % 4][0] * p[i][1];
}
area /= 2;
area = Math.abs(area);
return box;
}
private static double t = 0.5;
private static double dt = .05;
public static void main(final String[] args) throws Exception {
JFrame frame = new JFrame("AA Lines");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
final JComponent comp = new JComponent() {
@Override
public void paint(final Graphics g) {
final BufferedImage image = new BufferedImage(150, 100, BufferedImage.TYPE_INT_ARGB);
final Graphics2D g2d = image.createGraphics();
g2d.setColor(Color.WHITE);
g2d.fillRect(0, 0, 150, 100);
int x = 7;
int y = 13 - 5;
int x2 = 85;
int y2 = 24 - 5;
g2d.setColor(Color.BLACK);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
Point2D.Double pt;
int dy;
dy = 0;
g2d.drawString("New Java2D", 80, 12 + dy);
pt = getT(t, x, y + dy, x2, y2 + dy);
g2d.draw(new Line2D.Double(x, y + dy, pt.x, pt.y));
g2d.draw(new Line2D.Double(x, y + dy + 5, x2, y2 + dy + 5));
dy = 25;
g2d.drawString("Old Java2D", 80, 12 + dy);
pt = getT(t, x, y + dy, x2, y2 + dy);
g2d.drawLine(x, y + dy, (int) pt.x, (int) pt.y);
g2d.drawLine(x, y + dy + 5, x2, y2 + dy + 5);
dy = 50;
g2d.drawString("Wu Line", 80, 12 + dy);
pt = getT(t, x, y + dy, x2, y2 + dy);
drawWuLine(image, x, y + dy, pt.x, pt.y);
drawWuLine(image, x, y + dy + 5, x2, y2 + dy + 5);
dy = 75;
g2d.drawString("Line Shape", 80, 12 + dy);
pt = getT(t, x, y + dy, x2, y2 + dy);
g2d.fill(getLineBox(x, y + dy, pt.x, pt.y));
g2d.fill(getLineBox(x, y + dy + 5, x2, y2 + dy + 5));
g2d.dispose();
g.drawImage(image.getScaledInstance(1200, 800, BufferedImage.SCALE_REPLICATE), 0, 0, null);
}
private Point2D.Double getT(final double t, final int x1, final int y1, final int x2, final int y2) {
return new Point2D.Double((1 - t) * x1 + t * x2, (1 - t) * y1 + t * y2);
}
};
Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
t += dt;
if (t <= 0.25 || t >= 1) {
dt = -dt;
}
t = Math.max(0, Math.min(t, 1));
comp.repaint();
}
}, 0, 250);
comp.setPreferredSize(new Dimension(1200, 800));
frame.setLayout(new BorderLayout());
frame.add(comp, BorderLayout.CENTER);
frame.pack();
frame.setVisible(true);
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
Use case #3 or #4. See description and attached code for details.