Search code examples
javaswingbufferedimagegraphics2daffinetransform

Java : How to do image manipulation (pan, zoom, flip and rotate)?


I am making an Image viewer application for my own interest. I am using JPanel to display a bufferedImage.

Objective - I want to manipulate the image and give user the options to zoom, pan, flip or rotate images.

I am now able to zoom, pan and rotate. But the problem is that it is not the same as what you see when you open a .pdf file in Microsoft Edge.

I have added the code below:

import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionAdapter;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.*;

public class ZoomAndPan1 extends JPanel {

    BufferedImage img;
    private boolean init = true;
    private int zoomLevel = 0;
    private int minZoomLevel = -20;
    private int maxZoomLevel = 10;
    private double zoomMultiplicationFactor = 1.2;

    private Point dragStartScreen;
    private Point dragEndScreen;
    private AffineTransform coordTransform = new AffineTransform();

    public ZoomAndPan1() throws IOException {
        this.addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                dragStartScreen = e.getPoint();
                dragEndScreen = null;
            }
        });
        this.addMouseMotionListener(new MouseMotionAdapter() {
            @Override
            public void mouseDragged(MouseEvent e) {
                pan(e);
            }
        });
        this.addMouseWheelListener(new MouseWheelListener() {
            @Override
            public void mouseWheelMoved(MouseWheelEvent e) {
                if (e.isControlDown()) {
                    zoom(e);
                }
            }
        });
        img = ImageIO.read(new File("file.path"));
    }

    @Override
    protected void paintComponent(Graphics g) {

        super.paintComponent(g);
        Graphics2D g2 = (Graphics2D) g;
        int x = (int) (this.size().getWidth() - (img.getWidth() * .2)) / 2;
        int y = (int) (this.size().getHeight() - (img.getHeight() * .2)) / 2;

        AffineTransform at = new AffineTransform();
        at.translate(x, y);
        at.scale(.2, .2);
        if (init) {
            g2.setTransform(at);
            init = false;
            coordTransform = g2.getTransform();
        } else {
            g2.setTransform(coordTransform);
        }

        g2.drawImage(img, 0, 0, this);

        g2.dispose();
    }

    private void pan(MouseEvent e) {
        try {
            dragEndScreen = e.getPoint();
            Point2D.Float dragStart = transformPoint(dragStartScreen);
            Point2D.Float dragEnd = transformPoint(dragEndScreen);
            double dx = dragEnd.getX() - dragStart.getX();
            double dy = dragEnd.getY() - dragStart.getY();
            coordTransform.translate(dx, dy);
            dragStartScreen = dragEndScreen;
            dragEndScreen = null;
            repaint();
        } catch (NoninvertibleTransformException ex) {
            ex.printStackTrace();
        }
    }

    private void zoom(MouseWheelEvent e) {
        try {
            int wheelRotation = e.getWheelRotation();
            Point p = e.getPoint();
            if (wheelRotation > 0) {
                if (zoomLevel < maxZoomLevel) {
                    zoomLevel++;
                    Point2D p1 = transformPoint(p);
                    coordTransform.scale(1 / zoomMultiplicationFactor, 1 / zoomMultiplicationFactor);
                    Point2D p2 = transformPoint(p);
                    coordTransform.translate(p2.getX() - p1.getX(), p2.getY() - p1.getY());
                    repaint();
                }
            } else {
                if (zoomLevel > minZoomLevel) {
                    zoomLevel--;
                    Point2D p1 = transformPoint(p);
                    coordTransform.scale(zoomMultiplicationFactor, zoomMultiplicationFactor);
                    Point2D p2 = transformPoint(p);
                    coordTransform.translate(p2.getX() - p1.getX(), p2.getY() - p1.getY());
                    repaint();
                }
            }
        } catch (NoninvertibleTransformException ex) {
            ex.printStackTrace();
        }
    }

    private Point2D.Float transformPoint(Point p1) throws NoninvertibleTransformException {
        AffineTransform inverse = coordTransform.createInverse();
        Point2D.Float p2 = new Point2D.Float();
        inverse.transform(p1, p2);
        return p2;
    }

    public Dimension getPreferredSize() {
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        return new Dimension(screenSize.width, screenSize.height);
    }

    public static void main(String[] args) {
        try {
            JFrame frame = new JFrame("Zoom and Pan ");
            frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
            frame.add(new ZoomAndPan1(), BorderLayout.CENTER);
            frame.pack();
            frame.setVisible(true);
        } catch (IOException ex) {
            Logger.getLogger(ZoomAndPan1.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}

This code can do following things :

  1. Zoom in/out on CTRL + mouse scroll

  2. Pan image on mouse drag.

Limitations :

  1. Return to center when zoomed out.

  2. Pan is not restricted with in the Jpanel or viewport boundaries.

I want to achieve this :

enter image description here

Unfortunately, the code does like this :

enter image description here

How to solve this ?

Thank you in advance..


Solution

  • Here is a simple program that demonstrates what I suggested, namely a large image set as the icon of a JLabel and the JLabel is the scrollable client for a JScrollPane. Note that I set the preferred size of the JScrollPane to 800 x 600 pixels because the default behavior is to make the JScrollPane the same size as its scrollable client. The image dimensions are 2312 x 1536 pixels which means it is larger than my computer screen. So if I don't set the preferred size, when you run the program the JFrame overflows the screen. You don't see all of the JFrame. This is on purpose so you can see how the image scrolls. The URL for the image is https://unsplash.com/photos/l68Z6eF2peA. And here is the code, which is a MCVE

    By the way, I downloaded and saved the image in a file that I named worldmap.jpg

    import java.awt.BorderLayout;
    import java.awt.Dimension;
    import java.awt.EventQueue;
    
    import javax.swing.Icon;
    import javax.swing.ImageIcon;
    import javax.swing.JFrame;
    import javax.swing.JLabel;
    import javax.swing.JScrollPane;
    import javax.swing.WindowConstants;
    
    public class ScrolImg implements Runnable {
        private JFrame  frame;
    /* Start 'Runnable' interface methods. */
        public void run() {
            showGui();
        }
    /* End 'Runnable' interface methods. */
        private JScrollPane createMainPanel() {
            Icon ico = new ImageIcon("worldmap.jpg");
            JLabel label = new JLabel(ico);
            JScrollPane scrollPane = new JScrollPane(label);
            scrollPane.setPreferredSize(new Dimension(800, 600));
            return scrollPane;
        }
    
        private void showGui() {
            frame = new JFrame("ScrolImg");
            frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
            frame.add(createMainPanel(), BorderLayout.CENTER);
            frame.pack();
            frame.setLocationByPlatform(true);
            frame.setVisible(true);
        }
    
        public static void main(String[] args) {
            ScrolImg instance = new ScrolImg();
            EventQueue.invokeLater(instance);
        }
    }
    

    Hopefully this code will help you complete your project.