GrayscalePicture.java


Below is the syntax highlighted version of GrayscalePicture.java from § Standard Libraries.   Here is the Javadoc.


/******************************************************************************
 *  Compilation:  javac GrayscalePicture.java
 *  Execution:    java GrayscalePicture filename
 *  Dependencies: none
 *
 *  Data type for manipulating individual pixels of a grayscale image. The
 *  original image can be read from a file in JPEG, GIF, or PNG format, or the
 *  user can create a blank image of a given dimension. Includes methods for
 *  displaying the image in a window on the screen or saving to a file.
 *
 *  % java GrayscalePicture mandrill.jpg
 *
 *  Remarks
 *  -------
 *   - uses BufferedImage.TYPE_INT_ARGB because BufferedImage.TYPE_BYTE_GRAY
 *     seems to do some undesirable color correction when calling getRGB() and
 *     setRGB()
 *
 ******************************************************************************/


import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Dimension; 
import java.awt.FileDialog;
import java.awt.Graphics;  
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Toolkit;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import java.awt.image.BufferedImage;

import javax.imageio.ImageIO;

import java.io.File;
import java.io.IOException;

import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;

import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.KeyStroke;


/**
 *  The <code>GrayscalePicture</code> data type provides a basic capability
 *  for manipulating the individual pixels of a grayscale image.
 *  The original image can be read from a {@code PNG}, {@code GIF},
 *  or {@code JPEG} file or the user can create a blank image of a given dimension.
 *  This class includes methods for displaying the image in a window on
 *  the screen or saving it to a file.
 *  <p>
 *  Pixel (<em>col</em>, <em>row</em>) is column <em>col</em> and row <em>row</em>.
 *  By default, the origin (0, 0) is the pixel in the top-left corner.
 *  These are common conventions in image processing and consistent with Java's
 *  {@link java.awt.image.BufferedImage} data type.
 *  The method {@link #setOriginLowerLeft()} change the origin to the lower left.
 *  <p>
 *  The {@code get()} and {@code set()} methods use {@link Color} objects to get
 *  or set the color of the specified pixel. The {@link Color} objects are converted
 *  to grayscale (using the NTSC formula) if the R, G, and B channels are not all equal.
 *  The alpha channel for transparency is preserved.
 *  The {@code getGrayscale()} and {@code setGrayscale()} methods use an
 *  8-bit {@code int} to encode the grayscale value, thereby avoiding the need to
 *  create temporary {@code Color} objects.
 *  <p>
 *  A <em>W</em>-by-<em>H</em> picture uses ~ 4 <em>W H</em> bytes of memory,
 *  since the color of each pixel is encoded as a 32-bit <code>int</code>
 *  <p>
 *  For additional documentation, see
 *  <a href="https://introcs.cs.princeton.edu/31datatype">Section 3.1</a> of
 *  <i>Computer Science: An Interdisciplinary Approach</i>
 *  by Robert Sedgewick and Kevin Wayne.
 *  See {@link Picture} for a version that supports 32-bit ARGB color images.
 *
 *  @author Robert Sedgewick
 *  @author Kevin Wayne
 */
public final class GrayscalePicture implements ActionListener {
    private final static boolean HAS_ALPHA = true;

    private BufferedImage image;               // the rasterized image
    private JFrame jframe;                     // on-screen view
    private String title;                      // name of file
    private boolean isOriginUpperLeft = true;  // location of origin
    private boolean isVisible = false;         // is the frame visible?
    private boolean isDisposed = false;        // has the window been disposed?
    private final int width, height;           // width and height

   /**
     * Creates a {@code width}-by-{@code height} picture, with {@code width} columns
     * and {@code height} rows, where each pixel is black.
     *
     * @param width the width of the picture
     * @param height the height of the picture
     * @throws IllegalArgumentException if {@code width} is negative or zero
     * @throws IllegalArgumentException if {@code height} is negative or zero
     */
    public GrayscalePicture(int width, int height) {
        if (width  <= 0) throw new IllegalArgumentException("width must be positive");
        if (height <= 0) throw new IllegalArgumentException("height must be positive");
        this.width  = width;
        this.height = height;
        image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
    }

   /**
     * Creates a new grayscale picture that is a deep copy of the argument picture.
     *
     * @param  picture the picture to copy
     * @throws IllegalArgumentException if {@code picture} is {@code null}
     */
    public GrayscalePicture(GrayscalePicture picture) {
        if (picture == null) throw new IllegalArgumentException("constructor argument is null");

        width  = picture.width();
        height = picture.height();
        image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        title = picture.title;
        isOriginUpperLeft = picture.isOriginUpperLeft;
        for (int col = 0; col < width(); col++)
            for (int row = 0; row < height(); row++)
                image.setRGB(col, row, picture.image.getRGB(col, row));
    }

   /**
     * Creates a grayscale picture by reading an image from a file or URL.
     *
     * @param  filename the name of the file (.png, .gif, or .jpg) or URL.
     * @throws IllegalArgumentException if {@code filename} is {@code null}
     * @throws IllegalArgumentException if cannot read image from file or URL
     */
    public GrayscalePicture(String filename) {
        if (filename == null) throw new IllegalArgumentException("constructor argument is null");
        title = filename;
        try {
            // try to read from file in working directory
            File file = new File(filename);
            if (file.isFile()) {
                image = ImageIO.read(file);
            }

            else {

                // resource relative to .class file
                URL url = getClass().getResource(filename);

                // resource relative to classloader root
                if (url == null) {
                    url = getClass().getClassLoader().getResource(filename);
                }

                // or URL from web or jar
                if (url == null) {
                    URI uri = new URI(filename);
                    if (uri.isAbsolute()) url = uri.toURL();
                    else throw new IllegalArgumentException("could not read image: '" + filename + "'");
                }

                image = ImageIO.read(url);
            }

            if (image == null) {
                throw new IllegalArgumentException("could not read image: '" + filename + "'");
            }

            width  = image.getWidth(null);
            height = image.getHeight(null);

            // convert to ARGB if necessary
            if (image.getType() != BufferedImage.TYPE_INT_ARGB) {
                BufferedImage imageARGB = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
                Graphics2D g = imageARGB.createGraphics();
                g.setComposite(AlphaComposite.Src);
                g.drawImage(image, 0, 0, null);
                g.dispose();
                image = imageARGB;
            }

            // convert to grayscale in-place
            for (int col = 0; col < width; col++) {
                for (int row = 0; row < height; row++) {
                    Color color = new Color(image.getRGB(col, row), HAS_ALPHA);
                    Color gray = toGray(color);
                    image.setRGB(col, row, gray.getRGB());
                }
            }
        }
        catch (IOException | URISyntaxException e) {
            throw new IllegalArgumentException("could not open image: " + filename, e);
        }
    }

   /**
     * Creates a picture by reading the image from a JPEG, PNG, GIF, BMP, or TIFF file.
     * The filetype extension must be {@code .jpg}, {@code .png}, {@code .gif},
     * {@code .bmp}, or {@code .tif}.
     *
     * @param file the file
     * @throws IllegalArgumentException if cannot read image
     * @throws IllegalArgumentException if {@code file} is {@code null}
     */         
    public GrayscalePicture(File file) {
        if (file == null) throw new IllegalArgumentException("constructor argument is null");
    
        try {
            image = ImageIO.read(file);
            if (image == null) {
                throw new IllegalArgumentException("could not read image: '" + file + "'");
            }
    
            width  = image.getWidth(null);
            height = image.getHeight(null);
            title = file.getName();

            // convert to ARGB
            if (image.getType() != BufferedImage.TYPE_INT_ARGB) {
                BufferedImage imageARGB = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
                Graphics2D g = imageARGB.createGraphics();
                g.setComposite(AlphaComposite.Src);
                g.drawImage(image, 0, 0, null);
                g.dispose();
                image = imageARGB;
            }

            // convert to grayscale in-place
            for (int col = 0; col < width; col++) {
                for (int row = 0; row < height; row++) {
                    Color color = new Color(image.getRGB(col, row), HAS_ALPHA);
                    Color gray = toGray(color);
                    image.setRGB(col, row, gray.getRGB());
                }
            }
            
        }

        catch (IOException ioe) {
            throw new IllegalArgumentException("could not open file: " + file, ioe);
        }
    }

    // Returns a grayscale version of the given color as a Color object.
    private static Color toGray(Color color) {
        int a = color.getAlpha();
        int r = color.getRed();
        int g = color.getGreen();
        int b = color.getBlue();
        int y = (int) (Math.round(0.299*r + 0.587*g + 0.114*b));
        return new Color(y, y, y, a);

    }

    private JPanel createViewPanel() {
        return new JPanel() {
                
            @Override
            public Dimension getPreferredSize() {
                // Start at image size, but clamp to ~90% of screen.
                Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
                int maxW = (int) (0.90 * screen.width);
                int maxH = (int) (0.90 * screen.height);
                                    
                double aspect = (double) width / (double) height;
                
                int prefW = Math.min(width, maxW);
                int prefH = (int) Math.round(prefW / aspect);
                if (prefH > maxH) {
                    prefH = Math.min(height, maxH); 
                   prefW = (int) Math.round(prefH * aspect);
                }
                return new Dimension(Math.max(1, prefW), Math.max(1, prefH));
            }

            @Override
            protected void paintComponent(Graphics g) {
                super.paintComponent(g);
                
                Graphics2D g2 = (Graphics2D) g.create();
                // g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
                //                    RenderingHints.VALUE_INTERPOLATION_BILINEAR);
                g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
                                    RenderingHints.VALUE_INTERPOLATION_BICUBIC);
                g2.setRenderingHint(RenderingHints.KEY_RENDERING,
                                    RenderingHints.VALUE_RENDER_QUALITY);
             
                int availW = getWidth();
                int availH = getHeight();
                 
                // Uniform scale to fit, preserving aspect ratio.
                double sx = (double) availW / width;
                double sy = (double) availH / height;
                double scale = Math.min(sx, sy);
    
                int drawW = (int) Math.round(width  * scale);
                int drawH = (int) Math.round(height * scale);
    
                int x = (availW - drawW) / 2;
                int y = (availH - drawH) / 2;
        
                // Optional: make letterbox bars black
                // (not using for transparent images)
                // g2.setColor(Color.BLACK);
                // g2.fillRect(0, 0, availW, availH);
        
                g2.drawImage(image, x, y, drawW, drawH, null);
                g2.dispose();
            }
        };
    }  

    // create the GUI for viewing the image if needed
    // getMenuShortcutKeyMask() deprecated in Java 10 but its replacement
    // getMenuShortcutKeyMaskEx() is not available in Java 8
    private JFrame createGUI() {
        JFrame frame = new JFrame();
        JMenuBar menuBar = new JMenuBar();
        JMenu menu = new JMenu("File");
        menuBar.add(menu);
        JMenuItem menuItem1 = new JMenuItem(" Save...   ");
        menuItem1.addActionListener(this);
        // Java 11:  use getMenuShortcutKeyMaskEx()
        // Java 8:   use getMenuShortcutKeyMask()
        menuItem1.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S,
                                 Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()));
        menu.add(menuItem1);
        frame.setJMenuBar(menuBar);
                    
        frame.setContentPane(createViewPanel());
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.setTitle(title);
             
        // window is resizable (preserving aspect ratio)
        frame.setResizable(true);
            
        frame.pack();
        
        frame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                isVisible = false;
            }

            @Override
            public void windowClosed(WindowEvent e) {
                isVisible = false;
                isDisposed = true;
                jframe = null;
            }
        });

        return frame;
    }

   /**
     * Returns a {@link JLabel} containing this picture, for embedding in a {@link JPanel},
     * {@link JFrame} or other GUI widget.
     *
     * @return the {@code JLabel}
     */
    public JLabel getJLabel() {
        if (image == null) return null;         // no image available
        ImageIcon icon = new ImageIcon(image);
        return new JLabel(icon);
    }

   /**
     * Sets the origin to be the upper left pixel. This is the default.
     */
    public void setOriginUpperLeft() {
        isOriginUpperLeft = true;
    }

   /**
     * Sets the origin to be the lower left pixel.
     */
    public void setOriginLowerLeft() {
        isOriginUpperLeft = false;
    }


   /**
     * Displays the picture in a window on the screen.
     */
    public void show() {
        if (isDisposed) return;
        if (jframe == null) jframe = createGUI();
        isVisible = true;
        // Toolkit.getDefaultToolkit().setDynamicLayout(false);
        jframe.setVisible(true);
        jframe.repaint();
    }

   /**
     * Hides the window on the screen.
     */
    public void hide() {
        if (jframe != null) {
            isVisible = false;
            jframe.setVisible(false);
        }
    }

   /**
     * Is the window containing the picture visible?
     * @return {@code true} if the picture is visible, and {@code false} otherwise
     */
    public boolean isVisible() {
        return isVisible;
    }

   /**
     * Returns the height of the picture.
     *
     * @return the height of the picture (in pixels)
     */
    public int height() {
        return height;
    }

   /**
     * Returns the width of the picture.
     *
     * @return the width of the picture (in pixels)
     */
    public int width() {
        return width;
    }

    private void validateRowIndex(int row) {
        if (row < 0 || row >= height())
            throw new IndexOutOfBoundsException("row index must be between 0 and " + (height() - 1) + ": " + row);
    }

    private void validateColumnIndex(int col) {
        if (col < 0 || col >= width())
            throw new IndexOutOfBoundsException("column index must be between 0 and " + (width() - 1) + ": " + col);
    }

    private void validateGrayscaleValue(int gray) {
        if (gray < 0 || gray >= 256)
            throw new IllegalArgumentException("grayscale value must be between 0 and 255");
    }

   /**
     * Returns the grayscale value of pixel ({@code col}, {@code row}) as a {@link java.awt.Color}.
     *
     * @param col the column index
     * @param row the row index
     * @return the grayscale value of pixel ({@code col}, {@code row})
     * @throws IndexOutOfBoundsException unless both {@code 0 <= col < width} and {@code 0 <= row < height}
     */
    public Color get(int col, int row) {
        validateColumnIndex(col);
        validateRowIndex(row);
        int argb;
        if (isOriginUpperLeft) argb = image.getRGB(col, row);
        else                   argb = image.getRGB(col, height - row - 1);
        Color color = new Color(argb, HAS_ALPHA);
        return toGray(color);
    }

   /**
     * Returns the grayscale value of pixel ({@code col}, {@code row}) as an {@code int}
     * between 0 and 255.
     * Using this method can be more efficient than {@link #get(int, int)} because
     * it does not create a {@code Color} object.
     *
     * @param col the column index
     * @param row the row index
     * @return the 8-bit integer representation of the grayscale value of pixel ({@code col}, {@code row})
     * @throws IndexOutOfBoundsException unless both {@code 0 <= col < width} and {@code 0 <= row < height}
     */
    public int getGrayscale(int col, int row) {
        validateColumnIndex(col);
        validateRowIndex(row);
        if (isOriginUpperLeft) return image.getRGB(col, row) & 0xFF;
        else                   return image.getRGB(col, height - row - 1) & 0xFF;
    }

   /**
     * Sets the color of pixel ({@code col}, {@code row}) to the given grayscale value.
     *
     * @param col the column index
     * @param row the row index
     * @param color the color (converts to grayscale if color is not a shade of gray)
     * @throws IndexOutOfBoundsException unless both {@code 0 <= col < width} and {@code 0 <= row < height}
     * @throws IllegalArgumentException if {@code color} is {@code null}
     */
    public void set(int col, int row, Color color) {
        validateColumnIndex(col);
        validateRowIndex(row);
        if (color == null) throw new IllegalArgumentException("color argument is null");
        Color gray = toGray(color);
        int argb = gray.getRGB();
        if (isOriginUpperLeft) image.setRGB(col, row, argb);
        else                   image.setRGB(col, height - row - 1, argb);
    }

   /**
     * Sets the color of pixel ({@code col}, {@code row}) to the given grayscale value
     * between 0 and 255.
     *
     * @param col the column index
     * @param row the row index
     * @param gray the 8-bit integer representation of the grayscale value
     * @throws IndexOutOfBoundsException unless both {@code 0 <= col < width} and {@code 0 <= row < height}
     */
    public void setGrayscale(int col, int row, int gray) {
        validateColumnIndex(col);
        validateRowIndex(row);
        validateGrayscaleValue(gray);
        int argb = (0xFF << 24) | (gray << 16) | (gray << 8) | gray;
        if (isOriginUpperLeft) image.setRGB(col, row, argb);
        else                   image.setRGB(col, height - row - 1, argb);
    }

   /**
     * Returns true if this picture is equal to the argument picture.
     *
     * @param other the other picture
     * @return {@code true} if this picture is the same dimension as {@code other}
     *         and if all pixels have the same color; {@code false} otherwise
     */
    public boolean equals(Object other) {
        if (other == this) return true;
        if (other == null) return false;
        if (other.getClass() != this.getClass()) return false;
        GrayscalePicture that = (GrayscalePicture) other;
        if (this.width()  != that.width())  return false;
        if (this.height() != that.height()) return false;
        for (int col = 0; col < width(); col++)
            for (int row = 0; row < height(); row++)
                if (this.getGrayscale(col, row) != that.getGrayscale(col, row)) return false;
        return true;
    }

   /**
     * Returns a string representation of this picture.
     * The result is a <code>width</code>-by-<code>height</code> matrix of pixels,
     * where the grayscale value of a pixel is an integer between 0 and 255.
     * It does not include the alpha channel.
     *
     * @return a string representation of this picture
     */
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(width +"-by-" + height + " grayscale picture (grayscale values given in hex)\n");
        for (int row = 0; row < height; row++) {
            for (int col = 0; col < width; col++) {
                int gray;
                if (isOriginUpperLeft) gray = 0xFF & image.getRGB(col, row);
                else                   gray = 0xFF & image.getRGB(col, height - row - 1);
                sb.append(String.format("%3d ", gray));
            }
            sb.append("\n");
        }
        return sb.toString().trim();
    }

    /**
     * This operation is not supported because pictures are mutable.
     *
     * @return does not return a value
     * @throws UnsupportedOperationException if called
     */
    public int hashCode() {
        throw new UnsupportedOperationException("hashCode() is not supported because pictures are mutable");
    }

   /**
     * Saves the picture to a file in either PNG or JPEG format.
     * The filetype extension must be either .png or .jpg.
     *
     * @param  filename the name of the file
     * @throws IllegalArgumentException if {@code filename} is {@code null}
     * @throws IllegalArgumentException if {@code filename} is the empty string
     * @throws IllegalArgumentException if {@code filename} has invalid filetype extension
     * @throws IllegalArgumentException if cannot write the file {@code filename}
     */
    public void save(String filename) {
        if (filename == null) throw new IllegalArgumentException("argument to save() is null");
        save(new File(filename));
        title = filename;
    }

   /**
     * Saves the picture to a file in a PNG or JPEG image format.
     *
     * @param  file the file
     * @throws IllegalArgumentException if {@code file} is {@code null}
     */
    public void save(File file) {
        if (file == null) throw new IllegalArgumentException("argument to save() is null");
        title = file.getName();
        if (jframe != null) jframe.setTitle(title);

        String suffix = title.substring(title.lastIndexOf('.') + 1);
        if (!title.contains(".") || suffix.length() == 0) {
            System.out.printf("Error: the filename '%s' has no file extension, such as .jpg or .png\n", title);
            return;
        }

        try {
            // for formats that support transparency (e.g., PNG and GIF)
            if (ImageIO.write(image, suffix, file)) return;

            // for formats that don't support transparency (e.g., JPG and BMP)
            // create BufferedImage in RGB format and use white background
            BufferedImage imageRGB = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
            Graphics2D g = imageRGB.createGraphics();
            g.drawImage(image, 0, 0, Color.WHITE, null);
            g.dispose();


            if (ImageIO.write(imageRGB, suffix, file)) return;

            // failed to save the file; probably wrong format
            throw new IllegalArgumentException("The filetype '" + suffix + "' is not supported");
        }
        catch (IOException e) {
            throw new IllegalArgumentException("could not write file '" + title + "'", e);
        }
    }

   /**
     * Opens a save dialog box when the user selects "Save As" from the menu.
     */
    @Override
    public void actionPerformed(ActionEvent event) {
        FileDialog chooser = new FileDialog(jframe,
                             "The filetype extension must be either .jpg or .png", FileDialog.SAVE);
        chooser.setVisible(true);
        String selectedDirectory = chooser.getDirectory();
        String selectedFilename = chooser.getFile();
        if (selectedDirectory != null && selectedFilename != null) {
            try {
                save(selectedDirectory + selectedFilename);
            }
            catch (IllegalArgumentException e) {
                System.err.println(e.getMessage());
            }
        }
    } 


   /**
     * Unit tests this {@code Picture} data type.
     * Reads a picture specified by the command-line argument,
     * and shows it in a window on the screen.
     *
     * @param args the command-line arguments
     */
    public static void main(String[] args) {
        GrayscalePicture picture = new GrayscalePicture(args[0]);
        System.out.printf("%d-by-%d\n", picture.width(), picture.height());
        picture.show();
    }

}


Copyright © 2000–2025, Robert Sedgewick and Kevin Wayne.
Last updated: Thu Feb 12 09:47:29 AM EST 2026.