001    /*
002     * Created on May 6, 2007
003     *
004     * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
005     * in compliance with the License. You may obtain a copy of the License at
006     *
007     * http://www.apache.org/licenses/LICENSE-2.0
008     *
009     * Unless required by applicable law or agreed to in writing, software distributed under the License
010     * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
011     * or implied. See the License for the specific language governing permissions and limitations under
012     * the License.
013     *
014     * Copyright @2007-2010 the original author or authors.
015     */
016    package org.fest.swing.image;
017    
018    import static org.fest.swing.core.FocusOwnerFinder.focusOwner;
019    import static org.fest.swing.edt.GuiActionRunner.execute;
020    import static org.fest.swing.image.ImageFileExtensions.PNG;
021    import static org.fest.swing.query.ComponentLocationOnScreenQuery.locationOnScreen;
022    import static org.fest.swing.query.ComponentSizeQuery.sizeOf;
023    import static org.fest.util.Strings.*;
024    
025    import java.awt.*;
026    import java.awt.image.BufferedImage;
027    import java.util.Locale;
028    
029    import javax.swing.text.Caret;
030    import javax.swing.text.JTextComponent;
031    
032    import org.fest.swing.annotation.RunsInEDT;
033    import org.fest.swing.edt.GuiQuery;
034    import org.fest.swing.edt.GuiTask;
035    import org.fest.swing.util.RobotFactory;
036    import org.fest.util.VisibleForTesting;
037    
038    /**
039     * Understands taking screenshots of the desktop and GUI components.
040     *
041     * @author Alex Ruiz
042     * @author Yvonne Wang
043     */
044    public class ScreenshotTaker {
045    
046      /**
047       * Extension of the image files containing the screenshots taken by instances of this class (png).
048       * @deprecated use <code>{@link ImageFileExtensions#PNG}</code> instead.
049       */
050      @Deprecated public static final String PNG_EXTENSION = "png";
051    
052      private final Robot robot;
053      private final ImageFileWriter writer;
054    
055      /**
056       * Creates a new <code>{@link ScreenshotTaker}</code>.
057       * @throws ImageException if a AWT Robot (the responsible for taking screenshots) cannot be instantiated.
058       */
059      public ScreenshotTaker() {
060        this(new ImageFileWriter(), new RobotFactory());
061      }
062    
063      @VisibleForTesting
064      ScreenshotTaker(ImageFileWriter writer, RobotFactory robotFactory) {
065        this.writer = writer;
066        try {
067          robot = robotFactory.newRobotInPrimaryScreen();
068        } catch (AWTException e) {
069          throw new ImageException("Unable to create AWT Robot", e);
070        }
071      }
072    
073      /**
074       * Takes a screenshot of the desktop and saves it as a PNG file.
075       * @param imageFilePath the path of the file to save the screenshot to.
076       * @throws ImageException if the given file path is <code>null</code> or empty.
077       * @throws ImageException if the given file path does not end with ".png".
078       * @throws ImageException if the given file path belongs to a non-empty directory.
079       * @throws ImageException if an I/O error prevents the image from being saved as a file.
080       */
081      public void saveDesktopAsPng(String imageFilePath) {
082        saveImage(takeDesktopScreenshot(), imageFilePath);
083      }
084    
085      /**
086       * Takes a screenshot of the desktop.
087       * @return the screenshot of the desktop.
088       * @throws SecurityException if <code>readDisplayPixels</code> permission is not granted.
089       */
090      public BufferedImage takeDesktopScreenshot() {
091        Rectangle r = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());
092        return takeScreenshot(r);
093      }
094    
095      /**
096       * Takes a screenshot of the given <code>{@link java.awt.Component}</code> and saves it as a PNG file.
097       * @param c the given component.
098       * @param imageFilePath the path of the file to save the screenshot to.
099       * @throws ImageException if the given file path is <code>null</code> or empty.
100       * @throws ImageException if the given file path does not end with ".png".
101       * @throws ImageException if the given file path belongs to a non-empty directory.
102       * @throws ImageException if an I/O error prevents the image from being saved as a file.
103       */
104      public void saveComponentAsPng(Component c, String imageFilePath) {
105        saveImage(takeScreenshotOf(c), imageFilePath);
106      }
107    
108      /**
109       * Takes a screenshot of the given <code>{@link java.awt.Component}</code>.
110       * @param c the given component.
111       * @return a screenshot of the given component.
112       * @throws SecurityException if <code>readDisplayPixels</code> permission is not granted.
113       */
114      public BufferedImage takeScreenshotOf(Component c) {
115        Point locationOnScreen = locationOnScreen(c);
116        Dimension size = sizeOf(c);
117        Rectangle r = new Rectangle(locationOnScreen.x,  locationOnScreen.y, size.width, size.height);
118        return takeScreenshot(r);
119      }
120    
121      private BufferedImage takeScreenshot(Rectangle r) {
122        JTextComponent textComponent = findFocusOwnerAndHideItsCaret();
123        robot.waitForIdle();
124        try {
125          return takeScreenshot(robot, r);
126        } finally {
127          showCaretIfPossible(textComponent);
128        }
129      }
130    
131      @RunsInEDT
132      private static JTextComponent findFocusOwnerAndHideItsCaret() {
133        return execute(new GuiQuery<JTextComponent>() {
134          protected JTextComponent executeInEDT() {
135            Component focusOwner = focusOwner();
136            if (!(focusOwner instanceof JTextComponent)) return null;
137            JTextComponent textComponent = (JTextComponent)focusOwner;
138            Caret caret = textComponent.getCaret();
139            if (caret == null || !caret.isVisible()) return null;
140            caret.setVisible(false);
141            return textComponent;
142          }
143        });
144      }
145    
146      private static BufferedImage takeScreenshot(final Robot robot, final Rectangle r) {
147        return execute(new GuiQuery<BufferedImage>() {
148          protected BufferedImage executeInEDT() {
149            return robot.createScreenCapture(r);
150          }
151        });
152      }
153    
154      private void showCaretIfPossible(JTextComponent textComponent) {
155        if (textComponent == null) return;
156        showCaretOf(textComponent);
157        robot.waitForIdle();
158      }
159    
160      @RunsInEDT
161      private static void showCaretOf(final JTextComponent textComponent) {
162        execute(new GuiTask() {
163          protected void executeInEDT() {
164            Caret caret = textComponent.getCaret();
165            if (caret != null) caret.setVisible(true);
166          }
167        });
168      }
169    
170      /**
171       * Save the given image as a PNG file.
172       * @param image the image to save.
173       * @param filePath the path of the file to save the image to.
174       * @throws ImageException if the given file path is <code>null</code> or empty.
175       * @throws ImageException if the given file path does not end with ".png".
176       * @throws ImageException if the given file path belongs to a non-empty directory.
177       * @throws ImageException if an I/O error prevents the image from being saved as a file.
178       */
179      public void saveImage(BufferedImage image, String filePath) {
180        validate(filePath);
181        try {
182          writer.writeAsPng(image, filePath);
183        } catch (Exception e) {
184          throw new ImageException(concat("Unable to save image as ", quote(filePath)), e);
185        }
186      }
187    
188      private void validate(String imageFilePath) {
189        if (isEmpty(imageFilePath)) throw new ImageException("The image path cannot be empty");
190        if (!imageFilePath.endsWith(PNG))
191          throw new ImageException(concat("The image file should be a ", PNG.toUpperCase(Locale.getDefault())));
192      }
193    }