package com.gluonhq.dl.mnist.app; import com.gluonhq.charm.down.Platform; import com.gluonhq.charm.down.Services; import com.gluonhq.charm.down.plugins.StorageService; import com.gluonhq.charm.glisten.control.Toast; import com.gluonhq.charm.glisten.mvc.View; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import javafx.animation.PauseTransition; import javafx.event.EventHandler; import javafx.geometry.Point2D; import javafx.geometry.Rectangle2D; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; import javafx.scene.input.ScrollEvent; import javafx.scene.input.ZoomEvent; import javafx.util.Duration; public class MnistImageView extends ImageView { private static final int MIN_SIZE = 200; private static final double MARGIN = 20; private double IMAGE_WIDTH; private double INNER_WIDTH; private double IMAGE_HEIGHT; private double INNER_HEIGHT; private boolean zooming = false; private boolean enableDragging = false; private double initialMousePressedX, initialMousePressedY; private double mouseDownX, mouseDownY; private final EventHandler<MouseEvent> pressedHandler = e -> { initialMousePressedX = e.getX(); initialMousePressedY = e.getY(); Point2D mouseDown = getImageCoordinates(initialMousePressedX, initialMousePressedY); mouseDownX = mouseDown.getX(); mouseDownY = mouseDown.getY(); enableDragging = true; }; private final EventHandler<MouseEvent> draggedHandler = e -> { if (zooming || !enableDragging) { return; } final Point2D delta = getImageCoordinates(e.getX(), e.getY()).subtract(mouseDownX, mouseDownY); translateViewport(delta.getX(), delta.getY()); Point2D mouseDown = getImageCoordinates(e.getX(), e.getY()); mouseDownX = mouseDown.getX(); mouseDownY = mouseDown.getY(); }; private final EventHandler<MouseEvent> releasedHandler = e -> enableDragging = false; private final EventHandler<ScrollEvent> scrollHandler = e -> zoom(Math.pow(1.01, -e.getDeltaY()), e.getX(), e.getY()); private final EventHandler<ZoomEvent> zoomStartedHandler = e -> { enableDragging = false; zooming = true; }; private final EventHandler<ZoomEvent> zoomFinishedHandler = e -> zooming = false; private final EventHandler<ZoomEvent> zoomHandler = e -> zoom(1 / e.getZoomFactor(), e.getX(), e.getY()); public MnistImageView() { setPreserveRatio(true); } public Image getFilteredPicture() { return this.getImage(); } public void updateImage(View view, Image image) { removeListeners(); this.setImage(image); this.setPickOnBounds(true); IMAGE_HEIGHT = image.getHeight(); IMAGE_WIDTH = image.getWidth(); INNER_HEIGHT = 1 * IMAGE_HEIGHT; INNER_WIDTH = 1 * IMAGE_WIDTH; this.fitWidthProperty().bind(view.widthProperty().subtract(MARGIN)); this.fitHeightProperty().bind(view.heightProperty().subtract(MARGIN)); double fitScale = Math.max(INNER_WIDTH / (view.getWidth() - MARGIN), INNER_HEIGHT / (view.getHeight() - MARGIN)); this.setViewport(new Rectangle2D(0, 0, (view.getWidth() - MARGIN) * fitScale, (view.getWidth() - MARGIN) * fitScale)); addListeners(); zoomIn(); Toast toast = new Toast("Zoom and center over the number.\nClick Crop when ready"); toast.show(); } private void addListeners() { this.addEventHandler(MouseEvent.MOUSE_PRESSED, pressedHandler); this.addEventHandler(MouseEvent.MOUSE_DRAGGED, draggedHandler); this.addEventHandler(MouseEvent.MOUSE_RELEASED, releasedHandler); if (Platform.isDesktop()) { this.addEventHandler(ScrollEvent.ANY, scrollHandler); } else { this.addEventHandler(ZoomEvent.ZOOM_STARTED, zoomStartedHandler); this.addEventHandler(ZoomEvent.ZOOM_FINISHED, zoomFinishedHandler); this.addEventHandler(ZoomEvent.ZOOM, zoomHandler); } } private void removeListeners() { this.removeEventHandler(MouseEvent.MOUSE_PRESSED, pressedHandler); this.removeEventHandler(MouseEvent.MOUSE_DRAGGED, draggedHandler); this.removeEventHandler(MouseEvent.MOUSE_RELEASED, releasedHandler); if (Platform.isDesktop()) { this.removeEventHandler(ScrollEvent.ANY, scrollHandler); } else { this.removeEventHandler(ZoomEvent.ZOOM_STARTED, zoomStartedHandler); this.removeEventHandler(ZoomEvent.ZOOM_FINISHED, zoomFinishedHandler); this.removeEventHandler(ZoomEvent.ZOOM, zoomHandler); } } private Point2D getImageCoordinates(double eX, double eY) { double factorX = eX / this.getBoundsInLocal().getWidth(); double factorY = eY / this.getBoundsInLocal().getHeight(); Rectangle2D viewport = this.getViewport(); return new Point2D(viewport.getMinX() + factorX * viewport.getWidth(), viewport.getMinY() + factorY * viewport.getHeight()); } private void translateViewport(double deltaX, double deltaY) { Rectangle2D viewport = this.getViewport(); double minX = clamp(viewport.getMinX() - deltaX, 0, IMAGE_WIDTH - viewport.getWidth()); double minY = clamp(viewport.getMinY() - deltaY, 0, IMAGE_HEIGHT - viewport.getHeight()); this.setViewport(new Rectangle2D(minX, minY, viewport.getWidth(), viewport.getHeight())); } private void zoom(double factor, double pivotX, double pivotY) { Rectangle2D viewport = this.getViewport(); double scale = clamp(factor, Math.min(MIN_SIZE / viewport.getWidth(), MIN_SIZE / viewport.getHeight()), Math.max(INNER_WIDTH / viewport.getWidth(), INNER_HEIGHT / viewport.getHeight())); Point2D pivot = getImageCoordinates(pivotX, pivotY); double newWidth = viewport.getWidth() * scale; double newHeight = viewport.getHeight() * scale; // to zoom over the pivot, we have for x, y: // (x - newMinX) / (x - viewport.getMinX()) = scale // solving for newMinX, newMinY: double newMinX = clamp(pivot.getX() - (pivot.getX() - viewport.getMinX()) * scale, 0, IMAGE_WIDTH - newWidth); double newMinY = clamp(pivot.getY() - (pivot.getY() - viewport.getMinY()) * scale, 0, IMAGE_HEIGHT - newHeight); this.setViewport(new Rectangle2D(newMinX, newMinY, newWidth, newHeight)); } // The initial imageview is scaled up, we need to zoom it out private void zoomIn() { PauseTransition p = new PauseTransition(Duration.millis(30)); p.setOnFinished(e -> { double factor = Math.max(INNER_WIDTH / this.getViewport().getWidth(), INNER_HEIGHT / this.getViewport().getHeight()); zoom(factor, this.getFitWidth()/2, this.getFitHeight()/2); translateViewport(-(IMAGE_WIDTH - this.getViewport().getWidth()) / 2, -(IMAGE_HEIGHT - this.getViewport().getHeight()) / 2); }); p.play(); } private double clamp(double value, double min, double max) { double minMax = Math.max(0, max); return value < min ? min : (value > minMax ? minMax : value); } public File getImageFile() throws FileNotFoundException { File privateStorage = Services.get(StorageService.class) .flatMap(StorageService::getPrivateStorage) .orElseThrow(() -> new FileNotFoundException ("Could not access private storage")); Image image = this.getImage(); PngEncoderFX encoder = new PngEncoderFX(image, true); byte[] bytes = encoder.pngEncode(); File file = new File(privateStorage, "Image-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("uuuuMMdd-HHmmss")) + ".png"); try (FileOutputStream fos = new FileOutputStream(file)) { fos.write(bytes); return file; } catch (IOException ex) { System.out.println("Error: " + ex); return null; } } }