jp.co.heppokoact.autocapture.FXMLDocumentController.java Source code

Java tutorial

Introduction

Here is the source code for jp.co.heppokoact.autocapture.FXMLDocumentController.java

Source

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */

package jp.co.heppokoact.autocapture;

import java.awt.AWTException;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Robot;
import java.awt.event.InputEvent;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.ResourceBundle;

import javafx.animation.Animation.Status;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.binding.Bindings;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundImage;
import javafx.scene.layout.BackgroundPosition;
import javafx.scene.layout.BackgroundRepeat;
import javafx.scene.layout.BackgroundSize;
import javafx.scene.layout.Pane;
import javafx.scene.media.AudioClip;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.DirectoryChooser;
import javafx.stage.Modality;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.stage.Window;
import javafx.util.Duration;

import javax.imageio.ImageIO;

import jp.co.heppokoact.util.ImageUtil;

import org.apache.commons.io.FileUtils;
import org.controlsfx.dialog.Dialogs;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * ?
 *
 * @author M.Yoshida
 */
public class FXMLDocumentController implements Initializable {

    private static final Logger LOGGER = LoggerFactory.getLogger(FXMLDocumentController.class);

    private static final File CONFIG_FILE = new File("config.xml");
    private static final double CAPTURE_INTERVAL = 1000.0;

    private Stage stage;

    @FXML
    private AnchorPane anchorPane;
    @FXML
    private Label areaStartXLabel;
    @FXML
    private Label areaStartYLabel;
    @FXML
    private Label areaEndXLabel;
    @FXML
    private Label areaEndYLabel;
    @FXML
    private Label nextPointXLabel;
    @FXML
    private Label nextPointYLabel;
    @FXML
    private Label prevPointXLabel;
    @FXML
    private Label prevPointYLabel;
    @FXML
    private Label saveDirectoryLabel;
    @FXML
    private Button areaButton;
    @FXML
    private Button pointButton;
    @FXML
    private Button saveDirectoryButton;
    @FXML
    private Button startButton;
    @FXML
    private Button stopButton;

    private IntegerProperty areaStartX = new SimpleIntegerProperty(0);
    private IntegerProperty areaStartY = new SimpleIntegerProperty(0);
    private IntegerProperty areaEndX = new SimpleIntegerProperty(0);
    private IntegerProperty areaEndY = new SimpleIntegerProperty(0);
    private IntegerProperty nextPointX = new SimpleIntegerProperty(0);
    private IntegerProperty nextPointY = new SimpleIntegerProperty(0);
    private IntegerProperty prevPointX = new SimpleIntegerProperty(0);
    private IntegerProperty prevPointY = new SimpleIntegerProperty(0);
    private ObjectProperty<File> saveDirectory = new SimpleObjectProperty<>();

    private int dragStartX;
    private int dragStartY;
    private Rectangle areaRect;

    /**  */
    private Properties prop;

    /** ?? */
    private CaptureService captureService;
    /** ??? */
    private Timeline captureTimeline;
    /** ?? */
    private Robot robot;

    /**  */
    private AudioClip clip;

    @Override
    public void initialize(URL url, ResourceBundle rb) {
        // ???
        prop = new Properties();
        if (CONFIG_FILE.exists()) {
            try (InputStream in = new FileInputStream(CONFIG_FILE)) {
                prop.loadFromXML(in);
            } catch (IOException e) {
                throw new RuntimeException("????????", e);
            }
        }

        // ????????
        String saveDirectoryPath = prop.getProperty("saveDirectoryPath");
        if (saveDirectoryPath != null) {
            File tempSaveDirectory = new File(saveDirectoryPath);
            if (tempSaveDirectory.exists()) {
                saveDirectory.set(tempSaveDirectory);
            }
        }

        // ???
        saveDirectoryLabel.textProperty().bind(Bindings.createStringBinding(() -> {
            File sd = saveDirectory.get();
            return (sd == null) ? "" : sd.getName();
        }, saveDirectory));
        areaStartXLabel.textProperty().bind(Bindings.convert(areaStartX));
        areaStartYLabel.textProperty().bind(Bindings.convert(areaStartY));
        areaEndXLabel.textProperty().bind(Bindings.convert(areaEndX));
        areaEndYLabel.textProperty().bind(Bindings.convert(areaEndY));
        nextPointXLabel.textProperty().bind(Bindings.convert(nextPointX));
        nextPointYLabel.textProperty().bind(Bindings.convert(nextPointY));
        prevPointXLabel.textProperty().bind(Bindings.convert(prevPointX));
        prevPointYLabel.textProperty().bind(Bindings.convert(prevPointY));

        // ???
        stopButton.setDisable(true);

        // ????
        captureService = new CaptureService();
        captureTimeline = new Timeline(new KeyFrame(new Duration(CAPTURE_INTERVAL), e -> {
            captureService.restart();
        }));
        captureTimeline.setCycleCount(Timeline.INDEFINITE);
        try {
            robot = new Robot();
        } catch (AWTException e) {
            throw new RuntimeException("???????", e);
        }

        // ??
        clip = new AudioClip(ClassLoader.getSystemResource("ayashi.wav").toString());
    }

    @FXML
    private void areaButtonClicked(ActionEvent event) throws IOException {
        System.out.println("areaButtonClicked");

        // ??
        Stage transparentStage = createTransparentStage();

        // ?????
        Scene scene = transparentStage.getScene();
        scene.setOnMousePressed(e -> {
            // 
            dragStartX = (int) e.getScreenX();
            dragStartY = (int) e.getScreenY();
            // ??
            areaRect = new Rectangle(e.getX(), e.getY(), 0, 0);
            areaRect.setStroke(Color.RED);
            areaRect.setStrokeWidth(1.0);
            areaRect.setFill(Color.TRANSPARENT);
            ((Pane) scene.getRoot()).getChildren().add(areaRect);
        });

        // ?????
        scene.setOnMouseDragged(e -> {
            // ??????
            areaRect.setWidth(e.getScreenX() - dragStartX);
            areaRect.setHeight(e.getScreenY() - dragStartY);

        });

        // ?????
        scene.setOnMouseReleased(e -> {
            // ?
            areaStartX.set(dragStartX);
            areaStartY.set(dragStartY);
            areaEndX.set((int) e.getScreenX());
            areaEndY.set((int) e.getScreenY());
            // ??
            transparentStage.close();
        });

        // ??????
        transparentStage.setOnCloseRequest(e -> {
            // ?????
            dragStartX = 0;
            dragStartY = 0;
            areaRect = null;
        });

        transparentStage.show();
    }

    @FXML
    private void pointButtonClicked(ActionEvent event) throws IOException {
        System.out.println("pointButtonClicked");

        // ??
        Stage transparentStage = createTransparentStage();

        // ????
        Scene scene = transparentStage.getScene();
        // 
        EventHandler<? super MouseEvent> setPrevPoint = e -> {
            // ????
            prevPointX.set((int) e.getScreenX());
            prevPointY.set((int) e.getScreenY());
            transparentStage.close();
        };
        // ?
        EventHandler<MouseEvent> setNextPoint = e -> {
            // ????
            nextPointX.set((int) e.getScreenX());
            nextPointY.set((int) e.getScreenY());
            scene.setOnMouseClicked(setPrevPoint);
        };
        scene.setOnMouseClicked(setNextPoint);

        transparentStage.show();
    }

    @FXML
    private void saveDirectoryButtonClicked(ActionEvent event) throws IOException {
        System.out.println("saveDirectoryButtonClicked");

        // ???
        DirectoryChooser dc = new DirectoryChooser();
        dc.setTitle("??");
        File sd = null;
        Window window = anchorPane.getScene().getWindow();
        try {
            dc.setInitialDirectory(saveDirectory.get());
            sd = dc.showDialog(window);
        } catch (IllegalArgumentException e) {
            dc.setInitialDirectory(null);
            sd = dc.showDialog(window);
        }
        saveDirectory.set(sd);

        // ?????
        if (sd != null) {
            prop.setProperty("saveDirectoryPath", sd.getAbsolutePath());
            try (OutputStream out = new FileOutputStream(CONFIG_FILE)) {
                prop.storeToXML(out, "AutoCapture");
            }
        }
    }

    @FXML
    private void startButtonClicked(ActionEvent event) {
        System.out.println("startButtonClicked");

        if (!validateStartButtonClicked()) {
            return;
        }

        // ??????
        areaButton.setDisable(true);
        pointButton.setDisable(true);
        saveDirectoryButton.setDisable(true);
        startButton.setDisable(true);
        stopButton.setDisable(false);

        // ?
        captureService.init();
        captureTimeline.play();
    }

    /**
     * ????????????????
     * ?????????
     *
     * @return ???true
     */
    private boolean validateStartButtonClicked() {
        if (calcAreaHeight() == 0 || calcAreaWidth() == 0) {
            Dialogs.create()//
                    .owner(stage)//
                    .title("ERROR")//
                    .masthead(null)//
                    .message("???????????")//
                    .showError();
            return false;
        }

        if (saveDirectory.get() == null) {
            Dialogs.create()//
                    .owner(stage)//
                    .title("ERROR")//
                    .masthead(null)//
                    .message("???????")//
                    .showError();
            return false;
        }

        return true;
    }

    @FXML
    private void stopButtonClicked(ActionEvent event) {
        System.out.println("stopButtonClicked");
        stopCapture();
    }

    /**
     * ?????
     */
    private void stopCapture() {
        // ?
        captureTimeline.stop();
        captureService.cancel();

        // ??????
        areaButton.setDisable(false);
        pointButton.setDisable(false);
        saveDirectoryButton.setDisable(false);
        startButton.setDisable(false);
        stopButton.setDisable(true);

        clip.play();
    }

    /**
     * ???????????
     * ????????
     * ???ESC????????
     *
     * @return ???????
     * @throws IOException ?????
     */
    private Stage createTransparentStage() throws IOException {
        // ??????????
        Stage transparentStage = new Stage(StageStyle.TRANSPARENT);
        transparentStage.initOwner(anchorPane.getScene().getWindow());
        transparentStage.initModality(Modality.APPLICATION_MODAL);
        transparentStage.setResizable(false);
        Rectangle2D rect = Screen.getPrimary().getVisualBounds();
        transparentStage.setWidth(rect.getWidth());
        transparentStage.setHeight(rect.getHeight());

        // ???
        java.awt.Rectangle awtRect = new java.awt.Rectangle((int) rect.getWidth(), (int) rect.getHeight());
        BufferedImage captureImage = robot.createScreenCapture(awtRect);

        // ??????
        ByteArrayInputStream in = ImageUtil.convToInputStream(captureImage);
        BackgroundImage bgImage = new BackgroundImage(new Image(in), BackgroundRepeat.NO_REPEAT,
                BackgroundRepeat.NO_REPEAT, BackgroundPosition.DEFAULT, BackgroundSize.DEFAULT);
        Pane pane = new Pane();
        pane.setBackground(new Background(bgImage));
        pane.setStyle("-fx-border-color: rgba(255, 255, 0, 0.5); -fx-border-style: solid; -fx-border-width: 15;");

        // ???ESC?????
        Scene scene = new Scene(pane);
        transparentStage.setScene(scene);
        scene.setOnKeyPressed(e -> {
            if (e.getCode() == KeyCode.ESCAPE) {
                transparentStage.close();
            }
        });

        return transparentStage;
    }

    /**
     * ??
     *
     * @author M.Yoshida
     */
    class CaptureService extends Service<Void> {

        /** ? */
        private java.awt.Rectangle captureRect;
        /** ?? */
        private int currentPageNumber;
        /** ????????? */
        private int youngestPageNumber;
        /** ?? */
        private Map<Integer, Page> pages = new HashMap<>();

        /** ??? */
        private Direction prevDirection1;
        /** ??? */
        private Direction prevDirection2;
        /** ?? */
        private BufferedImage prevImage;
        /** ????????? */
        private boolean isSameImage1;
        /** ?????????? */
        private boolean isSameImage2;
        /** ????? */
        private boolean completed;

        /**
         * ????
         */
        public void init() {
            captureRect = new java.awt.Rectangle(areaStartX.get(), areaStartY.get(), calcAreaWidth(),
                    calcAreaHeight());

            youngestPageNumber = 1;
            currentPageNumber = 1;
            pages = new HashMap<>();
            prevDirection1 = null;
            prevDirection2 = null;
            prevImage = null;
            isSameImage1 = false;
            isSameImage2 = false;
            completed = false;
        }

        @Override
        protected Task<Void> createTask() {
            return new Task<Void>() {
                @Override
                protected Void call() throws Exception {
                    // ????????
                    Page currentPage = pages.get(currentPageNumber);
                    if (currentPage == null) {
                        currentPage = new Page(currentPageNumber);
                        pages.put(currentPageNumber, currentPage);
                    }

                    // ?
                    BufferedImage image = capture();
                    isSameImage2 = isSameImage1;
                    isSameImage1 = ImageUtil.equals(prevImage, image);
                    prevImage = image;

                    // 
                    if (isCompleted()) {
                        completed = true;
                        return null;
                    }

                    // ????????????
                    if (!currentPage.isFixed()) {
                        currentPage.submitImage(image);

                        // ???
                        if (currentPage.isFixed()) {
                            savePage(currentPage);
                            updateYoungestPageNumber();
                        }
                    }

                    // ???
                    Direction direction = decideDirection();
                    prevDirection2 = prevDirection1;
                    prevDirection1 = direction;
                    direction.turnPage();

                    return null;
                }
            };
        }

        @Override
        protected void ready() {
            captureTimeline.pause();
            System.out.printf("RADY -> cur:%d, you:%d, dir:(%s,%s), img:(%b,%b)%n", currentPageNumber,
                    youngestPageNumber, prevDirection1, prevDirection2, isSameImage1, isSameImage2);
        }

        @Override
        protected void succeeded() {
            System.out.printf("SUCC -> cur:%d, you:%d, dir:(%s,%s), img:(%b,%b)%n", currentPageNumber,
                    youngestPageNumber, prevDirection1, prevDirection2, isSameImage1, isSameImage2);

            if (completed) {
                stopCapture();
            } else if (captureTimeline.getStatus() == Status.PAUSED) {
                captureTimeline.play();
            }
        }

        @Override
        protected void failed() {
            System.out.printf("FAIL -> cur:%d, you:%d, dir:(%s,%s), img:(%b,%b)%n", currentPageNumber,
                    youngestPageNumber, prevDirection1, prevDirection2, isSameImage1, isSameImage2);
            stopCapture();
            LOGGER.error("?", getException());
            Dialogs.create()//
                    .title("FATAL")//
                    .masthead("?")//
                    .showException(getException());
        }

        /**
         * ??????
         *
         * @return ???
         */
        private BufferedImage capture() {
            return robot.createScreenCapture(captureRect);
        }

        /**
         * ???
         *
         * ?????????
         *
         * <ul>
         *   <li>?1,2???????????????FIX???
         *     <ol>
         *       <li>??????????????</li>
         *       <li>??????????????</li>
         *     </ol>
         *   </li>
         *   <li>?1,2????????FIX??????
         *     <ol>
         *       <li>??????????????</li>
         *       <li>??????????</li>
         *     </ol>
         *   </li>
         * </ul>
         *
         * @return ????true
         */
        private boolean isCompleted() {
            if (prevDirection2 == FORWARD && isSameImage2) {

                if (prevDirection1 == FORWARD && isSameImage1) {
                    return true;
                } else if (prevDirection1 == BACKWARD && !isSameImage1) {
                    return true;
                }
            }

            return false;
        }

        /**
         * ???
         *
         * @param page ??
         * @throws IOException ??????
         */
        private void savePage(Page page) throws IOException {
            String stringSeq = new DecimalFormat("00000").format(page.getPageNumber());
            File captureFile = FileUtils.getFile(saveDirectory.get(), stringSeq + ".bmp");
            System.out.println("Save " + captureFile.getPath());
            ImageIO.write(page.getImage(), "bmp", captureFile);
        }

        /**
         * ??????????
         */
        private void updateYoungestPageNumber() {
            Page page = pages.get(youngestPageNumber);
            while (page != null && page.isFixed()) {
                youngestPageNumber++;
                page = pages.get(youngestPageNumber);
            }
        }

        /**
         * ???????????
         *
         * @return ???true
         */
        private Direction decideDirection() {
            if (currentPageNumber <= youngestPageNumber) {
                return FORWARD;
            } else {
                return BACKWARD;
            }
        }

        /**
         * ???
         *
         * @param x X
         * @param y Y
         */
        private void clickPoint(IntegerProperty x, IntegerProperty y) {
            Point mousePoint = MouseInfo.getPointerInfo().getLocation();
            robot.mouseMove(x.get(), y.get());
            robot.mousePress(InputEvent.BUTTON1_MASK);
            robot.mouseRelease(InputEvent.BUTTON1_MASK);
            robot.mouseMove(mousePoint.x, mousePoint.y);
        }

        /**
         * ??????
         * ?Enum????enum?static???????????????
         */
        private abstract class Direction {
            abstract void turnPage();
        }

        /** ??? */
        private final Direction FORWARD = new Direction() {
            @Override
            void turnPage() {
                clickPoint(nextPointX, nextPointY);
                currentPageNumber++;
            }

            @Override
            public String toString() {
                return "FORWARD";
            }
        };

        /** ??? */
        private final Direction BACKWARD = new Direction() {
            @Override
            void turnPage() {
                clickPoint(prevPointX, prevPointY);
                currentPageNumber--;
            }

            @Override
            public String toString() {
                return "BACKWARD";
            }
        };
    }

    private int calcAreaWidth() {
        return areaEndX.get() - areaStartX.get();
    }

    private int calcAreaHeight() {
        return areaEndY.get() - areaStartY.get();
    }

    public void setStage(Stage stage) {
        this.stage = stage;
    }

}