import java.awt.Color;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.image.DirectColorModel;
import java.awt.image.ImageObserver;
import java.awt.image.MemoryImageSource;
import java.awt.image.PixelGrabber;
import java.awt.Rectangle;
import java.awt.Toolkit;

/*
 * This class implements a TurtleGraphics FILL graphics operation.
 * 

* Complexity in performing a FILL has its roots in the AWT's * mechanism for manipulating the RGB values of pixels representing * a Component. The Component in TGCanvas' case is the Canvas * that has been drawn on. Access to the pixels comes via the * in-memory Image which is maintained by TGCanvas. * * Due to an early emphasis placed on supporting the WWW (java * applets), image manipulation is asynchronous. Specifically, * Graphics.drawImage() schedules/requests that the task is to be * done. IT DOESN'T DO IT. The situation is the same going in * the other direction, but PixelGrabber.grabPixels() waits for * completion. *

* Soapbox Opinion: Asynchronous manipulation of an Image makes * sense when its source is bits out on the net, it makes no * sense when the bits are in memory. In fact, Microsoft's * implementation appears to treat the in-memory case specially * and do it synchronously. *

* Second Complexity... once the FILL has been done on the pixels, * the new Image needs to be generated. This is done with the * createImage() method. The problem is that this is a "peer" * method, i.e., is different for each system. On Apple OS-X * and Sun systems, createImage() dithers the pixels in the * process. Nasty!!! This means that back-to-back FILLs do not * work! Worse yet, the dithering effects all pixels in the * rectangle passed to createImage(). So, I've added complexity * to limit the rectangle of pixels given to createImage(). *

* I'm not even going to comment on how ludicrus the decision * to dither supplied pixels was, especially with no alternative. *

* @author Guy Haas */ class TGFillOp implements ImageObserver, TGGraphicsOp { // constants // private static final int EXPAND_SIZE = 10; // variables with class-wide scope // private boolean waitingForImage; private Color color; // FILL Color private Image sourceImage; private int maxFloodX; private int maxFloodY; private int minFloodX; private int minFloodY; private int sourceHeight; private int sourceWidth; private int subPixHt; private int subPixLeftX; private int subPixTopY; private int subPixWd; private int xCenter; private int yCenter; private int[] pixels; // holds the RGB pixel representation // of an Image private TGPoint point; // center of the FILL operation // // constructor // public TGFillOp( TGPoint point, Color color ) { this.color = color; this.point = point; waitingForImage = false; } private void addLeftPixels() { int addedPixWidth = EXPAND_SIZE; if ( subPixLeftX < EXPAND_SIZE ) addedPixWidth = subPixLeftX; subPixLeftX -= addedPixWidth; getPixels( subPixLeftX, subPixTopY, addedPixWidth, subPixHt ); subPixWd += addedPixWidth; } // end addLeftPixels() private void addLowerPixels() { int topY = subPixTopY + subPixHt; int addedPixHeight = EXPAND_SIZE; if ( topY+EXPAND_SIZE > sourceHeight ) addedPixHeight = sourceHeight - topY; getPixels( subPixLeftX, topY, subPixWd, addedPixHeight ); subPixHt += addedPixHeight; } // end addLowerPixels() private void addRightPixels() { int leftX = subPixLeftX + subPixWd; int addedPixWidth = EXPAND_SIZE; if ( leftX+EXPAND_SIZE > sourceWidth ) addedPixWidth = sourceWidth - leftX; getPixels( leftX, subPixTopY, addedPixWidth, subPixHt ); subPixWd += addedPixWidth; } // end addRightPixels() private void addUpperPixels() { int addedPixHeight = EXPAND_SIZE; if ( subPixTopY < EXPAND_SIZE ) addedPixHeight = subPixTopY; subPixTopY -= addedPixHeight; getPixels( subPixLeftX, subPixTopY, subPixWd, addedPixHeight ); subPixHt += addedPixHeight; } // end addUpperPixels() private void floodFill(int x, int y, int wd, int ht, int curRGB, int newRGB) { int pixIdx; int leftLimit = x, rightLimit = x; if ( y < minFloodY ) minFloodY = y; if ( y > maxFloodY ) maxFloodY = y; for ( int i = x; i >= 0; i-- ) { if ( i < subPixLeftX ) addLeftPixels(); pixIdx = y * wd + i; if ( (pixels[pixIdx] & 0xFFFFFF) != curRGB ) break; pixels[pixIdx] = (pixels[pixIdx] & 0xFF000000) | newRGB; leftLimit = i; } if ( leftLimit < minFloodX ) minFloodX = leftLimit; for ( int i = x+1; i < wd; i++ ) { if ( i == (subPixLeftX + subPixWd) ) addRightPixels(); pixIdx = y * wd + i; if ( (pixels[pixIdx] & 0xFFFFFF) != curRGB ) break; pixels[pixIdx] = (pixels[pixIdx] & 0xFF000000) | newRGB; rightLimit = i; } if ( rightLimit > maxFloodX ) maxFloodX = rightLimit; if ( y > 0 ) { for ( int i = leftLimit; i <= rightLimit; i++ ) { int newY = y - 1; if ( newY < subPixTopY ) addUpperPixels(); pixIdx = newY * wd + i; if ( (pixels[pixIdx] & 0xFFFFFF) == curRGB ) floodFill( i, newY, wd, ht, curRGB, newRGB ); } } if ( y < ht-1 ) { for ( int i = leftLimit; i <= rightLimit; i++ ) { int newY = y + 1; if ( newY == (subPixTopY + subPixHt) ) addLowerPixels(); pixIdx = newY * wd + i; if ( (pixels[pixIdx] & 0xFFFFFF) == curRGB ) floodFill( i, newY, wd, ht, curRGB, newRGB ); } } } // end floodFill // get color under my point and flood fill it and all its // neighbors, and their neighbors, etc... that are the // same color, with this operation's color // // *NOTE* the point where the fill operation is to start // may not be within current bounds of the graphics // Image. in this case, the operation can not be // performed. // public Rectangle doIt( Image canvasImage ) { sourceImage = canvasImage; sourceWidth = canvasImage.getWidth( this ); if ( sourceWidth == -1 ) return null; int canvasX = point.canvasX( sourceWidth ); if ( canvasX < 0 || canvasX >= sourceWidth ) return null; xCenter = sourceWidth / 2; sourceHeight = canvasImage.getHeight( this ); if ( sourceHeight == -1 ) return null; int canvasY = point.canvasY( sourceHeight ); if ( canvasY < 0 || canvasY >= sourceHeight ) return null; pixels = new int[sourceWidth * sourceHeight]; yCenter = sourceHeight / 2; subPixLeftX = canvasX - EXPAND_SIZE; if ( subPixLeftX < 0 ) subPixLeftX = 0; subPixTopY = canvasY - EXPAND_SIZE; if ( subPixTopY < 0 ) subPixTopY = 0; subPixHt = EXPAND_SIZE * 2; if ( subPixHt > sourceHeight ) subPixHt = sourceHeight; subPixWd = EXPAND_SIZE * 2; if ( subPixWd > sourceWidth ) subPixWd = sourceWidth; if ( ! getPixels(subPixLeftX, subPixTopY, subPixWd, subPixHt) ) return null; int curRGB = pixels[ canvasY * sourceWidth + canvasX ]; curRGB &= 0xFFFFFF; int newRGB = color.getRGB(); newRGB &= 0xFFFFFF; if ( curRGB == newRGB ) return null; maxFloodX = minFloodX = canvasX; maxFloodY = minFloodY = canvasY; floodFill( canvasX, canvasY, sourceWidth, sourceHeight, curRGB, newRGB ); int pixelOffset = (sourceWidth * minFloodY) + minFloodX; int floodWidth = (maxFloodX+1) - minFloodX; int floodHeight = (maxFloodY+1) - minFloodY; MemoryImageSource mis = new MemoryImageSource( floodWidth, floodHeight, pixels, pixelOffset, sourceWidth ); Image newImage = Toolkit.getDefaultToolkit().createImage( mis ); drawImage( newImage, minFloodX, minFloodY ); return new Rectangle( minFloodX, minFloodY, floodWidth, floodHeight ); } // end doIt() private synchronized void drawImage( Image newImg, int x, int y ) { Graphics g = sourceImage.getGraphics(); waitingForImage = true; boolean retVal = g.drawImage( newImg, x, y, this ); if ( retVal ) while ( waitingForImage ) { try { wait(); } catch (InterruptedException ie ) { } } waitingForImage = false; pixels = null; g.dispose(); } // end drawImage() public Color getColor() { return color; } private boolean getPixels( int x, int y, int width, int height ) { String me = "TGFillOp.getPixels: "; int pixelOffset = (sourceWidth * y) + x; PixelGrabber pg = new PixelGrabber( sourceImage, x, y, width, height, pixels, pixelOffset, sourceWidth ); try { pg.grabPixels(); } catch (InterruptedException e) { System.err.println( me + "grabPixels interrupted" ); return false; } if ( (pg.getStatus() & ImageObserver.ABORT) != 0 ) { System.err.println( me + "grabPixels ImageObserver.ABORT" ); return false; } return true; } // end getPixels() /* * This method is called when information about an image which was * previously requested using an asynchronous interface becomes available. * * This method should return true if further updates are needed or false * if the required information has been acquired. The image which was being * tracked is passed in using the img argument. Various constants are * combined to form the infoflags argument which indicates what information * about the image is now available. The interpretation of the x, y, width, * and height arguments depends on the contents of the infoflags argument. */ public synchronized boolean imageUpdate(Image img, int flags, int x, int y, int wd, int ht) { if ( ! waitingForImage ) return false; if ( (flags & (ImageObserver.ABORT | ImageObserver.ALLBITS | ImageObserver.ERROR)) != 0 ) { waitingForImage = false; notifyAll(); return false; } return true; } // end imageUpdate() } // end class TGFillOp