drawPolygon/fillPolygon bug in JDK 1.4.0 beta

NOTE: This report was assigned bug id 4465509 and was fixed in beta 3. It seems that 4151279 has also been fixed in beta 3.

When a polygon is filled and drawn using the same parameters in JDK 1.4.0 beta on Linux the outline and fill don't match. This behaviour is different from all previous releases.

I want to use small circles as markers on a map of monuments in the vicinity of Stonehenge. Since this is in an applet I want to support JDK 1.0 and 1.1. As previously reported (4151279, see also my message to the java2d-interest mailing list), since JDK 1.2 there is a difference between the way ovals are drawn when drawing directly to a canvas and when drawing to an offscreen image. This bug has been open for nearly three years and still hasn't been fixed in JDK 1.4.0.

Figure 1 What the circles should look like

Figure 2 Ovals drawn to an offscreen image in JDK 1.2 and above

To produce acceptable-looking circles in JDK 1.2 and above I've been forced to draw them by hand using polygons. This no longer works in JDK 1.4.0 beta, as the polygon outline and fill don't match.

The program below illustrates the problem. It fills and draws a number of small circles. When the mouse is clicked in the canvas it cycles through three different ways of drawing: directly to the canvas; to an offscreen image using fillOval/drawOval; to an offscreen image with the circles drawn as polygons.

In JDK 1.0 and 1.1 these three methods all produce the same result. In JDK 1.2 and 1.3 the second method results in horrible-looking circles, while drawing by hand gives the same result as drawing direct to the canvas. In JDK 1.4.0 beta drawing by hand also produces unacceptable results because of the offset between the polygon fill and draw.

Curiously, if I run the program across the network to a remote display using JDK 1.4.0 it works perfectly: the circles look good in all three cases. This is not true of JDK 1.2 or 1.3, the offscreen ovals are still ugly.

Figure 3 Polygons in JDK 1.4.0

import java.awt.* ;

public class PolygonBug extends Canvas {
	private Image offscreen = null ;
	private Graphics g0 = null ;
	private int click = 0 ;

	// offsets used to draw circles as polygons
	static private final int xcircle[][] = {
		null,
		null,
		null,
		{ 0,1,2,2,1,0,-1,-1,0 },
		{ -1,1,2,2,1,-1,-2,-2,-1 },
		{ 0,1,3,3,1,0,-2,-2,0 },
		{ -1,1,3,3,1,-1,-3,-3,-1 },
		{ -1,2,4,4,2,-1,-3,-3,-1 },
		{ -1,1,2,3,3,4,4,3,3,2,1,-1,-2,-3,-3,-4,-4,-3,-3,-2,-1},
	} ;

	static private final int ycircle[][] = {
		null,
		null,
		null,
		{ -1,-1,0,1,2,2,1,0,-1 },
		{ -2,-2,-1,1,2,2,1,-1,-2 },
		{ -2,-2,0,1,3,3,1,0,-2 },
		{ -3,-3,-1,1,3,3,1,-1,-3 },
		{ -3,-3,-1,2,4,4,2,-1,-3 },
		{ -4,-4,-3,-3,-2,-1,1,2,3,3,4,4,3,3,2,1,-1,-2,-3,-3,-4},
	} ;

	static String[] message = { "direct", "offscreen", "by hand" } ;

	public void addNotify() {
		super.addNotify() ;

		// create an offscreen image to draw into
		Rectangle r = bounds() ;
		offscreen = createImage(r.width, r.height) ;
		g0 = offscreen.getGraphics() ;
	}

	public void paint(Graphics g) {
		g.clearRect(0, 0, 100, 100) ;

		// draw crosshairs
		g.setColor(Color.black) ;
		g.drawLine(60, 70, 80, 70) ;
		g.drawLine(70, 60, 70, 80) ;

		boolean byHand = (click%3 == 2) ;

		// fill some circles
		g.setColor(Color.white) ;
		fillCircle(g, 10, 10, 4, byHand) ;
		fillCircle(g, 20, 20, 5, byHand) ;
		fillCircle(g, 35, 35, 6, byHand) ;
		fillCircle(g, 50, 50, 7, byHand) ;
		fillCircle(g, 70, 70, 8, byHand) ;

		// draw some circles
		g.setColor(Color.black) ;
		drawCircle(g, 10, 10, 4, byHand) ;
		drawCircle(g, 20, 20, 5, byHand) ;
		drawCircle(g, 35, 35, 6, byHand) ;
		drawCircle(g, 50, 50, 7, byHand) ;
		drawCircle(g, 70, 70, 8, byHand) ;

		// indicate how we drew the circles
		g.drawString(message[click%3], 5, 90) ;
	}

	public boolean mouseDown(Event ev, int x, int y ) {
		++click ;

		if ( click%3 == 0 ) {
			// draw directly to canvas
			repaint() ;
		}
		else {
			// draw to offscreen image
			if ( offscreen != null ) {
				paint(g0) ;

				Graphics g = getGraphics() ;
				g.drawImage(offscreen, 0, 0, this) ;
				g.dispose() ;
			}
		}

		return true ;
	}

	void drawCircle(Graphics g, int x, int y, int d, boolean byHand) {
		if ( byHand ) {
			circleByHand(g, x, y, d, true) ;
		}
		else {
			int d2 = d/2 ;

			g.drawOval(x-d2, y-d2, d, d) ;
		}
	}

	void fillCircle(Graphics g, int x, int y, int d, boolean byHand) {
		if ( byHand ) {
			circleByHand(g, x, y, d, false) ;
		}
		else {
			int d2 = d/2 ;

			g.fillOval(x-d2, y-d2, d, d) ;
		}
	}

	// draw or fill a circle using a polygon
	void circleByHand(Graphics g, int x, int y, int d, boolean draw) {
		int xc[] = xcircle[d] ;
		int yc[] = ycircle[d] ;
		int len = xc.length ;
		int px[] = new int[len] ;
		int py[] = new int[len] ;

		for ( int j=0; j<len; ++j ) {
			px[j] = x + xc[j] ;
			py[j] = y + yc[j] ;
		}

		if ( draw ) {
			g.drawPolygon(px, py, len) ;
		}
		else {
			g.fillPolygon(px, py, len) ;
		}
	}

	public static void main(String argv[]) {
		Frame f = new Frame("PolygonBug") ;
		Canvas c = new PolygonBug() ;
		c.resize(100,100) ;

		f.add("Center", c) ;
		f.pack() ;
		f.show() ;
	}
}