import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.io.*;

/**
 *  A demo program that manipulates colors of individual pixels in
 *  a BufferedImage.  The program lets the user draw using some basic
 *  shapes.  The user can also load an image from a file; the loaded
 *  image is scaled to exactly fill the panel.  A copy of the panel
 *  content is stored in a BufferedImage.  The user can apply "filters"
 *  such as "Blur" to the image.  The filter is computed by hand, using
 *  the RGB color data in the BufferedImage.  There is also a "Smudge"
 *  tool that the user can drag on the image to spread color around like
 *  wet paint; it works with the pixel data from the BufferedImage.
 */
public class JavaPixelManipulation extends JPanel {

    /**
     * The main routine simply opens a window that shows a JavaPixelManipulation panel.
     * The window is fixed size, and the size is set to allow the panel to have its
     * preferred size.
     */
    public static void main(String[] args) {
        JFrame window = new JFrame("Java Paint Demo");
        JavaPixelManipulation content = new JavaPixelManipulation();
        window.setContentPane(content);
        window.setJMenuBar(content.getMenuBar());
        window.pack();  
        window.setResizable(false); 
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        window.setLocation( (screenSize.width - window.getWidth())/2,
                (screenSize.height - window.getHeight())/2 );
        window.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
        window.setVisible(true);
    }

    private BufferedImage OSC;  // Stores a copy of the panel content.
    private Graphics2D OSG;     // Graphics context for drawing to OSC/
    private String tool = "Sketch";    // Identifies the current drawing tool.
    private Color color = Color.BLACK; // The current drawing color.
    private BasicStroke stroke; // The current stroke, used for lines and curves. 

    private Image saveLoadedImage = null;  // Keeps a copy of the last loaded image,
                                           // for convenience.  A "Reload Image" menu
                                           // item will load the same image.
    private JMenuItem reloadImageMenuItem; // The "Reload Image" menu item.
    
    private String dragShape = null; // When non-null, the user is dragging with
                                     // the Oval, Rectangle, or Line tool.  The
                                     // current shape is drawn in paintComponent()
                                     // over the BufferedImage.  The shape is only
                                     // added to the image when the drag action ends.
    private int dragStartX, dragStartY;     // Start point of drag for use with dragShape.
    private int dragCurrentX, dragCurrentY; // Current mouse position for use with dragShape.

    private double[][] smudgeRed, smudgeBlue, smudgeGreen; // Data used by "Smudge" tool.

    
    /**
     * The constructor sets the preferred size of the panel, creates the BufferedImage,
     * and installs a mouse listener on the panel to implement drawing actions.
     */
    public JavaPixelManipulation() {
        setPreferredSize(new Dimension(640,480));
        OSC = new BufferedImage(640,480,BufferedImage.TYPE_INT_RGB);
        OSG = OSC.createGraphics();
        OSG.setColor(Color.WHITE);
        OSG.fillRect(0,0,640,480);
        OSG.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        stroke = new BasicStroke(5, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
        smudgeRed = new double[7][7];
        smudgeBlue = new double[7][7];
        smudgeGreen = new double[7][7];
        addMouseListener( new MouseHandler() ); // nested class MouseHandler is defined below.
    }


    /**
     * The paintComponent() copies the BufferedImage to the screen.  If the user is dragging
     * with the "Line", "Rectangle", or "Oval" tool, the shape is drawn over the image from
     * the BufferedImage.
     */
    protected void paintComponent(Graphics g) {
        g.drawImage(OSC,0,0,null);
        if (dragShape != null) {
            Graphics2D g2 = (Graphics2D)g.create();
            g2.setStroke( stroke );
            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            putShape(g2,dragShape,dragStartX,dragStartY,dragCurrentX,dragCurrentY,false);
        }
    }

    /**
     *  For drawing the Line, Rectangle, or OvalShape defined by the two points (x1,y1) and (x2,y2).
     *  If the repaint parameter is true, then the panel's repaint() method is called for a rectangle
     *  that contains the shape.  When this method is used to draw to the BufferedImage, repaint is
     *  true, so that the change to the BufferedImage will also be shown on the screen. When it
     *  is called to draw the dragShape in paintComponent(), repaint is false.
     */
    private void putShape(Graphics2D g, String shape, int x1, int y1, int x2, int y2, boolean repaint) {
        int x = Math.min(x1,x2);
        int y = Math.min(y1,y2);
        int w = Math.abs(x1-x2);
        int h = Math.abs(y1-y2);
        g.setColor(color);
        g.setStroke(stroke);
        switch (shape) {
        case "Line": g.drawLine(x1,y1,x2,y2); break;
        case "Rectangle": g.fillRect(x,y,w,h); break;
        case "Oval": g.fillOval(x,y,w,h); break;
        }
        if (repaint) {
            repaint(x-13,y-13,w+26,h+26); // large enough to contain widest stroke
        }
    }


    /**
     * Apply the "Smudge" or "Erase" tool at the point (x,y)
     */
    private void applyTool(String tool, int x, int y) {
        if (tool.equals("Erase")) { // Clear a 10-by-10 square, centered at (x,y).
            OSG.setColor(Color.WHITE);
            OSG.fillRect(x-5,y-5,10,10);  // Erase the sqaure in the BufferedImage.
            repaint(x-5,y-5,10,10);  // Make change visible on the screen.
        }
        else { // For the "Smudge" tool, mix some of the "paint" on the tool with the image,
               // in a 7-by-7 square centered at x,y.
            swapSmudgeData(x, y);
            repaint(x-4,y-4,8,8);  // Make change visible on the screen.
        }
    }
    
    
    /**
     * Copy pixel colors from a 7-by-7 square centered at (x,y) into the
     * smudge data arrays.  The color components are separated into their
     * red, green, and blue components, which are stored in separate arrays.
     * The values are stored as type double, not int, since they will be
     * used in averaging calculations that require real arithmetic.
     * This method is called at the point where the user starts a drag
     * operation with the "Smudge" tool.
     */
    private void grabSmudgeData(int x, int y) {
        int w = OSC.getWidth();
        int h = OSC.getHeight();
        for (int i = 0; i < 7; i++) {
            for (int j = 0; j < 7; j++) {
                int r = y + j - 3;
                int c = x + i - 3;
                if (r < 0 || r >= h || c < 0 || c >= w) {
                    // A -1 in the smudgeRed array indicates that the
                    // corresponding pixel was outside the canvas.
                    smudgeRed[i][j] = -1;
                }
                else {
                    int color = OSC.getRGB(c,r);
                    smudgeRed[i][j] = (color >> 16) & 0xFF;
                    smudgeGreen[i][j] = (color >> 8) & 0xFF;
                    smudgeBlue[i][j] = color & 0xFF;
                }
            }
        }
    }
    
    
    /**
     *  Swap some of the color stored in the smudge data arrays with
     *  color in a 7-by-7 square centered at (x,y) in the BufferedImage.
     *  That is, the color values in the arrays are replaced by a weighted
     *  average of the color values in the arrays and the color values in
     *  the image.  At the same time, the color values in the image are
     *  replaced by a weighted average of the color values in the image
     *  and the color values in the arrays.  This method is called at
     *  each point along the path that the mouse visits as the user
     *  drags the "Smudge" tool.
     */
    private void swapSmudgeData(int x, int y) {
        int w = OSC.getWidth();  
        int h = OSC.getHeight();  
        for (int i = 0; i < 7; i++) { // row number in the smudge data arrays
            int c = x + i - 3;  // column number (x-coord) of a pixel in the image.
            for (int j = 0; j < 7; j++) {  // column number in the smudge data arrays
                int r = y + j - 3;  // row number (y-coord) of a pixel in the image
                if ( ! (r < 0 || r >= h || c < 0 || c >= w || smudgeRed[i][j] == -1) ) {
                    int curCol = OSC.getRGB(c,r);  // Current color of the pixel in the image.
                    int curRed = (curCol >> 16) & 0xFF;  // RGB components from image
                    int curGreen = (curCol >> 8) & 0xFF;
                    int curBlue = curCol & 0xFF;
                    int newRed = (int)(curRed*0.7 + smudgeRed[i][j]*0.3);  // New RGB's for image.
                    int newGreen = (int)(curGreen*0.7 + smudgeGreen[i][j]*0.3);
                    int newBlue = (int)(curBlue*0.7 + smudgeBlue[i][j]*0.3);
                    int newCol = newRed << 16 | newGreen << 8 | newBlue;
                    OSC.setRGB(c,r,newCol); // Replace the color of the pixel in the image.
                    smudgeRed[i][j] = curRed*0.3 + smudgeRed[i][j]*0.7; // New RGBs for smudge arrays
                    smudgeGreen[i][j] = curGreen*0.3 + smudgeGreen[i][j]*0.7;
                    smudgeBlue[i][j] = curBlue*0.3 + smudgeBlue[i][j]*0.7;
                }
            }
        }
    }

    
    /**
     *  For the "Smudge" and "Erase" tools, apply the tool to every point along the
     *  line from (x1,y1) to (x2,y2).  This is called each time the mouse moves as
     *  the user drags the tool.
     */
    private void applyToolAlongLine(String tool, int x1, int y1, int x2, int y2) {
        if (Math.abs(x1-x2) >= Math.abs(y1-y2)) {
               // Horizontal distance is greater than vertical distance.  Apply the
               // tool once for each x-value between x1 and x2, computing the
               // y-value for each x-value from the equation of a line. 
            double slope = (double)(y2-y1)/(x2-x1);
            if (x1 <= x2) { // Increment up from x1 to x2.
                for (int x = x1; x <= x2; x++) {
                    int y = (int)(y1 + slope*(x-x1) + 0.5);
                    applyTool(tool,x,y);
                }
            }
            else { // Decrement down from x1 to x2
                for (int x = x1; x >= x2; x--) {
                    int y = (int)(y1 + slope*(x-x1) + 0.5);
                    applyTool(tool,x,y);
                }
            }
        }
        else {
               // Vertical distance is greater than horizontal distance.  Apply the
               // tool once for each y-value between y1 and y2, computing the
               // x-value for each y-value from the equation of a line. 
            double slope = (double)(x2-x1)/(y2-y1);
            if (y1 <= y2) {  // Increment up from y1 to y2.
                for (int y = y1; y <= y2; y++) {
                    int x = (int)(x1 + slope*(y-y1) + 0.5);
                    applyTool(tool,x,y);
                }
            }
            else {  // Decrement down from y1 to y2.
                for (int y = y1; y >= y2; y--) {
                    int x = (int)(x1 + slope*(y-y1) + 0.5);
                    applyTool(tool,x,y);
                }
            }
        }
    }

    
    /**
     *  Defines the mouse listener object that responds to user mouse actions on
     *  the panel.
     */
    private class MouseHandler extends MouseAdapter {
        boolean dragging = false;  // Set to true if a dragging operation is in progress.
        int startX, startY;  // The point at which the drag action started.
        int prevX, prevY;   // The location of the mouse the previous time mousePressed 
                            // or mouseDragged was called.
        public void mousePressed(MouseEvent evt) {
            if (dragging)
                return;  // There is already a mouse drag in progress; don't try to start a new one.
            dragging = true;
            startX = prevX = evt.getX();
            startY = prevY = evt.getY();
            if (tool.equals("Line") || tool.equals("Oval") || tool.equals("Rectangle")) {
                dragShape = tool;  // Tells paintComponent about the drag action.
                dragStartX = startX;
                dragStartY = startY;
            }
            else if (tool.equals("Erase")) {
                applyTool("Erase",startX,startY);  // Erase a square around the starting point.
            }
            else if (tool.equals("Smudge")) {
                grabSmudgeData(startX,startY);  // Get data from the image that is needed for the Smudge tool.
            }
            addMouseMotionListener(this);  // Monitor mouse moves during the drag operation.
        }
        public void mouseDragged(MouseEvent evt) {
            if (!dragging)
                return;
            int x = evt.getX();
            int y = evt.getY();
            if (tool.equals("Line") || tool.equals("Oval") || tool.equals("Rectangle")) {
                dragCurrentX = x;
                dragCurrentY = y;
                repaint();  // paintComponent() will draw the current shape on top of the image content.
            }
            else if (tool.equals("Sketch"))
                putShape(OSG, "Line", prevX, prevY, x, y, true);  // Draw line segment directly in BufferedImage.
            else 
                applyToolAlongLine(tool,prevX,prevY,x,y); // For Smudge and Erase tools.
            prevX = x;
            prevY = y;
        }
        public void mouseReleased(MouseEvent evt) {
            if (!dragging)
                return;
            removeMouseMotionListener(this);  // Stop monitoring mouse motions, since drag is ending.
            dragging = false;
            dragShape = null;  // paintComponent will no longer draw the extra shape
            if (tool.equals("Line") || tool.equals("Oval") || tool.equals("Rectangle")) {
                putShape(OSG, tool, startX, startY, prevX, prevY, true);  // add shape to BufferedImage
                repaint();  // should be unnecessary; just to be sure that the panel shows the right thing.
            }
        }
    }
    
    
    /**
     *  Apply one of the image filters from the "Filter" menu to the BufferedImage, and
     *  copy the result to the screen.  A filters is implemented as a "convolution" with
     *  3-by-3 arrays.  That is, the RGB components of each pixel in the image is replaced
     *  with a weighted average of the RGB components of the pixels in a 3-by-3 square.
     *  The weighting factors are given by the convolution array.  For example, for the
     *  "Blur" filter, all the weight factors in the array are equal, and the filter is
     *  just a simple averaging operation.  (To make things easy on myself, I don't change
     *  the colors of the pixels along the border of the image; this lets me assume that
     *  when I work on a pixel, the 3-by-3 square centerd at that pixel is entirely within
     *  the image.)  The filter must be one of the strings form the "Filter" menu.
     */
    private void applyFilter(String filter) {
        int w = OSC.getWidth();
        int h = OSC.getHeight();
        double[] filterArray = null;  // The convolution array for the filter.
        int[] rgbArray = new int[ w*h ];  // An array to hold the RGB colors of the entire image. 
        OSC.getRGB(0, 0, w, h, rgbArray, 0, w);  // Grab the RGB color data from the image.
        double v;
        switch (filter) {
        case "Blur":
            v = 1.0/9.0;
            filterArray = new double[] { v,v,v, v,v,v, v,v,v }; 
            break;
        case "Sharpen":
            v = 1.0/3.0;
            filterArray = new double[] { 0,-v,0, -v,7*v,-v, 0,-v,0 };
            break;
        case "Emboss":
            filterArray = new double[] { -2,-1,0, -1,1,1, 0,1,2 };
            break;
        case "Edge Detect":
            filterArray = new double[] { 0,1,0, 1,-4,1, 0,1,0 }; 
            break;
        }
        for (int x = 1; x < w-1; x++) {
            for (int y = 1; y < h-1; y++) {
                double rNew = 0, gNew = 0, bNew = 0;
                int k = 0;
                int rgb, r, g, b;
                for (int j = y-1; j <= y+1; j++) {
                    for (int i = x-1; i <= x+1; i++) {
                        rgb = rgbArray[i + j*w];
                        r = (rgb >> 16) & 255;
                        g = (rgb >> 8) & 255;
                        b = rgb & 255;
                        rNew += r*filterArray[k];
                        gNew += g*filterArray[k];
                        bNew += b*filterArray[k];
                        k++;
                    }
                }
                r = (int)Math.round(Math.min(255, Math.abs(rNew)));
                g = (int)Math.round(Math.min(255, Math.abs(gNew)));
                b = (int)Math.round(Math.min(255, Math.abs(bNew)));
                rgb = (r << 16) | (g << 8) | b;
                OSC.setRGB(x, y, rgb);
            }
        }
        repaint();
    }
    
    
    /**
     * Load an image from a file selected by the user.  The image is scaled to
     * exactly fill the panel, possibly changing the aspect ratio.
     */
    private void loadImageFile() {
        JFileChooser fileDialog;
        fileDialog = new JFileChooser();
        fileDialog.setSelectedFile(null); 
        int option = fileDialog.showOpenDialog(this);
        if (option != JFileChooser.APPROVE_OPTION)
            return;  // User canceled or clicked the dialog's close box.
        File selectedFile = fileDialog.getSelectedFile();
        FileInputStream stream;
        try {
            stream = new FileInputStream(selectedFile);
        }
        catch (Exception e) {
            JOptionPane.showMessageDialog(this,
                    "Sorry, but an error occurred while trying to open the file:\n" + e);
            return;
        }
        try {
            BufferedImage image = ImageIO.read(stream);
            if (image == null)
                throw new Exception("File does not contain a recognized image format.");
            Graphics g = OSC.createGraphics();
            g.drawImage(image,0,0,OSC.getWidth(),OSC.getHeight(),null);
            g.dispose();
            repaint();
            saveLoadedImage = image;  // Keep a copy of the image so it can be reused with "Reload Image" command.
            reloadImageMenuItem.setEnabled(true);  // Enable the "Reload Image command.
        }
        catch (Exception e) {
            JOptionPane.showMessageDialog(this,
                    "Sorry, but an error occurred while trying to read the image:\n" + e);
        }   
    }

    
    /**
     * Create the menus for the program, and provide listeners to implement the menu commands.
     */
    private JMenuBar getMenuBar() {
        JMenuBar menuBar = new JMenuBar();
        ActionListener flistener = new ActionListener() {
            public void actionPerformed(ActionEvent evt) {
                switch (evt.getActionCommand()) {
                case "Clear":
                    OSG.setColor(Color.WHITE);
                    OSG.fillRect(0,0,OSC.getWidth(),OSC.getHeight());
                    repaint();
                    break;
                case "Quit":
                    System.exit(0);
                    break;
                case "Load Image...":
                    loadImageFile();
                    break;
                case "Reload Image":
                    OSG.drawImage(saveLoadedImage,0,0,OSC.getWidth(),OSC.getHeight(),null);
                    repaint();
                }
            }
        };
        JMenu file = new JMenu("File");
        file.add( makeMenuItem("Clear",flistener) );
        file.add( makeMenuItem("Load Image...",flistener));
        reloadImageMenuItem = makeMenuItem("Reload Image",flistener);
        file.add(reloadImageMenuItem);
        reloadImageMenuItem.setEnabled(false);  // Command will be enabled when an image is loaded.
        file.addSeparator();
        file.add( makeMenuItem("Quit",flistener));
        menuBar.add(file);
        ActionListener tlistener = new ActionListener(){
            public void actionPerformed(ActionEvent evt) {
                tool = evt.getActionCommand();
            }
        };
        JMenu tools = new JMenu("Tool");
        tools.add( makeMenuItem("Sketch",tlistener) );
        tools.add( makeMenuItem("Line",tlistener) );
        tools.add( makeMenuItem("Rectangle",tlistener) );
        tools.add( makeMenuItem("Oval",tlistener) );
        tools.addSeparator();
        tools.add( makeMenuItem("Smudge",tlistener) );
        tools.add( makeMenuItem("Erase",tlistener) );
        menuBar.add(tools);
        ActionListener clistener = new ActionListener(){
            public void actionPerformed(ActionEvent evt) {
                if (tool.equals("Smudge") || tool.equals("Erase"))
                    tool = "Sketch";
                switch (evt.getActionCommand()) {
                case "Black": color = Color.BLACK; return;
                case "Red": color = Color.RED; return;
                case "Green": color = Color.GREEN; return;
                case "Blue": color = Color.BLUE; return;
                case "Cyan": color = Color.CYAN; return;
                case "Magenta": color = Color.MAGENTA; return;
                case "Yellow": color = Color.YELLOW; return;
                case "Gray": color = Color.GRAY; return;
                case "Custom...":
                    Color c = JColorChooser.showDialog(
                            JavaPixelManipulation.this, "Select Drawing Color", color);
                    if (c != null) {
                        color = c;
                    }
                }
            }
        };
        JMenu colors = new JMenu("Color");
        colors.add( makeMenuItem("Black",clistener) );
        colors.add( makeMenuItem("Red",clistener) );
        colors.add( makeMenuItem("Green",clistener) );
        colors.add( makeMenuItem("Blue",clistener) );
        colors.add( makeMenuItem("Cyan",clistener) );
        colors.add( makeMenuItem("Yellow",clistener) );
        colors.add( makeMenuItem("Magenta",clistener) );
        colors.add( makeMenuItem("Gray",clistener) );
        colors.add( makeMenuItem("Custom...",clistener) );
        menuBar.add(colors);
        ActionListener wlistener = new ActionListener() {
            public void actionPerformed(ActionEvent evt) {
                int lineWidth = Integer.parseInt(evt.getActionCommand());
                stroke = new BasicStroke(lineWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
                if (tool.equals("Smudge") || tool.equals("Erase"))
                    tool = "Sketch";
            }
        };
        JMenu width = new JMenu("LineWidth");
        width.add( makeMenuItem("1",wlistener) );
        width.add( makeMenuItem("2",wlistener) );
        width.add( makeMenuItem("3",wlistener) );
        width.add( makeMenuItem("5",wlistener) );
        width.add( makeMenuItem("7",wlistener) );
        width.add( makeMenuItem("10",wlistener) );
        width.add( makeMenuItem("15",wlistener) );
        width.add( makeMenuItem("20",wlistener) );
        width.add( makeMenuItem("25",wlistener) );
        menuBar.add(width);
        ActionListener filterlistener = new ActionListener() {
            public void actionPerformed(ActionEvent evt) {
                if (evt.getActionCommand().startsWith("Blur 5")) {
                    for (int i = 0; i < 5; i++)
                        applyFilter("Blur");
                    if (evt.getActionCommand().equals("Blur 5, Emboss"))
                        applyFilter("Emboss");
                }
                else {
                    applyFilter(evt.getActionCommand());
                }
            }
        };
        JMenu filter = new JMenu("Filter");
        filter.add( makeMenuItem("Blur", filterlistener) );
        filter.add( makeMenuItem("Sharpen", filterlistener) );
        filter.add( makeMenuItem("Emboss", filterlistener) );
        filter.add( makeMenuItem("Edge Detect", filterlistener) );
        filter.addSeparator();
        filter.add( makeMenuItem("Blur 5 Times", filterlistener) );
        filter.add( makeMenuItem("Blur 5, Emboss", filterlistener) );
        menuBar.add(filter);
        return menuBar;
    }

    
    /**
     * Utility method used by getMenuBar to create menu items.
     */
    private JMenuItem makeMenuItem(String itemName, ActionListener listener) {
        JMenuItem item = new JMenuItem(itemName);
        item.addActionListener(listener);
        return item;
    }

}

