/****************************************************************************** * Compilation: javac Picture.java * Execution: java Picture filename.jpg * Dependencies: none * ******************************************************************************/ import java.awt.Color; import java.awt.FileDialog; 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 java.io.File; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import javax.imageio.ImageIO; 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 Picture} data type provides a basic capability for manipulating * the individual pixels of an image. * You can either create a blank image (of a given dimension) or read an * image in a supported file format (typically JPEG, PNG, GIF, TIFF, and BMP). * This class also includes methods for displaying the image in a window * and saving it to a file. * *
* Use in the curriculum. * The {@code Picture} class is intended for use in the * curriculum once objects are introduced. * The {@link StdPicture} class is intended for earlier use in * the curriculum, before objects (but it can support only one * picture at a time). * See {@link GrayscalePicture} for a version that supports * grayscale images. * *
* Getting started. * To use this class, you must have {@code Picture} in your Java classpath. * Here are three possible ways to do this: *
* As a test, cut-and-paste the following short program into your editor: *
* public class TestPicture { * public static void main(String[] args) { * Picture picture = new Picture("https://introcs.cs.princeton.edu/java/stdlib/mandrill.jpg"); * picture.show(); * } * } **
* If you compile and execute the program, you should see a picture of a mandrill * (a colorful monkey native to west-central Africa) in a window. * *
* Anatomy of an image. * An image is a width-by-height grid of pixels, with pixel (0, 0) * in the upper-left corner. * Each pixel has a color that is represented using the RGB color model, * which specifies the levels of red (R), green (G), and blue (B) * on an integer scale from 0 to 255. * *
** **
* Creating pictures. * You can use the following constructors to create new {@code Picture} objects: *
* The first constructor read an image in a supported file format * (typically JPEG, PNG, GIF, TIFF, and BMP) * and initializes the picture to that image. * The second constructor creates a width-by-height picture, * with each pixel black. * *
* Getting and setting the colors of the individual pixels. * You can use the following methods to get and set the color of a * specified pixel: *
* The first method returns the color of pixel (col, row) * as a {@code Color} object. * The second method sets the color of pixel (col, row) to * the specified color. * *
Iterating over the pixels. * A common operation in image processing is to iterate over and process * all of the pixels in an image. * Here is a prototypical example that creates a grayscale version of a color image, * using the NTSC formula * Y = 0.299r + 0.587g + 0.114b. * Note that if the red, green, and blue components of an RGB color * are all equal, the color is a shade of gray. *
* Picture picture = new Picture("https://introcs.cs.princeton.edu/java/stdlib/mandrill.jpg"); * Picture grayscale = new Picture(picture.width(), picture.height()); * for (int col = 0; col < picture.width(); col++) { * for (int row = 0; row < picture.height(); row++) { * Color color = picture.get(col, row); * 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)); * Color gray = new Color(y, y, y); * grayscale.set(col, row, gray); * } * } * picture.show(); * grayscale.show(); ** *
Transparency. * Both the {@link Color} and {@code Picture} classes support * transparency, using the alpha channel. * The alpha value defines the transparency of a color, with 0 corresponding to * completely transparent and 255 to completely opaque. If transparency is not * explicitly used, the alpha values is 255. * *
32-bit color. * Sometimes it is more convenient (or efficient) to manipulate the * color of a pixel as a single 32-bit integers instead of four 8-bit components. * The following methods support this: *
* The alpha (A), red (R), green (G), and blue (B) components * are encoded as a single 32-bit integer. * Given a 32-bit {@code int} encoding the color, the following code extracts * the ARGB components: *
* Given the ARGB components (8-bits each) of a color, * the following statement packs it into a 32-bit {@code int}: ** int a = (rgb >> 24) & 0xFF; * int r = (rgb >> 16) & 0xFF; * int g = (rgb >> 8) & 0xFF; * int b = (rgb >> 0) & 0xFF; *
* ** int argb = (a << 24) | (r << 16) | (g << 8) | (b << 0); *
Coordinates. * Pixel (col, row) is column col and row row. * By default, the origin (0, 0) is the pixel in the upper-left corner. * These are common conventions in image processing and consistent with Java's * {@link java.awt.image.BufferedImage} data type. The following * two methods allow you to change this convention: *
Saving files. * The {@code Picture} class supports writing images to a supported * file format (typically JPEG, PNG, GIF, TIFF, and BMP). * You can save the picture to a file using these two methods: *
Alternatively, you can save the picture interactively * by using the menu option File → Save from the picture window. * *
File formats. * The {@code Picture} class supports reading and writing images to any of the * file formats supported by {@link javax.imageio} (typically JPEG, PNG, * GIF, TIFF, and BMP). * The file extensions corresponding to JPEG, PNG, GIF, TIFF, and BMP, * are {@code .jpg}, {@code .png}, {@code .gif}, {@code .tif}, * and {@code .bmp}, respectively. * The file formats JPEG and BMP do not support transparency. * *
Memory usage.
* A W-by-H picture uses ~ 4 W H bytes of memory,
* since the color of each pixel is encoded as a 32-bit int
.
*
*
Additional documentation.
* For additional documentation, see
* Section 3.1 of
* Computer Science: An Interdisciplinary Approach
* by Robert Sedgewick and Kevin Wayne.
*
* @author Robert Sedgewick
* @author Kevin Wayne
*/
public final class Picture implements ActionListener {
private BufferedImage image; // the rasterized image
private JFrame jframe; // on-screen view
private String title; // window title (typically the name of the 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 Picture(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;
this.title = width + "-by-" + height;
image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
}
/**
* Creates a new 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 Picture(Picture 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 picture by reading a JPEG, PNG, GIF , BMP, or TIFF image
* from a file or URL.
* The filetype extension must be {@code .jpg}, {@code .png}, {@code .gif},
* {@code .bmp}, or {@code .tif}.
*
* @param filename the name of the file or URL
* @throws IllegalArgumentException if {@code filename} is {@code null}
* @throws IllegalArgumentException if cannot read image from file or URL
*/
public Picture(String filename) {
if (filename == null) throw new IllegalArgumentException("constructor argument is null");
if (filename.length() == 0) throw new IllegalArgumentException("constructor argument is the empty string");
title = filename;
try {
// try to read from file in working directory
File file = new File(filename);
if (file.isFile()) {
title = file.getName();
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);
// the next line causes JVM app icon to previous display in Dock on OS X
imageARGB.createGraphics().drawImage(image, 0, 0, null);
image = imageARGB;
}
}
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 Picture(File file) {
if (file == null) throw new IllegalArgumentException("constructor argument is null");
try {
image = ImageIO.read(file);
width = image.getWidth(null);
height = image.getHeight(null);
title = file.getName();
// convert to ARGB
if (image.getType() != BufferedImage.TYPE_INT_RGB) {
BufferedImage imageARGB = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
imageARGB.createGraphics().drawImage(image, 0, 0, null);
image = imageARGB;
}
}
catch (IOException ioe) {
throw new IllegalArgumentException("could not open file: " + file, ioe);
}
}
// create the GUI for viewing the image if needed
@SuppressWarnings("deprecation")
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(getJLabel());
// f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setTitle(title);
frame.setResizable(false);
frame.pack();
frame.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent event){
isVisible = false;
isDisposed = true;
super.windowClosing(event);
}
});
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 (0, 0) to be the upper left pixel. This is the default.
*/
public void setOriginUpperLeft() {
isOriginUpperLeft = true;
}
/**
* Sets the origin (0, 0) to be the lower left pixel.
*/
public void setOriginLowerLeft() {
isOriginUpperLeft = false;
}
/**
* Displays the picture in a window on the screen.
*/
// getMenuShortcutKeyMask() deprecated in Java 10 but its replacement
// getMenuShortcutKeyMaskEx() is not available in Java 8
@SuppressWarnings("deprecation")
/**
* Displays the picture in a window on the screen.
*/
public void show() {
if (jframe == null && !isDisposed) {
jframe = createGUI();
isVisible = true;
jframe.setVisible(true);
jframe.repaint();
}
if (jframe != null && !isDisposed) {
isVisible = true;
jframe.setVisible(true);
jframe.repaint();
}
}
/**
* Hides the window containing the picture.
*/
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);
}
/**
* Returns the color of pixel ({@code col}, {@code row}) as a {@link java.awt.Color} object.
*
* @param col the column index
* @param row the row index
* @return the color 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 = getARGB(col, row);
return new Color(argb, true);
}
/**
* Returns the ARGB color of pixel ({@code col}, {@code row}) as a 32-bit integer.
* 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 32-bit integer representation of the ARGB color of pixel ({@code col}, {@code row})
* @throws IndexOutOfBoundsException unless both {@code 0 <= col < width} and {@code 0 <= row < height}
*/
public int getARGB(int col, int row) {
validateColumnIndex(col);
validateRowIndex(row);
if (isOriginUpperLeft) return image.getRGB(col, row);
else return image.getRGB(col, height - row - 1);
}
/**
* Sets the color of pixel ({@code col}, {@code row}) to the given color.
*
* @param col the column index
* @param row the row index
* @param color the color
* @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");
int argb = color.getRGB();
setARGB(col, row, argb);
}
/**
* Sets the color of pixel ({@code col}, {@code row}) to the given ARGB color.
*
* @param col the column index
* @param row the row index
* @param argb the 32-bit integer representation of the color
* @throws IndexOutOfBoundsException unless both {@code 0 <= col < width} and {@code 0 <= row < height}
*/
public void setARGB(int col, int row, int argb) {
validateColumnIndex(col);
validateRowIndex(row);
if (isOriginUpperLeft) image.setRGB(col, row, argb);
else image.setRGB(col, height - row - 1, argb);
}
/**
* Returns {@code true} if this picture is equal to the argument picture,
* and {@code false} otherwise.
*
* @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;
Picture that = (Picture) 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.getARGB(col, row) != that.getARGB(col, row)) return false;
return true;
}
/**
* Returns a string representation of this picture.
* The result is a width
-by-height
matrix of pixels,
* where the color of a pixel is represented using 6 hex digits to encode
* the red, green, and blue components.
*
* @return a string representation of this picture
*/
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(width +"-by-" + height + " picture (RGB values given in hex)\n");
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
int rgb;
if (isOriginUpperLeft) rgb = image.getRGB(col, row);
else rgb = image.getRGB(col, height - row - 1);
sb.append(String.format("#%06X ", rgb & 0xFFFFFF));
}
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");
}
/**
* Sets the title of this picture.
* @param title the title
* @throws IllegalArgumentException if {@code title} is {@code null}
*/
public void setTitle(String title) {
if (title == null) throw new IllegalArgumentException("title is null");
this.title = title;
}
// does this picture use transparency (i.e., alpha < 255 for some pixel)?
private boolean hasAlpha() {
for (int col = 0; col < width; col++) {
for (int row = 0; row < height; row++) {
int argb = image.getRGB(col, row);
int alpha = (argb >> 24) & 0xFF;
if (alpha != 255) return true;
}
}
return false;
}
/**
* Saves the picture to a file in a supported file format
* (typically JPEG, PNG, GIF, TIFF, and BMP).
* The filetype extension must be {@code .jpg}, {@code .png}, {@code .gif},
* {@code .bmp}, or {@code .tif}.
* If the file format does not support transparency (such as JPEG
* or BMP), it will be converted to be opaque (with purely
* transparent pixels converted to black).
*
* @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");
if (filename.length() == 0) throw new IllegalArgumentException("argument to save() is the empty string");
File file = new File(filename);
save(file);
}
/**
* Saves the picture to a file in a supported file format
* (typically JPEG, PNG, GIF, TIFF, and BMP).
* The filetype extension must be {@code .jpg}, {@code .png}, {@code .gif},
* {@code .bmp}, or {@code .tif}.
* If the file format does not support transparency (such as JPEG
* or BMP), it will be converted to be opaque (with purely
* transparent pixels converted to black).
*
* @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();
String suffix = title.substring(title.lastIndexOf('.') + 1);
if (!title.contains(".") || suffix.length() == 0) {
throw new IllegalArgumentException("The filename '" + title + "' has no filetype extension, such as .jpg or .png");
}
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);
imageRGB.createGraphics().drawImage(image, 0, 0, Color.WHITE, null);
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) {
Picture picture = new Picture(args[0]);
System.out.printf("%d-by-%d\n", picture.width(), picture.height());
picture.show();
}
}