Capture the screen

Build a screen-capture utility based on Java's Robot class

The java.awt.Robot class provides several opportunities for fun. One opportunity involves building a screen-capture utility. This Java Fun and Games installment presents a utility that uses Robot to capture the contents of the primary screen device.

This installment diverges from my previous installments by not focusing on an applet implementation. Instead, this article presents my screen-capture utility as a Swing application. After introducing this application from a GUI perspective, I explain key areas of its implementation.

Application GUI

My Capture application reveals a GUI that lets you select a portion of a captured image, crop that image to the selection's contents, and save the resulting image to a jpeg file. Figure 1 shows Capture's GUI with an example capture.

Figure 1. A red-and-white dashed rectangle identifies the current selection

Capture's GUI consists of a menu bar and a scrollable window that reveals the captured image. As shown in Figure 1, a selection rectangle (that you drag with the mouse) identifies a rectangular area of the captured image.

The menu bar presents File and Capture menus:

  • File offers Save As... and Exit menu items for saving the current capture to a jpeg file, via a file-chooser, and quitting Capture. Although you can directly select these menu items, you'll probably find it more convenient to use their Alt-S and Alt-X keyboard shortcuts.
  • Capture offers Capture and Crop menu items for capturing primary screen device contents and cropping a capture to the contents of the selection rectangle. As with File's menu items, these menu items have their own convenient keyboard shortcuts: Alt-C for Capture and Alt-K for Crop.

Application implementation

Three source files describe Capture's GUI: Capture.java (starts the application and constructs the GUI), ImageArea.java (describes a component that displays, and lets you select part of and crop a screen capture), and ImageFileFilter.java (restricts file-chooser selections to directories and jpeg filenames). In the sections below, I excerpt code fragments from these source files to illustrate how Capture works.

Robotic screen capture

To capture the screen with the Robot class, Capture must first create a Robot object. The Capture class's public static void main(String [] args) method attempts to create this object by invoking Robot's public Robot() constructor. If successful, a reference to a Robot configured to the primary screen device's coordinate system returns. If the platform does not support low-level control (which is true of an environment without a screen device), a java.awt.AWTException is thrown. A java.lang.SecurityException is thrown if the platform doesn't grant permission to create a Robot. Hopefully, you will not encounter either exception.

Assuming a Robot object is created, main() invokes the Capture class's constructor to create the GUI. As part of its GUI creation, Capture obtains the dimensions of the primary screen device by invoking dimScreenSize = Toolkit.getDefaultToolkit ().getScreenSize ();. Because Robot's public BufferedImage createScreenCapture(Rectangle screenRect) method, which is used to perform a screen capture, requires a java.awt.Rectangle argument, the constructor converts the java.awt.Dimension object to a Rectangle object via rectScreenSize = new Rectangle (dimScreenSize);. The Capture.java excerpt below calls createScreenCapture() when the Capture menu item's action listener is invoked:

 

// Hide Capture's main window so that it does not appear in // the screen capture.

setVisible (false);

// Perform the screen capture.

BufferedImage biScreen; biScreen = robot.createScreenCapture (rectScreenSize);

// Show Capture's main window for continued user interaction.

setVisible (true);

// Update ImageArea component with the new image and adjust // the scrollbars.

ia.setImage (biScreen);

jsp.getHorizontalScrollBar ().setValue (0); jsp.getVerticalScrollBar ().setValue (0);

You don't want Capture's GUI to hide whatever you are trying to capture. That is why the code fragment hides Capture's GUI prior to making the capture. After obtaining a java.awt.image.BufferedImage that contains a copy of the screen's pixels, the code fragment shows the GUI and displays the contents of the BufferedImage via the image area component.

Sub-image selection

A selection rectangle is needed to select a sub-image from a captured image. The ImageArea class provides code to create, manipulate, and draw that rectangle. As shown in the ImageArea.java excerpt below, that class's constructor creates the selection rectangle as a Rectangle instance, creates java.awt.BasicStroke and java.awt.GradientPaint objects to define the appearance of the rectangle's outline (to keep it distinct from the underlying image), and registers mouse and mouse-motion listeners that let you manipulate the selection rectangle:

 

// Create a selection Rectangle. It's better to create one Rectangle // here than a Rectangle each time paintComponent() is called, to reduce // unnecessary object creation.

rectSelection = new Rectangle ();

// Define the stroke for drawing selection rectangle outline.

bs = new BasicStroke (5, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float [] { 12, 12 }, 0);

// Define the gradient paint for coloring selection rectangle outline.

gp = new GradientPaint (0.0f, 0.0f, Color.red, 1.0f, 1.0f, Color.white, true);

// Install a mouse listener that sets things up for a selection drag.

MouseListener ml; ml = new MouseAdapter () { public void mousePressed (MouseEvent e) { // When you start Capture, there is no captured image. // Therefore, it makes no sense to try and select a sub-image. // This is the reason for the if (image == null) test.

if (image == null) return;

destx = srcx = e.getX (); desty = srcy = e.getY ();

repaint (); } }; addMouseListener (ml);

// Install a mouse motion listener to update the selection rectangle // during drag operations.

MouseMotionListener mml; mml = new MouseMotionAdapter () { public void mouseDragged (MouseEvent e) { // When you start Capture, there is no captured image. // Therefore, it makes no sense to try and select a // sub-image. This is the reason for the if (image == null) // test.

if (image == null) return;

destx = e.getX (); desty = e.getY ();

repaint (); } }; addMouseMotionListener (mml);

The mouse event handler sets destx and srcx to the same horizontal mouse coordinate when you press a mouse button. It does the same for the vertical mouse coordinate. The source and destination variables are made equal to signify that any displayed selection rectangle should be removed. This is carried out by calling repaint(), which results in public void paintComponent(Graphics g) being called. That method compares srcx with destx, and srcy with desty. If they differ, a selection rectangle is drawn:

 

// Draw the selection rectangle if present.

if (srcx != destx || srcy != desty) { // Compute upper-left and lower-right coordinates for selection // rectangle corners.

int x1 = (srcx < destx) ? srcx : destx; int y1 = (srcy < desty) ? srcy : desty;

int x2 = (srcx > destx) ? srcx : destx; int y2 = (srcy > desty) ? srcy : desty;

// Establish selection rectangle origin.

rectSelection.x = x1; rectSelection.y = y1;

// Establish selection rectangle extents.

rectSelection.width = (x2-x1)+1; rectSelection.height = (y2-y1)+1;

// Draw selection rectangle.

Graphics2D g2d = (Graphics2D) g; g2d.setStroke (bs); g2d.setPaint (gp); g2d.draw (rectSelection); }

Before the selection rectangle can be drawn, its upper-left and lower-right corners must be identified to determine the rectangle's origin and extents. So that you can drag the selection rectangle in various directions (a lower-right or an upper-left direction, for example), the minimums of srcx/destx and srcy/desty are obtained to identify the upper-left corner. Similarly, the maximums of their values are obtained to identify the lower-right corner.

Image cropping

After selecting a sub-image, you'll want to crop the captured image to that sub-image. Image cropping begins in the Crop menu item's action listener, which requests that ImageArea crop a captured image to the selected sub-image. If successful, the listener resets ImageArea's scrollbars. If not successful, the listener presents an "Out of bounds" error message via a dialog box:

 

// Crop ImageArea component and adjust the scrollbars if // cropping succeeds.

if (ia.crop ()) { jsp.getHorizontalScrollBar ().setValue (0); jsp.getVerticalScrollBar ().setValue (0); } else showError ("Out of bounds.");

Because a crop operation does not resize Capture's GUI, the main window's background can be seen along with the resulting image (after an initial crop). Figure 2 reveals that it is possible to select part of that background while selecting part of the image.

Figure 2. An attempt to select more than the image

The main window's background pixels are not part of the captured image; including them in a cropped image is impossible. For that reason, cropping fails and an "Out of bounds" error message displays whenever background pixels are included in a selection.

Cropping is handled by ImageArea's public boolean crop() method. That method (shown below) returns Boolean true if it crops the image or if no sub-image has been selected (it's convenient to call this method when there is no selection). Boolean false returns whenever background pixels are in the selection:

 

public boolean crop () { // There is nothing to crop if the selection rectangle is only a single // point.

if (srcx == destx && srcy == desty) return true;

// Assume success.

boolean succeeded = true;

// Compute upper-left and lower-right coordinates for selection rectangle // corners.

int x1 = (srcx < destx) ? srcx : destx; int y1 = (srcy < desty) ? srcy : desty;

int x2 = (srcx > destx) ? srcx : destx; int y2 = (srcy > desty) ? srcy : desty;

// Compute width and height of selection rectangle.

int width = (x2-x1)+1; int height = (y2-y1)+1;

// Create a buffer to hold cropped image.

BufferedImage biCrop = new BufferedImage (width, height, BufferedImage.TYPE_INT_RGB); Graphics2D g2d = biCrop.createGraphics ();

// Perform the crop operation.

try { BufferedImage bi = (BufferedImage) image; BufferedImage bi2 = bi.getSubimage (x1, y1, width, height); g2d.drawImage (bi2, null, 0, 0); } catch (RasterFormatException e) { succeeded = false; }

g2d.dispose ();

if (succeeded) setImage (biCrop); // Implicitly remove selection rectangle. else { // Prepare to remove selection rectangle.

srcx = destx; srcy = desty;

// Explicitly remove selection rectangle.

repaint (); }

return succeeded; }

The crop() method invokes BufferedImage's public BufferedImage getSubimage(int x, int y, int w, int h) method to extract the selection rectangle's sub-image. This method throws a java.awt.image.RasterFormatException if its arguments do not specify a region within the BufferedImage, hence the Boolean false return value.

Image saving

Capture lets you save a captured image to a jpeg file of your choice. You identify that file's name via a save file-chooser, which is created in the Capture class's constructor:

 

// Construct a save file-chooser. Initialize the starting directory to // the current directory, do not allow the user to select the "all files" // filter, and restrict the files that can be selected to those ending // with .jpg or .jpeg extensions.

final JFileChooser fcSave = new JFileChooser (); fcSave.setCurrentDirectory (new File (System.getProperty ("user.dir"))); fcSave.setAcceptAllFileFilterUsed (false); fcSave.setFileFilter (new ImageFileFilter ());

To restrict file-chooser selections to directories and files ending in .jpg or .jpeg extensions, an instance of the ImageFileFilter class is installed as the save file-chooser's file filter. That class's public boolean accept (File f) method returns false for anything other than directories and files with .jpg/.jpeg extensions:

 

public boolean accept (File f) { // Allow the user to select directories so that the user can navigate the // file system.

if (f.isDirectory ()) return true;

// Allow the user to select files ending with a .jpg or a .jpeg // extension.

String s = f.getName (); int i = s.lastIndexOf ('.');

if (i > 0 && i < s.length ()-1) { String ext = s.substring (i+1).toLowerCase ();

if (ext.equals ("jpg") || ext.equals ("jpeg")) return true; }

// Nothing else can be selected.

return false; }

When you select the Save As... menu item, its listener displays the save file-chooser. Assuming you don't cancel the chooser, the listener makes sure your chosen filename ends with a .jpg or .jpeg extension. Moving on, the listener determines if that file exists so you don't accidentally overwrite a file:

 

// Present the "save" file-chooser without any file selected. // If the user cancels this file-chooser, exit this method.

fcSave.setSelectedFile (null); if (fcSave.showSaveDialog (Capture.this) != JFileChooser.APPROVE_OPTION) return;

// Obtain the selected file. Validate its extension, which // must be .jpg or .jpeg. If extension not present, append // .jpg extension.

File file = fcSave.getSelectedFile (); String path = file.getAbsolutePath ().toLowerCase (); if (!path.endsWith (".jpg") && !path.endsWith (".jpeg")) file = new File (path += ".jpg");

// If the file exists, inform the user, who might not want // to accidentally overwrite an existing file. Exit method // if the user specifies that it is not okay to overwrite // the file. if (file.exists ()) { int choice = JOptionPane. showConfirmDialog (null, "Overwrite file?", "Capture", JOptionPane. YES_NO_OPTION); if (choice == JOptionPane.NO_OPTION) return; }

If the file does not exist or if you give permission to overwrite an existing file, the listener saves the captured image to the chosen file. To accomplish this task, the listener uses Java's ImageIO framework to select a jpeg writer, specify the file as that writer's destination, set the writer's compression quality to 95 percent, and write the image to the file:

 

ImageWriter writer = null; ImageOutputStream ios = null;

try { // Obtain a writer based on the jpeg format.

Iterator iter; iter = ImageIO.getImageWritersByFormatName ("jpeg");

// Validate existence of writer.

if (!iter.hasNext ()) { showError ("Unable to save image to jpeg file type."); return; }

// Extract writer.

writer = (ImageWriter) iter.next();

// Configure writer output destination.

ios = ImageIO.createImageOutputStream (file); writer.setOutput (ios);

// Set jpeg compression quality to 95%.

ImageWriteParam iwp = writer.getDefaultWriteParam (); iwp.setCompressionMode (ImageWriteParam.MODE_EXPLICIT); iwp.setCompressionQuality (0.95f);

// Write the image.

writer.write (null, new IIOImage ((BufferedImage) ia.getImage (), null, null), iwp); } catch (IOException e2) { showError (e2.getMessage ()); } finally { try { // Cleanup.

if (ios != null) { ios.flush (); ios.close (); }

if (writer != null) writer.dispose (); } catch (IOException e2) { } }

It's always a good idea for code to clean up after itself. I've placed the ImageIO cleanup code in a finally clause so that it executes regardless of normal completion or a thrown exception.

Conclusion

Capture is limited to capturing the contents of the primary screen device. You might want to enhance Capture to capture the contents of all attached screen devices (perhaps configured to present a giant virtual screen). One of the enhancements you will need to make involves integrating the following code, which captures the contents of all screen devices, with existing code in Capture.java:

 GraphicsEnvironment graphenv = GraphicsEnvironment.getLocalGraphicsEnvironment ();
GraphicsDevice [] screens = graphenv.getScreenDevices ();
BufferedImage [] captures = new BufferedImage [screens.length];
 
for (int i = 0; i < screens.length; i++)
{
    DisplayMode mode = screens [i].getDisplayMode ();
    Rectangle bounds = new Rectangle (0, 0, mode.getWidth (), mode.getHeight ());
    captures [i] = new Robot (screens [i]).createScreenCapture (bounds);
}

Place this code into the Capture menu item's action listener. Then introduce code to create a biScreen-referenced BufferedImage large enough to hold the contents of all BufferedImages referenced by the captures array, and code to draw their contents into biScreen. Capture will now regard a multiple-screen capture as if it were a single-screen capture.

Jeff Friesen is a freelance software developer and educator specializing in C, C++, and Java technology.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies